import 'dart:developer' as developer; import 'dart:io'; import 'dart:async'; import 'package:path_provider/path_provider.dart'; import 'package:intl/intl.dart'; /// 日志级别 enum LogLevel { debug, info, warning, error, } /// 日志工具类 /// /// 提供统一的日志记录接口,支持控制台输出和文件存储 class Logger { /// 当前日志级别,低于此级别的日志不会被输出 static LogLevel _currentLevel = LogLevel.debug; /// 默认标签 static const String _defaultTag = 'App'; /// 文件日志相关 static IOSink? _logSink; static File? _currentLogFile; static bool _fileLogEnabled = false; static String? _logDirPath; static final DateFormat _dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS'); static final DateFormat _fileDateFormat = DateFormat('yyyy-MM-dd'); /// 最大单个日志文件大小 (2MB) static const int _maxFileSize = 2 * 1024 * 1024; /// 最多保留日志文件数量 static const int _maxLogFiles = 7; /// 写入计数器,用于节流轮转检查和刷盘 static int _writeCount = 0; /// 轮转检查间隔(每 100 次写入检查一次) static const int _rotationCheckInterval = 100; /// 刷盘间隔(每 50 次写入刷一次) static const int _flushInterval = 50; /// 轮转锁,防止并发轮转 static bool _isRotating = false; /// 当前日志文件的日期标识,用于快速判断是否需要换天 static String _currentFileDate = ''; /// 初始化文件日志 static Future initFileLog() async { try { final dir = await getApplicationDocumentsDirectory(); _logDirPath = '${dir.path}/logs'; final logDir = Directory(_logDirPath!); if (!await logDir.exists()) { await logDir.create(recursive: true); } await _openLogFile(); _fileLogEnabled = true; Logger.i('Logger', '文件日志初始化成功: $_logDirPath'); } catch (e) { developer.log('[Logger] 文件日志初始化失败: $e'); } } /// 打开/创建当前日志文件 static Future _openLogFile() async { final today = _fileDateFormat.format(DateTime.now()); final filePath = '$_logDirPath/app_$today.log'; _currentLogFile = File(filePath); // 先关闭旧 sink,必须 await 确保关闭完成 await _logSink?.flush(); await _logSink?.close(); _logSink = _currentLogFile!.openWrite(mode: FileMode.append); _currentFileDate = today; } /// 检查日志文件是否需要轮转(受节流和锁保护) static Future _checkRotation() async { if (_isRotating || _currentLogFile == null) return; _isRotating = true; try { // 检查日期是否变更 final today = _fileDateFormat.format(DateTime.now()); if (_currentFileDate != today) { await _openLogFile(); } // 检查文件大小 if (await _currentLogFile!.exists()) { final size = await _currentLogFile!.length(); if (size > _maxFileSize) { final now = DateTime.now(); final suffix = DateFormat('HHmmss').format(now); final todayStr = _fileDateFormat.format(now); final newPath = '$_logDirPath/app_${todayStr}_$suffix.log'; await _logSink?.flush(); await _logSink?.close(); _currentLogFile = File(newPath); _logSink = _currentLogFile!.openWrite(mode: FileMode.append); } } // 清理旧日志文件 await _cleanOldLogs(); } catch (_) {} finally { _isRotating = false; } } /// 清理旧日志文件,保留最近 _maxLogFiles 个 static Future _cleanOldLogs() async { if (_logDirPath == null) return; try { final logDir = Directory(_logDirPath!); final files = await logDir .list() .where((e) => e is File && e.path.endsWith('.log')) .cast() .toList(); if (files.length > _maxLogFiles) { files.sort((a, b) => a.path.compareTo(b.path)); final toDelete = files.sublist(0, files.length - _maxLogFiles); for (final f in toDelete) { await f.delete(); } } } catch (_) {} } /// 单行最大字符数,超过则分批写入 static const int _maxLineLength = 4000; /// 写入日志到文件 static void _writeToFile(String level, String tag, String message) { if (!_fileLogEnabled || _logSink == null) return; try { final timestamp = _dateFormat.format(DateTime.now()); if (message.length <= _maxLineLength) { _logSink!.writeln('$timestamp [$level][$tag] $message'); } else { final totalParts = (message.length + _maxLineLength - 1) ~/ _maxLineLength; for (var i = 0; i < totalParts; i++) { final start = i * _maxLineLength; final end = start + _maxLineLength > message.length ? message.length : start + _maxLineLength; final chunk = message.substring(start, end); _logSink!.writeln('$timestamp [$level][$tag] [${i + 1}/$totalParts] $chunk'); } } _writeCount++; // 定期刷盘,确保数据写入磁盘 if (_writeCount % _flushInterval == 0) { _logSink!.flush(); } // 定期检查轮转(节流,不是每次写入都检查) if (_writeCount % _rotationCheckInterval == 0) { _checkRotation(); } } catch (e) { // 文件写入失败不应影响业务逻辑(尤其是 Dio 拦截器链) developer.log('[Logger] 文件写入失败: $e'); } } /// 获取日志目录路径 static String? get logDirPath => _logDirPath; /// 获取所有日志文件列表 static Future> getLogFiles() async { if (_logDirPath == null) return []; final logDir = Directory(_logDirPath!); if (!await logDir.exists()) return []; final files = await logDir .list() .where((e) => e is File && e.path.endsWith('.log')) .cast() .toList(); files.sort((a, b) => b.path.compareTo(a.path)); // 最新在前 return files; } /// 刷新缓冲区(导出前调用) static Future flush() async { await _logSink?.flush(); } /// 设置日志级别 static void setLevel(LogLevel level) { _currentLevel = level; } /// 输出调试日志 (不带标签版本) static void debug(String message) { if (_currentLevel.index <= LogLevel.debug.index) { _log('DEBUG', _defaultTag, message); } } /// 输出调试日志 static void d(String tag, String message) { if (_currentLevel.index <= LogLevel.debug.index) { _log('DEBUG', tag, message); } } /// 输出信息日志 (不带标签版本) static void info(String message) { if (_currentLevel.index <= LogLevel.info.index) { _log('INFO', _defaultTag, message); } } /// 输出信息日志 static void i(String tag, String message) { if (_currentLevel.index <= LogLevel.info.index) { _log('INFO', tag, message); } } /// 输出警告日志 (不带标签版本) static void warning(String message) { if (_currentLevel.index <= LogLevel.warning.index) { _log('WARNING', _defaultTag, message); } } /// 输出警告日志 static void w(String tag, String message) { if (_currentLevel.index <= LogLevel.warning.index) { _log('WARNING', tag, message); } } /// 输出错误日志 (不带标签版本) static void error(String message, [dynamic error, StackTrace? stackTrace]) { if (_currentLevel.index <= LogLevel.error.index) { _log('ERROR', _defaultTag, message); if (error != null) { _log('ERROR', _defaultTag, 'Original error: $error'); } if (stackTrace != null) { _log('ERROR', _defaultTag, 'Stack trace: $stackTrace'); } } } /// 输出错误日志 static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) { if (_currentLevel.index <= LogLevel.error.index) { _log('ERROR', tag, message); if (error != null) { _log('ERROR', tag, 'Original error: $error'); } if (stackTrace != null) { _log('ERROR', tag, 'Stack trace: $stackTrace'); } } } /// 内部日志输出方法 static void _log(String level, String tag, String message) { if (message.length <= _maxLineLength) { developer.log('[$tag]: $message'); } else { final totalParts = (message.length + _maxLineLength - 1) ~/ _maxLineLength; for (var i = 0; i < totalParts; i++) { final start = i * _maxLineLength; final end = start + _maxLineLength > message.length ? message.length : start + _maxLineLength; developer.log('[$tag]: [${i + 1}/$totalParts] ${message.substring(start, end)}'); } } _writeToFile(level, tag, message); } }