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 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 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 payload) { if (_string(payload['platform']) == 'web') return 'web'; final osFamily = _string(payload['os_family']); if (osFamily == 'ios') return 'ios'; return 'android'; } static List _androidFingerprintValues(Map 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 _iosFingerprintValues(Map 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 _webFingerprintValues(Map 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 payload, List 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 payload, String key) => '$key=${_string(payload[key])}'; static String _kvInt(Map 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); }