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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DMS.WPF/Views/Triggers/TriggerEditorView.xaml.cs b/DMS.WPF/Views/Triggers/TriggerEditorView.xaml.cs
new file mode 100644
index 0000000..c892388
--- /dev/null
+++ b/DMS.WPF/Views/Triggers/TriggerEditorView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace DMS.WPF.Views.Triggers
+{
+ ///
+ /// TriggerEditorView.xaml 的交互逻辑
+ ///
+ public partial class TriggerEditorView : UserControl
+ {
+ public TriggerEditorView()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/DMS.WPF/Views/Triggers/TriggersView.xaml b/DMS.WPF/Views/Triggers/TriggersView.xaml
new file mode 100644
index 0000000..0d8b454
--- /dev/null
+++ b/DMS.WPF/Views/Triggers/TriggersView.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DMS.WPF/Views/Triggers/TriggersView.xaml.cs b/DMS.WPF/Views/Triggers/TriggersView.xaml.cs
new file mode 100644
index 0000000..651b5be
--- /dev/null
+++ b/DMS.WPF/Views/Triggers/TriggersView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace DMS.WPF.Views.Triggers
+{
+ ///
+ /// TriggersView.xaml 的交互逻辑
+ ///
+ public partial class TriggersView : Page
+ {
+ public TriggersView()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file