import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'package:package_info_plus/package_info_plus.dart'; import '../network/api_client.dart'; /// 用户操作日志上报器(仅白名单邮箱用户启用) /// /// 用法: /// ```dart /// ActionLogger.I.bindUser(userEmail); /// ActionLogger.I.log('route_push', page: '/search/feed', detail: {...}); /// ``` /// /// 设计: /// - 白名单:只有 email == 256kb@gmail.com 时启用,其他账号调用为 no-op /// - 缓冲:本地 Queue 缓冲,达到 BATCH_SIZE 或超过 FLUSH_INTERVAL 自动批量上传 /// - 静默失败:网络错误/服务端 403 不重试不上抛,避免影响业务 class ActionLogger { ActionLogger._(); static final ActionLogger I = ActionLogger._(); /// 白名单邮箱(与后端 ALLOWED_EMAILS 保持一致) static const String _allowedEmail = '256kb@gmail.com'; static const int _batchSize = 30; static const Duration _flushInterval = Duration(seconds: 10); static const int _maxQueueLen = 500; // 防止离线积压撑爆内存 bool _enabled = false; String _userEmail = ''; String _deviceInfo = ''; String _appVersion = ''; final Queue> _queue = Queue(); Timer? _flushTimer; bool _flushing = false; bool get enabled => _enabled; /// 登录成功后调用,开启上报;非白名单邮箱则关闭 void bindUser(String? email) { _userEmail = email ?? ''; _enabled = _userEmail.toLowerCase() == _allowedEmail; if (_enabled) { _initMeta(); _startFlushTimer(); log('log_session_start', page: 'app', detail: {'email': _userEmail}); } else { _stopFlushTimer(); _queue.clear(); } } /// 登出/切换账号时调用 void unbind() { if (_enabled) { log('log_session_end', page: 'app', detail: {'email': _userEmail}); // 立即上传残余 unawaited(flush()); } _enabled = false; _userEmail = ''; _stopFlushTimer(); // 不清 queue:flush 已在飞,让它带完最后这批 } Future _initMeta() async { try { final info = await PackageInfo.fromPlatform(); _appVersion = '${info.version}+${info.buildNumber}'; } catch (_) { _appVersion = 'unknown'; } _deviceInfo = '${Platform.operatingSystem} ${Platform.operatingSystemVersion}'; } void _startFlushTimer() { _flushTimer?.cancel(); _flushTimer = Timer.periodic(_flushInterval, (_) => unawaited(flush())); } void _stopFlushTimer() { _flushTimer?.cancel(); _flushTimer = null; } /// 记一条日志。enabled=false 时直接 no-op void log( String action, { String page = '', Map? detail, String? detailRaw, }) { if (!_enabled) return; if (_queue.length >= _maxQueueLen) { _queue.removeFirst(); // 丢最老的,保留最新 } _queue.add({ 'action': action, 'page': page, 'detail': detailRaw ?? (detail != null ? jsonEncode(detail) : ''), 'device_info': _deviceInfo, 'app_version': _appVersion, 'client_ts': DateTime.now().millisecondsSinceEpoch, }); if (_queue.length >= _batchSize) { unawaited(flush()); } } /// 立即上传队列里的所有日志 Future flush() async { if (!_enabled || _flushing || _queue.isEmpty) return; _flushing = true; try { final batch = >[]; while (_queue.isNotEmpty && batch.length < 100) { batch.add(_queue.removeFirst()); } await ApiClient.I.post>( '/logs/action', data: {'items': batch}, ); } catch (_) { // 静默失败:上报通道绝不能影响业务 } finally { _flushing = false; } } }