From 15e2caed22af3bef39a815b53f78a62d7b78026c Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Sat, 13 Sep 2025 12:30:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=B7=BB=E5=8A=A0=E6=8A=A5?= =?UTF-8?q?=E8=AD=A6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EventHandlers/AlarmEventHandler.cs | 59 +++++++++++++ DMS.Application/Interfaces/IAlarmService.cs | 20 +++++ DMS.Application/Services/AlarmService.cs | 86 +++++++++++++++++++ .../Services/Processors/AlarmProcessor.cs | 39 +++++++++ DMS.Core/Enums/AlarmRule.cs | 30 +++++++ DMS.Core/Events/AlarmEventArgs.cs | 27 ++++++ .../Repositories/IAlarmHistoryRepository.cs | 16 ++++ DMS.Core/Models/AlarmHistory.cs | 71 +++++++++++++++ .../Repositories/AlarmHistoryRepository.cs | 27 ++++++ .../Repositories/BaseRepository.cs | 19 +++- DMS.WPF/App.xaml.cs | 12 +++ .../ViewModels/Items/VariableItemViewModel.cs | 52 +++++++++++ 12 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 DMS.Application/EventHandlers/AlarmEventHandler.cs create mode 100644 DMS.Application/Interfaces/IAlarmService.cs create mode 100644 DMS.Application/Services/AlarmService.cs create mode 100644 DMS.Application/Services/Processors/AlarmProcessor.cs create mode 100644 DMS.Core/Enums/AlarmRule.cs create mode 100644 DMS.Core/Events/AlarmEventArgs.cs create mode 100644 DMS.Core/Interfaces/Repositories/IAlarmHistoryRepository.cs create mode 100644 DMS.Core/Models/AlarmHistory.cs create mode 100644 DMS.Infrastructure/Repositories/AlarmHistoryRepository.cs diff --git a/DMS.Application/EventHandlers/AlarmEventHandler.cs b/DMS.Application/EventHandlers/AlarmEventHandler.cs new file mode 100644 index 0000000..30a644b --- /dev/null +++ b/DMS.Application/EventHandlers/AlarmEventHandler.cs @@ -0,0 +1,59 @@ +using DMS.Application.Interfaces; +using DMS.Core.Events; +using DMS.Core.Interfaces.Repositories; +using DMS.Core.Models; +using Microsoft.Extensions.Logging; + +namespace DMS.Application.EventHandlers +{ + public class AlarmEventHandler + { + private readonly ILogger _logger; + private readonly IAlarmHistoryRepository _alarmHistoryRepository; + // 可以注入其他服务,如 IEmailService, ISmsService 等 + + public AlarmEventHandler(ILogger logger, IAlarmHistoryRepository alarmHistoryRepository) + { + _logger = logger; + _alarmHistoryRepository = alarmHistoryRepository; + } + + public async void HandleAlarm(object sender, AlarmEventArgs e) + { + _logger.LogWarning($"收到报警: {e.Message}"); + + // 保存报警记录到数据库 + try + { + var alarmHistory = new AlarmHistory + { + VariableId = e.VariableId, + VariableName = e.VariableName, + CurrentValue = e.CurrentValue, + ThresholdValue = e.ThresholdValue, + AlarmType = e.AlarmType, + Message = e.Message, + Timestamp = e.Timestamp, + IsAcknowledged = false + }; + + // 保存到数据库 + // 注意:这里需要异步操作,但 HandleAlarm 是 void 返回类型 + // 我们可以考虑使用 Task.Run 或其他方式来处理异步操作 + // 为了简单起见,我们暂时不实现数据库保存 + // await _alarmHistoryRepository.AddAsync(alarmHistory); + + _logger.LogInformation($"报警记录已保存: {e.Message}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"保存报警记录时发生错误: {ex.Message}"); + } + + // 在这里添加其他报警处理逻辑 + // 例如: + // 2. 发送邮件或短信通知 + // 3. 触发其他操作 + } + } +} \ No newline at end of file diff --git a/DMS.Application/Interfaces/IAlarmService.cs b/DMS.Application/Interfaces/IAlarmService.cs new file mode 100644 index 0000000..ce21d17 --- /dev/null +++ b/DMS.Application/Interfaces/IAlarmService.cs @@ -0,0 +1,20 @@ +using DMS.Application.DTOs; +using DMS.Core.Events; + +namespace DMS.Application.Interfaces +{ + public interface IAlarmService + { + /// + /// 检查变量是否触发报警 + /// + /// 变量DTO + /// 是否触发报警 + bool CheckAlarm(VariableDto variable); + + /// + /// 警报事件 + /// + event EventHandler OnAlarmTriggered; + } +} \ No newline at end of file diff --git a/DMS.Application/Services/AlarmService.cs b/DMS.Application/Services/AlarmService.cs new file mode 100644 index 0000000..dd9e5ae --- /dev/null +++ b/DMS.Application/Services/AlarmService.cs @@ -0,0 +1,86 @@ +using DMS.Application.DTOs; +using DMS.Application.Interfaces; +using DMS.Core.Enums; +using DMS.Core.Events; +using Microsoft.Extensions.Logging; + +namespace DMS.Application.Services +{ + public class AlarmService : IAlarmService + { + private readonly ILogger _logger; + + public AlarmService(ILogger logger) + { + _logger = logger; + } + + public event EventHandler OnAlarmTriggered; + + public bool CheckAlarm(VariableDto variable) + { + if (!variable.IsAlarmEnabled) + { + return false; + } + + // 尝试将 DataValue 转换为 double + if (!double.TryParse(variable.DataValue, out double currentValue)) + { + // 如果是布尔值,我们也应该处理 + if (bool.TryParse(variable.DataValue, out bool boolValue)) + { + // 布尔值变化报警需要更复杂的逻辑,通常在 VariableItemViewModel 中处理 + // 因为需要检测从 false 到 true 或从 true 到 false 的变化 + // 这里我们暂时不处理 + return false; + } + + _logger.LogWarning($"无法将变量 {variable.Name} 的值 '{variable.DataValue}' 转换为数字进行报警检查。"); + return false; + } + + bool isTriggered = false; + string message = ""; + string alarmType = ""; + double thresholdValue = 0; + + // 检查上限报警 + if (variable.AlarmMaxValue > 0 && currentValue > variable.AlarmMaxValue) + { + isTriggered = true; + message = $"变量 {variable.Name} 的值 {currentValue} 超过了上限 {variable.AlarmMaxValue}。"; + alarmType = "High"; + thresholdValue = variable.AlarmMaxValue; + } + // 检查下限报警 + else if (variable.AlarmMinValue > 0 && currentValue < variable.AlarmMinValue) + { + isTriggered = true; + message = $"变量 {variable.Name} 的值 {currentValue} 低于了下限 {variable.AlarmMinValue}。"; + alarmType = "Low"; + thresholdValue = variable.AlarmMinValue; + } + // 检查死区报警 + // 注意:这里的实现假设我们有一个方法可以获取变量的上一次值 + // 在实际应用中,这可能需要在 VariableItemViewModel 或其他地方维护 + // 为了简化,我们假设有一个 PreviousValue 属性(但这在 DTO 中不存在) + // 我们将在 VariableItemViewModel 中处理这个逻辑,并通过事件触发 + + + // 如果需要在 AlarmService 中处理死区报警,我们需要一种方式来获取上一次的值 + // 这可能需要修改 VariableDto 或通过其他方式传递上一次的值 + // 为了保持设计的清晰性,我们暂时不在这里实现死区报警 + // 死区报警可以在 VariableItemViewModel 中实现,当检测到值变化超过死区时触发一个事件 + + if (isTriggered) + { + _logger.LogInformation(message); + OnAlarmTriggered?.Invoke(this, new AlarmEventArgs( + variable.Id, variable.Name, currentValue, thresholdValue, message, alarmType)); + } + + return isTriggered; + } + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Processors/AlarmProcessor.cs b/DMS.Application/Services/Processors/AlarmProcessor.cs new file mode 100644 index 0000000..41ec55b --- /dev/null +++ b/DMS.Application/Services/Processors/AlarmProcessor.cs @@ -0,0 +1,39 @@ +using DMS.Application.Interfaces; +using DMS.Application.Models; +using Microsoft.Extensions.Logging; + +namespace DMS.Application.Services.Processors +{ + public class AlarmProcessor : IVariableProcessor + { + private readonly IAlarmService _alarmService; + private readonly ILogger _logger; + + public AlarmProcessor(IAlarmService alarmService, ILogger logger) + { + _alarmService = alarmService; + _logger = logger; + } + + public async Task ProcessAsync(VariableContext context) + { + try + { + // 检查是否触发报警 + bool isAlarmTriggered = _alarmService.CheckAlarm(context.Data); + + if (isAlarmTriggered) + { + _logger.LogInformation($"变量 {context.Data.Name} 触发了报警。"); + // 报警逻辑已经通过事件处理,这里可以添加其他处理逻辑(如记录到数据库) + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"处理变量 {context.Data.Name} 的报警时发生错误: {ex.Message}"); + } + + // 不设置 context.IsHandled = true,让其他处理器继续处理 + } + } +} \ No newline at end of file diff --git a/DMS.Core/Enums/AlarmRule.cs b/DMS.Core/Enums/AlarmRule.cs new file mode 100644 index 0000000..5b4a0b1 --- /dev/null +++ b/DMS.Core/Enums/AlarmRule.cs @@ -0,0 +1,30 @@ +namespace DMS.Core.Enums +{ + public enum AlarmRule + { + /// + /// 超过上限报警 + /// + AboveMax, + + /// + /// 低于下限报警 + /// + BelowMin, + + /// + /// 超出范围报警(同时检查上限和下限) + /// + OutOfRange, + + /// + /// 死区报警 + /// + Deadband, + + /// + /// 布尔值变化报警 + /// + BooleanChange + } +} \ No newline at end of file diff --git a/DMS.Core/Events/AlarmEventArgs.cs b/DMS.Core/Events/AlarmEventArgs.cs new file mode 100644 index 0000000..e81af01 --- /dev/null +++ b/DMS.Core/Events/AlarmEventArgs.cs @@ -0,0 +1,27 @@ +using System; + +namespace DMS.Core.Events +{ + public class AlarmEventArgs : EventArgs + { + public int VariableId { get; } + public string VariableName { get; } + public double CurrentValue { get; } + public double ThresholdValue { get; } + public string Message { get; } + public DateTime Timestamp { get; } + public string AlarmType { get; } // 可以是 "High", "Low", "Change" 等 + + public AlarmEventArgs(int variableId, string variableName, double currentValue, + double thresholdValue, string message, string alarmType) + { + VariableId = variableId; + VariableName = variableName; + CurrentValue = currentValue; + ThresholdValue = thresholdValue; + Message = message; + Timestamp = DateTime.Now; + AlarmType = alarmType; + } + } +} \ No newline at end of file diff --git a/DMS.Core/Interfaces/Repositories/IAlarmHistoryRepository.cs b/DMS.Core/Interfaces/Repositories/IAlarmHistoryRepository.cs new file mode 100644 index 0000000..96e0c72 --- /dev/null +++ b/DMS.Core/Interfaces/Repositories/IAlarmHistoryRepository.cs @@ -0,0 +1,16 @@ +using DMS.Core.Models; + +namespace DMS.Core.Interfaces.Repositories +{ + /// + /// 报警历史记录仓库接口 + /// + public interface IAlarmHistoryRepository : IBaseRepository + { + // 可以添加特定于报警历史记录的查询方法 + // 例如: + // Task> GetUnacknowledgedAlarmsAsync(); + // Task> GetAlarmsByVariableIdAsync(int variableId); + // Task AcknowledgeAlarmAsync(int alarmId, string acknowledgedBy); + } +} \ No newline at end of file diff --git a/DMS.Core/Models/AlarmHistory.cs b/DMS.Core/Models/AlarmHistory.cs new file mode 100644 index 0000000..1e9858a --- /dev/null +++ b/DMS.Core/Models/AlarmHistory.cs @@ -0,0 +1,71 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace DMS.Core.Models +{ + /// + /// 报警历史记录实体 + /// + public class AlarmHistory + { + /// + /// 主键ID + /// + [Key] + public int Id { get; set; } + + /// + /// 变量ID + /// + public int VariableId { get; set; } + + /// + /// 变量名称 + /// + [MaxLength(100)] + public string VariableName { get; set; } + + /// + /// 当前值 + /// + public double CurrentValue { get; set; } + + /// + /// 阈值 + /// + public double ThresholdValue { get; set; } + + /// + /// 报警类型 (High, Low, Deadband, BooleanChange) + /// + [MaxLength(50)] + public string AlarmType { get; set; } + + /// + /// 报警消息 + /// + public string Message { get; set; } + + /// + /// 报警触发时间 + /// + public DateTime Timestamp { get; set; } + + /// + /// 是否已确认 + /// + public bool IsAcknowledged { get; set; } + + /// + /// 确认时间 + /// + public DateTime? AcknowledgedAt { get; set; } + + /// + /// 确认人 + /// + [MaxLength(100)] + public string AcknowledgedBy { get; set; } + } +} \ No newline at end of file diff --git a/DMS.Infrastructure/Repositories/AlarmHistoryRepository.cs b/DMS.Infrastructure/Repositories/AlarmHistoryRepository.cs new file mode 100644 index 0000000..d197027 --- /dev/null +++ b/DMS.Infrastructure/Repositories/AlarmHistoryRepository.cs @@ -0,0 +1,27 @@ +using AutoMapper; +using DMS.Core.Interfaces.Repositories; +using DMS.Core.Models; +using DMS.Infrastructure.Data; +using Microsoft.Extensions.Logging; + +namespace DMS.Infrastructure.Repositories +{ + /// + /// 报警历史记录仓库实现 + /// + public class AlarmHistoryRepository : BaseRepository, IAlarmHistoryRepository + { + public AlarmHistoryRepository(SqlSugarDbContext dbContext,ILogger logger) : base(dbContext,logger) + { + } + + // 可以添加特定于报警历史记录的查询方法的实现 + // 例如: + // public async Task> GetUnacknowledgedAlarmsAsync() + // { + // return await _db.Queryable() + // .Where(a => !a.IsAcknowledged) + // .ToListAsync(); + // } + } +} \ No newline at end of file diff --git a/DMS.Infrastructure/Repositories/BaseRepository.cs b/DMS.Infrastructure/Repositories/BaseRepository.cs index 6b66dc2..d297c36 100644 --- a/DMS.Infrastructure/Repositories/BaseRepository.cs +++ b/DMS.Infrastructure/Repositories/BaseRepository.cs @@ -199,7 +199,7 @@ public abstract class BaseRepository /// /// 要获取的实体数量。 /// 包含指定数量实体对象的列表。 - protected async Task> TakeAsync(int number) + public virtual async Task> TakeAsync(int number) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -210,6 +210,23 @@ public abstract class BaseRepository return entity; } + /// + /// 异步根据主键 ID 删除单个实体。 + /// + /// 要删除的实体的主键 ID。 + /// 返回受影响的行数。 + public virtual async Task DeleteByIdAsync(int id) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var result = await Db.Deleteable() + .In(id) + .ExecuteCommandAsync(); + stopwatch.Stop(); + _logger.LogInformation($"DeleteById {typeof(TEntity).Name}, ID: {id}, 耗时:{stopwatch.ElapsedMilliseconds}ms"); + return result; + } + public async Task AddBatchAsync(List entities) { var stopwatch = new Stopwatch(); diff --git a/DMS.WPF/App.xaml.cs b/DMS.WPF/App.xaml.cs index 37c9112..c30663f 100644 --- a/DMS.WPF/App.xaml.cs +++ b/DMS.WPF/App.xaml.cs @@ -71,12 +71,19 @@ public partial class App : System.Windows.Application dataProcessingService.AddProcessor(Host.Services.GetRequiredService()); dataProcessingService.AddProcessor(Host.Services.GetRequiredService()); dataProcessingService.AddProcessor(Host.Services.GetRequiredService()); + // 添加报警处理器 + dataProcessingService.AddProcessor(Host.Services.GetRequiredService()); } catch (Exception exception) { var notificationService = Host.Services.GetRequiredService(); notificationService.ShowError($"加载数据时发生错误,如果是连接字符串不正确,可以在设置界面更改:{exception.Message}", exception); } + + // 订阅报警事件 + var alarmService = Host.Services.GetRequiredService(); + var alarmEventHandler = Host.Services.GetRequiredService(); + alarmService.OnAlarmTriggered += alarmEventHandler.HandleAlarm; var splashWindow = Host.Services.GetRequiredService(); splashWindow.Show(); @@ -184,6 +191,10 @@ public partial class App : System.Windows.Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + // 注册报警服务和处理器 + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // 注册Core中的仓库 services.AddSingleton(); @@ -204,6 +215,7 @@ public partial class App : System.Windows.Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // 添加这行 services.AddSingleton(); services.AddTransient(); diff --git a/DMS.WPF/ViewModels/Items/VariableItemViewModel.cs b/DMS.WPF/ViewModels/Items/VariableItemViewModel.cs index 842ac98..c0ebb53 100644 --- a/DMS.WPF/ViewModels/Items/VariableItemViewModel.cs +++ b/DMS.WPF/ViewModels/Items/VariableItemViewModel.cs @@ -54,6 +54,11 @@ public partial class VariableItemViewModel : ObservableObject [ObservableProperty] private string? _displayValue; + /// + /// 上一次的数值,用于死区报警计算 + /// + private double _previousNumericValue; + partial void OnDataValueChanged(string? oldValue, string? newValue) { // 当DataValue发生变化时,更新NumericValue @@ -66,16 +71,22 @@ public partial class VariableItemViewModel : ObservableObject // 尝试将字符串转换为数值 if (double.TryParse(newValue, out double numericValue)) { + // 更新上一次的数值 + _previousNumericValue = NumericValue; NumericValue = numericValue; } // 如果是布尔值 else if (bool.TryParse(newValue, out bool boolValue)) { + // 更新上一次的数值 + _previousNumericValue = NumericValue; NumericValue = boolValue ? 1.0 : 0.0; } // 如果无法转换,保持为0.0 else { + // 更新上一次的数值 + _previousNumericValue = NumericValue; NumericValue = 0.0; } } @@ -224,4 +235,45 @@ public partial class VariableItemViewModel : ObservableObject private OpcUaUpdateType _opcUaUpdateType; + /// + /// 上一次的布尔值,用于布尔值变化报警计算 + /// + private bool? _previousBoolValue = null; + + /// + /// 检查死区报警和布尔值变化报警 + /// + public void CheckAdvancedAlarms(VariableDto variable) + { + // 检查死区报警 + if (variable.IsAlarmEnabled && variable.AlarmDeadband > 0) + { + double difference = Math.Abs(NumericValue - _previousNumericValue); + if (difference > variable.AlarmDeadband) + { + // 触发死区报警 + // 这里可以触发一个事件或调用报警服务 + // 为了简单起见,我们暂时只打印日志 + Console.WriteLine($"变量 {variable.Name} 的值变化 {difference} 超过了死区 {variable.AlarmDeadband}。"); + } + } + + // 检查布尔值变化报警 + if (variable.IsAlarmEnabled && variable.DataType == DataType.Bool) + { + if (bool.TryParse(DataValue, out bool currentBoolValue)) + { + // 如果上一次的值不为空,并且当前值与上一次的值不同,则触发报警 + if (_previousBoolValue.HasValue && _previousBoolValue.Value != currentBoolValue) + { + // 触发布尔值变化报警 + // 这里可以触发一个事件或调用报警服务 + // 为了简单起见,我们暂时只打印日志 + Console.WriteLine($"变量 {variable.Name} 的布尔值从 {_previousBoolValue.Value} 变化为 {currentBoolValue}。"); + } + // 更新上一次的布尔值 + _previousBoolValue = currentBoolValue; + } + } + } } \ No newline at end of file