Files
xianyan/lib/core/utils/logger.dart
Developer ae6804e8bd refactor: 兼容后端返回数字类型波动,清理废弃代码
主要变更:
1.  全局修复类型转换问题,将多处`as int?`改为`(num?)?.toInt()`兼容浮点/字符串类型的数字字段
2.  移除废弃的nearby_p2p配对方式和对应的依赖包
3.  优化鸿蒙端快捷方式、引导页、路由导航的稳定性
4.  合并日志输出避免鸿蒙端IDE卡顿
5.  修复安卓端蓝牙权限冗余声明
2026-06-07 08:04:38 +08:00

537 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 日志工具
/// 创建时间: 2026-04-20
/// 更新时间: 2026-06-06
/// 作用: 统一日志封装,支持分级与格式化 + 日志查看器 + 日志导出 + 按模块分类控制
/// 上次更新: 优化日志输出简洁Printer模式 + 提高高频分类默认日志级别
/// ============================================================
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
/// 全局日志器release模式仅输出error级别避免敏感信息泄露
/// 简洁模式减少调用栈、关闭颜色和emoji、缩短行宽
final appLogger = Logger(
printer: PrettyPrinter(
methodCount: 0, // 不打印调用栈
errorMethodCount: 5, // 错误时只打印5层调用栈
lineLength: 80, // 缩短行宽
// colors: true, // 颜色减少ANSI转义序列
printEmojis: false, // 关闭emoji减少输出
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, // 只显示时间
),
level: _isDebugMode ? Level.debug : Level.error,
);
/// 是否为调试模式编译期常量release模式下为false
bool get _isDebugMode {
bool? val;
assert(() {
val = true;
return true;
}());
return val ?? false;
}
/// 日志级别
enum LogLevel {
all('全部', 0),
verbose('详细', 1),
debug('调试', 2),
info('信息', 3),
warning('警告', 4),
error('错误', 5);
const LogLevel(this.label, this.value);
/// 级别中文名
final String label;
/// 级别数值(越大越严格)
final int value;
}
/// ============================================================
/// 日志分类 — 按模块控制日志级别
/// 创建时间: 2026-06-05
/// 作用: 支持按模块设置日志级别,调试时可只关注特定模块
/// ============================================================
enum LogCategory {
ui('UI', LogLevel.warning), // UI渲染、布局减少高频UI日志
network('网络', LogLevel.warning), // API请求、响应减少网络日志噪音
router('路由', LogLevel.warning), // 页面导航(减少路由日志)
storage('存储', LogLevel.warning), // 数据库、KV存储减少存储日志
device('设备', LogLevel.warning), // 设备信息、传感器(减少设备日志)
auth('认证', LogLevel.info), // 登录、鉴权保留info级别关注认证流程
transfer('传输', LogLevel.info), // 文件传输保留info级别
search('搜索', LogLevel.warning), // 搜索功能(减少搜索日志)
chart('图表', LogLevel.error), // Syncfusion图表仅输出错误
haptic('触觉', LogLevel.error), // 震动反馈(仅输出错误,极高频调用)
provider('状态', LogLevel.warning), // Riverpod Provider减少状态日志
service('服务', LogLevel.info), // 后台服务保留info级别
sync('同步', LogLevel.info), // 数据同步保留info级别
offline('离线', LogLevel.info), // 离线模式保留info级别
onboarding('引导', LogLevel.warning), // 引导页(减少引导日志)
push('推送', LogLevel.error), // 推送通知(仅输出错误)
general('通用', LogLevel.warning); // 其他(减少通用日志)
const LogCategory(this.label, this.defaultLevel);
/// 分类中文名
final String label;
/// 默认日志级别
final LogLevel defaultLevel;
/// 自定义级别映射(可动态调整)
static final Map<LogCategory, LogLevel> _customLevels = {};
/// 获取当前级别(优先使用自定义级别,否则使用默认级别)
LogLevel get currentLevel => _customLevels[this] ?? defaultLevel;
/// 分类标签(用于日志输出,如 [UI], [网络]
String get tag => '[$label]';
/// 设置单个分类级别
static void setLevel(LogCategory category, LogLevel level) {
_customLevels[category] = level;
}
/// 重置所有分类为默认级别
static void resetLevels() {
_customLevels.clear();
}
/// 设置所有分类为同一级别
static void setAllLevels(LogLevel level) {
for (final cat in LogCategory.values) {
_customLevels[cat] = level;
}
}
/// 获取所有分类的当前级别用于UI展示
static Map<LogCategory, LogLevel> get allCurrentLevels {
return {for (final cat in LogCategory.values) cat: cat.currentLevel};
}
/// 获取所有自定义级别(用于持久化)
static Map<String, String> exportCustomLevels() {
return _customLevels.map((k, v) => MapEntry(k.name, v.name));
}
/// 从持久化数据恢复自定义级别
static void importCustomLevels(Map<String, String> data) {
_customLevels.clear();
for (final entry in data.entries) {
final cat = LogCategory.values
.where((c) => c.name == entry.key)
.firstOrNull;
final level = LogLevel.values
.where((l) => l.name == entry.value)
.firstOrNull;
if (cat != null && level != null) {
_customLevels[cat] = level;
}
}
}
}
/// 日志条目
class LogEntry {
const LogEntry({
required this.level,
required this.message,
required this.time,
required this.category,
this.error,
this.stackTrace,
});
final LogLevel level;
final String message;
final DateTime time;
/// 日志分类
final LogCategory category;
final dynamic error;
final StackTrace? stackTrace;
}
/// 日志工具 — 静态便捷方法 + 内存缓冲 + 分类级别控制
class Log {
Log._();
/// 内存缓冲区最大条目数从500减少到200降低内存占用
static const _maxEntries = 200;
static final List<LogEntry> _entries = [];
/// Log.i 节流同一消息5秒内不重复输出到控制台
static final Map<String, DateTime> _infoThrottleMap = {};
static const _infoThrottleDuration = Duration(seconds: 5);
/// 获取所有日志条目
static List<LogEntry> get entries => List.unmodifiable(_entries);
/// 日志条目数量
static int get entryCount => _entries.length;
/// 清空日志
static void clearEntries() => _entries.clear();
/// 删除单条日志
static void removeEntry(LogEntry entry) => _entries.remove(entry);
/// 添加日志条目(内部方法)
static void _addEntry(
LogLevel level,
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory category = LogCategory.general,
]) {
_entries.add(
LogEntry(
level: level,
message: message?.toString() ?? '',
time: DateTime.now(),
category: category,
error: error,
stackTrace: stackTrace,
),
);
if (_entries.length > _maxEntries) {
_entries.removeRange(0, _entries.length - _maxEntries);
}
}
/// 判断分类是否应该输出日志
/// 如果日志级别 >= 分类的当前级别,则输出
static bool _shouldLog(LogLevel messageLevel, LogCategory category) {
return messageLevel.value >= category.currentLevel.value;
}
/// 格式化带分类标签的消息
static String _formatMessage(dynamic message, LogCategory category) {
if (category == LogCategory.general) {
return message?.toString() ?? '';
}
return '${category.tag} ${message?.toString() ?? ''}';
}
/// 调试日志release模式下不输出
/// [category] 日志分类,默认为 LogCategory.general
static void d(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
if (!_isDebugMode) return;
final cat = category ?? LogCategory.general;
if (!_shouldLog(LogLevel.debug, cat)) return;
appLogger.d(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.debug, message, error, stackTrace, cat);
}
/// 信息日志release模式下不输出
/// 添加节流机制同一消息5秒内不重复输出到控制台
/// [category] 日志分类,默认为 LogCategory.general
static void i(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
if (!_isDebugMode) return;
final cat = category ?? LogCategory.general;
if (!_shouldLog(LogLevel.info, cat)) {
// 即使不输出到控制台,仍然记录到内存缓冲区
_addEntry(LogLevel.info, message, error, stackTrace, cat);
return;
}
// 节流检查同一消息5秒内不重复输出到控制台
final msgStr = message?.toString() ?? '';
final now = DateTime.now();
final lastTime = _infoThrottleMap[msgStr];
if (lastTime == null || now.difference(lastTime) > _infoThrottleDuration) {
appLogger.i(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_infoThrottleMap[msgStr] = now;
// 清理过期的节流记录,避免内存泄漏
_infoThrottleMap.removeWhere(
(_, time) => now.difference(time) > _infoThrottleDuration,
);
}
_addEntry(LogLevel.info, message, error, stackTrace, cat);
}
/// 警告日志release模式下不输出
/// [category] 日志分类,默认为 LogCategory.general
static void w(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
if (!_isDebugMode) return;
final cat = category ?? LogCategory.general;
if (!_shouldLog(LogLevel.warning, cat)) {
_addEntry(LogLevel.warning, message, error, stackTrace, cat);
return;
}
appLogger.w(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.warning, message, error, stackTrace, cat);
}
/// 错误日志(始终输出,错误必须记录)
/// [category] 日志分类,默认为 LogCategory.general
static void e(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
final cat = category ?? LogCategory.general;
appLogger.e(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.error, message, error, stackTrace, cat);
}
/// 致命错误日志(始终输出,错误必须记录)
/// [category] 日志分类,默认为 LogCategory.general
static void f(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
final cat = category ?? LogCategory.general;
appLogger.f(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.error, message, error, stackTrace, cat);
}
/// 按级别过滤日志
static List<LogEntry> filterByLevel(LogLevel level) {
if (level == LogLevel.all) return entries;
return _entries.where((e) => e.level.value >= level.value).toList();
}
/// 按分类过滤日志
static List<LogEntry> filterByCategory(LogCategory? category) {
if (category == null) return entries;
return _entries.where((e) => e.category == category).toList();
}
/// 按时间范围筛选
static List<LogEntry> filterByTimeRange(DateTime? start, DateTime? end) {
return _entries.where((e) {
if (start != null && e.time.isBefore(start)) return false;
if (end != null && e.time.isAfter(end)) return false;
return true;
}).toList();
}
/// 按关键词搜索
static List<LogEntry> searchByKeyword(String keyword) {
if (keyword.isEmpty) return entries;
final lower = keyword.toLowerCase();
return _entries.where((e) {
if (e.message.toLowerCase().contains(lower)) return true;
if (e.error != null && e.error.toString().toLowerCase().contains(lower)) {
return true;
}
return false;
}).toList();
}
/// 综合筛选(支持分类过滤)
static List<LogEntry> filter({
LogLevel level = LogLevel.all,
LogCategory? category,
DateTime? startTime,
DateTime? endTime,
String keyword = '',
}) {
return _entries.where((e) {
if (level != LogLevel.all && e.level.value < level.value) return false;
if (category != null && e.category != category) return false;
if (startTime != null && e.time.isBefore(startTime)) return false;
if (endTime != null && e.time.isAfter(endTime)) return false;
if (keyword.isNotEmpty) {
final lower = keyword.toLowerCase();
if (!e.message.toLowerCase().contains(lower) &&
!(e.error != null &&
e.error.toString().toLowerCase().contains(lower))) {
return false;
}
}
return true;
}).toList();
}
/// 按级别统计
static Map<LogLevel, int> countByLevel() {
final counts = <LogLevel, int>{};
for (final level in LogLevel.values) {
if (level == LogLevel.all) continue;
counts[level] = _entries.where((e) => e.level == level).length;
}
return counts;
}
/// 按分类统计
static Map<LogCategory, int> countByCategory() {
final counts = <LogCategory, int>{};
for (final cat in LogCategory.values) {
counts[cat] = _entries.where((e) => e.category == cat).length;
}
return counts;
}
/// 导出日志为 JSON 字符串
static String exportToJson({LogLevel level = LogLevel.all}) {
final filtered = filterByLevel(level);
final data = filtered
.map(
(e) => {
'time': e.time.toIso8601String(),
'level': e.level.label,
'category': e.category.label,
'message': e.message,
if (e.error != null) 'error': e.error.toString(),
if (e.stackTrace != null) 'stackTrace': e.stackTrace.toString(),
},
)
.toList();
return const JsonEncoder.withIndent(' ').convert({
'export_time': DateTime.now().toIso8601String(),
'app_name': '闲言',
'log_count': data.length,
'filter_level': level.label,
'entries': data,
});
}
/// 导出日志为纯文本
static String exportToText({LogLevel level = LogLevel.all}) {
final filtered = filterByLevel(level);
final buffer = StringBuffer();
buffer.writeln('闲言APP 日志导出');
buffer.writeln('导出时间: ${DateTime.now().toIso8601String()}');
buffer.writeln('过滤级别: ${level.label}');
buffer.writeln('条目数量: ${filtered.length}');
buffer.writeln('=' * 60);
for (final e in filtered) {
buffer.writeln(
'[${e.time.toString().substring(0, 19)}] [${e.level.label}] [${e.category.label}] ${e.message}',
);
if (e.error != null) buffer.writeln(' Error: ${e.error}');
if (e.stackTrace != null) buffer.writeln(' Stack: ${e.stackTrace}');
}
return buffer.toString();
}
/// 导出日志为 CSV 格式
static String exportToCsv({LogLevel level = LogLevel.all}) {
final filtered = filterByLevel(level);
final buffer = StringBuffer();
buffer.writeln('时间,级别,分类,消息,错误,堆栈');
for (final e in filtered) {
final time = e.time.toString().substring(0, 19);
final levelLabel = e.level.label;
final categoryLabel = e.category.label;
final message = _escapeCsv(e.message);
final error = e.error != null ? _escapeCsv(e.error.toString()) : '';
final stack = e.stackTrace != null
? _escapeCsv(e.stackTrace.toString())
: '';
buffer.writeln('$time,$levelLabel,$categoryLabel,$message,$error,$stack');
}
return buffer.toString();
}
static String _escapeCsv(String value) {
if (value.contains(',') || value.contains('"') || value.contains('\n')) {
return '"${value.replaceAll('"', '""')}"';
}
return value;
}
/// 导出日志到文件Web端不可用
static Future<String> exportToFile({
LogLevel level = LogLevel.all,
bool asJson = true,
bool asCsv = false,
}) async {
if (kIsWeb) throw UnsupportedError('日志导出在Web端不可用');
try {
final String content;
final String ext;
if (asCsv) {
content = exportToCsv(level: level);
ext = 'csv';
} else if (asJson) {
content = exportToJson(level: level);
ext = 'json';
} else {
content = exportToText(level: level);
ext = 'log';
}
Directory dir;
try {
dir = await getTemporaryDirectory();
} catch (_) {
dir = Directory.systemTemp;
}
final timestamp = DateTime.now().millisecondsSinceEpoch;
final file = File('${dir.path}/xianyan_logs_$timestamp.$ext');
await file.writeAsString(content);
return file.path;
} catch (e) {
appLogger.e('日志导出失败', error: e);
rethrow;
}
}
/// 分享日志文件Web端不可用
static Future<void> shareLogs({LogLevel level = LogLevel.all}) async {
if (kIsWeb) return;
try {
final path = await exportToFile(level: level);
await SharePlus.instance.share(
ShareParams(files: [XFile(path)], text: '闲言APP 日志文件'),
);
} catch (e) {
appLogger.e('日志分享失败', error: e);
}
}
}