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