Initial commit: 91dj android client dev_1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import 'device_fingerprint_native.dart'
|
||||
// ignore: uri_does_not_exist
|
||||
if (dart.library.html) 'device_fingerprint_web.dart';
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
/// 设备指纹工具类
|
||||
///
|
||||
/// SDK 内部自动调用,业务方无需手动传入。
|
||||
///
|
||||
/// **生成规则**
|
||||
/// 1. 现行外层 `device_fingerprint` 使用 v3 规则:
|
||||
/// 基于 `sdk_init` payload 内的平台稳定字段,按固定顺序规范化后做
|
||||
/// SHA-256,输出 64 位小写十六进制。
|
||||
/// 2. 网络、电量、页面、行为等易变字段不参与 v3 指纹。
|
||||
/// 3. [getFingerprint] 保留为旧调用方兼容入口;SDK 自动上报链路使用
|
||||
/// [generateV3FromSdkInitPayload]。
|
||||
///
|
||||
/// **平台差异**
|
||||
/// - Native(Android/iOS/桌面):降级 UUID 存于 `applicationDocumentsDirectory/analytics_fingerprint.txt`
|
||||
/// - Web:降级 UUID 存于 `localStorage['analytics_fp']`
|
||||
///
|
||||
/// **失败兜底**:任何异常均返回空字符串,事件正常上报,仅 `device_fingerprint` 字段为空。
|
||||
///
|
||||
/// **注意**:`device_fingerprint` 不参与 `event_id` 的计算。
|
||||
class DeviceFingerprintUtil {
|
||||
/// 指纹生成规则版本号。
|
||||
///
|
||||
/// 每次生成规则(因子组合、哈希算法)发生变更时,此版本号必须同步递增,
|
||||
/// 以便服务端区分不同规则下生成的指纹,支持跨版本数据对比分析。
|
||||
///
|
||||
/// 版本历史:
|
||||
/// - `3.0.0`:按 sdk_init 启动快照中的平台稳定字段生成。
|
||||
static const String kVersion = '3.0.0';
|
||||
|
||||
static String? _cache;
|
||||
|
||||
/// 获取设备指纹。
|
||||
///
|
||||
/// 首次调用时根据传入的设备因子计算并内存缓存,后续直接返回缓存值。
|
||||
/// 若设备因子发生变化需重新计算,请先调用 [clearCache]。
|
||||
static Future<String> getFingerprint({
|
||||
String deviceId = '',
|
||||
String device = '',
|
||||
String deviceBrand = '',
|
||||
String deviceModel = '',
|
||||
String systemName = '',
|
||||
String systemVersion = '',
|
||||
String userAgent = '',
|
||||
}) async {
|
||||
if (_cache != null) return _cache!;
|
||||
_cache = await generateOrLoadFingerprint(
|
||||
deviceId: deviceId,
|
||||
device: device,
|
||||
deviceBrand: deviceBrand,
|
||||
deviceModel: deviceModel,
|
||||
systemName: systemName,
|
||||
systemVersion: systemVersion,
|
||||
userAgent: userAgent,
|
||||
);
|
||||
return _cache!;
|
||||
}
|
||||
|
||||
/// 清除内存缓存(通常仅测试时使用,不会删除本地存储文件)
|
||||
static void clearCache() {
|
||||
_cache = null;
|
||||
}
|
||||
|
||||
/// 按产品文档 v3 规则,基于 sdk_init payload 生成外层设备指纹。
|
||||
///
|
||||
/// 该方法只使用启动快照内相对稳定的字段;网络、电量、页面、行为等
|
||||
/// 易变字段不参与。输入缺失时保留字段名并使用文档默认值,确保同一
|
||||
/// 平台的参与字段顺序稳定。
|
||||
static String generateV3FromSdkInitPayload(Map<String, dynamic> payload) {
|
||||
try {
|
||||
final platform = _fingerprintPlatform(payload);
|
||||
final values = platform == 'web'
|
||||
? _webFingerprintValues(payload)
|
||||
: platform == 'ios'
|
||||
? _iosFingerprintValues(payload)
|
||||
: _androidFingerprintValues(payload);
|
||||
final canonical =
|
||||
'fp_v3|fingerprint_platform=$platform|${values.join('|')}';
|
||||
return sha256.convert(utf8.encode(canonical)).toString();
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static String _fingerprintPlatform(Map<String, dynamic> payload) {
|
||||
if (_string(payload['platform']) == 'web') return 'web';
|
||||
final osFamily = _string(payload['os_family']);
|
||||
if (osFamily == 'ios') return 'ios';
|
||||
return 'android';
|
||||
}
|
||||
|
||||
static List<String> _androidFingerprintValues(Map<String, dynamic> payload) {
|
||||
final id = _selectFirstId(payload, const [
|
||||
'oaid',
|
||||
'aaid',
|
||||
'android_id',
|
||||
'guid',
|
||||
]);
|
||||
return [
|
||||
_kvString(payload, 'os_family'),
|
||||
_kvString(payload, 'brand'),
|
||||
_kvString(payload, 'manufacturer'),
|
||||
_kvString(payload, 'model'),
|
||||
_kvString(payload, 'abi'),
|
||||
_kvInt(payload, 'screen_width'),
|
||||
_kvInt(payload, 'screen_height'),
|
||||
_kvInt(payload, 'screen_dpi'),
|
||||
_kvInt(payload, 'device_pixel_ratio'),
|
||||
_kvInt(payload, 'cpu_cores'),
|
||||
_kvInt(payload, 'device_memory_gb'),
|
||||
_kvString(payload, 'build_hardware'),
|
||||
_kvString(payload, 'build_product'),
|
||||
_kvString(payload, 'build_fingerprint'),
|
||||
_kvInt(payload, 'has_baseband'),
|
||||
_kvString(payload, 'language'),
|
||||
_kvString(payload, 'timezone'),
|
||||
'id_source=${id.source}',
|
||||
'id_value=${id.value}',
|
||||
];
|
||||
}
|
||||
|
||||
static List<String> _iosFingerprintValues(Map<String, dynamic> payload) {
|
||||
final idfv = _string(payload['idfv']);
|
||||
return [
|
||||
_kvString(payload, 'os_family'),
|
||||
_kvString(payload, 'ios_device_model_raw'),
|
||||
_kvInt(payload, 'screen_width'),
|
||||
_kvInt(payload, 'screen_height'),
|
||||
_kvInt(payload, 'screen_dpi'),
|
||||
_kvInt(payload, 'device_pixel_ratio'),
|
||||
_kvInt(payload, 'cpu_cores'),
|
||||
_kvInt(payload, 'device_memory_gb'),
|
||||
_kvString(payload, 'language'),
|
||||
_kvString(payload, 'timezone'),
|
||||
'id_source=${idfv.isEmpty ? 'none' : 'idfv'}',
|
||||
'id_value=$idfv',
|
||||
];
|
||||
}
|
||||
|
||||
static List<String> _webFingerprintValues(Map<String, dynamic> payload) {
|
||||
return [
|
||||
_kvString(payload, 'os_family'),
|
||||
_kvString(payload, 'browser_kernel'),
|
||||
_kvString(payload, 'browser_platform'),
|
||||
_kvInt(payload, 'screen_width'),
|
||||
_kvInt(payload, 'screen_height'),
|
||||
_kvInt(payload, 'screen_dpi'),
|
||||
_kvInt(payload, 'device_pixel_ratio'),
|
||||
_kvInt(payload, 'color_depth'),
|
||||
_kvInt(payload, 'max_touch_points'),
|
||||
_kvInt(payload, 'hardware_concurrency'),
|
||||
_kvString(payload, 'language'),
|
||||
_kvString(payload, 'timezone'),
|
||||
_kvInt(payload, 'cookie_enabled'),
|
||||
_kvInt(payload, 'local_storage_enabled'),
|
||||
_kvInt(payload, 'session_storage_enabled'),
|
||||
_kvInt(payload, 'indexeddb_enabled'),
|
||||
_kvInt(payload, 'service_worker_enabled'),
|
||||
_kvString(payload, 'canvas_fp'),
|
||||
_kvString(payload, 'webgl_fp'),
|
||||
_kvString(payload, 'audio_fp'),
|
||||
_kvString(payload, 'fonts_hash'),
|
||||
_kvString(payload, 'plugins_hash'),
|
||||
_kvString(payload, 'mimes_hash'),
|
||||
];
|
||||
}
|
||||
|
||||
static _SelectedId _selectFirstId(
|
||||
Map<String, dynamic> payload,
|
||||
List<String> keys,
|
||||
) {
|
||||
for (final key in keys) {
|
||||
final value = _string(payload[key]);
|
||||
if (value.isNotEmpty) return _SelectedId(key, value);
|
||||
}
|
||||
return const _SelectedId('none', '');
|
||||
}
|
||||
|
||||
static String _kvString(Map<String, dynamic> payload, String key) =>
|
||||
'$key=${_string(payload[key])}';
|
||||
|
||||
static String _kvInt(Map<String, dynamic> payload, String key) =>
|
||||
'$key=${_int(payload[key])}';
|
||||
|
||||
static String _string(dynamic value) {
|
||||
if (value == null) return '';
|
||||
return value.toString().trim().toLowerCase();
|
||||
}
|
||||
|
||||
static int _int(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is double && value.isFinite) return value.round();
|
||||
if (value is String) {
|
||||
final text = value.trim();
|
||||
final asInt = int.tryParse(text);
|
||||
if (asInt != null) return asInt;
|
||||
final asDouble = double.tryParse(text);
|
||||
if (asDouble != null && asDouble.isFinite) return asDouble.round();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectedId {
|
||||
final String source;
|
||||
final String value;
|
||||
|
||||
const _SelectedId(this.source, this.value);
|
||||
}
|
||||
Reference in New Issue
Block a user