diff --git a/Helper/NotificationHelper.cs b/Helper/NotificationHelper.cs index 320e219..b592e2e 100644 --- a/Helper/NotificationHelper.cs +++ b/Helper/NotificationHelper.cs @@ -1,6 +1,9 @@ + +using System; +using System.Collections.Concurrent; using System.Runtime.CompilerServices; +using System.Threading; using CommunityToolkit.Mvvm.Messaging; -using NLog; using PMSWPF.Enums; using PMSWPF.Message; @@ -8,113 +11,147 @@ namespace PMSWPF.Helper; /// /// 通知帮助类,用于显示各种类型的通知消息,并集成日志记录功能。 +/// 新增了通知节流功能,以防止在短时间内向用户发送大量重复的通知。 /// -public class NotificationHelper +public static class NotificationHelper { /// - /// 获取当前类的 NLog 日志实例。 + /// 内部类,用于存储节流通知的状态信息。 /// - private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + private class ThrottledNotificationInfo + { + public int Count; + public Timer Timer; + public NotificationType NotificationType; + } + + private static readonly ConcurrentDictionary ThrottledNotifications = new ConcurrentDictionary(); + private const int ThrottleTimeSeconds = 30; /// - /// 显示一个通用通知消息,并根据通知类型记录日志。 + /// 内部核心通知发送方法,包含了节流逻辑。 /// - /// 通知消息内容。 - /// 通知类型(如信息、错误、成功等),默认为信息。 - /// 是否为全局通知(目前未使用,保留参数)。 - /// 自动捕获:调用此方法的源文件完整路径。 - /// 自动捕获:调用此方法的行号。 - public static void ShowMessage(string msg, NotificationType notificationType = NotificationType.Info, - bool isGlobal = false, [CallerFilePath] string callerFilePath = "", - [CallerLineNumber] int callerLineNumber = 0) + private static void SendNotificationInternal(string msg, NotificationType notificationType, bool throttle, Exception exception, string callerFilePath, string callerMember, int callerLineNumber) { - // 根据通知类型记录日志 + // 根据通知类型决定日志级别,并使用 NlogHelper 记录日志(利用其自身的节流逻辑) if (notificationType == NotificationType.Error) { - Logger.Error($"{msg} (File: {callerFilePath}, Line: {callerLineNumber})"); + NlogHelper.Error(msg, exception, throttle, callerFilePath, callerMember, callerLineNumber); } else { - Logger.Info($"{msg} (File: {callerFilePath}, Line: {callerLineNumber})"); + NlogHelper.Info(msg, throttle, callerFilePath, callerMember, callerLineNumber); } - // 通过消息总线发送通知消息,以便UI层可以订阅并显示 - WeakReferenceMessenger.Default.Send( - new NotificationMessage(msg, notificationType)); + // 如果不启用通知节流,则直接发送通知并返回。 + if (!throttle) + { + WeakReferenceMessenger.Default.Send(new NotificationMessage(msg, notificationType)); + return; + } + + var key = $"{callerFilePath}:{callerLineNumber}:{msg}"; + + ThrottledNotifications.AddOrUpdate( + key, + // --- 添加逻辑:当通知第一次被节流时执行 --- + _ => + { + // 1. 首次出现,立即发送一次通知。 + WeakReferenceMessenger.Default.Send(new NotificationMessage(msg, notificationType)); + + // 2. 创建新的节流信息对象。 + var newThrottledNotification = new ThrottledNotificationInfo + { + Count = 1, + NotificationType = notificationType + }; + + // 3. 创建并启动计时器。 + newThrottledNotification.Timer = new Timer(s => + { + if (ThrottledNotifications.TryRemove(key, out var finishedNotification)) + { + finishedNotification.Timer.Dispose(); + if (finishedNotification.Count > 1) + { + var summaryMsg = $"消息 '{msg}' 在过去 {ThrottleTimeSeconds} 秒内出现了 {finishedNotification.Count} 次。"; + WeakReferenceMessenger.Default.Send(new NotificationMessage(summaryMsg, finishedNotification.NotificationType)); + } + } + }, null, ThrottleTimeSeconds * 1000, Timeout.Infinite); + + return newThrottledNotification; + }, + // --- 更新逻辑:当通知在节流窗口内再次出现时执行 --- + (_, existingNotification) => + { + Interlocked.Increment(ref existingNotification.Count); + return existingNotification; + }); } /// - /// 显示一个错误通知消息,并记录错误日志。 + /// 显示一个通用通知消息,并根据通知类型记录日志。支持节流。 + /// + /// 通知消息内容。 + /// 通知类型(如信息、错误、成功等)。 + /// 是否启用通知节流。 + /// 自动捕获:调用此方法的源文件完整路径。 + /// 自动捕获:调用此方法的行号。 + public static void ShowMessage(string msg, NotificationType notificationType = NotificationType.Info, bool throttle = false, + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + SendNotificationInternal(msg, notificationType, throttle, null, callerFilePath, "", callerLineNumber); + } + + /// + /// 显示一个错误通知消息,并记录错误日志。支持节流。 /// /// 错误消息内容。 /// 可选:要记录的异常对象。 + /// 是否启用通知和日志节流。 /// 自动捕获:调用此方法的源文件完整路径。 /// 自动捕获:调用此方法的成员或属性名称。 /// 自动捕获:调用此方法的行号。 - public static void ShowError(string msg, Exception exception = null, + public static void ShowError(string msg, Exception exception = null, bool throttle = false, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = 0) { - // 使用 using 语句确保 MappedDiagnosticsLogicalContext 在作用域结束时被清理。 - // 这对于异步方法尤其重要,因为上下文会随着异步操作的流转而传递。 - using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) - { - // 记录错误日志,包含异常信息(如果提供) - Logger.Error(exception, msg); - // 通过消息总线发送错误通知 - WeakReferenceMessenger.Default.Send( - new NotificationMessage(msg, NotificationType.Error)); - } - } - - /// - /// 显示一个成功通知消息,并记录信息日志。 - /// - /// 成功消息内容。 - /// 自动捕获:调用此方法的源文件完整路径。 - /// 自动捕获:调用此方法的成员或属性名称。 - /// 自动捕获:调用此方法的行号。 - public static void ShowSuccess(string msg, - [CallerFilePath] string callerFilePath = "", - [CallerMemberName] string callerMember = "", - [CallerLineNumber] int callerLineNumber = 0) - { - using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) - { - // 记录信息日志 - Logger.Info(msg); - // 通过消息总线发送成功通知 - WeakReferenceMessenger.Default.Send( - new NotificationMessage(msg, NotificationType.Success)); - } + SendNotificationInternal(msg, NotificationType.Error, throttle, exception, callerFilePath, callerMember, callerLineNumber); } /// - /// 显示一个信息通知消息,并记录信息日志。 + /// 显示一个成功通知消息,并记录信息日志。支持节流。 /// - /// 信息消息内容。 + /// 成功消息内容。 + /// 是否启用通知和日志节流。 /// 自动捕获:调用此方法的源文件完整路径。 /// 自动捕获:调用此方法的成员或属性名称。 /// 自动捕获:调用此方法的行号。 - public static void ShowInfo(string msg, + public static void ShowSuccess(string msg, bool throttle = false, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = 0) { - using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) - { - // 记录信息日志 - Logger.Info(msg); - // 通过消息总线发送信息通知 - WeakReferenceMessenger.Default.Send( - new NotificationMessage(msg, NotificationType.Info)); - } + SendNotificationInternal(msg, NotificationType.Success, throttle, null, callerFilePath, callerMember, callerLineNumber); } -} \ No newline at end of file + + /// + /// 显示一个信息通知消息,并记录信息日志。支持节流。 + /// + /// 信息消息内容。 + /// 是否启用通知和日志节流。 + /// 自动捕获:调用此方法的源文件完整路径。 + /// 自动捕获:调用此方法的成员或属性名称。 + /// 自动捕获:调用此方法的行号。 + public static void ShowInfo(string msg, bool throttle = false, + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMember = "", + [CallerLineNumber] int callerLineNumber = 0) + { + SendNotificationInternal(msg, NotificationType.Info, throttle, null, callerFilePath, callerMember, callerLineNumber); + } +}