From c999a8004817d7597475aff7fd0a421e0f4762ea Mon Sep 17 00:00:00 2001 From: chfei Date: Mon, 19 Oct 2026 15:09:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=97=A5=E5=BF=97=20?= =?UTF-8?q?=E5=B7=A5=E5=8D=95=E8=BF=9E=E6=8E=A5=E5=A2=9E=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EventSourceTicketRealtimeService.cs | 4 +- WxOrder.Framework/Helpers/NLogger.cs | 275 ++++++++++++++++++ 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 WxOrder.Framework/Helpers/NLogger.cs diff --git a/OrderSupportTicket/Services/EventSourceTicketRealtimeService.cs b/OrderSupportTicket/Services/EventSourceTicketRealtimeService.cs index 26c8765..93c960a 100644 --- a/OrderSupportTicket/Services/EventSourceTicketRealtimeService.cs +++ b/OrderSupportTicket/Services/EventSourceTicketRealtimeService.cs @@ -6,6 +6,7 @@ using OrderSupportTicket.Models.Api; using System; using System.Linq; using System.Threading.Tasks; +using WxOrder.Framework.Helpers; namespace OrderSupportTicket.Services; @@ -76,6 +77,7 @@ public sealed class EventSourceTicketRealtimeService { try { + NLogger.Instance.Info("工单服务连接成"); await source.StartAsync(); lock (_sync) { @@ -86,7 +88,7 @@ public sealed class EventSourceTicketRealtimeService } catch (Exception ex) { - LogSave.Add("工单服务连接失败:" + ex.Message); + NLogger.Instance.Error("工单服务连接失败:" + ex.Message); lock (_sync) { if (!_running || !ReferenceEquals(_eventSource, source)) diff --git a/WxOrder.Framework/Helpers/NLogger.cs b/WxOrder.Framework/Helpers/NLogger.cs new file mode 100644 index 0000000..b26ea2e --- /dev/null +++ b/WxOrder.Framework/Helpers/NLogger.cs @@ -0,0 +1,275 @@ +using System; +using System.IO; +using System.Text; + +namespace WxOrder.Framework.Helpers +{ + /// 日志消息级别。 + public enum LogMessageLevel + { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Fatal = 5, + None = 6 + } + + /// + /// 调试输出桥接。默认输出到 System.Diagnostics.Debug, + /// 可在外部重新赋值 DebugPrint 以接管控件台/UI 输出。 + /// + public static class DebugLog + { + /// 调试输出回调。默认写到 Debug 输出窗口。 + public static Action? DebugPrint { get; set; } + = msg => System.Diagnostics.Debug.WriteLine(msg); + } + + /// + /// 默认日志逻辑类(不依赖任何第三方日志库)。 + /// 日志文件按日期分目录存放,并在跨天时自动切换: + /// {logDirectory}/yyyy-MM/yyyy-MM-dd.log,例如 log/2022-12/2022-12-12.log + /// + public sealed class NLogger : IDisposable + { + private const string DefaultName = "Default"; + + private static readonly object _lock = new object(); + private static NLogger? _instance; + + /// + /// 同一进程内所有实例共用的写入锁。 + /// 多个实例(不同 Name)会写入同一个按日期命名的文件,需要串行化写入。 + /// + private static readonly object _fileLock = new object(); + + /// 获取 NLogger 的唯一单例实例(线程安全)。 + public static NLogger Instance + { + get + { + if (_instance == null) + { + lock (_lock) + { + if (_instance == null) + _instance = new NLogger(DefaultName); + } + } + return _instance; + } + } + + /// 静态工厂:根据名称创建日志实例。 + public static NLogger Create(string name, string? logDirectory = null) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("日志名称不能为空。", nameof(name)); + + return new NLogger(name, logDirectory); + } + + /// 静态工厂:创建使用默认名称的日志实例。 + public static NLogger Create() => new NLogger(DefaultName); + + /// 日志名称(写入每行日志中,用于区分来源,不再决定文件名)。 + public string Name { get; } + + public bool IsDebugEnabled { get; set; } = true; + + public bool IsDebugWriteLineEnabled { get; set; } = true; + + public LogMessageLevel MinimumLevel { get; set; } = LogMessageLevel.Trace; + + /// 日志根目录。 + public string LogDirectory { get; } + + private string? _currentFilePath; + private StreamWriter? _writer; + private bool _disposed; + + private NLogger(string name, string? logDirectory = null) + { + Name = name; + LogDirectory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "log"); + // 文件在第一次写入时按当前日期惰性创建,构造阶段不打开文件。 + } + + #region 日志方法 + + public void Trace(string message, Exception? exception = null) + => Execute(LogMessageLevel.Trace, message, exception); + + public void Debug(string message, Exception? exception = null) + => Execute(LogMessageLevel.Debug, message, exception); + + public void Info(string message, Exception? exception = null) + => Execute(LogMessageLevel.Info, message, exception); + + public void Warn(string message, Exception? exception = null) + => Execute(LogMessageLevel.Warn, message, exception); + + public void Error(string message, Exception? exception = null) + => Execute(LogMessageLevel.Error, message, exception); + + public void Fatal(string message, Exception? exception = null) + => Execute(LogMessageLevel.Fatal, message, exception); + + public void Log(string message, LogMessageLevel level, Exception? exception = null) + => Execute(level, message, exception); + + #endregion + + #region 内部逻辑 + + private bool CheckEnabled(LogMessageLevel level) + { + if (level == LogMessageLevel.None) + return false; + + if (level < MinimumLevel) + return false; + + if (level == LogMessageLevel.Trace || level == LogMessageLevel.Debug) + return IsDebugEnabled; + + return true; + } + + private void Execute(LogMessageLevel level, string message, Exception? exception) + { + if (CheckEnabled(level)) + { + WriteToFile(level, message, exception); + } + + if (IsDebugWriteLineEnabled) + { + string logOutput = "[" + level + "] " + message; + if (exception != null) + { + logOutput += Environment.NewLine + exception; + } + + Action? handler = DebugLog.DebugPrint; + if (handler != null) + { + handler(logOutput); + } + } + } + + /// 根据日期生成日志文件路径:{根目录}/yyyy-MM/yyyy-MM-dd.log + private string GetLogFilePath(DateTime time) + { + return Path.Combine( + LogDirectory, + time.ToString("yyyy-MM"), + time.ToString("yyyy-MM-dd") + ".log"); + } + + /// 写入当天的日志文件,跨天时自动切换到新文件。 + private void WriteToFile(LogMessageLevel level, string message, Exception? exception) + { + DateTime now = DateTime.Now; + + var sb = new StringBuilder(); + sb.Append('[') + .Append(now.ToString("yyyy-MM-dd HH:mm:ss.fff")) + .Append("] [") + .Append(level) + .Append("] [") + .Append(Name) + .Append("] ") + .AppendLine(message); + + if (exception != null) + { + sb.AppendLine(exception.ToString()); + } + + string line = sb.ToString(); + + lock (_fileLock) + { + if (_disposed) return; + + try + { + EnsureWriter(GetLogFilePath(now)); + _writer!.Write(line); + _writer.Flush(); + } + catch + { + // 日志写入失败不应影响主流程,这里吞掉异常。 + // 重置写入器,下次写入时重新尝试打开文件。 + CloseWriter(); + } + } + } + + /// 确保写入器指向指定路径;路径变化(跨天/跨月)时关闭旧文件并打开新文件。 + private void EnsureWriter(string path) + { + if (_writer != null && string.Equals(_currentFilePath, path, StringComparison.Ordinal)) + return; + + CloseWriter(); + + string? dir = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + var fs = new FileStream( + path, + FileMode.Append, + FileAccess.Write, + FileShare.ReadWrite); + + _writer = new StreamWriter(fs, new UTF8Encoding(false)) + { + AutoFlush = false + }; + _currentFilePath = path; + } + + private void CloseWriter() + { + try + { + if (_writer != null) + { + _writer.Flush(); + _writer.Dispose(); + } + } + catch { /* ignore */ } + finally + { + _writer = null; + _currentFilePath = null; + } + } + + #endregion + + #region IDisposable + + public void Dispose() + { + lock (_fileLock) + { + if (_disposed) return; + _disposed = true; + CloseWriter(); + } + } + + #endregion + } +} \ No newline at end of file