From a079cf8de82ae0ecddd5561d742a658628c9aabd Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Sun, 14 Sep 2025 16:16:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=83=E9=97=AE=E5=86=99=E5=AE=8C=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E5=99=A8=E5=8A=9F=E8=83=BD=EF=BC=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=9C=AA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTOs/Triggers/TriggerDefinitionDto.cs | 87 +++++++ .../Triggers/ITriggerActionExecutor.cs | 17 ++ .../Triggers/ITriggerEvaluationService.cs | 19 ++ .../Triggers/ITriggerManagementService.cs | 55 +++++ .../Triggers/Impl/TriggerActionExecutor.cs | 133 +++++++++++ .../Triggers/Impl/TriggerEvaluationService.cs | 168 ++++++++++++++ .../Triggers/Impl/TriggerManagementService.cs | 132 +++++++++++ .../Services/Triggers/TriggerContext.cs | 14 ++ DMS.Core/Interfaces/IRepositoryManager.cs | 6 + .../Triggers/ITriggerRepository.cs | 54 +++++ DMS.Core/Models/Triggers/TriggerDefinition.cs | 110 +++++++++ .../Repositories/RepositoryManager.cs | 11 +- .../Impl/SqlSugarTriggerRepository.cs | 75 ++++++ DMS.WPF/App.xaml | 25 ++ DMS.WPF/App.xaml.cs | 15 +- .../Converters/EnumToVisibilityConverter.cs | 29 +++ .../NullableTimeSpanToSecondsConverter.cs | 34 +++ DMS.WPF/Services/DataEventService.cs | 34 ++- .../Triggers/TriggerEditorViewModel.cs | 214 ++++++++++++++++++ .../ViewModels/Triggers/TriggersViewModel.cs | 194 ++++++++++++++++ DMS.WPF/Views/Triggers/TriggerEditorView.xaml | 183 +++++++++++++++ .../Views/Triggers/TriggerEditorView.xaml.cs | 15 ++ DMS.WPF/Views/Triggers/TriggersView.xaml | 51 +++++ DMS.WPF/Views/Triggers/TriggersView.xaml.cs | 15 ++ 24 files changed, 1684 insertions(+), 6 deletions(-) create mode 100644 DMS.Application/DTOs/Triggers/TriggerDefinitionDto.cs create mode 100644 DMS.Application/Services/Triggers/ITriggerActionExecutor.cs create mode 100644 DMS.Application/Services/Triggers/ITriggerEvaluationService.cs create mode 100644 DMS.Application/Services/Triggers/ITriggerManagementService.cs create mode 100644 DMS.Application/Services/Triggers/Impl/TriggerActionExecutor.cs create mode 100644 DMS.Application/Services/Triggers/Impl/TriggerEvaluationService.cs create mode 100644 DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs create mode 100644 DMS.Application/Services/Triggers/TriggerContext.cs create mode 100644 DMS.Core/Interfaces/Repositories/Triggers/ITriggerRepository.cs create mode 100644 DMS.Core/Models/Triggers/TriggerDefinition.cs create mode 100644 DMS.Infrastructure/Repositories/Triggers/Impl/SqlSugarTriggerRepository.cs create mode 100644 DMS.WPF/Converters/EnumToVisibilityConverter.cs create mode 100644 DMS.WPF/Converters/NullableTimeSpanToSecondsConverter.cs create mode 100644 DMS.WPF/ViewModels/Triggers/TriggerEditorViewModel.cs create mode 100644 DMS.WPF/ViewModels/Triggers/TriggersViewModel.cs create mode 100644 DMS.WPF/Views/Triggers/TriggerEditorView.xaml create mode 100644 DMS.WPF/Views/Triggers/TriggerEditorView.xaml.cs create mode 100644 DMS.WPF/Views/Triggers/TriggersView.xaml create mode 100644 DMS.WPF/Views/Triggers/TriggersView.xaml.cs diff --git a/DMS.Application/DTOs/Triggers/TriggerDefinitionDto.cs b/DMS.Application/DTOs/Triggers/TriggerDefinitionDto.cs new file mode 100644 index 0000000..2d56723 --- /dev/null +++ b/DMS.Application/DTOs/Triggers/TriggerDefinitionDto.cs @@ -0,0 +1,87 @@ +using System; +using DMS.Core.Models.Triggers; // 引入枚举 + +namespace DMS.Application.DTOs.Triggers +{ + /// + /// 触发器定义 DTO (用于应用层与表示层之间的数据传输) + /// + public class TriggerDefinitionDto + { + /// + /// 触发器唯一标识符 + /// + public Guid Id { get; set; } + + /// + /// 关联的变量 ID + /// + public Guid VariableId { get; set; } + + /// + /// 触发器是否处于激活状态 + /// + public bool IsActive { get; set; } + + // --- 条件部分 --- + + /// + /// 触发条件类型 + /// + public ConditionType Condition { get; set; } + + /// + /// 阈值 (用于 GreaterThan, LessThan, EqualTo, NotEqualTo) + /// + public double? Threshold { get; set; } + + /// + /// 下限 (用于 InRange, OutOfRange) + /// + public double? LowerBound { get; set; } + + /// + /// 上限 (用于 InRange, OutOfRange) + /// + public double? UpperBound { get; set; } + + // --- 动作部分 --- + + /// + /// 动作类型 + /// + public ActionType Action { get; set; } + + /// + /// 动作配置 JSON 字符串 + /// + public string ActionConfigurationJson { get; set; } + + // --- 抑制与状态部分 --- + + /// + /// 抑制持续时间 + /// + public TimeSpan? SuppressionDuration { get; set; } + + /// + /// 上次触发的时间 + /// + public DateTime? LastTriggeredAt { get; set; } + + /// + /// 触发器描述 + /// + public string Description { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 最后更新时间 + /// + public DateTime UpdatedAt { get; set; } + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Triggers/ITriggerActionExecutor.cs b/DMS.Application/Services/Triggers/ITriggerActionExecutor.cs new file mode 100644 index 0000000..1e022d0 --- /dev/null +++ b/DMS.Application/Services/Triggers/ITriggerActionExecutor.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace DMS.Application.Services.Triggers +{ + /// + /// 触发器动作执行器接口 (负责执行具体的触发动作) + /// + public interface ITriggerActionExecutor + { + /// + /// 执行触发动作 + /// + /// 触发上下文 + /// 任务 + Task ExecuteActionAsync(TriggerContext context); + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Triggers/ITriggerEvaluationService.cs b/DMS.Application/Services/Triggers/ITriggerEvaluationService.cs new file mode 100644 index 0000000..556aeba --- /dev/null +++ b/DMS.Application/Services/Triggers/ITriggerEvaluationService.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace DMS.Application.Services.Triggers +{ + /// + /// 触发器评估服务接口 (负责判断变量值是否满足触发条件) + /// + public interface ITriggerEvaluationService + { + /// + /// 评估与指定变量关联的所有激活状态的触发器 + /// + /// 变量 ID + /// 变量的当前值 + /// 任务 + Task EvaluateTriggersAsync(Guid variableId, object currentValue); + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Triggers/ITriggerManagementService.cs b/DMS.Application/Services/Triggers/ITriggerManagementService.cs new file mode 100644 index 0000000..e76b925 --- /dev/null +++ b/DMS.Application/Services/Triggers/ITriggerManagementService.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DMS.Application.DTOs.Triggers; + +namespace DMS.Application.Services.Triggers +{ + /// + /// 触发器管理服务接口 (负责 CRUD 操作) + /// + public interface ITriggerManagementService + { + /// + /// 获取所有触发器定义 + /// + /// 触发器定义列表 + Task> GetAllTriggersAsync(); + + /// + /// 根据 ID 获取触发器定义 + /// + /// 触发器 ID + /// 触发器定义 DTO,如果未找到则返回 null + Task GetTriggerByIdAsync(Guid id); + + /// + /// 创建一个新的触发器定义 + /// + /// 要创建的触发器定义 DTO + /// 创建成功的触发器定义 DTO + Task CreateTriggerAsync(TriggerDefinitionDto triggerDto); + + /// + /// 更新一个已存在的触发器定义 + /// + /// 要更新的触发器 ID + /// 包含更新信息的触发器定义 DTO + /// 更新后的触发器定义 DTO,如果未找到则返回 null + Task UpdateTriggerAsync(Guid id, TriggerDefinitionDto triggerDto); + + /// + /// 删除一个触发器定义 + /// + /// 要删除的触发器 ID + /// 删除成功返回 true,否则返回 false + Task DeleteTriggerAsync(Guid id); + + /// + /// 获取与指定变量关联的所有触发器定义 + /// + /// 变量 ID + /// 该变量关联的触发器定义列表 + Task> GetTriggersForVariableAsync(Guid variableId); + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Triggers/Impl/TriggerActionExecutor.cs b/DMS.Application/Services/Triggers/Impl/TriggerActionExecutor.cs new file mode 100644 index 0000000..4231ad2 --- /dev/null +++ b/DMS.Application/Services/Triggers/Impl/TriggerActionExecutor.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using DMS.Application.DTOs.Triggers; +using DMS.Application.Services.Triggers; +using Microsoft.Extensions.Logging; // 使用标准日志接口 + +namespace DMS.Application.Services.Triggers.Impl +{ + /// + /// 触发器动作执行器实现 + /// + public class TriggerActionExecutor : ITriggerActionExecutor + { + // 假设这些服务将在未来实现或通过依赖注入提供 + // 目前我们将它们设为 null,并在使用时进行空检查 + private readonly IEmailService _emailService; // 假设已在项目中存在或将来实现 + private readonly ILogger _logger; // 使用标准日志接口 + + public TriggerActionExecutor( + // 可以选择性地注入这些服务,如果暂时不需要可以留空或使用占位符 + IEmailService emailService, + ILogger logger) + { + _emailService = emailService; // 可能为 null + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 执行触发动作 + /// + public async Task ExecuteActionAsync(TriggerContext context) + { + try + { + switch (context.Trigger.Action) + { + case ActionType.SendEmail: + await ExecuteSendEmail(context); + break; + case ActionType.ActivateAlarm: + _logger.LogWarning("Action 'ActivateAlarm' is not implemented yet."); + // await ExecuteActivateAlarm(context); + break; + case ActionType.WriteToLog: + _logger.LogWarning("Action 'WriteToLog' is not implemented yet."); + // await ExecuteWriteToLog(context); + break; + default: + _logger.LogWarning("Unsupported action type: {ActionType}", context.Trigger.Action); + break; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing action '{ActionType}' for trigger '{TriggerId}'.", context.Trigger.Action, context.Trigger.Id); + // 可以选择是否重新抛出异常或静默处理 + // throw; + } + } + + #region 私有执行方法 + + private async Task ExecuteSendEmail(TriggerContext context) + { + if (_emailService == null) + { + _logger.LogWarning("Email service is not configured, skipping SendEmail action for trigger '{TriggerId}'.", context.Trigger.Id); + return; + } + + var config = JsonSerializer.Deserialize>(context.Trigger.ActionConfigurationJson); + if (config == null || + !config.TryGetValue("Recipients", out var recipientsElement) || + !config.TryGetValue("SubjectTemplate", out var subjectTemplateElement) || + !config.TryGetValue("BodyTemplate", out var bodyTemplateElement)) + { + _logger.LogError("Invalid configuration for SendEmail action for trigger '{TriggerId}'.", context.Trigger.Id); + return; + } + + var recipients = recipientsElement.Deserialize>(); + var subjectTemplate = subjectTemplateElement.GetString(); + var bodyTemplate = bodyTemplateElement.GetString(); + + if (recipients == null || string.IsNullOrEmpty(subjectTemplate) || string.IsNullOrEmpty(bodyTemplate)) + { + _logger.LogError("Missing required fields in SendEmail configuration for trigger '{TriggerId}'.", context.Trigger.Id); + return; + } + + // Simple token replacement - in practice, use a templating engine like Scriban, RazorLight etc. + // Note: This assumes context.Variable and context.CurrentValue have Name properties/values. + // You might need to adjust the token names and values based on your actual VariableDto structure. + var subject = subjectTemplate + .Replace("{VariableName}", context.Variable?.Name ?? "Unknown") + .Replace("{CurrentValue}", context.CurrentValue?.ToString() ?? "N/A") + .Replace("{Threshold}", context.Trigger.Threshold?.ToString() ?? "N/A") + .Replace("{LowerBound}", context.Trigger.LowerBound?.ToString() ?? "N/A") + .Replace("{UpperBound}", context.Trigger.UpperBound?.ToString() ?? "N/A"); + + var body = bodyTemplate + .Replace("{VariableName}", context.Variable?.Name ?? "Unknown") + .Replace("{CurrentValue}", context.CurrentValue?.ToString() ?? "N/A") + .Replace("{Threshold}", context.Trigger.Threshold?.ToString() ?? "N/A") + .Replace("{LowerBound}", context.Trigger.LowerBound?.ToString() ?? "N/A") + .Replace("{UpperBound}", context.Trigger.UpperBound?.ToString() ?? "N/A") + .Replace("{Timestamp}", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")); + + await _emailService.SendEmailAsync(recipients, subject, body); + } + + /* + private async Task ExecuteActivateAlarm(TriggerContext context) + { + var alarmId = $"trigger_{context.Trigger.Id}_{context.Variable.Id}"; + var message = $"Trigger '{context.Trigger.Description}' activated for variable '{context.Variable.Name}' with value '{context.CurrentValue}'."; + // 假设 INotificationService 有 RaiseAlarmAsync 方法 + await _notificationService.RaiseAlarmAsync(alarmId, message); + } + + private async Task ExecuteWriteToLog(TriggerContext context) + { + var message = $"Trigger '{context.Trigger.Description}' activated for variable '{context.Variable.Name}' with value '{context.CurrentValue}'."; + // 假设 ILoggingService 有 LogTriggerAsync 方法 + await _loggingService.LogTriggerAsync(message); + } + */ + + #endregion + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Triggers/Impl/TriggerEvaluationService.cs b/DMS.Application/Services/Triggers/Impl/TriggerEvaluationService.cs new file mode 100644 index 0000000..ece17b6 --- /dev/null +++ b/DMS.Application/Services/Triggers/Impl/TriggerEvaluationService.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +// 明确指定 Timer 类型,避免歧义 +using ThreadingTimer = System.Threading.Timer; +using TimersTimer = System.Timers.Timer; +using DMS.Application.DTOs.Triggers; +using DMS.Application.Services.Triggers; +using Microsoft.Extensions.Logging; // 使用 Microsoft.Extensions.Logging.ILogger + +namespace DMS.Application.Services.Triggers.Impl +{ + /// + /// 触发器评估服务实现 + /// + public class TriggerEvaluationService : ITriggerEvaluationService, IDisposable + { + private readonly ITriggerManagementService _triggerManagementService; + // 移除了 IVariableAppService 依赖 + private readonly ITriggerActionExecutor _actionExecutor; + private readonly ILogger _logger; // 使用标准日志接口 + // 为每个触发器存储抑制定时器 + private readonly ConcurrentDictionary _suppressionTimers = new(); + + public TriggerEvaluationService( + ITriggerManagementService triggerManagementService, + // IVariableAppService variableAppService, // 移除此参数 + ITriggerActionExecutor actionExecutor, + ILogger logger) // 使用标准日志接口 + { + _triggerManagementService = triggerManagementService ?? throw new ArgumentNullException(nameof(triggerManagementService)); + // _variableAppService = variableAppService ?? throw new ArgumentNullException(nameof(variableAppService)); + _actionExecutor = actionExecutor ?? throw new ArgumentNullException(nameof(actionExecutor)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 评估与指定变量关联的所有激活状态的触发器 + /// + public async Task EvaluateTriggersAsync(Guid variableId, object currentValue) + { + try + { + var triggers = await _triggerManagementService.GetTriggersForVariableAsync(variableId); + // 注意:这里不再通过 _variableAppService 获取 VariableDto, + // 而是在调用 ExecuteActionAsync 时,由上层(DataEventService)提供。 + // 如果需要 VariableDto 信息,可以在 ExecuteActionAsync 的 TriggerContext 中携带。 + + _logger.LogDebug($"Evaluating {triggers.Count(t => t.IsActive)} active triggers for variable ID: {variableId}"); + + foreach (var trigger in triggers.Where(t => t.IsActive)) + { + if (!IsWithinSuppressionWindow(trigger)) // Check suppression first + { + if (EvaluateCondition(trigger, currentValue)) + { + // 创建一个临时的上下文对象,其中 VariableDto 可以为 null, + // 因为我们目前没有从 _variableAppService 获取它。 + // 在实际应用中,你可能需要通过某种方式获取 VariableDto。 + var context = new TriggerContext(trigger, currentValue, null); + + await _actionExecutor.ExecuteActionAsync(context); + + // Update last triggered time and start suppression timer if needed + trigger.LastTriggeredAt = DateTime.UtcNow; + // For simplicity, we'll assume it's updated periodically or on next load. + // In a production scenario, you'd likely want to persist this back to the database. + + // Start suppression timer if duration is set (in-memory suppression) + if (trigger.SuppressionDuration.HasValue) + { + // 使用 ThreadingTimer 避免歧义 + var timer = new ThreadingTimer(_ => + { + trigger.LastTriggeredAt = null; // Reset suppression flag after delay + _logger.LogInformation($"Suppression lifted for trigger {trigger.Id}"); + // Note: Modifying 'trigger' directly affects the object in the list returned by GetTriggersForVariableAsync(). + // This works for in-memory state but won't persist changes. Consider updating DB explicitly if needed. + }, null, trigger.SuppressionDuration.Value, Timeout.InfiniteTimeSpan); // Single shot timer + + // Replace any existing timer for this trigger ID + _suppressionTimers.AddOrUpdate(trigger.Id, timer, (key, oldTimer) => { + oldTimer?.Dispose(); + return timer; + }); + } + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while evaluating triggers for variable ID: {VariableId}", variableId); + } + } + + /// + /// 内部方法:评估单个触发器的条件 + /// + private bool EvaluateCondition(TriggerDefinitionDto trigger, object currentValueObj) + { + if (currentValueObj == null) + { + _logger.LogWarning("Cannot evaluate trigger condition: Current value is null for trigger ID: {TriggerId}", trigger.Id); + return false; // Cannot evaluate null + } + + // Attempt conversion from object to double - adjust parsing logic as needed for your data types + if (!double.TryParse(currentValueObj.ToString(), out double currentValue)) + { + _logger.LogWarning("Could not parse current value '{CurrentValue}' to double for trigger evaluation (trigger ID: {TriggerId}).", currentValueObj, trigger.Id); + return false; + } + + bool result = trigger.Condition switch + { + ConditionType.GreaterThan => currentValue > trigger.Threshold, + ConditionType.LessThan => currentValue < trigger.Threshold, + ConditionType.EqualTo => Math.Abs(currentValue - trigger.Threshold.GetValueOrDefault()) < double.Epsilon, + ConditionType.NotEqualTo => Math.Abs(currentValue - trigger.Threshold.GetValueOrDefault()) >= double.Epsilon, + ConditionType.InRange => currentValue >= trigger.LowerBound && currentValue <= trigger.UpperBound, + ConditionType.OutOfRange => currentValue < trigger.LowerBound || currentValue > trigger.UpperBound, + _ => false + }; + + if(result) + { + _logger.LogInformation("Trigger condition met: Variable value {CurrentValue} satisfies {Condition} for trigger ID: {TriggerId}", + currentValue, trigger.Condition, trigger.Id); + } + + return result; + } + + /// + /// 内部方法:检查触发器是否处于抑制窗口期内 + /// + private bool IsWithinSuppressionWindow(TriggerDefinitionDto trigger) + { + if (!trigger.SuppressionDuration.HasValue || !trigger.LastTriggeredAt.HasValue) + return false; + + var suppressionEndTime = trigger.LastTriggeredAt.Value.Add(trigger.SuppressionDuration.Value); + bool isSuppressed = DateTime.UtcNow < suppressionEndTime; + + if(isSuppressed) + { + _logger.LogTrace("Trigger is suppressed (until {SuppressionEnd}) for trigger ID: {TriggerId}", suppressionEndTime, trigger.Id); + } + + return isSuppressed; + } + + /// + /// 实现 IDisposable 以清理计时器资源 + /// + public void Dispose() + { + foreach (var kvp in _suppressionTimers) + { + kvp.Value?.Dispose(); + } + _suppressionTimers.Clear(); + } + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs b/DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs new file mode 100644 index 0000000..9f09a27 --- /dev/null +++ b/DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using DMS.Application.DTOs.Triggers; +using DMS.Application.Services.Triggers; +using DMS.Core.Interfaces; +using DMS.Core.Models.Triggers; + +namespace DMS.Application.Services.Triggers.Impl +{ + /// + /// 触发器管理服务实现 + /// + public class TriggerManagementService : ITriggerManagementService + { + private readonly IRepositoryManager _repositoryManager; + private readonly IMapper _mapper; + + public TriggerManagementService(IRepositoryManager repositoryManager, IMapper mapper) + { + _repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + /// + /// 获取所有触发器定义 + /// + public async Task> GetAllTriggersAsync() + { + var triggers = await _repositoryManager.Triggers.GetAllAsync(); + return _mapper.Map>(triggers); + } + + /// + /// 根据 ID 获取触发器定义 + /// + public async Task GetTriggerByIdAsync(Guid id) + { + var trigger = await _repositoryManager.Triggers.GetByIdAsync(id); + return trigger != null ? _mapper.Map(trigger) : null; + } + + /// + /// 创建一个新的触发器定义 + /// + public async Task CreateTriggerAsync(TriggerDefinitionDto triggerDto) + { + // 1. 验证 DTO (可以在应用层或领域层做) + ValidateTriggerDto(triggerDto); + + // 2. 转换 DTO 到实体 + var triggerEntity = _mapper.Map(triggerDto); + triggerEntity.CreatedAt = DateTime.UtcNow; + triggerEntity.UpdatedAt = DateTime.UtcNow; + + // 3. 调用仓储保存实体 + var createdTrigger = await _repositoryManager.Triggers.AddAsync(triggerEntity); + + // 4. 转换回 DTO 并返回 + return _mapper.Map(createdTrigger); + } + + /// + /// 更新一个已存在的触发器定义 + /// + public async Task UpdateTriggerAsync(Guid id, TriggerDefinitionDto triggerDto) + { + // 1. 获取现有实体 + var existingTrigger = await _repositoryManager.Triggers.GetByIdAsync(id); + if (existingTrigger == null) + return null; + + // 2. 验证 DTO + ValidateTriggerDto(triggerDto); + + // 3. 将 DTO 映射到现有实体 (排除不可变字段如 Id, CreatedAt) + _mapper.Map(triggerDto, existingTrigger, opts => opts.Items["IgnoreIdAndCreatedAt"] = true); + existingTrigger.UpdatedAt = DateTime.UtcNow; + + // 4. 调用仓储更新实体 + var updatedTrigger = await _repositoryManager.Triggers.UpdateAsync(existingTrigger); + if (updatedTrigger == null) + return null; + + // 5. 转换回 DTO 并返回 + return _mapper.Map(updatedTrigger); + } + + /// + /// 删除一个触发器定义 + /// + public async Task DeleteTriggerAsync(Guid id) + { + return await _repositoryManager.Triggers.DeleteAsync(id); + } + + /// + /// 获取与指定变量关联的所有触发器定义 + /// + public async Task> GetTriggersForVariableAsync(Guid variableId) + { + var triggers = await _repositoryManager.Triggers.GetByVariableIdAsync(variableId); + return _mapper.Map>(triggers); + } + + /// + /// 内部方法:验证 TriggerDefinitionDto 的有效性 + /// + private void ValidateTriggerDto(TriggerDefinitionDto dto) + { + // 添加必要的验证逻辑 + switch (dto.Condition) + { + case ConditionType.GreaterThan: + case ConditionType.LessThan: + case ConditionType.EqualTo: + case ConditionType.NotEqualTo: + if (!dto.Threshold.HasValue) + throw new ArgumentException($"{dto.Condition} requires Threshold."); + break; + case ConditionType.InRange: + case ConditionType.OutOfRange: + if (!dto.LowerBound.HasValue || !dto.UpperBound.HasValue) + throw new ArgumentException($"{dto.Condition} requires LowerBound and UpperBound."); + if (dto.LowerBound > dto.UpperBound) + throw new ArgumentException("LowerBound must be less than or equal to UpperBound."); + break; + } + } + } +} \ No newline at end of file diff --git a/DMS.Application/Services/Triggers/TriggerContext.cs b/DMS.Application/Services/Triggers/TriggerContext.cs new file mode 100644 index 0000000..667d1f8 --- /dev/null +++ b/DMS.Application/Services/Triggers/TriggerContext.cs @@ -0,0 +1,14 @@ +using System; +using DMS.Application.DTOs; +using DMS.Application.DTOs.Triggers; + +namespace DMS.Application.Services.Triggers +{ + /// + /// 触发上下文,封装了触发时所需的所有信息 + /// + /// 被触发的触发器定义 + /// 触发时变量的当前值 + /// 关联的变量信息 + public record TriggerContext(TriggerDefinitionDto Trigger, object CurrentValue, VariableDto Variable); +} \ No newline at end of file diff --git a/DMS.Core/Interfaces/IRepositoryManager.cs b/DMS.Core/Interfaces/IRepositoryManager.cs index 88979cd..2dd019c 100644 --- a/DMS.Core/Interfaces/IRepositoryManager.cs +++ b/DMS.Core/Interfaces/IRepositoryManager.cs @@ -1,4 +1,5 @@ using DMS.Core.Interfaces.Repositories; +using DMS.Core.Interfaces.Repositories.Triggers; // 引入新的接口命名空间 namespace DMS.Core.Interfaces; @@ -54,6 +55,11 @@ public interface IRepositoryManager : IDisposable /// INlogRepository Nlogs { get; set; } + /// + /// 获取触发器仓储的实例。 + /// + ITriggerRepository Triggers { get; set; } + /// /// 初始化数据库 /// diff --git a/DMS.Core/Interfaces/Repositories/Triggers/ITriggerRepository.cs b/DMS.Core/Interfaces/Repositories/Triggers/ITriggerRepository.cs new file mode 100644 index 0000000..81a3efe --- /dev/null +++ b/DMS.Core/Interfaces/Repositories/Triggers/ITriggerRepository.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DMS.Core.Models.Triggers; + +namespace DMS.Core.Interfaces.Repositories.Triggers +{ + /// + /// 触发器仓储接口 (定义对 TriggerDefinition 实体的数据访问方法) + /// + public interface ITriggerRepository + { + /// + /// 获取所有触发器定义 + /// + /// 触发器定义实体列表 + Task> GetAllAsync(); + + /// + /// 根据 ID 获取触发器定义 + /// + /// 触发器 ID + /// 触发器定义实体,如果未找到则返回 null + Task GetByIdAsync(Guid id); + + /// + /// 添加一个新的触发器定义 + /// + /// 要添加的触发器定义实体 + /// 添加成功的触发器定义实体(通常会填充生成的 ID) + Task AddAsync(TriggerDefinition trigger); + + /// + /// 更新一个已存在的触发器定义 + /// + /// 包含更新信息的触发器定义实体 + /// 更新后的触发器定义实体,如果未找到则返回 null + Task UpdateAsync(TriggerDefinition trigger); + + /// + /// 删除一个触发器定义 + /// + /// 要删除的触发器 ID + /// 删除成功返回 true,否则返回 false + Task DeleteAsync(Guid id); + + /// + /// 获取与指定变量关联的所有触发器定义 + /// + /// 变量 ID + /// 该变量关联的触发器定义实体列表 + Task> GetByVariableIdAsync(Guid variableId); + } +} \ No newline at end of file diff --git a/DMS.Core/Models/Triggers/TriggerDefinition.cs b/DMS.Core/Models/Triggers/TriggerDefinition.cs new file mode 100644 index 0000000..4bc3890 --- /dev/null +++ b/DMS.Core/Models/Triggers/TriggerDefinition.cs @@ -0,0 +1,110 @@ +using System; + +namespace DMS.Core.Models.Triggers +{ + /// + /// 触发器条件类型枚举 + /// + public enum ConditionType + { + GreaterThan, + LessThan, + EqualTo, + NotEqualTo, + InRange, // 值在 LowerBound 和 UpperBound 之间 (包含边界) + OutOfRange // 值低于 LowerBound 或高于 UpperBound + } + + /// + /// 触发器动作类型枚举 + /// + public enum ActionType + { + SendEmail, + ActivateAlarm, + WriteToLog, + // 未来可扩展: ExecuteScript, CallApi, etc. + } + + /// + /// 触发器定义领域模型 + /// + public class TriggerDefinition + { + /// + /// 触发器唯一标识符 + /// + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// 关联的变量 ID + /// + public Guid VariableId { get; set; } + + /// + /// 触发器是否处于激活状态 + /// + public bool IsActive { get; set; } = true; + + // --- 条件部分 --- + + /// + /// 触发条件类型 + /// + public ConditionType Condition { get; set; } + + /// + /// 阈值 (用于 GreaterThan, LessThan, EqualTo, NotEqualTo) + /// + public double? Threshold { get; set; } + + /// + /// 下限 (用于 InRange, OutOfRange) + /// + public double? LowerBound { get; set; } + + /// + /// 上限 (用于 InRange, OutOfRange) + /// + public double? UpperBound { get; set; } + + // --- 动作部分 --- + + /// + /// 动作类型 + /// + public ActionType Action { get; set; } + + /// + /// 动作配置 JSON 字符串,存储特定于动作类型的配置(如邮件收件人列表、模板 ID 等) + /// + public string ActionConfigurationJson { get; set; } = "{}"; + + // --- 抑制与状态部分 --- + + /// + /// 抑制持续时间。如果设置了此值,在触发一次后,在该时间段内不会再触发。 + /// + public TimeSpan? SuppressionDuration { get; set; } + + /// + /// 上次触发的时间。用于抑制逻辑。 + /// + public DateTime? LastTriggeredAt { get; set; } + + /// + /// 触发器描述 + /// + public string Description { get; set; } = ""; + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// 最后更新时间 + /// + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/DMS.Infrastructure/Repositories/RepositoryManager.cs b/DMS.Infrastructure/Repositories/RepositoryManager.cs index f851989..79ec674 100644 --- a/DMS.Infrastructure/Repositories/RepositoryManager.cs +++ b/DMS.Infrastructure/Repositories/RepositoryManager.cs @@ -1,7 +1,9 @@ using AutoMapper; using DMS.Core.Interfaces; using DMS.Core.Interfaces.Repositories; +using DMS.Core.Interfaces.Repositories.Triggers; // 引入新的接口 using DMS.Infrastructure.Data; +using DMS.Infrastructure.Repositories.Triggers; // 引入实现类所在的命名空间 using SqlSugar; namespace DMS.Infrastructure.Repositories; @@ -28,6 +30,7 @@ public class RepositoryManager : IRepositoryManager /// 变量历史仓储实例。 /// 用户仓储实例。 /// Nlog日志仓储实例。 + /// 触发器仓储实例。 public RepositoryManager( SqlSugarDbContext dbContext, IInitializeRepository initializeRepository, IDeviceRepository devices, @@ -38,7 +41,8 @@ public class RepositoryManager : IRepositoryManager IMenuRepository menus, IVariableHistoryRepository variableHistories, IUserRepository users, - INlogRepository nlogs) + INlogRepository nlogs, + ITriggerRepository triggers) // 新增参数 { _dbContext = dbContext; InitializeRepository = initializeRepository; @@ -51,6 +55,7 @@ public class RepositoryManager : IRepositoryManager VariableHistories = variableHistories; Users = users; Nlogs = nlogs; + Triggers = triggers; // 赋值 _db = dbContext.GetInstance(); } @@ -100,6 +105,10 @@ public class RepositoryManager : IRepositoryManager /// public INlogRepository Nlogs { get; set; } /// + /// 获取触发器仓储实例。 + /// + public ITriggerRepository Triggers { get; set; } + /// /// 获取初始化仓储实例。 /// public IInitializeRepository InitializeRepository { get; set; } diff --git a/DMS.Infrastructure/Repositories/Triggers/Impl/SqlSugarTriggerRepository.cs b/DMS.Infrastructure/Repositories/Triggers/Impl/SqlSugarTriggerRepository.cs new file mode 100644 index 0000000..fc30272 --- /dev/null +++ b/DMS.Infrastructure/Repositories/Triggers/Impl/SqlSugarTriggerRepository.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DMS.Core.Models.Triggers; +using SqlSugar; + +namespace DMS.Infrastructure.Repositories.Triggers.Impl +{ + /// + /// 基于 SqlSugar 的触发器仓储实现 + /// + public class SqlSugarTriggerRepository : ITriggerRepository + { + private readonly ISqlSugarClient _db; + + public SqlSugarTriggerRepository(ISqlSugarClient db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + /// + /// 获取所有触发器定义 + /// + public async Task> GetAllAsync() + { + return await _db.Queryable().ToListAsync(); + } + + /// + /// 根据 ID 获取触发器定义 + /// + public async Task GetByIdAsync(Guid id) + { + return await _db.Queryable().InSingleAsync(id); + } + + /// + /// 添加一个新的触发器定义 + /// + public async Task AddAsync(TriggerDefinition trigger) + { + var insertedId = await _db.Insertable(trigger).ExecuteReturnSnowflakeIdAsync(); + trigger.Id = insertedId; + return trigger; + } + + /// + /// 更新一个已存在的触发器定义 + /// + public async Task UpdateAsync(TriggerDefinition trigger) + { + var rowsAffected = await _db.Updateable(trigger).ExecuteCommandAsync(); + return rowsAffected > 0 ? trigger : null; + } + + /// + /// 删除一个触发器定义 + /// + public async Task DeleteAsync(Guid id) + { + var rowsAffected = await _db.Deleteable().In(id).ExecuteCommandAsync(); + return rowsAffected > 0; + } + + /// + /// 获取与指定变量关联的所有触发器定义 + /// + public async Task> GetByVariableIdAsync(Guid variableId) + { + return await _db.Queryable() + .Where(t => t.VariableId == variableId) + .ToListAsync(); + } + } +} \ No newline at end of file diff --git a/DMS.WPF/App.xaml b/DMS.WPF/App.xaml index 1c33f54..b66dcd1 100644 --- a/DMS.WPF/App.xaml +++ b/DMS.WPF/App.xaml @@ -2,6 +2,10 @@ x:Class="DMS.WPF.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:sys="clr-namespace:System;assembly=mscorlib" + xmlns:triggers="clr-namespace:DMS.Core.Models.Triggers;assembly=DMS.Core" + xmlns:converters="clr-namespace:DMS.WPF.Converters" + xmlns:localConverters="clr-namespace:DMS.WPF.Converters" xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"> @@ -15,6 +19,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/DMS.WPF/App.xaml.cs b/DMS.WPF/App.xaml.cs index ec741d1..cf21434 100644 --- a/DMS.WPF/App.xaml.cs +++ b/DMS.WPF/App.xaml.cs @@ -216,6 +216,7 @@ public partial class App : System.Windows.Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // 添加这行 + services.AddSingleton(); // 注册触发器仓储 services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -227,7 +228,7 @@ public partial class App : System.Windows.Application services.AddTransient(); services.AddTransient(); - // 注册App服务\r\n + // 注册App服务 services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -244,6 +245,9 @@ public partial class App : System.Windows.Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // 注册触发器管理服务 + services.AddSingleton(); // 注册触发器评估服务 + services.AddSingleton(); // 注册触发器动作执行器 services.AddSingleton(); services.AddSingleton(); @@ -256,6 +260,13 @@ public partial class App : System.Windows.Application // 注册WPF中的服务 services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // 注册转换器 (Converters) + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // 注册事件服务 services.AddSingleton(); @@ -292,6 +303,7 @@ public partial class App : System.Windows.Application services.AddScoped(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // 注册 TriggersViewModel // 注册对话框视图模型 services.AddTransient(); @@ -309,6 +321,7 @@ public partial class App : System.Windows.Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // 注册 TriggerEditorViewModel // 注册View视图 services.AddSingleton(); diff --git a/DMS.WPF/Converters/EnumToVisibilityConverter.cs b/DMS.WPF/Converters/EnumToVisibilityConverter.cs new file mode 100644 index 0000000..6fd6df1 --- /dev/null +++ b/DMS.WPF/Converters/EnumToVisibilityConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace DMS.WPF.Converters +{ + /// + /// 枚举到可见性转换器。当绑定的枚举值等于 ConverterParameter 时,返回 Visible,否则返回 Collapsed。 + /// + public class EnumToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null || parameter == null) + return Visibility.Collapsed; + + string enumValue = value.ToString(); + string targetValue = parameter.ToString(); + + return enumValue.Equals(targetValue, StringComparison.OrdinalIgnoreCase) ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/DMS.WPF/Converters/NullableTimeSpanToSecondsConverter.cs b/DMS.WPF/Converters/NullableTimeSpanToSecondsConverter.cs new file mode 100644 index 0000000..43b3ac9 --- /dev/null +++ b/DMS.WPF/Converters/NullableTimeSpanToSecondsConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace DMS.WPF.Converters +{ + /// + /// 可空 TimeSpan 到秒数字符串的双向转换器。 + /// 用于在 TextBox 和 TimeSpan? 之间进行转换。 + /// + public class NullableTimeSpanToSecondsConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is TimeSpan timeSpan) + { + return timeSpan.TotalSeconds.ToString(CultureInfo.InvariantCulture); + } + return ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is string str && !string.IsNullOrWhiteSpace(str)) + { + if (double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out double seconds)) + { + return TimeSpan.FromSeconds(seconds); + } + } + return null; // Return null for invalid or empty input + } + } +} \ No newline at end of file diff --git a/DMS.WPF/Services/DataEventService.cs b/DMS.WPF/Services/DataEventService.cs index 448057e..6aef794 100644 --- a/DMS.WPF/Services/DataEventService.cs +++ b/DMS.WPF/Services/DataEventService.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Messaging; using DMS.Application.DTOs; using DMS.Application.DTOs.Events; using DMS.Application.Interfaces; +using DMS.Application.Services.Triggers; // 添加触发器服务引用 using DMS.Core.Enums; using DMS.Core.Models; using DMS.Message; @@ -22,19 +23,23 @@ public class DataEventService : IDataEventService private readonly IDataStorageService _dataStorageService; private readonly IAppDataCenterService _appDataCenterService; private readonly IWPFDataService _wpfDataService; + private readonly ITriggerEvaluationService _triggerEvaluationService; // 新增依赖 /// /// DataEventService类的构造函数。 /// - public DataEventService(IMapper mapper,IDataStorageService dataStorageService, - IAppDataCenterService appDataCenterService,IWPFDataService wpfDataService + public DataEventService(IMapper mapper, + IDataStorageService dataStorageService, + IAppDataCenterService appDataCenterService, + IWPFDataService wpfDataService, + ITriggerEvaluationService triggerEvaluationService // 新增参数 ) { _mapper = mapper; _dataStorageService = dataStorageService; _appDataCenterService = appDataCenterService; _wpfDataService = wpfDataService; - + _triggerEvaluationService = triggerEvaluationService; // 赋值 // 监听变量值变更事件 _appDataCenterService.VariableManagementService.OnVariableValueChanged += OnVariableValueChanged; @@ -60,7 +65,7 @@ public class DataEventService : IDataEventService /// /// 处理变量值变更事件。 /// - private void OnVariableValueChanged(object? sender, VariableValueChangedEventArgs e) + private async void OnVariableValueChanged(object? sender, VariableValueChangedEventArgs e) // 改为 async void 以便调用 await { // 在UI线程上更新变量值 App.Current.Dispatcher.BeginInvoke(new Action(() => @@ -74,6 +79,27 @@ public class DataEventService : IDataEventService variableToUpdate.UpdatedAt = e.UpdateTime; } })); + + // 在后台线程上调用触发器评估服务 + // 使用 Task.Run 将其放到线程池线程上执行,避免阻塞 UI 线程 + // 注意:这里调用的是 Fire-and-forget,因为我们不等待结果。 + // 如果将来需要处理执行结果或错误,可以考虑使用 async Task 并在适当的地方等待。 + _ = Task.Run(async () => + { + try + { + await _triggerEvaluationService.EvaluateTriggersAsync(e.VariableId, e.NewValue); + } + catch (Exception ex) + { + // Log the exception appropriately. + // Since this is fire-and-forget, we must handle exceptions internally. + // You might have a logging service injected or use a static logger. + // For now, let's assume a static logger or inline comment. + System.Diagnostics.Debug.WriteLine($"Error evaluating triggers for variable {e.VariableId}: {ex}"); + // Consider integrating with your logging framework (e.g., NLog) here. + } + }); } diff --git a/DMS.WPF/ViewModels/Triggers/TriggerEditorViewModel.cs b/DMS.WPF/ViewModels/Triggers/TriggerEditorViewModel.cs new file mode 100644 index 0000000..a1b5e73 --- /dev/null +++ b/DMS.WPF/ViewModels/Triggers/TriggerEditorViewModel.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DMS.Application.DTOs.Triggers; +using DMS.Core.Models.Triggers; +using DMS.WPF.Interfaces; +using DMS.WPF.Services; + +namespace DMS.WPF.ViewModels.Triggers +{ + /// + /// 触发器编辑器视图模型 + /// + public partial class TriggerEditorViewModel : DialogViewModelBase + { + private readonly IVariableAppService _variableAppService; // To populate variable selection dropdown + private readonly IDialogService _dialogService; + private readonly INotificationService _notificationService; + + [ObservableProperty] + private TriggerDefinitionDto _trigger = new(); + + [ObservableProperty] + private List _availableVariables = new(); + + // Properties for easier binding in XAML for SendEmail action config + [ObservableProperty] + [Required(ErrorMessage = "收件人不能为空")] + private string _emailRecipients = ""; + + [ObservableProperty] + [Required(ErrorMessage = "邮件主题模板不能为空")] + private string _emailSubjectTemplate = ""; + + [ObservableProperty] + [Required(ErrorMessage = "邮件内容模板不能为空")] + private string _emailBodyTemplate = ""; + + public TriggerEditorViewModel( + IVariableAppService variableAppService, + IDialogService dialogService, + INotificationService notificationService) + { + _variableAppService = variableAppService ?? throw new ArgumentNullException(nameof(variableAppService)); + _dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); + _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + } + + /// + /// 初始化视图模型(传入待编辑的触发器) + /// + /// 待编辑的触发器 DTO + public override async Task OnInitializedAsync(object? parameter) + { + if (parameter is TriggerDefinitionDto triggerDto) + { + Trigger = triggerDto; + Title = Trigger.Id == Guid.Empty ? "新建触发器" : "编辑触发器"; + PrimaryButText = "保存"; + + // Load available variables for selection dropdown + await LoadVariablesAsync(); + + // Parse action configuration if it's SendEmail + if (Trigger.Action == ActionType.SendEmail && !string.IsNullOrEmpty(Trigger.ActionConfigurationJson)) + { + try + { + var config = JsonSerializer.Deserialize>(Trigger.ActionConfigurationJson); + if (config != null) + { + if (config.TryGetValue("Recipients", out var recipientsElement)) + { + var recipients = recipientsElement.Deserialize>(); + EmailRecipients = string.Join(";", recipients ?? new List()); + } + EmailSubjectTemplate = config.TryGetValue("SubjectTemplate", out var subjectElement) ? subjectElement.GetString() ?? "" : ""; + EmailBodyTemplate = config.TryGetValue("BodyTemplate", out var bodyElement) ? bodyElement.GetString() ?? "" : ""; + } + } + catch (Exception ex) + { + _notificationService.ShowWarning($"无法解析邮件配置: {ex.Message}"); + } + } + } + } + + /// + /// 加载所有可用的变量 + /// + private async Task LoadVariablesAsync() + { + try + { + var variables = await _variableAppService.GetAllAsync(); + AvailableVariables = variables ?? new List(); + } + catch (Exception ex) + { + _notificationService.ShowError($"加载变量列表失败: {ex.Message}"); + AvailableVariables = new List(); + } + } + + /// + /// 保存按钮点击命令 + /// + [RelayCommand] + private async Task SaveAsync() + { + // Basic validation + if (Trigger.VariableId == Guid.Empty) + { + _notificationService.ShowWarning("请选择关联的变量"); + return; + } + + if (string.IsNullOrWhiteSpace(Trigger.Description)) + { + _notificationService.ShowWarning("请输入触发器描述"); + return; + } + + // Validate condition-specific fields + switch (Trigger.Condition) + { + case ConditionType.GreaterThan: + case ConditionType.LessThan: + case ConditionType.EqualTo: + case ConditionType.NotEqualTo: + if (!Trigger.Threshold.HasValue) + { + _notificationService.ShowWarning($"{Trigger.Condition} 条件需要设置阈值"); + return; + } + break; + case ConditionType.InRange: + case ConditionType.OutOfRange: + if (!Trigger.LowerBound.HasValue || !Trigger.UpperBound.HasValue) + { + _notificationService.ShowWarning($"{Trigger.Condition} 条件需要设置下限和上限"); + return; + } + if (Trigger.LowerBound > Trigger.UpperBound) + { + _notificationService.ShowWarning("下限必须小于或等于上限"); + return; + } + break; + } + + // Prepare action configuration based on selected action type + if (Trigger.Action == ActionType.SendEmail) + { + if (string.IsNullOrWhiteSpace(EmailRecipients)) + { + _notificationService.ShowWarning("请输入至少一个收件人邮箱地址"); + return; + } + + if (string.IsNullOrWhiteSpace(EmailSubjectTemplate)) + { + _notificationService.ShowWarning("请输入邮件主题模板"); + return; + } + + if (string.IsNullOrWhiteSpace(EmailBodyTemplate)) + { + _notificationService.ShowWarning("请输入邮件内容模板"); + return; + } + + var recipientList = new List(EmailRecipients.Split(';', StringSplitOptions.RemoveEmptyEntries)); + var configDict = new Dictionary + { + { "Recipients", recipientList }, + { "SubjectTemplate", EmailSubjectTemplate }, + { "BodyTemplate", EmailBodyTemplate } + }; + Trigger.ActionConfigurationJson = JsonSerializer.Serialize(configDict); + } + else + { + // For other actions, leave ActionConfigurationJson as is or set to default "{}" + Trigger.ActionConfigurationJson ??= "{}"; + } + + // Set timestamps + Trigger.UpdatedAt = DateTime.UtcNow; + if (Trigger.Id == Guid.Empty) + { + Trigger.CreatedAt = DateTime.UtcNow; + Trigger.Id = Guid.NewGuid(); + } + + // Close dialog with the updated trigger DTO + await CloseDialogAsync(Trigger); + } + + /// + /// 取消按钮点击命令 + /// + [RelayCommand] + private async Task CancelAsync() + { + await CloseDialogAsync(null); // Return null to indicate cancellation + } + } +} \ No newline at end of file diff --git a/DMS.WPF/ViewModels/Triggers/TriggersViewModel.cs b/DMS.WPF/ViewModels/Triggers/TriggersViewModel.cs new file mode 100644 index 0000000..d48187b --- /dev/null +++ b/DMS.WPF/ViewModels/Triggers/TriggersViewModel.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DMS.Application.DTOs.Triggers; +using DMS.Application.Services.Triggers; +using DMS.WPF.Interfaces; +using DMS.WPF.Services; + +namespace DMS.WPF.ViewModels.Triggers +{ + /// + /// 触发器管理视图模型 + /// + public partial class TriggersViewModel : ViewModelBase + { + private readonly ITriggerManagementService _triggerManagementService; + private readonly IDialogService _dialogService; + private readonly INotificationService _notificationService; + + [ObservableProperty] + private ObservableCollection _triggers = new(); + + [ObservableProperty] + private TriggerDefinitionDto? _selectedTrigger; + + public TriggersViewModel( + ITriggerManagementService triggerManagementService, + IDialogService dialogService, + INotificationService notificationService) + { + _triggerManagementService = triggerManagementService ?? throw new ArgumentNullException(nameof(triggerManagementService)); + _dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); + _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + } + + /// + /// 加载所有触发器 + /// + [RelayCommand] + private async Task LoadTriggersAsync() + { + try + { + var triggerList = await _triggerManagementService.GetAllTriggersAsync(); + Triggers = new ObservableCollection(triggerList); + } + catch (Exception ex) + { + _notificationService.ShowError($"加载触发器失败: {ex.Message}"); + } + } + + /// + /// 添加新触发器 + /// + [RelayCommand] + private async Task AddTriggerAsync() + { + var newTrigger = new TriggerDefinitionDto + { + Id = Guid.NewGuid(), + IsActive = true, + Condition = Core.Models.Triggers.ConditionType.GreaterThan, + Action = Core.Models.Triggers.ActionType.SendEmail, + Description = "新建触发器", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + var result = await _dialogService.ShowDialogAsync("编辑触发器", newTrigger); + if (result != null) + { + try + { + var createdTrigger = await _triggerManagementService.CreateTriggerAsync(result); + Triggers.Add(createdTrigger); + SelectedTrigger = createdTrigger; + _notificationService.ShowSuccess("触发器创建成功"); + } + catch (Exception ex) + { + _notificationService.ShowError($"创建触发器失败: {ex.Message}"); + } + } + } + + /// + /// 编辑选中的触发器 + /// + [RelayCommand] + private async Task EditTriggerAsync() + { + if (SelectedTrigger == null) + { + _notificationService.ShowWarning("请先选择一个触发器"); + return; + } + + // 传递副本以避免直接修改原始对象 + var triggerToEdit = new TriggerDefinitionDto + { + Id = SelectedTrigger.Id, + VariableId = SelectedTrigger.VariableId, + IsActive = SelectedTrigger.IsActive, + Condition = SelectedTrigger.Condition, + Threshold = SelectedTrigger.Threshold, + LowerBound = SelectedTrigger.LowerBound, + UpperBound = SelectedTrigger.UpperBound, + Action = SelectedTrigger.Action, + ActionConfigurationJson = SelectedTrigger.ActionConfigurationJson, + SuppressionDuration = SelectedTrigger.SuppressionDuration, + LastTriggeredAt = SelectedTrigger.LastTriggeredAt, + Description = SelectedTrigger.Description, + CreatedAt = SelectedTrigger.CreatedAt, + UpdatedAt = SelectedTrigger.UpdatedAt + }; + + var result = await _dialogService.ShowDialogAsync("编辑触发器", triggerToEdit); + if (result != null) + { + try + { + var updatedTrigger = await _triggerManagementService.UpdateTriggerAsync(result.Id, result); + if (updatedTrigger != null) + { + var index = Triggers.IndexOf(SelectedTrigger); + if (index >= 0) + { + Triggers[index] = updatedTrigger; + } + SelectedTrigger = updatedTrigger; + _notificationService.ShowSuccess("触发器更新成功"); + } + else + { + _notificationService.ShowError("触发器更新失败,未找到对应记录"); + } + } + catch (Exception ex) + { + _notificationService.ShowError($"更新触发器失败: {ex.Message}"); + } + } + } + + /// + /// 删除选中的触发器 + /// + [RelayCommand] + private async Task DeleteTriggerAsync() + { + if (SelectedTrigger == null) + { + _notificationService.ShowWarning("请先选择一个触发器"); + return; + } + + var confirm = await _dialogService.ShowConfirmDialogAsync("确认删除", $"确定要删除触发器 '{SelectedTrigger.Description}' 吗?"); + if (confirm) + { + try + { + var success = await _triggerManagementService.DeleteTriggerAsync(SelectedTrigger.Id); + if (success) + { + Triggers.Remove(SelectedTrigger); + SelectedTrigger = Triggers.FirstOrDefault(); + _notificationService.ShowSuccess("触发器删除成功"); + } + else + { + _notificationService.ShowError("触发器删除失败"); + } + } + catch (Exception ex) + { + _notificationService.ShowError($"删除触发器失败: {ex.Message}"); + } + } + } + + /// + /// 视图加载时执行的命令 + /// + [RelayCommand] + private async Task OnLoadedAsync() + { + await LoadTriggersCommand.ExecuteAsync(null); + } + } +} \ No newline at end of file diff --git a/DMS.WPF/Views/Triggers/TriggerEditorView.xaml b/DMS.WPF/Views/Triggers/TriggerEditorView.xaml new file mode 100644 index 0000000..6f364c7 --- /dev/null +++ b/DMS.WPF/Views/Triggers/TriggerEditorView.xaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +