Files
video/app/lib/core/log/action_logger.dart
T
2026-10-25 11:46:44 +08:00

135 lines
3.7 KiB
Dart

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<Map<String, dynamic>> _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<void> _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<String, dynamic>? 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<void> flush() async {
if (!_enabled || _flushing || _queue.isEmpty) return;
_flushing = true;
try {
final batch = <Map<String, dynamic>>[];
while (_queue.isNotEmpty && batch.length < 100) {
batch.add(_queue.removeFirst());
}
await ApiClient.I.post<Map<String, dynamic>>(
'/logs/action',
data: {'items': batch},
);
} catch (_) {
// 静默失败:上报通道绝不能影响业务
} finally {
_flushing = false;
}
}
}