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">
+
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
+
+
-
+
-
+
+ Width="200"
+ HorizontalAlignment="Left" />
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
+ Width="200"
+ HorizontalAlignment="Left" />
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
-
+
+
+
-
+
-
-
+
+
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