import 'package:hgdj/hj_utils/light_model.dart'; import 'package:hgdj/hj_utils/store_keys.dart'; import 'package:uuid/uuid.dart'; /// 会话管理器 - 生成并维护统一的会话 ID (sid) /// /// 会话 = 用户一次连续使用产生的一组行为,同一会话内所有请求/埋点共享同一个 sid。 /// /// 会话重建规则(满足任一即换新 sid): /// 1. App 冷启动 /// 2. 从后台回前台,且后台停留 > 30 分钟 /// 3. 从后台回前台,且距会话创建 > 30 分钟 /// /// 持久化在 MMKV (lightKV):当前 sid / 会话创建时间 / 进入后台时间 class TrackSessionManager { static final TrackSessionManager _instance = TrackSessionManager._(); factory TrackSessionManager() => _instance; TrackSessionManager._(); /// 会话超时阈值:30 分钟 static const Duration _timeout = Duration(minutes: 30); String _sid = ''; DateTime? _sessionAt; //会话创建时间 DateTime? _backgroundedAt; //App 进入后台的时间 bool _inited = false; /// 当前会话 ID,为空则立即新建(防御性) String get currentSid { if (_sid.isEmpty) _newSession(); return _sid; } /// App 冷启动时调用一次(main.dart):恢复旧状态后强制开新会话 Future onAppColdStart() async { if (_inited) return; await _restore(); _newSession(); } /// App 进入后台:记录时刻,用于回前台时判断停留时长 /// 调用时机:AppLifecycleState.paused / inactive / detached Future onAppPause() async { _backgroundedAt = DateTime.now(); await lightKV.setInt(StoreKeys.TRACK_SESSION_BACKGROUND_TS, _backgroundedAt!.millisecondsSinceEpoch); } /// App 回到前台:后台停留超时或会话本身超时都换新 sid(规则2、3) /// 调用时机:AppLifecycleState.resumed Future onAppResume() async { //没进过后台不算规则2,所以要先判非空 final bgExpired = _backgroundedAt != null && _isExpired(_backgroundedAt); if (bgExpired || _isExpired(_sessionAt)) _newSession(); _backgroundedAt = null; await lightKV.setInt(StoreKeys.TRACK_SESSION_BACKGROUND_TS, 0); } /// 从存储恢复上次的 sid 和时间戳,已过期则先开新会话 Future _restore() async { _sid = (await lightKV.getString(StoreKeys.TRACK_SESSION_SID)) ?? ''; //键名沿用历史的 LAST_EVENT_TS,存的是会话创建时间 final sessionTs = await lightKV.getInt(StoreKeys.TRACK_SESSION_LAST_EVENT_TS); if (sessionTs != null && sessionTs > 0) _sessionAt = DateTime.fromMillisecondsSinceEpoch(sessionTs); final backgroundTs = await lightKV.getInt(StoreKeys.TRACK_SESSION_BACKGROUND_TS); if (backgroundTs != null && backgroundTs > 0) _backgroundedAt = DateTime.fromMillisecondsSinceEpoch(backgroundTs); if (_sid.isEmpty || _isExpired(_sessionAt)) _newSession(); _inited = true; } /// 距 [time] 是否已超过 30 分钟(time 为空按已过期处理) bool _isExpired(DateTime? time) { if (time == null) return true; return DateTime.now().difference(time) > _timeout; } /// 新建会话并立即落盘,防止进程被杀丢数据 void _newSession() { _sid = Uuid().v4().replaceAll('-', ''); _sessionAt = DateTime.now(); lightKV.setString(StoreKeys.TRACK_SESSION_SID, _sid); lightKV.setInt(StoreKeys.TRACK_SESSION_LAST_EVENT_TS, _sessionAt!.millisecondsSinceEpoch); } }