Files
DMS/软件设计文档/10-MQTT别名关联设计.md

385 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 软件开发文档 - MQTT别名关联设计
本文档详细阐述了为满足“一个变量在关联不同MQTT服务器时可以有不同别名”这一需求而设计的“关联实体”架构方案。
## 1. 设计方案:关联实体
### 1.1. 设计思路与考量
* **挑战**:传统的简单多对多映射表(如 `Variable <-> MqttServer`)无法存储“关系”本身的属性。例如,当一个变量 `V1` 关联到 `MQTT_A` 时,其别名为 `Alias_A`;当 `V1` 关联到 `MQTT_B` 时,其别名为 `Alias_B`。这个别名 `Alias` 既不属于 `V1` 也不属于 `MQTT_A``MQTT_B`,它属于 `V1``MQTT_A` 之间的特定关联。
* **解决方案**:引入一个功能完整的“**关联实体**”Association Entity我们将其命名为 `VariableMqttAlias`。这个实体作为 `Variable``MqttServer` 之间关系的载体,自身还携带了关系特有的属性(即 `Alias`)。
### 1.2. 设计优势
* **数据完整性**:别名属性被牢固地绑定在“变量-服务器”的特定连接上,确保了数据的一致性和准确性。
* **高度灵活性**同一个变量可以为不同的MQTT服务器设置完全独立的别名完美适应各种MQTT Broker对Topic命名命名规则的差异化要求。
* **清晰的关注点分离**数据模型清晰地反映了业务规则使得数据处理链特别是MQTT发布逻辑能够明确地使用正确的别名。
* **可管理性**通过应用层提供的服务UI可以方便地实现对这些别名关联的增、删、改、查操作。
### 1.3. 设计劣势/权衡
* **复杂性增加**:相比于简单的多对多映射表,引入关联实体增加了额外的表、实体类和仓储,增加了代码量和理解成本。
* **查询复杂性**:在查询时,需要通过关联实体进行多表连接,可能会使查询语句稍微复杂一些。
## 2. 数据库与核心模型
我们将用新的 `VariableMqttAlias` 实体来取代之前简单的多对多映射表。
### 2.1. `DbVariableMqttAlias` 实体 (`DMS.Infrastructure`)
```csharp
// 文件: DMS.Infrastructure/Entities/DbVariableMqttAlias.cs
using SqlSugar;
namespace DMS.Infrastructure.Entities;
/// <summary>
/// 数据库实体:对应数据库中的 VariableMqttAliases 表用于存储变量与MQTT服务器的关联别名。
/// </summary>
[SugarTable("VariableMqttAliases")]
public class DbVariableMqttAlias
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 外键,指向 Variables 表的 Id。
/// </summary>
public int VariableId { get; set; }
/// <summary>
/// 外键,指向 MqttServers 表的 Id。
/// </summary>
public int MqttServerId { get; set; }
/// <summary>
/// 针对此特定[变量-服务器]连接的发布别名。此别名将用于构建MQTT Topic。
/// </summary>
[SugarColumn(Length = 200)]
public string Alias { get; set; }
}
```
### 2.2. 领域模型重构 (`DMS.Core`)
`Variable``MqttServer` 不再直接相互引用,而是都通过 `VariableMqttAlias` 集合进行关联。这意味着在领域模型层面,它们之间是“通过” `VariableMqttAlias` 建立联系的。
```csharp
// 文件: DMS.Core/Models/VariableMqttAlias.cs (新增)
namespace DMS.Core.Models;
/// <summary>
/// 领域模型代表一个变量到一个MQTT服务器的特定关联包含专属别名。
/// 这是一个关联实体,用于解决多对多关系中需要额外属性(别名)的问题。
/// </summary>
public class VariableMqttAlias
{
public int Id { get; set; }
public int VariableId { get; set; }
public int MqttServerId { get; set; }
public string Alias { get; set; }
// 导航属性,方便在代码中访问关联的领域对象
public Variable Variable { get; set; }
public MqttServer MqttServer { get; set; }
}
// 文件: DMS.Core/Models/Variable.cs (修改)
public class Variable
{
// ... 其他属性
// 移除旧的直接关联public List<MqttServer> MqttServers { get; set; }
/// <summary>
/// 此变量的所有MQTT发布别名关联。一个变量可以关联多个MQTT服务器每个关联可以有独立的别名。
/// </summary>
public List<VariableMqttAlias> MqttAliases { get; set; } = new();
}
// 文件: DMS.Core/Models/MqttServer.cs (修改)
public class MqttServer
{
// ... 其他属性
// 移除旧的直接关联public List<Variable> Variables { get; set; }
/// <summary>
/// 与此服务器关联的所有变量别名。通过此集合可以反向查找关联的变量。
/// </summary>
public List<VariableMqttAlias> VariableAliases { get; set; } = new();
}
```
## 3. 数据处理链更新 (`DMS.Infrastructure`)
### 3.1. 设计思路与考量
* **使用别名构建Topic**`MqttPublishProcessor` 现在必须遍历 `Variable``MqttAliases` 集合以获取每个目标服务器及其对应的专属别名来构建MQTT Topic。这确保了发布的消息Topic符合每个MQTT Broker的特定要求。
* **数据加载**:在处理之前,需要确保 `Variable` 对象的 `MqttAliases` 集合及其内部的 `MqttServer` 导航属性已被正确加载(通常通过仓储的 `Include``Mapper` 方法)。
### 3.2. 示例:`MqttPublishProcessor.cs`
```csharp
// 文件: DMS.Infrastructure/Services/Processors/MqttPublishProcessor.cs (修改)
using DMS.Core.Interfaces;
using DMS.Core.Models;
using DMS.Infrastructure.Services.Communication;
using CommunityToolkit.Mvvm.Messaging;
using System.Linq;
using System.Threading.Tasks;
using System.Text.Json;
using NLog;
namespace DMS.Infrastructure.Services.Processing;
/// <summary>
/// MQTT发布处理器负责将变量值发布到关联的MQTT服务器并使用专属别名。
/// </summary>
public class MqttPublishProcessor : VariableProcessorBase
{
private readonly IMqttPublishService _mqttService;
private readonly IRepositoryManager _repoManager; // 使用 RepositoryManager 来获取仓储
private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 构造函数。
/// </summary>
public MqttPublishProcessor(IMqttPublishService mqttService, IRepositoryManager repoManager)
{
_mqttService = mqttService;
_repoManager = repoManager;
}
protected override async Task HandleAsync(VariableContext context)
{
if (!context.IsValueChanged) return; // 如果值未变化,则不发布
// 1. 从仓储获取变量及其完整的别名关联列表
// 这要求 IVariableRepository 有一个方法能加载 VariableMqttAlias 及其 MqttServer
var variableWithAliases = await _repoManager.Variables.GetVariableWithMqttAliasesAsync(context.Variable.Id);
if (variableWithAliases?.MqttAliases == null || !variableWithAliases.MqttAliases.Any())
{
return; // 没有关联的MQTT服务器无需发布
}
foreach (var aliasInfo in variableWithAliases.MqttAliases)
{
try
{
// 确保 MqttServer 导航属性已加载且激活
var targetServer = aliasInfo.MqttServer;
if (targetServer == null || !targetServer.IsActive)
{
_logger.Warn($"MQTT发布失败变量 {context.Variable.Name} 关联的MQTT服务器 {aliasInfo.MqttServerId} 不存在或未激活。");
continue;
}
// 使用别名构建Topic
// 示例Topic格式DMS/DeviceName/VariableAlias
var topic = $"DMS/{context.Variable.VariableTable.Device.Name}/{aliasInfo.Alias}";
var payload = JsonSerializer.Serialize(new { value = context.CurrentValue, timestamp = context.Timestamp });
await _mqttService.PublishAsync(targetServer, topic, payload);
}
catch (Exception ex)
{
_logger.Error(ex, $"MQTT发布失败变量 {context.Variable.Name} 到服务器 {aliasInfo.MqttServer.ServerName},别名 {aliasInfo.Alias}");
}
}
}
}
```
## 4. 应用层支持 (`DMS.Application`)
### 4.1. 设计思路与考量
* **UI管理**为了让用户能够在UI上管理这些别名应用层需要提供相应的CRUD创建、读取、更新、删除服务。
* **DTOs**:定义 `VariableMqttAliasDto` 用于在应用层和表现层之间传输别名数据。
### 4.2. 示例:`VariableMqttAliasDto.cs`
```csharp
// 文件: DMS.Application/DTOs/VariableMqttAliasDto.cs
namespace DMS.Application.DTOs;
/// <summary>
/// 用于在UI上显示和管理变量与MQTT服务器关联别名的DTO。
/// </summary>
public class VariableMqttAliasDto
{
public int Id { get; set; }
public int VariableId { get; set; }
public int MqttServerId { get; set; }
public string MqttServerName { get; set; } // 用于UI显示关联的服务器名称
public string Alias { get; set; }
}
```
### 4.3. 应用服务接口扩展
```csharp
// 文件: DMS.Application/Interfaces/IMqttAliasAppService.cs
using DMS.Application.DTOs;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace DMS.Application.Interfaces;
/// <summary>
/// 定义了MQTT别名管理相关的应用服务操作。
/// </summary>
public interface IMqttAliasAppService
{
/// <summary>
/// 异步获取指定变量的所有MQTT别名关联。
/// </summary>
Task<List<VariableMqttAliasDto>> GetAliasesForVariableAsync(int variableId);
/// <summary>
/// 异步为变量分配或更新一个MQTT别名。
/// </summary>
/// <param name="variableId">变量ID。</param>
/// <param name="mqttServerId">MQTT服务器ID。</param>
/// <param name="alias">要设置的别名。</param>
Task AssignAliasAsync(int variableId, int mqttServerId, string alias);
/// <summary>
/// 异步更新一个已存在的MQTT别名。
/// </summary>
/// <param name="aliasId">别名关联的ID。</param>
/// <param name="newAlias">新的别名字符串。</param>
Task UpdateAliasAsync(int aliasId, string newAlias);
/// <summary>
/// 异步移除一个MQTT别名关联。
/// </summary>
/// <param name="aliasId">要移除的别名关联的ID。</param>
Task RemoveAliasAsync(int aliasId);
}
```
### 4.4. 应用服务实现
```csharp
// 文件: DMS.Application/Services/MqttAliasAppService.cs
using AutoMapper;
using DMS.Application.DTOs;
using DMS.Application.Interfaces;
using DMS.Core.Interfaces;
using DMS.Core.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DMS.Application.Services;
/// <summary>
/// IMqttAliasAppService 的实现负责管理变量与MQTT服务器的别名关联。
/// </summary>
public class MqttAliasAppService : IMqttAliasAppService
{
private readonly IRepositoryManager _repoManager;
private readonly IMapper _mapper;
/// <summary>
/// 构造函数。
/// </summary>
public MqttAliasAppService(IRepositoryManager repoManager, IMapper mapper)
{
_repoManager = repoManager;
_mapper = mapper;
}
/// <summary>
/// 异步获取指定变量的所有MQTT别名关联。
/// </summary>
public async Task<List<VariableMqttAliasDto>> GetAliasesForVariableAsync(int variableId)
{
// 从仓储获取别名并确保加载了关联的MqttServer信息
var aliases = await _repoManager.VariableMqttAliases.GetAliasesWithServerInfoAsync(variableId);
return _mapper.Map<List<VariableMqttAliasDto>>(aliases);
}
/// <summary>
/// 异步为变量分配或更新一个MQTT别名。
/// </summary>
public async Task AssignAliasAsync(int variableId, int mqttServerId, string alias)
{
try
{
_repoManager.BeginTransaction();
// 检查是否已存在该变量与该服务器的关联
var existingAlias = await _repoManager.VariableMqttAliases.GetByVariableAndServerAsync(variableId, mqttServerId);
if (existingAlias != null)
{
// 如果存在,则更新别名
existingAlias.Alias = alias;
await _repoManager.VariableMqttAliases.UpdateAsync(existingAlias);
}
else
{
// 如果不存在,则创建新的关联
var newAlias = new VariableMqttAlias
{
VariableId = variableId,
MqttServerId = mqttServerId,
Alias = alias
};
await _repoManager.VariableMqttAliases.AddAsync(newAlias);
}
await _repoManager.CommitAsync();
}
catch (Exception ex)
{
await _repoManager.RollbackAsync();
throw new ApplicationException("分配/更新MQTT别名失败。", ex);
}
}
/// <summary>
/// 异步更新一个已存在的MQTT别名。
/// </summary>
public async Task UpdateAliasAsync(int aliasId, string newAlias)
{
try
{
_repoManager.BeginTransaction();
var aliasToUpdate = await _repoManager.VariableMqttAliases.GetByIdAsync(aliasId);
if (aliasToUpdate == null)
{
throw new KeyNotFoundException($"未找到ID为 {aliasId} 的MQTT别名关联。");
}
aliasToUpdate.Alias = newAlias;
await _repoManager.VariableMqttAliases.UpdateAsync(aliasToUpdate);
await _repoManager.CommitAsync();
}
catch (Exception ex)
{
await _repoManager.RollbackAsync();
throw new ApplicationException("更新MQTT别名失败。", ex);
}
}
/// <summary>
/// 异步移除一个MQTT别名关联。
/// </summary>
public async Task RemoveAliasAsync(int aliasId)
{
try
{
_repoManager.BeginTransaction();
await _repoManager.VariableMqttAliases.DeleteAsync(aliasId);
await _repoManager.CommitAsync();
}
catch (Exception ex)
{
await _repoManager.RollbackAsync();
throw new ApplicationException("移除MQTT别名失败。", ex);
}
}
}
```