From 042bc15288ed625f7fe111f70945e9820018c2bb Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Mon, 22 Sep 2025 22:58:51 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E4=BF=AE=E6=94=B9=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E5=99=A8=EF=BC=88=E6=9C=AA=E5=AE=8C=E6=88=90=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=80=E4=B8=AA=E8=A7=A6=E5=8F=91=E5=99=A8?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E6=B7=BB=E5=8A=A0=E5=A4=9A=E4=B8=AA=E5=8F=98?= =?UTF-8?q?=E9=87=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 23 ++ DMS.Application/DTOs/TriggerDefinitionDto.cs | 5 +- DMS.Application/Profiles/MappingProfile.cs | 5 +- .../Triggers/Impl/TriggerManagementService.cs | 4 + DMS.Core/Models/Triggers/TriggerDefinition.cs | 5 +- .../Entities/DbTriggerDefinition.cs | 12 +- .../Entities/DbTriggerVariable.cs | 22 ++ .../001_CreateTriggerVariablesTable.sql | 23 ++ DMS.Infrastructure/Profiles/MappingProfile.cs | 4 +- .../Repositories/TriggerRepository.cs | 104 ++++++- .../Dialogs/TriggerDialogViewModel.cs | 46 ++- DMS.WPF/ViewModels/TriggersViewModel.cs | 2 +- DMS.WPF/Views/Dialogs/TriggerDialog.xaml | 288 ++++++++++++------ README.md | 12 +- 14 files changed, 434 insertions(+), 121 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 DMS.Infrastructure/Entities/DbTriggerVariable.cs create mode 100644 DMS.Infrastructure/Migrations/001_CreateTriggerVariablesTable.sql diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5ed10fb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# 更新日志 + +## v1.1.0 (2025-09-22) + +### 新增功能 +- **触发器系统重构**:将触发器与变量的关联关系从一对一改为多对多 + - 修改了 `TriggerDefinition` 领域模型,将 `VariableId` 属性改为 `VariableIds` 列表 + - 添加了新的数据库实体 `DbTriggerVariable` 来维护多对多关系 + - 更新了数据库映射配置和仓储实现 + - 修改了 DTO 和服务层以支持新的多对多关系 + - 更新了 WPF UI 以支持多变量选择 + +### 技术实现细节 +- 添加了新的 `TriggerVariables` 数据库表来维护触发器与变量的多对多关系 +- 更新了 `TriggerRepository` 以处理新的关联表 +- 修改了 `TriggerManagementService` 的验证逻辑,确保至少选择一个变量 +- 更新了 WPF 视图模型以支持多选变量 +- 提供了数据库迁移脚本以更新现有数据库结构 + +### 兼容性说明 +- 此更新涉及数据库结构变更,需要运行迁移脚本 +- 现有的触发器数据将被迁移到新的表结构中 +- API 接口保持向后兼容,但返回的数据结构已更新 \ No newline at end of file diff --git a/DMS.Application/DTOs/TriggerDefinitionDto.cs b/DMS.Application/DTOs/TriggerDefinitionDto.cs index dbf1d5b..16e6225 100644 --- a/DMS.Application/DTOs/TriggerDefinitionDto.cs +++ b/DMS.Application/DTOs/TriggerDefinitionDto.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using DMS.Core.Models.Triggers; // 引入枚举 @@ -15,9 +16,9 @@ namespace DMS.Application.DTOs public int Id { get; set; } /// - /// 关联的变量 ID + /// 关联的变量 ID 列表 /// - public int VariableId { get; set; } + public List VariableIds { get; set; } = new List(); /// /// 触发器是否处于激活状态 diff --git a/DMS.Application/Profiles/MappingProfile.cs b/DMS.Application/Profiles/MappingProfile.cs index 5e54cde..4645918 100644 --- a/DMS.Application/Profiles/MappingProfile.cs +++ b/DMS.Application/Profiles/MappingProfile.cs @@ -57,6 +57,9 @@ public class MappingProfile : Profile CreateMap().ReverseMap(); CreateMap().ReverseMap(); - CreateMap().ReverseMap(); + CreateMap() + .ForMember(dest => dest.VariableIds, opt => opt.MapFrom(src => src.VariableIds)) + .ReverseMap() + .ForMember(dest => dest.VariableIds, opt => opt.MapFrom(src => src.VariableIds)); } } diff --git a/DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs b/DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs index ff8bfbb..15a547c 100644 --- a/DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs +++ b/DMS.Application/Services/Triggers/Impl/TriggerManagementService.cs @@ -109,6 +109,10 @@ namespace DMS.Application.Services.Triggers.Impl /// private void ValidateTriggerDto(TriggerDefinitionDto dto) { + // 检查是否至少关联了一个变量 + if (dto.VariableIds == null || !dto.VariableIds.Any()) + throw new ArgumentException("触发器必须至少关联一个变量。"); + // 添加必要的验证逻辑 switch (dto.Condition) { diff --git a/DMS.Core/Models/Triggers/TriggerDefinition.cs b/DMS.Core/Models/Triggers/TriggerDefinition.cs index 852d070..bf7aa54 100644 --- a/DMS.Core/Models/Triggers/TriggerDefinition.cs +++ b/DMS.Core/Models/Triggers/TriggerDefinition.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace DMS.Core.Models.Triggers { @@ -37,9 +38,9 @@ namespace DMS.Core.Models.Triggers public int Id { get; set; } /// - /// 关联的变量 ID + /// 关联的变量列表 /// - public int VariableId { get; set; } + public List VariableIds { get; set; } = new List(); /// /// 触发器是否处于激活状态 diff --git a/DMS.Infrastructure/Entities/DbTriggerDefinition.cs b/DMS.Infrastructure/Entities/DbTriggerDefinition.cs index ce842bb..0aed3b2 100644 --- a/DMS.Infrastructure/Entities/DbTriggerDefinition.cs +++ b/DMS.Infrastructure/Entities/DbTriggerDefinition.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using DMS.Core.Models.Triggers; using SqlSugar; using SqlSugar.DbConvert; @@ -17,11 +18,6 @@ public class DbTriggerDefinition [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] public int Id { get; set; } - /// - /// 关联的变量 ID。 - /// - public int VariableId { get; set; } - /// /// 触发器是否处于激活状态。 /// @@ -96,4 +92,10 @@ public class DbTriggerDefinition /// 最后更新时间。 /// public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + /// + /// 关联的变量 ID 列表(通过中间表关联)。 + /// + [SugarColumn(IsIgnore = true)] + public List VariableIds { get; set; } = new List(); } \ No newline at end of file diff --git a/DMS.Infrastructure/Entities/DbTriggerVariable.cs b/DMS.Infrastructure/Entities/DbTriggerVariable.cs new file mode 100644 index 0000000..03515fc --- /dev/null +++ b/DMS.Infrastructure/Entities/DbTriggerVariable.cs @@ -0,0 +1,22 @@ +using SqlSugar; + +namespace DMS.Infrastructure.Entities; + +/// +/// 数据库实体:表示触发器与变量的多对多关联关系。 +/// +public class DbTriggerVariable +{ + [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] + public int Id { get; set; } + + /// + /// 外键,指向 TriggerDefinitions 表的 Id。 + /// + public int TriggerDefinitionId { get; set; } + + /// + /// 外键,指向 Variables 表的 Id。 + /// + public int VariableId { get; set; } +} \ No newline at end of file diff --git a/DMS.Infrastructure/Migrations/001_CreateTriggerVariablesTable.sql b/DMS.Infrastructure/Migrations/001_CreateTriggerVariablesTable.sql new file mode 100644 index 0000000..13c7e5c --- /dev/null +++ b/DMS.Infrastructure/Migrations/001_CreateTriggerVariablesTable.sql @@ -0,0 +1,23 @@ +-- 创建触发器与变量关联表 +CREATE TABLE TriggerVariables ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + TriggerDefinitionId INTEGER NOT NULL, + VariableId INTEGER NOT NULL, + FOREIGN KEY (TriggerDefinitionId) REFERENCES TriggerDefinitions(Id) ON DELETE CASCADE, + FOREIGN KEY (VariableId) REFERENCES Variables(Id) ON DELETE CASCADE +); + +-- 创建索引以提高查询性能 +CREATE INDEX IX_TriggerVariables_TriggerDefinitionId ON TriggerVariables (TriggerDefinitionId); +CREATE INDEX IX_TriggerVariables_VariableId ON TriggerVariables (VariableId); + +-- 迁移现有数据:将TriggerDefinitions表中的VariableId迁移到新的关联表中 +INSERT INTO TriggerVariables (TriggerDefinitionId, VariableId) +SELECT Id, VariableId +FROM TriggerDefinitions +WHERE VariableId IS NOT NULL AND VariableId > 0; + +-- 删除TriggerDefinitions表中的VariableId列(如果数据库支持) +-- 注意:SQLite不支持直接删除列,所以这里只是注释说明 +-- 在支持的数据库中,可以使用以下语句: +-- ALTER TABLE TriggerDefinitions DROP COLUMN VariableId; \ No newline at end of file diff --git a/DMS.Infrastructure/Profiles/MappingProfile.cs b/DMS.Infrastructure/Profiles/MappingProfile.cs index c218956..4ca067f 100644 --- a/DMS.Infrastructure/Profiles/MappingProfile.cs +++ b/DMS.Infrastructure/Profiles/MappingProfile.cs @@ -50,10 +50,12 @@ public class MappingProfile : Profile opt => opt.MapFrom(src => src.SuppressionDurationTicks.HasValue ? TimeSpan.FromTicks(src.SuppressionDurationTicks.Value) : (TimeSpan?)null)) + .ForMember(dest => dest.VariableIds, opt => opt.MapFrom(src => src.VariableIds)) .ReverseMap() .ForMember(dest => dest.SuppressionDurationTicks, opt => opt.MapFrom(src => src.SuppressionDuration.HasValue ? src.SuppressionDuration.Value.Ticks : - (long?)null)); + (long?)null)) + .ForMember(dest => dest.VariableIds, opt => opt.MapFrom(src => src.VariableIds)); } } diff --git a/DMS.Infrastructure/Repositories/TriggerRepository.cs b/DMS.Infrastructure/Repositories/TriggerRepository.cs index 55c3929..ec45702 100644 --- a/DMS.Infrastructure/Repositories/TriggerRepository.cs +++ b/DMS.Infrastructure/Repositories/TriggerRepository.cs @@ -34,6 +34,16 @@ namespace DMS.Infrastructure.Repositories public async Task> GetAllAsync() { var dbList = await base.GetAllAsync(); + // 加载关联的变量ID + foreach (var dbTrigger in dbList) + { + var variableIds = await _dbContext.GetInstance() + .Queryable() + .Where(tv => tv.TriggerDefinitionId == dbTrigger.Id) + .Select(tv => tv.VariableId) + .ToListAsync(); + dbTrigger.VariableIds = variableIds; + } return _mapper.Map>(dbList); } @@ -43,6 +53,16 @@ namespace DMS.Infrastructure.Repositories public async Task GetByIdAsync(int id) { var dbTrigger = await base.GetByIdAsync(id); + if (dbTrigger != null) + { + // 加载关联的变量ID + var variableIds = await _dbContext.GetInstance() + .Queryable() + .Where(tv => tv.TriggerDefinitionId == dbTrigger.Id) + .Select(tv => tv.VariableId) + .ToListAsync(); + dbTrigger.VariableIds = variableIds; + } return _mapper.Map(dbTrigger); } @@ -51,7 +71,21 @@ namespace DMS.Infrastructure.Repositories /// public async Task AddAsync(TriggerDefinition trigger) { - var dbTrigger = await base.AddAsync(_mapper.Map(trigger)); + var dbTrigger = _mapper.Map(trigger); + dbTrigger = await base.AddAsync(dbTrigger); + + // 保存关联的变量ID + if (trigger.VariableIds != null && trigger.VariableIds.Any()) + { + var triggerVariables = trigger.VariableIds.Select(variableId => new DbTriggerVariable + { + TriggerDefinitionId = dbTrigger.Id, + VariableId = variableId + }).ToList(); + + await _dbContext.GetInstance().Insertable(triggerVariables).ExecuteCommandAsync(); + } + return _mapper.Map(dbTrigger, trigger); } @@ -60,8 +94,33 @@ namespace DMS.Infrastructure.Repositories /// public async Task UpdateAsync(TriggerDefinition trigger) { - var rowsAffected = await base.UpdateAsync(_mapper.Map(trigger)); - return rowsAffected > 0 ? trigger : null; + var dbTrigger = _mapper.Map(trigger); + var rowsAffected = await base.UpdateAsync(dbTrigger); + + if (rowsAffected > 0) + { + // 删除旧的关联关系 + await _dbContext.GetInstance() + .Deleteable() + .Where(tv => tv.TriggerDefinitionId == dbTrigger.Id) + .ExecuteCommandAsync(); + + // 插入新的关联关系 + if (trigger.VariableIds != null && trigger.VariableIds.Any()) + { + var triggerVariables = trigger.VariableIds.Select(variableId => new DbTriggerVariable + { + TriggerDefinitionId = dbTrigger.Id, + VariableId = variableId + }).ToList(); + + await _dbContext.GetInstance().Insertable(triggerVariables).ExecuteCommandAsync(); + } + + return trigger; + } + + return null; } /// @@ -71,6 +130,14 @@ namespace DMS.Infrastructure.Repositories { var stopwatch = new Stopwatch(); stopwatch.Start(); + + // 先删除关联的变量关系 + await _dbContext.GetInstance() + .Deleteable() + .Where(tv => tv.TriggerDefinitionId == id) + .ExecuteCommandAsync(); + + // 再删除触发器本身 var rowsAffected = await _dbContext.GetInstance().Deleteable() .In(id) .ExecuteCommandAsync(); @@ -86,9 +153,34 @@ namespace DMS.Infrastructure.Repositories { var stopwatch = new Stopwatch(); stopwatch.Start(); - var dbList = await _dbContext.GetInstance().Queryable() - .Where(t => t.VariableId == variableId) - .ToListAsync(); + + // 先查询关联表获取触发器ID + var triggerIds = await _dbContext.GetInstance() + .Queryable() + .Where(tv => tv.VariableId == variableId) + .Select(tv => tv.TriggerDefinitionId) + .ToListAsync(); + + // 再查询触发器定义 + var dbList = new List(); + if (triggerIds.Any()) + { + dbList = await _dbContext.GetInstance().Queryable() + .In(it => it.Id, triggerIds) + .ToListAsync(); + + // 加载每个触发器的变量ID列表 + foreach (var dbTrigger in dbList) + { + var variableIds = await _dbContext.GetInstance() + .Queryable() + .Where(tv => tv.TriggerDefinitionId == dbTrigger.Id) + .Select(tv => tv.VariableId) + .ToListAsync(); + dbTrigger.VariableIds = variableIds; + } + } + stopwatch.Stop(); _logger.LogInformation($"GetByVariableId {typeof(DbTriggerDefinition).Name},VariableId={variableId},耗时:{stopwatch.ElapsedMilliseconds}ms"); return _mapper.Map>(dbList); diff --git a/DMS.WPF/ViewModels/Dialogs/TriggerDialogViewModel.cs b/DMS.WPF/ViewModels/Dialogs/TriggerDialogViewModel.cs index 1f1e897..74c6f93 100644 --- a/DMS.WPF/ViewModels/Dialogs/TriggerDialogViewModel.cs +++ b/DMS.WPF/ViewModels/Dialogs/TriggerDialogViewModel.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Text.Json; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; @@ -5,8 +6,10 @@ using CommunityToolkit.Mvvm.Input; using DMS.Application.DTOs; using DMS.Application.Interfaces; using DMS.Application.Interfaces.Database; +using DMS.Core.Interfaces; using DMS.Core.Models.Triggers; using DMS.WPF.Interfaces; +using DMS.WPF.ViewModels.Items; namespace DMS.WPF.ViewModels.Dialogs { @@ -17,13 +20,20 @@ namespace DMS.WPF.ViewModels.Dialogs { private readonly IVariableAppService _variableAppService; // To populate variable selection dropdown private readonly IDialogService _dialogService; + private readonly IDataStorageService _dataStorageService; private readonly INotificationService _notificationService; + [ObservableProperty] + private string _searchText = ""; + [ObservableProperty] private TriggerDefinitionDto _trigger = new(); [ObservableProperty] private List _availableVariables = new(); + + [ObservableProperty] + private ObservableCollection _selectedVariables = new(); // Properties for easier binding in XAML for SendEmail action config [ObservableProperty] @@ -38,13 +48,29 @@ namespace DMS.WPF.ViewModels.Dialogs public TriggerDialogViewModel( IVariableAppService variableAppService, IDialogService dialogService, + IDataStorageService dataStorageService, INotificationService notificationService) { _variableAppService = variableAppService ?? throw new ArgumentNullException(nameof(variableAppService)); _dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); + _dataStorageService = dataStorageService; _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); } + partial void OnSearchTextChanged(string searchText) + { + SelectedVariables.Clear(); + foreach (var variableKv in _dataStorageService.Variables) + { + if (variableKv.Value.Name.Contains(SearchText)) + { + SelectedVariables.Add(variableKv.Value); + } + } + } + + + /// /// 初始化视图模型(传入待编辑的触发器) /// @@ -59,6 +85,19 @@ namespace DMS.WPF.ViewModels.Dialogs // Load available variables for selection dropdown await LoadVariablesAsync(); + + // Load selected variables + if (Trigger.VariableIds != null && Trigger.VariableIds.Any()) + { + foreach (var variableId in Trigger.VariableIds) + { + var variable = AvailableVariables.FirstOrDefault(v => v.Id == variableId); + if (variable != null) + { + // SelectedVariables.Add(variable); + } + } + } // Parse action configuration if it's SendEmail if (Trigger.Action == ActionType.SendEmail && !string.IsNullOrEmpty(Trigger.ActionConfigurationJson)) @@ -109,9 +148,9 @@ namespace DMS.WPF.ViewModels.Dialogs private async Task SaveAsync() { // Basic validation - if (Trigger.VariableId == default(int)) + if (SelectedVariables == null || !SelectedVariables.Any()) { - _notificationService.ShowWarn("请选择关联的变量"); + _notificationService.ShowWarn("请至少选择一个关联的变量"); return; } @@ -121,6 +160,9 @@ namespace DMS.WPF.ViewModels.Dialogs return; } + // 设置选中的变量ID + Trigger.VariableIds = SelectedVariables.Select(v => v.Id).ToList(); + // Validate condition-specific fields switch (Trigger.Condition) { diff --git a/DMS.WPF/ViewModels/TriggersViewModel.cs b/DMS.WPF/ViewModels/TriggersViewModel.cs index 6c14180..aff7c24 100644 --- a/DMS.WPF/ViewModels/TriggersViewModel.cs +++ b/DMS.WPF/ViewModels/TriggersViewModel.cs @@ -107,7 +107,7 @@ namespace DMS.WPF.ViewModels.Triggers var triggerToEdit = new TriggerDefinitionDto { Id = SelectedTrigger.Id, - VariableId = SelectedTrigger.VariableId, + VariableIds = new List(SelectedTrigger.VariableIds), IsActive = SelectedTrigger.IsActive, Condition = SelectedTrigger.Condition, Threshold = SelectedTrigger.Threshold, diff --git a/DMS.WPF/Views/Dialogs/TriggerDialog.xaml b/DMS.WPF/Views/Dialogs/TriggerDialog.xaml index 67d6376..bde075b 100644 --- a/DMS.WPF/Views/Dialogs/TriggerDialog.xaml +++ b/DMS.WPF/Views/Dialogs/TriggerDialog.xaml @@ -1,173 +1,261 @@ - + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" + xmlns:enums="clr-namespace:DMS.Core.Models.Triggers;assembly=DMS.Core" + xmlns:vmd="clr-namespace:DMS.WPF.ViewModels.Dialogs" + xmlns:hc="https://handyorg.github.io/handycontrol" + xmlns:ex="clr-namespace:DMS.Extensions" + xmlns:converters="clr-namespace:DMS.WPF.Converters" + Title="{Binding Title}" + mc:Ignorable="d" + d:DesignHeight="600" + d:DesignWidth="600" + d:DataContext="{d:DesignInstance vmd:TriggerDialogViewModel}" + MinWidth="500" + MinHeight="500" + PrimaryButtonText="{Binding PrimaryButText}" + CloseButtonText="取消" + PrimaryButtonCommand="{Binding SaveCommand}" + CloseButtonCommand="{Binding CancelCommand}" + DefaultButton="Primary"> + - - - + + + - + + + + + + + + + + + + + + + - - - - - - + + - + - - + - - - + + - - - + + - - - + + - - - + + - - - - + + - - - + - - + - - - - + - - + + - - - + + + - + - diff --git a/README.md b/README.md index 1ecd62e..02bc876 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,14 @@ opcUaService.Disconnect(); ### Testing -Unit tests for the OPC UA service are included in the `DMS.Infrastructure.UnitTests` project. Run them using your preferred test runner. \ No newline at end of file +Unit tests for the OPC UA service are included in the `DMS.Infrastructure.UnitTests` project. Run them using your preferred test runner. + +## Trigger System + +The trigger system has been updated to support associating triggers with multiple variables instead of just one. This allows for more flexible trigger configurations where a single trigger can monitor multiple variables. + +### Key Changes +- Modified `TriggerDefinition` to use a list of variable IDs instead of a single variable ID +- Added a new `TriggerVariables` table to maintain the many-to-many relationship between triggers and variables +- Updated the UI to support selecting multiple variables when creating or editing triggers +- Updated all related services and repositories to handle the new many-to-many relationship \ No newline at end of file