3ddb0ea2d4
Co-Authored-By: Claude Code <noreply@anthropic.com>
92 lines
3.1 KiB
Dart
92 lines
3.1 KiB
Dart
/// 设备标识(P3 计划 Task 6,A19 设备来源语义)。
|
||
///
|
||
/// 每安装一次生成一个随机 32 位十六进制串(16 字节 `Random.secure`),
|
||
/// 持久化后跨进程重启稳定;随 `initUpload` 上报为可选 sourceDeviceId,
|
||
/// 服务端历史(GET /uploads)据此区分上传来源设备。
|
||
///
|
||
/// 非机密(不可当作设备指纹做安全决策,服务端仅作展示归属),明文存
|
||
/// shared_preferences 足够——不占用 secure storage。卸载重装视为新设备
|
||
/// (per-install 语义,符合「客户端首次生成并持久化」的契约描述)。
|
||
library;
|
||
|
||
import 'dart:math';
|
||
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
/// 存储抽象(测试注入内存实现;生产=shared_preferences)。
|
||
abstract class DeviceIdStore {
|
||
Future<String?> read();
|
||
Future<void> write(String value);
|
||
}
|
||
|
||
/// shared_preferences 生产实现(单键)。
|
||
class SharedPrefsDeviceIdStore implements DeviceIdStore {
|
||
const SharedPrefsDeviceIdStore();
|
||
|
||
static const String kKey = 'device.id';
|
||
|
||
@override
|
||
Future<String?> read() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
return prefs.getString(kKey);
|
||
}
|
||
|
||
@override
|
||
Future<void> write(String value) async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString(kKey, value);
|
||
}
|
||
}
|
||
|
||
/// 设备标识:懒初始化 + 进程内缓存(并发调用共享首次生成结果)。
|
||
class DeviceId {
|
||
DeviceId({DeviceIdStore? store, Random? random})
|
||
: _store = store ?? const SharedPrefsDeviceIdStore(),
|
||
_random = random ?? Random.secure();
|
||
|
||
final DeviceIdStore _store;
|
||
final Random _random;
|
||
|
||
Future<String>? _pending;
|
||
|
||
/// 合法取值形状:32 位小写十六进制。
|
||
static final RegExp kPattern = RegExp(r'^[0-9a-f]{32}$');
|
||
|
||
/// 当前设备标识(首次调用生成并持久化;后续读缓存)。
|
||
Future<String> value() => _pending ??= _loadOrCreate();
|
||
|
||
Future<String> _loadOrCreate() async {
|
||
String? stored;
|
||
try {
|
||
stored = await _store.read();
|
||
} on Object {
|
||
// 存储读取失败按未生成处理:重新生成并尽力写回。
|
||
}
|
||
if (stored != null && kPattern.hasMatch(stored)) {
|
||
return stored;
|
||
}
|
||
final generated = _generate();
|
||
try {
|
||
await _store.write(generated);
|
||
} on Object {
|
||
// 写回失败不阻断:本次进程内仍可用(下次启动重新生成——
|
||
// sourceDeviceId 可选,服务端容忍 null/变更)。
|
||
}
|
||
return generated;
|
||
}
|
||
|
||
/// 16 随机字节 → 32 位小写十六进制。
|
||
String _generate() {
|
||
final bytes = List<int>.generate(16, (_) => _random.nextInt(256));
|
||
return hexEncode(bytes);
|
||
}
|
||
|
||
/// 字节 → 小写十六进制(独立函数,单测直接覆盖)。
|
||
static String hexEncode(List<int> bytes) =>
|
||
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||
}
|
||
|
||
/// 设备标识 Provider(全局单例语义:整个 App 共享同一 per-install 值)。
|
||
final deviceIdProvider = Provider<DeviceId>((ref) => DeviceId());
|