using System.IO.Compression; using System.Text; using WorkCameraExport.Services.Interfaces; namespace WorkCameraExport.Services { /// /// 日志服务实现 - 负责日志记录和导出 /// public class LogService : ILogService { private readonly string _logPath; private readonly object _lockObject = new(); private readonly string _appLogPrefix = "app_"; private readonly string _errorLogPrefix = "error_"; public LogService(string logPath) { _logPath = logPath; EnsureLogDirectoryExists(); } public LogService(IConfigService configService) { _logPath = configService.LogPath; EnsureLogDirectoryExists(); } #region ILogService 实现 public void Info(string message) { WriteLog("INFO", message, _appLogPrefix); } public void Error(string message, Exception? ex = null) { var fullMessage = ex != null ? $"{message}\nException: {ex.GetType().Name}: {ex.Message}\nStackTrace: {ex.StackTrace}" : message; WriteLog("ERROR", fullMessage, _errorLogPrefix); // 同时写入应用日志 WriteLog("ERROR", fullMessage, _appLogPrefix); } public void Warn(string message) { WriteLog("WARN", message, _appLogPrefix); } public void Debug(string message) { #if DEBUG WriteLog("DEBUG", message, _appLogPrefix); #endif } public async Task ExportLogsAsync(string outputPath, DateTime? startDate = null, DateTime? endDate = null) { var logFiles = GetLogFilesInRange(startDate, endDate); if (logFiles.Count == 0) { throw new InvalidOperationException("没有找到符合条件的日志文件"); } // 如果输出路径是 .zip 文件,则打包 if (outputPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { await Task.Run(() => { using var zipStream = new FileStream(outputPath, FileMode.Create); using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create); foreach (var logFile in logFiles) { var entryName = Path.GetFileName(logFile); archive.CreateEntryFromFile(logFile, entryName); } }); } else { // 否则合并到单个文件 var sb = new StringBuilder(); foreach (var logFile in logFiles.OrderBy(f => f)) { sb.AppendLine($"=== {Path.GetFileName(logFile)} ==="); sb.AppendLine(await File.ReadAllTextAsync(logFile)); sb.AppendLine(); } await File.WriteAllTextAsync(outputPath, sb.ToString()); } } public List GetLogFiles() { if (!Directory.Exists(_logPath)) return new List(); return Directory.GetFiles(_logPath, "*.log") .OrderByDescending(f => f) .ToList(); } public void CleanOldLogs(int retentionDays = 30) { if (!Directory.Exists(_logPath)) return; var cutoffDate = DateTime.Now.AddDays(-retentionDays); var logFiles = Directory.GetFiles(_logPath, "*.log"); foreach (var file in logFiles) { try { var fileInfo = new FileInfo(file); if (fileInfo.LastWriteTime < cutoffDate) { fileInfo.Delete(); } } catch { // 忽略删除错误 } } } #endregion #region 私有方法 private void EnsureLogDirectoryExists() { if (!Directory.Exists(_logPath)) { Directory.CreateDirectory(_logPath); } } private void WriteLog(string level, string message, string filePrefix) { var timestamp = DateTime.Now; var logFileName = $"{filePrefix}{timestamp:yyyy-MM-dd}.log"; var logFilePath = Path.Combine(_logPath, logFileName); var logEntry = FormatLogEntry(timestamp, level, message); lock (_lockObject) { try { File.AppendAllText(logFilePath, logEntry + Environment.NewLine); } catch { // 忽略写入错误 } } } /// /// 格式化日志条目 /// public static string FormatLogEntry(DateTime timestamp, string level, string message) { return $"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}"; } /// /// 解析日志条目 /// public static (DateTime? Timestamp, string? Level, string? Message) ParseLogEntry(string logEntry) { if (string.IsNullOrWhiteSpace(logEntry)) return (null, null, null); // 格式: [yyyy-MM-dd HH:mm:ss.fff] [LEVEL] message var match = System.Text.RegularExpressions.Regex.Match( logEntry, @"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\] \[(\w+)\] (.*)$"); if (match.Success) { if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var timestamp)) { return (timestamp, match.Groups[2].Value, match.Groups[3].Value); } } return (null, null, null); } private List GetLogFilesInRange(DateTime? startDate, DateTime? endDate) { var allFiles = GetLogFiles(); if (startDate == null && endDate == null) return allFiles; var result = new List(); foreach (var file in allFiles) { var fileName = Path.GetFileNameWithoutExtension(file); // 提取日期部分 (app_2025-01-05 或 error_2025-01-05) var datePart = fileName.Contains('_') ? fileName.Substring(fileName.IndexOf('_') + 1) : fileName; if (DateTime.TryParseExact(datePart, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var fileDate)) { var inRange = true; if (startDate.HasValue && fileDate < startDate.Value.Date) inRange = false; if (endDate.HasValue && fileDate > endDate.Value.Date) inRange = false; if (inRange) result.Add(file); } } return result; } #endregion } }