|
|
@@ -0,0 +1,319 @@
|
|
|
+using Serilog;
|
|
|
+using System.Net.Http.Json;
|
|
|
+using System.Text;
|
|
|
+using System.Text.Json;
|
|
|
+
|
|
|
+namespace YZWater.Core.Services;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 通知渠道接口
|
|
|
+/// </summary>
|
|
|
+public interface INotificationChannel
|
|
|
+{
|
|
|
+ string Name { get; }
|
|
|
+ Task SendAsync(string title, string message, NotificationLevel level);
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 通知级别
|
|
|
+/// </summary>
|
|
|
+public enum NotificationLevel
|
|
|
+{
|
|
|
+ Info,
|
|
|
+ Warning,
|
|
|
+ Error,
|
|
|
+ Critical
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 通知服务 - 管理多种通知渠道(邮件、Webhook 等)
|
|
|
+/// </summary>
|
|
|
+public class NotificationService : INotificationService, IDisposable
|
|
|
+{
|
|
|
+ private static NotificationService? _instance;
|
|
|
+ private static readonly object _instanceLock = new();
|
|
|
+ public static NotificationService Instance
|
|
|
+ {
|
|
|
+ get
|
|
|
+ {
|
|
|
+ lock (_instanceLock)
|
|
|
+ {
|
|
|
+ return _instance ??= new NotificationService();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private readonly List<INotificationChannel> _channels = new();
|
|
|
+ private readonly HttpClient _httpClient;
|
|
|
+ private bool _disposed;
|
|
|
+
|
|
|
+ // 通知限流:防止报警风暴
|
|
|
+ private readonly Dictionary<string, DateTime> _lastNotificationTimes = new();
|
|
|
+ private readonly object _throttleLock = new();
|
|
|
+ public int ThrottleSeconds { get; set; } = 60; // 同一报警至少间隔 60 秒
|
|
|
+
|
|
|
+ // 配置
|
|
|
+ public bool IsEnabled { get; set; } = false;
|
|
|
+ public bool NotifyOnAlarm { get; set; } = true;
|
|
|
+ public bool NotifyOnEscalation { get; set; } = true;
|
|
|
+ public bool NotifyOnPlcDisconnect { get; set; } = true;
|
|
|
+
|
|
|
+ private NotificationService()
|
|
|
+ {
|
|
|
+ _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 添加通知渠道
|
|
|
+ /// </summary>
|
|
|
+ public void AddChannel(INotificationChannel channel)
|
|
|
+ {
|
|
|
+ _channels.Add(channel);
|
|
|
+ Log.Information("已添加通知渠道: {ChannelName}", channel.Name);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 移除通知渠道
|
|
|
+ /// </summary>
|
|
|
+ public void RemoveChannel(string channelName)
|
|
|
+ {
|
|
|
+ _channels.RemoveAll(c => c.Name == channelName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 发送通知(带限流)
|
|
|
+ /// </summary>
|
|
|
+ public async Task SendNotificationAsync(string key, string title, string message, NotificationLevel level)
|
|
|
+ {
|
|
|
+ if (!IsEnabled) return;
|
|
|
+
|
|
|
+ // 限流检查
|
|
|
+ if (!CheckThrottle(key))
|
|
|
+ {
|
|
|
+ Log.Debug("通知被限流跳过: {Key}", key);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并行发送到所有渠道
|
|
|
+ var tasks = _channels.Select(c => SendToChannelAsync(c, title, message, level));
|
|
|
+ await Task.WhenAll(tasks);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 发送报警通知
|
|
|
+ /// </summary>
|
|
|
+ public async Task SendAlarmNotificationAsync(Models.AlarmRecord alarm)
|
|
|
+ {
|
|
|
+ if (!IsEnabled || !NotifyOnAlarm) return;
|
|
|
+
|
|
|
+ var level = alarm.AlarmLevel switch
|
|
|
+ {
|
|
|
+ 1 => NotificationLevel.Info,
|
|
|
+ 2 => NotificationLevel.Warning,
|
|
|
+ 3 => NotificationLevel.Error,
|
|
|
+ _ => NotificationLevel.Critical
|
|
|
+ };
|
|
|
+
|
|
|
+ var key = $"alarm_{alarm.AlarmType}_{alarm.AlarmMessage}";
|
|
|
+ var title = $"[报警] {alarm.AlarmType}";
|
|
|
+ await SendNotificationAsync(key, title, alarm.AlarmMessage, level);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 发送报警升级通知
|
|
|
+ /// </summary>
|
|
|
+ public async Task SendEscalationNotificationAsync(Models.AlarmRecord alarm)
|
|
|
+ {
|
|
|
+ if (!IsEnabled || !NotifyOnEscalation) return;
|
|
|
+
|
|
|
+ var key = $"escalation_{alarm.AlarmType}_{alarm.AlarmMessage}";
|
|
|
+ var title = $"[报警升级] {alarm.AlarmType}";
|
|
|
+ await SendNotificationAsync(key, title, alarm.AlarmMessage, NotificationLevel.Critical);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 发送 PLC 连接状态变化通知
|
|
|
+ /// </summary>
|
|
|
+ public async Task SendPlcConnectionNotificationAsync(bool isConnected)
|
|
|
+ {
|
|
|
+ if (!IsEnabled || !NotifyOnPlcDisconnect) return;
|
|
|
+
|
|
|
+ var key = $"plc_connection_{(isConnected ? "restored" : "lost")}";
|
|
|
+ if (isConnected)
|
|
|
+ {
|
|
|
+ await SendNotificationAsync(key, "[PLC] 连接已恢复", "PLC 通信已恢复正常", NotificationLevel.Info);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ await SendNotificationAsync(key, "[PLC] 连接断开", "PLC 通信中断,请检查网络连接", NotificationLevel.Error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 检查限流(同一 key 在 ThrottleSeconds 内只发送一次)
|
|
|
+ /// </summary>
|
|
|
+ private bool CheckThrottle(string key)
|
|
|
+ {
|
|
|
+ lock (_throttleLock)
|
|
|
+ {
|
|
|
+ var now = DateTime.Now;
|
|
|
+ if (_lastNotificationTimes.TryGetValue(key, out var lastTime))
|
|
|
+ {
|
|
|
+ if ((now - lastTime).TotalSeconds < ThrottleSeconds)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _lastNotificationTimes[key] = now;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 发送到指定渠道
|
|
|
+ /// </summary>
|
|
|
+ private async Task SendToChannelAsync(INotificationChannel channel, string title, string message, NotificationLevel level)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await channel.SendAsync(title, message, level);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Log.Error(ex, "发送通知到 {ChannelName} 失败", channel.Name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void Dispose()
|
|
|
+ {
|
|
|
+ if (_disposed) return;
|
|
|
+ _disposed = true;
|
|
|
+ _httpClient.Dispose();
|
|
|
+ _channels.Clear();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 企业微信 Webhook 通知
|
|
|
+/// </summary>
|
|
|
+public class WeChatWorkNotification : INotificationChannel
|
|
|
+{
|
|
|
+ private readonly HttpClient _httpClient;
|
|
|
+ private readonly string _webhookUrl;
|
|
|
+
|
|
|
+ public string Name => "WeChatWork";
|
|
|
+
|
|
|
+ public WeChatWorkNotification(string webhookUrl, HttpClient httpClient)
|
|
|
+ {
|
|
|
+ _webhookUrl = webhookUrl;
|
|
|
+ _httpClient = httpClient;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task SendAsync(string title, string message, NotificationLevel level)
|
|
|
+ {
|
|
|
+ var emoji = level switch
|
|
|
+ {
|
|
|
+ NotificationLevel.Info => "ℹ️",
|
|
|
+ NotificationLevel.Warning => "⚠️",
|
|
|
+ NotificationLevel.Error => "❌",
|
|
|
+ NotificationLevel.Critical => "🚨",
|
|
|
+ _ => "📢"
|
|
|
+ };
|
|
|
+
|
|
|
+ var content = new
|
|
|
+ {
|
|
|
+ msgtype = "markdown",
|
|
|
+ markdown = new
|
|
|
+ {
|
|
|
+ content = $"## {emoji} {title}\n\n" +
|
|
|
+ $"> 时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n\n" +
|
|
|
+ $"> 内容:{message}\n\n" +
|
|
|
+ $"> 级别:<font color=\"{(level >= NotificationLevel.Error ? "warning" : "info")}\">{level}</font>"
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ var response = await _httpClient.PostAsJsonAsync(_webhookUrl, content);
|
|
|
+ response.EnsureSuccessStatusCode();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 钉钉 Webhook 通知
|
|
|
+/// </summary>
|
|
|
+public class DingTalkNotification : INotificationChannel
|
|
|
+{
|
|
|
+ private readonly HttpClient _httpClient;
|
|
|
+ private readonly string _webhookUrl;
|
|
|
+
|
|
|
+ public string Name => "DingTalk";
|
|
|
+
|
|
|
+ public DingTalkNotification(string webhookUrl, HttpClient httpClient)
|
|
|
+ {
|
|
|
+ _webhookUrl = webhookUrl;
|
|
|
+ _httpClient = httpClient;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task SendAsync(string title, string message, NotificationLevel level)
|
|
|
+ {
|
|
|
+ var emoji = level switch
|
|
|
+ {
|
|
|
+ NotificationLevel.Info => "ℹ️",
|
|
|
+ NotificationLevel.Warning => "⚠️",
|
|
|
+ NotificationLevel.Error => "❌",
|
|
|
+ NotificationLevel.Critical => "🚨",
|
|
|
+ _ => "📢"
|
|
|
+ };
|
|
|
+
|
|
|
+ var content = new
|
|
|
+ {
|
|
|
+ msgtype = "markdown",
|
|
|
+ markdown = new
|
|
|
+ {
|
|
|
+ title = $"{emoji} {title}",
|
|
|
+ text = $"## {emoji} {title}\n\n" +
|
|
|
+ $"> 时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n\n" +
|
|
|
+ $"> 内容:{message}\n\n" +
|
|
|
+ $"> 级别:{level}"
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ var response = await _httpClient.PostAsJsonAsync(_webhookUrl, content);
|
|
|
+ response.EnsureSuccessStatusCode();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 邮件通知(使用 SMTP)
|
|
|
+/// </summary>
|
|
|
+public class EmailNotification : INotificationChannel
|
|
|
+{
|
|
|
+ private readonly string _smtpServer;
|
|
|
+ private readonly int _smtpPort;
|
|
|
+ private readonly string _fromEmail;
|
|
|
+ private readonly string _fromPassword;
|
|
|
+ private readonly string[] _toEmails;
|
|
|
+
|
|
|
+ public string Name => "Email";
|
|
|
+
|
|
|
+ public EmailNotification(string smtpServer, int smtpPort, string fromEmail, string fromPassword, string[] toEmails)
|
|
|
+ {
|
|
|
+ _smtpServer = smtpServer;
|
|
|
+ _smtpPort = smtpPort;
|
|
|
+ _fromEmail = fromEmail;
|
|
|
+ _fromPassword = fromPassword;
|
|
|
+ _toEmails = toEmails;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task SendAsync(string title, string message, NotificationLevel level)
|
|
|
+ {
|
|
|
+ // 简单实现:使用 MailKit 或 System.Net.Mail
|
|
|
+ // 这里只记录日志,实际使用需要引入 MailKit 包
|
|
|
+ Log.Information("发送邮件通知: {Title} -> {Recipients}", title, string.Join(", ", _toEmails));
|
|
|
+
|
|
|
+ // TODO: 实现 SMTP 发送
|
|
|
+ // using var client = new SmtpClient(_smtpServer, _smtpPort);
|
|
|
+ // await client.SendAsync(...)
|
|
|
+
|
|
|
+ await Task.CompletedTask;
|
|
|
+ }
|
|
|
+}
|