Browse Source

feat: 限制设备编辑时只能修改名称和描述

- 移除设备表单中的启用和启动复选框
- 移除未使用的Checkbox导入语句
- 限制编辑模式下只能修改设备名称和描述
- IP地址和端口在编辑模式下显示为只读状态
- 添加灰色背景和提示文本,明确表示不可修改
- 更新修改记录文档
feature/x1-web-request
hyh 5 days ago
parent
commit
83942627be
  1. 8
      src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs
  2. 284
      src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs
  3. 5
      src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs
  4. 20
      src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs
  5. 22
      src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs
  6. 5
      src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceResponse.cs
  7. 1
      src/X1.Application/X1.Application.csproj
  8. 11
      src/X1.Domain/Entities/Device/CellularDevice.cs
  9. 10
      src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs
  10. 4
      src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs
  11. 34
      src/X1.DynamicClientCore/Features/IBaseInstrumentClient.cs
  12. 30
      src/X1.DynamicClientCore/Features/IInstrumentHttpClient.cs
  13. 48
      src/X1.DynamicClientCore/Features/IInstrumentProtocolClient.cs
  14. 163
      src/X1.DynamicClientCore/Features/Service/InstrumentProtocolClient.cs
  15. 104
      src/X1.DynamicClientCore/Models/ApiActionResult.cs
  16. 24
      src/X1.DynamicClientCore/Models/DeviceInfoResponse.cs
  17. 2
      src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs
  18. 16
      src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs
  19. 3
      src/X1.WebAPI/Program.cs
  20. 142
      src/X1.WebUI/src/pages/instruments/DeviceForm.tsx
  21. 60
      src/X1.WebUI/src/pages/instruments/DevicesView.tsx
  22. 7
      src/X1.WebUI/src/services/instrumentService.ts
  23. 863
      src/modify.md

8
src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs

@ -17,12 +17,7 @@ public class CreateDeviceCommand : IRequest<OperationResult<CreateDeviceResponse
[MaxLength(100, ErrorMessage = "设备名称不能超过100个字符")] [MaxLength(100, ErrorMessage = "设备名称不能超过100个字符")]
public string DeviceName { get; set; } public string DeviceName { get; set; }
/// <summary>
/// 序列号
/// </summary>
[Required(ErrorMessage = "序列号不能为空")]
[MaxLength(50, ErrorMessage = "序列号不能超过50个字符")]
public string SerialNumber { get; set; }
/// <summary> /// <summary>
/// 设备描述 /// 设备描述
@ -30,7 +25,6 @@ public class CreateDeviceCommand : IRequest<OperationResult<CreateDeviceResponse
[MaxLength(500, ErrorMessage = "设备描述不能超过500个字符")] [MaxLength(500, ErrorMessage = "设备描述不能超过500个字符")]
public string Description { get; set; } public string Description { get; set; }
/// <summary> /// <summary>
/// IP地址 /// IP地址
/// </summary> /// </summary>

284
src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs

@ -6,6 +6,9 @@ using CellularManagement.Domain.Repositories;
using CellularManagement.Domain.Repositories.Device; using CellularManagement.Domain.Repositories.Device;
using CellularManagement.Domain.Repositories.Base; using CellularManagement.Domain.Repositories.Base;
using CellularManagement.Domain.Services; using CellularManagement.Domain.Services;
using X1.DynamicClientCore.Features;
using X1.DynamicClientCore.Models;
using X1.DynamicClientCore.Interfaces;
namespace CellularManagement.Application.Features.Devices.Commands.CreateDevice; namespace CellularManagement.Application.Features.Devices.Commands.CreateDevice;
@ -18,6 +21,8 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
private readonly ILogger<CreateDeviceCommandHandler> _logger; private readonly ILogger<CreateDeviceCommandHandler> _logger;
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly ICurrentUserService _currentUserService; private readonly ICurrentUserService _currentUserService;
private readonly IBaseInstrumentClient _instrumentClient;
private readonly IServiceEndpointManager _endpointManager;
/// <summary> /// <summary>
/// 初始化命令处理器 /// 初始化命令处理器
@ -26,12 +31,16 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
ICellularDeviceRepository deviceRepository, ICellularDeviceRepository deviceRepository,
ILogger<CreateDeviceCommandHandler> logger, ILogger<CreateDeviceCommandHandler> logger,
IUnitOfWork unitOfWork, IUnitOfWork unitOfWork,
ICurrentUserService currentUserService) ICurrentUserService currentUserService,
IBaseInstrumentClient instrumentClient,
IServiceEndpointManager endpointManager)
{ {
_deviceRepository = deviceRepository; _deviceRepository = deviceRepository;
_logger = logger; _logger = logger;
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_currentUserService = currentUserService; _currentUserService = currentUserService;
_instrumentClient = instrumentClient;
_endpointManager = endpointManager;
} }
/// <summary> /// <summary>
@ -39,30 +48,251 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
/// </summary> /// </summary>
public async Task<OperationResult<CreateDeviceResponse>> Handle(CreateDeviceCommand request, CancellationToken cancellationToken) public async Task<OperationResult<CreateDeviceResponse>> Handle(CreateDeviceCommand request, CancellationToken cancellationToken)
{ {
// 验证请求参数
var validationResult = ValidateRequest(request);
if (!validationResult.IsSuccess)
{
return OperationResult<CreateDeviceResponse>.CreateFailure(validationResult.ErrorMessages);
}
_logger.LogInformation("开始创建设备,设备名称: {DeviceName}, IP地址: {IpAddress}",
request.DeviceName, request.IpAddress);
// 验证用户认证
var currentUserId = ValidateUserAuthentication();
if (!currentUserId.IsSuccess)
{
return OperationResult<CreateDeviceResponse>.CreateFailure(currentUserId.ErrorMessages);
}
// 创建服务端点
var serviceEndpoint = CreateServiceEndpoint(request);
// 获取设备序列号
var serialNumberResult = await GetDeviceSerialNumberAsync(request, serviceEndpoint, cancellationToken);
if (!serialNumberResult.IsSuccess)
{
return OperationResult<CreateDeviceResponse>.CreateFailure(serialNumberResult.ErrorMessages);
}
var serialNumber = serialNumberResult.Data;
// 验证序列号
if (string.IsNullOrWhiteSpace(serialNumber))
{
_logger.LogError("获取到的序列号为空");
return OperationResult<CreateDeviceResponse>.CreateFailure("无法获取设备序列号");
}
// 检查序列号是否已存在
if (await _deviceRepository.SerialNumberExistsAsync(serialNumber, cancellationToken))
{
_logger.LogWarning("设备序列号已存在: {SerialNumber}", serialNumber);
return OperationResult<CreateDeviceResponse>.CreateFailure($"设备序列号 {serialNumber} 已存在");
}
// 生成设备编号
var deviceCode = await GenerateDeviceCodeAsync(serialNumber, cancellationToken);
// 验证设备编号
if (string.IsNullOrWhiteSpace(deviceCode))
{
_logger.LogError("生成的设备编号为空");
return OperationResult<CreateDeviceResponse>.CreateFailure("生成设备编号失败");
}
// 创建设备并保存
var deviceResult = await CreateAndSaveDeviceAsync(request, serialNumber, deviceCode, currentUserId.Data, cancellationToken);
if (!deviceResult.IsSuccess)
{
return OperationResult<CreateDeviceResponse>.CreateFailure(deviceResult.ErrorMessages);
}
var device = deviceResult.Data;
// 设备保存成功后,修改端点名称并重新添加
serviceEndpoint.Name = deviceCode;
_endpointManager.AddOrUpdateEndpoint(serviceEndpoint);
_logger.LogDebug("设备保存成功后,已添加服务端点: {EndpointName}", deviceCode);
// 构建响应
var response = new CreateDeviceResponse
{
DeviceId = device.Id,
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
DeviceCode = device.DeviceCode,
Description = device.Description,
AgentPort = device.AgentPort,
IsEnabled = device.IsEnabled,
IsRunning = device.IsRunning,
CreatedAt = device.CreatedAt
};
_logger.LogInformation("设备创建成功,设备ID: {DeviceId}, 设备名称: {DeviceName}, 序列号: {SerialNumber}",
device.Id, device.Name, device.SerialNumber);
return OperationResult<CreateDeviceResponse>.CreateSuccess(response);
}
/// <summary>
/// 验证请求参数
/// </summary>
private OperationResult<bool> ValidateRequest(CreateDeviceCommand request)
{
if (request == null)
{
_logger.LogError("请求参数为空");
return OperationResult<bool>.CreateFailure("请求参数不能为空");
}
if (string.IsNullOrWhiteSpace(request.DeviceName))
{
_logger.LogError("设备名称为空");
return OperationResult<bool>.CreateFailure("设备名称不能为空");
}
if (string.IsNullOrWhiteSpace(request.IpAddress))
{
_logger.LogError("IP地址为空");
return OperationResult<bool>.CreateFailure("IP地址不能为空");
}
if (request.AgentPort <= 0 || request.AgentPort > 65535)
{
_logger.LogError("Agent端口无效: {AgentPort}", request.AgentPort);
return OperationResult<bool>.CreateFailure("Agent端口必须在1-65535之间");
}
return OperationResult<bool>.CreateSuccess(true);
}
/// <summary>
/// 验证用户认证
/// </summary>
private OperationResult<string> ValidateUserAuthentication()
{
var currentUserId = _currentUserService.GetCurrentUserId();
if (string.IsNullOrEmpty(currentUserId))
{
_logger.LogError("无法获取当前用户ID,用户可能未认证");
return OperationResult<string>.CreateFailure("用户未认证,无法创建设备");
}
return OperationResult<string>.CreateSuccess(currentUserId);
}
/// <summary>
/// 获取设备序列号
/// </summary>
private async Task<OperationResult<string>> GetDeviceSerialNumberAsync(CreateDeviceCommand request, ServiceEndpoint serviceEndpoint, CancellationToken cancellationToken)
{
try try
{ {
_logger.LogInformation("开始创建设备,设备名称: {DeviceName}, 序列号: {SerialNumber}", // 添加服务端点到管理器
request.DeviceName, request.SerialNumber); _endpointManager.AddOrUpdateEndpoint(serviceEndpoint);
_logger.LogDebug("已添加服务端点: {EndpointName}", serviceEndpoint.Name);
// 检查序列号是否已存在 // 获取设备序列号
if (await _deviceRepository.SerialNumberExistsAsync(request.SerialNumber, cancellationToken)) var serialNumber = await _instrumentClient.GetDeviceSerialNumberAsync(serviceEndpoint.Name, null, cancellationToken);
if (string.IsNullOrEmpty(serialNumber))
{ {
_logger.LogWarning("设备序列号已存在: {SerialNumber}", request.SerialNumber); _logger.LogWarning("无法通过IP地址获取设备序列号,IP地址: {IpAddress}", request.IpAddress);
return OperationResult<CreateDeviceResponse>.CreateFailure($"设备序列号 {request.SerialNumber} 已存在"); // 获取序列号失败时清理服务端点
_endpointManager.RemoveEndpoint(serviceEndpoint.Name);
_logger.LogDebug("已清理服务端点: {EndpointName}", serviceEndpoint.Name);
return OperationResult<string>.CreateFailure($"无法通过IP地址 {request.IpAddress} 获取设备序列号,请检查设备连接状态");
} }
// 获取当前用户ID _logger.LogInformation("成功获取设备序列号: {SerialNumber}, IP地址: {IpAddress}", serialNumber, request.IpAddress);
var currentUserId = _currentUserService.GetCurrentUserId();
if (string.IsNullOrEmpty(currentUserId)) // 获取序列号成功后,移除临时服务端点
{ _endpointManager.RemoveEndpoint(serviceEndpoint.Name);
_logger.LogError("无法获取当前用户ID,用户可能未认证"); _logger.LogDebug("已移除临时服务端点: {EndpointName}", serviceEndpoint.Name);
return OperationResult<CreateDeviceResponse>.CreateFailure("用户未认证,无法创建设备");
} return OperationResult<string>.CreateSuccess(serialNumber);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取设备序列号时发生异常,IP地址: {IpAddress}", request.IpAddress);
// 发生异常时清理服务端点
_endpointManager.RemoveEndpoint(serviceEndpoint.Name);
_logger.LogDebug("已清理服务端点: {EndpointName}", serviceEndpoint.Name);
return OperationResult<string>.CreateFailure($"获取设备序列号时发生错误: {ex.Message}");
}
}
/// <summary>
/// 生成设备编号
/// </summary>
private async Task<string> GenerateDeviceCodeAsync(string serialNumber, CancellationToken cancellationToken)
{
// 获取当前设备总数
var deviceCount = await _deviceRepository.GetDeviceCountAsync(cancellationToken);
var nextNumber = deviceCount + 1;
// 计算需要的位数,确保至少3位数
var digitCount = CalculateRequiredDigits(nextNumber);
// 格式化序号,动态补0,确保从000开始
var formattedNumber = nextNumber.ToString($"D{digitCount}");
// 生成设备编号格式:DEV-000-SN, DEV-001-SN, DEV-002-SN 等
var deviceCode = $"DEV-{formattedNumber}-{serialNumber}";
_logger.LogDebug("生成设备编号: {DeviceCode}, 设备总数: {DeviceCount}, 位数: {DigitCount}", deviceCode, deviceCount, digitCount);
return deviceCode;
}
/// <summary>
/// 计算需要的位数
/// </summary>
private int CalculateRequiredDigits(int number)
{
if (number <= 0) return 3; // 从000开始,至少3位数
// 计算位数:确保至少3位数,从000开始
// 1-999用3位,1000-9999用4位,10000-99999用5位,以此类推
var calculatedDigits = (int)Math.Floor(Math.Log10(number)) + 1;
return Math.Max(calculatedDigits, 3); // 确保至少3位数
}
/// <summary>
/// 创建服务端点配置
/// </summary>
private ServiceEndpoint CreateServiceEndpoint(CreateDeviceCommand request)
{
return new ServiceEndpoint
{
Name = $"DEV-{Guid.NewGuid():N}",
Ip = request.IpAddress,
Port = request.AgentPort,
Protocol = "http",
BasePath= "/api/v1/",
Timeout = 10,
Enabled = true
};
}
/// <summary>
/// 创建设备并保存到数据库
/// </summary>
private async Task<OperationResult<CellularDevice>> CreateAndSaveDeviceAsync(
CreateDeviceCommand request,
string serialNumber,
string deviceCode,
string currentUserId,
CancellationToken cancellationToken)
{
try
{
// 创建设备实体 // 创建设备实体
var device = CellularDevice.Create( var device = CellularDevice.Create(
name: request.DeviceName, name: request.DeviceName,
serialNumber: request.SerialNumber, serialNumber: serialNumber,
deviceCode: deviceCode,
description: request.Description, description: request.Description,
agentPort: request.AgentPort, agentPort: request.AgentPort,
ipAddress: request.IpAddress, ipAddress: request.IpAddress,
@ -76,30 +306,12 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
// 保存更改到数据库 // 保存更改到数据库
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
// 构建响应 return OperationResult<CellularDevice>.CreateSuccess(device);
var response = new CreateDeviceResponse
{
DeviceId = device.Id,
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
Description = device.Description,
AgentPort = device.AgentPort,
IsEnabled = device.IsEnabled,
IsRunning = device.IsRunning,
CreatedAt = device.CreatedAt
};
_logger.LogInformation("设备创建成功,设备ID: {DeviceId}, 设备名称: {DeviceName}, 序列号: {SerialNumber}",
device.Id, device.Name, device.SerialNumber);
return OperationResult<CreateDeviceResponse>.CreateSuccess(response);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "创建设备时发生错误,设备名称: {DeviceName}, 序列号: {SerialNumber}", _logger.LogError(ex, "创建设备实体时发生错误,设备名称: {DeviceName}", request.DeviceName);
request.DeviceName, request.SerialNumber); return OperationResult<CellularDevice>.CreateFailure($"创建设备时发生错误: {ex.Message}");
return OperationResult<CreateDeviceResponse>.CreateFailure($"创建设备时发生错误: {ex.Message}");
} }
} }
} }

5
src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs

@ -20,6 +20,11 @@ public class CreateDeviceResponse
/// </summary> /// </summary>
public string SerialNumber { get; set; } public string SerialNumber { get; set; }
/// <summary>
/// 设备编码
/// </summary>
public string DeviceCode { get; set; }
/// <summary> /// <summary>
/// 设备描述 /// 设备描述
/// </summary> /// </summary>

20
src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs

@ -23,32 +23,12 @@ public class UpdateDeviceCommand : IRequest<OperationResult<UpdateDeviceResponse
[MaxLength(100)] [MaxLength(100)]
public string DeviceName { get; set; } public string DeviceName { get; set; }
/// <summary>
/// 序列号
/// </summary>
[Required]
[MaxLength(50)]
public string SerialNumber { get; set; }
/// <summary> /// <summary>
/// 设备描述 /// 设备描述
/// </summary> /// </summary>
[MaxLength(500)] [MaxLength(500)]
public string Description { get; set; } public string Description { get; set; }
/// <summary>
/// IP地址
/// </summary>
[Required]
[MaxLength(45)]
public string IpAddress { get; set; } = null!;
/// <summary>
/// Agent端口
/// </summary>
[Required]
public int AgentPort { get; set; }
/// <summary> /// <summary>
/// 是否启用 /// 是否启用
/// </summary> /// </summary>

22
src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs

@ -54,16 +54,6 @@ public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, O
return OperationResult<UpdateDeviceResponse>.CreateFailure($"未找到ID为 {request.DeviceId} 的设备"); return OperationResult<UpdateDeviceResponse>.CreateFailure($"未找到ID为 {request.DeviceId} 的设备");
} }
// 如果序列号发生变化,检查新序列号是否已存在
if (existingDevice.SerialNumber != request.SerialNumber)
{
if (await _deviceRepository.SerialNumberExistsAsync(request.SerialNumber, cancellationToken))
{
_logger.LogWarning("设备序列号已存在: {SerialNumber}", request.SerialNumber);
return OperationResult<UpdateDeviceResponse>.CreateFailure($"设备序列号 {request.SerialNumber} 已存在");
}
}
// 获取当前用户ID // 获取当前用户ID
var currentUserId = _currentUserService.GetCurrentUserId(); var currentUserId = _currentUserService.GetCurrentUserId();
if (string.IsNullOrEmpty(currentUserId)) if (string.IsNullOrEmpty(currentUserId))
@ -72,13 +62,14 @@ public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, O
return OperationResult<UpdateDeviceResponse>.CreateFailure("用户未认证,无法更新设备"); return OperationResult<UpdateDeviceResponse>.CreateFailure("用户未认证,无法更新设备");
} }
// 更新设备属性 // 只更新允许修改的字段,其他字段保持不变
existingDevice.Update( existingDevice.Update(
name: request.DeviceName, name: request.DeviceName,
serialNumber: request.SerialNumber, serialNumber: existingDevice.SerialNumber, // 保持原有序列号不变
deviceCode: existingDevice.DeviceCode, // 保持原有设备编码不变
description: request.Description, description: request.Description,
agentPort: request.AgentPort, agentPort: existingDevice.AgentPort, // 保持原有端口不变
ipAddress: request.IpAddress, ipAddress: existingDevice.IpAddress, // 保持原有IP地址不变
updatedBy: currentUserId, updatedBy: currentUserId,
isEnabled: request.IsEnabled, isEnabled: request.IsEnabled,
isRunning: request.IsRunning); isRunning: request.IsRunning);
@ -94,6 +85,7 @@ public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, O
DeviceId = existingDevice.Id, DeviceId = existingDevice.Id,
DeviceName = existingDevice.Name, DeviceName = existingDevice.Name,
SerialNumber = existingDevice.SerialNumber, SerialNumber = existingDevice.SerialNumber,
DeviceCode = existingDevice.DeviceCode,
Description = existingDevice.Description, Description = existingDevice.Description,
AgentPort = existingDevice.AgentPort, AgentPort = existingDevice.AgentPort,
IsEnabled = existingDevice.IsEnabled, IsEnabled = existingDevice.IsEnabled,
@ -112,6 +104,4 @@ public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, O
return OperationResult<UpdateDeviceResponse>.CreateFailure($"更新设备时发生错误: {ex.Message}"); return OperationResult<UpdateDeviceResponse>.CreateFailure($"更新设备时发生错误: {ex.Message}");
} }
} }
} }

5
src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceResponse.cs

@ -20,6 +20,11 @@ public class UpdateDeviceResponse
/// </summary> /// </summary>
public string SerialNumber { get; set; } public string SerialNumber { get; set; }
/// <summary>
/// 设备编码
/// </summary>
public string DeviceCode { get; set; }
/// <summary> /// <summary>
/// 设备描述 /// 设备描述
/// </summary> /// </summary>

1
src/X1.Application/X1.Application.csproj

@ -3,6 +3,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\X1.Domain\X1.Domain.csproj" /> <ProjectReference Include="..\X1.Domain\X1.Domain.csproj" />
<ProjectReference Include="..\X1.DynamicClientCore\X1.DynamicClientCore.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

11
src/X1.Domain/Entities/Device/CellularDevice.cs

@ -25,6 +25,13 @@ public class CellularDevice : AuditableEntity
[MaxLength(50)] [MaxLength(50)]
public string SerialNumber { get; private set; } = null!; public string SerialNumber { get; private set; } = null!;
/// <summary>
/// 设备编码
/// </summary>
[Required]
[MaxLength(50)]
public string DeviceCode { get; private set; } = null!;
/// <summary> /// <summary>
/// 设备描述 /// 设备描述
/// </summary> /// </summary>
@ -67,6 +74,7 @@ public class CellularDevice : AuditableEntity
public static CellularDevice Create( public static CellularDevice Create(
string name, string name,
string serialNumber, string serialNumber,
string deviceCode,
string description, string description,
int agentPort, int agentPort,
string ipAddress, string ipAddress,
@ -79,6 +87,7 @@ public class CellularDevice : AuditableEntity
Id = Guid.NewGuid().ToString(), Id = Guid.NewGuid().ToString(),
Name = name, Name = name,
SerialNumber = serialNumber, SerialNumber = serialNumber,
DeviceCode = deviceCode,
Description = description, Description = description,
AgentPort = agentPort, AgentPort = agentPort,
IpAddress = ipAddress, IpAddress = ipAddress,
@ -99,6 +108,7 @@ public class CellularDevice : AuditableEntity
public void Update( public void Update(
string name, string name,
string serialNumber, string serialNumber,
string deviceCode,
string description, string description,
int agentPort, int agentPort,
string ipAddress, string ipAddress,
@ -108,6 +118,7 @@ public class CellularDevice : AuditableEntity
{ {
Name = name; Name = name;
SerialNumber = serialNumber; SerialNumber = serialNumber;
DeviceCode = deviceCode;
Description = description; Description = description;
AgentPort = agentPort; AgentPort = agentPort;
IpAddress = ipAddress; IpAddress = ipAddress;

10
src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs

@ -65,4 +65,14 @@ public interface ICellularDeviceRepository : IBaseRepository<CellularDevice>
/// 检查序列号是否存在 /// 检查序列号是否存在
/// </summary> /// </summary>
Task<bool> SerialNumberExistsAsync(string serialNumber, CancellationToken cancellationToken = default); Task<bool> SerialNumberExistsAsync(string serialNumber, CancellationToken cancellationToken = default);
/// <summary>
/// 检查设备编码是否存在
/// </summary>
Task<bool> DeviceCodeExistsAsync(string deviceCode, CancellationToken cancellationToken = default);
/// <summary>
/// 获取设备总数
/// </summary>
Task<int> GetDeviceCountAsync(CancellationToken cancellationToken = default);
} }

4
src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs

@ -45,6 +45,10 @@ namespace X1.DynamicClientCore.Extensions
services.AddSingleton<IAsyncHttpClient, DynamicHttpClient>(); services.AddSingleton<IAsyncHttpClient, DynamicHttpClient>();
services.AddSingleton<ISyncHttpClient, DynamicHttpClient>(); services.AddSingleton<ISyncHttpClient, DynamicHttpClient>();
// 注册仪器协议客户端(单例)
services.AddSingleton<X1.DynamicClientCore.Features.IBaseInstrumentClient, X1.DynamicClientCore.Features.Service.InstrumentProtocolClient>();
services.AddSingleton<X1.DynamicClientCore.Features.IInstrumentHttpClient, X1.DynamicClientCore.Features.Service.InstrumentProtocolClient>();
return services; return services;
} }

34
src/X1.DynamicClientCore/Features/IBaseInstrumentClient.cs

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using X1.DynamicClientCore.Models;
namespace X1.DynamicClientCore.Features
{
/// <summary>
/// 基础仪器客户端接口
/// 提供仪器设备的基础通信功能,包括设备信息获取等通用操作
/// </summary>
/// <remarks>
/// 作为仪器客户端的基础接口,定义了所有仪器客户端都应该支持的基本功能
/// 主要用于设备信息查询和状态获取等基础操作
/// </remarks>
public interface IBaseInstrumentClient
{
/// <summary>
/// 获取设备序列号
/// </summary>
/// <param name="endpoint">服务端点配置,包含设备的连接信息</param>
/// <param name="options">请求选项,包含超时、请求头等配置</param>
/// <param name="cancellationToken">取消令牌,用于取消异步操作</param>
/// <returns>设备序列号,如果获取失败则返回空字符串</returns>
/// <exception cref="ArgumentNullException">当endpoint为null时抛出</exception>
/// <exception cref="ArgumentException">当endpoint配置无效时抛出</exception>
Task<string> GetDeviceSerialNumberAsync(
string instrumentNumber,
RequestOptions? options = null,
CancellationToken cancellationToken = default);
}
}

30
src/X1.DynamicClientCore/Features/IInstrumentHttpClient.cs

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using X1.DynamicClientCore.Models;
namespace X1.DynamicClientCore.Features
{
/// <summary>
/// 仪器HTTP客户端接口
/// 组合基础仪器客户端和协议客户端的功能,提供完整的仪器设备通信服务
/// </summary>
/// <remarks>
/// 该接口继承自 IBaseInstrumentClient 和 IInstrumentProtocolClient,
/// 提供仪器设备的基础信息获取和网络控制功能
/// 主要用于通过HTTP协议与仪器设备进行通信
/// </remarks>
public interface IInstrumentHttpClient : IBaseInstrumentClient, IInstrumentProtocolClient
{
// 继承自 IBaseInstrumentClient 的方法:
// - GetDeviceSerialNumberAsync: 获取设备序列号
// 继承自 IInstrumentProtocolClient 的方法:
// - StartNetworkAsync: 启动网络连接
// - StopNetworkAsync: 停止网络连接
// 可以在这里添加仪器HTTP客户端特有的方法
}
}

48
src/X1.DynamicClientCore/Features/IInstrumentProtocolClient.cs

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using X1.DynamicClientCore.Models;
namespace X1.DynamicClientCore.Features
{
/// <summary>
/// 仪器协议客户端接口
/// 提供仪器设备的网络控制功能,包括网络启动、停止等协议相关操作
/// </summary>
/// <remarks>
/// 作为仪器客户端的协议接口,定义了仪器设备网络控制相关的功能
/// 主要用于网络连接的管理和控制操作
/// </remarks>
public interface IInstrumentProtocolClient
{
/// <summary>
/// 启动网络连接
/// </summary>
/// <param name="instrumentNumber">仪器编号,用于标识特定的仪器设备</param>
/// <param name="options">请求选项,包含超时、请求头等配置</param>
/// <param name="cancellationToken">取消令牌,用于取消异步操作</param>
/// <returns>异步任务,表示网络启动操作的完成状态</returns>
/// <exception cref="ArgumentNullException">当instrumentNumber为null或空时抛出</exception>
/// <exception cref="ArgumentException">当instrumentNumber格式无效时抛出</exception>
Task StartNetworkAsync(
string instrumentNumber,
RequestOptions? options = null,
CancellationToken cancellationToken = default);
/// <summary>
/// 停止网络连接
/// </summary>
/// <param name="instrumentNumber">仪器编号,用于标识特定的仪器设备</param>
/// <param name="options">请求选项,包含超时、请求头等配置</param>
/// <param name="cancellationToken">取消令牌,用于取消异步操作</param>
/// <returns>异步任务,表示网络停止操作的完成状态</returns>
/// <exception cref="ArgumentNullException">当instrumentNumber为null或空时抛出</exception>
/// <exception cref="ArgumentException">当instrumentNumber格式无效时抛出</exception>
Task StopNetworkAsync(
string instrumentNumber,
RequestOptions? options = null,
CancellationToken cancellationToken = default);
}
}

163
src/X1.DynamicClientCore/Features/Service/InstrumentProtocolClient.cs

@ -0,0 +1,163 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using X1.DynamicClientCore.Interfaces;
using X1.DynamicClientCore.Models;
namespace X1.DynamicClientCore.Features.Service
{
/// <summary>
/// 设备协议客户端实现类
/// 提供与设备设备通信的协议服务,包括设备信息获取、网络控制等功能
/// </summary>
/// <remarks>
/// 实现 IInstrumentProtocolClient 和 IBaseInstrumentClient 接口
/// 支持动态服务端点管理和HTTP通信
/// </remarks>
public class InstrumentProtocolClient : IInstrumentHttpClient
{
private readonly ILogger<InstrumentProtocolClient> _logger;
private readonly IDynamicHttpClient _dynamicHttpClient;
private readonly IServiceEndpointManager _serviceEndpointManager;
/// <summary>
/// 初始化设备协议客户端
/// </summary>
/// <param name="dynamicHttpClient">动态HTTP客户端</param>
/// <param name="serviceEndpointManager">服务端点管理器</param>
/// <param name="logger">日志记录器</param>
/// <exception cref="ArgumentNullException">当任何必需参数为null时抛出</exception>
public InstrumentProtocolClient(
IDynamicHttpClient dynamicHttpClient,
IServiceEndpointManager serviceEndpointManager,
ILogger<InstrumentProtocolClient> logger)
{
_dynamicHttpClient = dynamicHttpClient ?? throw new ArgumentNullException(nameof(dynamicHttpClient));
_serviceEndpointManager = serviceEndpointManager ?? throw new ArgumentNullException(nameof(serviceEndpointManager));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 获取设备序列号
/// </summary>
/// <param name="endpoint">服务端点配置</param>
/// <param name="options">请求选项</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>设备序列号,如果获取失败则返回空字符串</returns>
/// <exception cref="ArgumentNullException">当endpoint为null时抛出</exception>
public async Task<string> GetDeviceSerialNumberAsync(
string instrumentNumber,
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(instrumentNumber))
throw new ArgumentException("设备编号不能为空", nameof(instrumentNumber));
try
{
_logger.LogDebug("开始获取设备序列号,端点:{EndpointName}", instrumentNumber);
// 发送HTTP请求获取设备信息
var response = await _dynamicHttpClient.GetAsync<ApiActionResult<DeviceInfoResponse>>(
instrumentNumber,
"System/serial-number",
options,
cancellationToken);
if (response == null)
{
_logger.LogWarning("获取设备序列号失败:响应为空,端点:{EndpointName}", instrumentNumber);
return string.Empty;
}
if (!response.IsSuccess)
{
_logger.LogWarning("获取设备序列号失败:请求未成功,端点:{EndpointName},错误:{ErrorMessage}",
instrumentNumber, response.Message ?? "未知错误");
return string.Empty;
}
var serialNumber = response.Data?.SerialNumber ?? string.Empty;
_logger.LogInformation("成功获取设备序列号:{SerialNumber},端点:{EndpointName}",
serialNumber, instrumentNumber);
return serialNumber;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取设备序列号时发生异常,端点:{EndpointName}", instrumentNumber);
return string.Empty;
}
}
/// <summary>
/// 启动网络连接
/// </summary>
/// <param name="instrumentNumber">设备编号</param>
/// <param name="options">请求选项</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>异步任务</returns>
/// <exception cref="ArgumentException">当instrumentNumber为空或null时抛出</exception>
public async Task StartNetworkAsync(
string instrumentNumber,
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(instrumentNumber))
throw new ArgumentException("设备编号不能为空", nameof(instrumentNumber));
try
{
_logger.LogInformation("开始启动网络连接,设备编号:{InstrumentNumber}", instrumentNumber);
// TODO: 实现网络启动逻辑
// 这里需要根据具体的协议实现来调用相应的API
await Task.CompletedTask.ConfigureAwait(false);
_logger.LogInformation("网络连接启动完成,设备编号:{InstrumentNumber}", instrumentNumber);
}
catch (Exception ex)
{
_logger.LogError(ex, "启动网络连接时发生异常,设备编号:{InstrumentNumber}", instrumentNumber);
throw;
}
}
/// <summary>
/// 停止网络连接
/// </summary>
/// <param name="instrumentNumber">设备编号</param>
/// <param name="options">请求选项</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>异步任务</returns>
/// <exception cref="ArgumentException">当instrumentNumber为空或null时抛出</exception>
public async Task StopNetworkAsync(
string instrumentNumber,
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(instrumentNumber))
throw new ArgumentException("设备编号不能为空", nameof(instrumentNumber));
try
{
_logger.LogInformation("开始停止网络连接,设备编号:{InstrumentNumber}", instrumentNumber);
// TODO: 实现网络停止逻辑
// 这里需要根据具体的协议实现来调用相应的API
await Task.CompletedTask.ConfigureAwait(false);
_logger.LogInformation("网络连接停止完成,设备编号:{InstrumentNumber}", instrumentNumber);
}
catch (Exception ex)
{
_logger.LogError(ex, "停止网络连接时发生异常,设备编号:{InstrumentNumber}", instrumentNumber);
throw;
}
}
}
}

104
src/X1.DynamicClientCore/Models/ApiActionResult.cs

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
namespace X1.DynamicClientCore.Models
{
/// <summary>
/// API操作结果
/// </summary>
public class ApiActionResult
{
/// <summary>
/// 是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 错误代码
/// </summary>
public string ErrorCode { get; set; }
/// <summary>
/// HTTP状态码
/// </summary>
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
/// <summary>
/// 创建成功结果
/// </summary>
/// <param name="message">消息</param>
/// <returns>API操作结果</returns>
public static ApiActionResult Ok(string message = "操作成功")
{
return new ApiActionResult
{
IsSuccess = true,
Message = message,
ErrorCode = string.Empty,
StatusCode = HttpStatusCode.OK
};
}
}
/// <summary>
/// 带数据的API操作结果
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
public class ApiActionResult<T> : ApiActionResult
{
/// <summary>
/// 数据
/// </summary>
public T Data { get; set; }
/// <summary>
/// 创建成功结果
/// </summary>
/// <param name="data">数据</param>
/// <param name="message">消息</param>
/// <returns>API操作结果</returns>
public static ApiActionResult<T> Ok(T data, string message = "操作成功")
{
return new ApiActionResult<T>
{
IsSuccess = true,
Message = message,
ErrorCode = string.Empty,
StatusCode = HttpStatusCode.OK,
Data = data
};
}
/// <summary>
/// 创建失败结果
/// </summary>
/// <param name="message">错误消息</param>
/// <param name="errorCode">错误代码</param>
/// <param name="statusCode">HTTP状态码</param>
/// <returns>API操作结果</returns>
public static ApiActionResult<T> Error(string message, string errorCode = "ERROR", HttpStatusCode statusCode = HttpStatusCode.BadRequest)
{
return new ApiActionResult<T>
{
IsSuccess = false,
Message = message,
ErrorCode = errorCode,
StatusCode = statusCode,
Data = default
};
}
}
}

24
src/X1.DynamicClientCore/Models/DeviceInfoResponse.cs

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace X1.DynamicClientCore.Models
{
/// <summary>
/// 设备信息响应模型(用于API响应)
/// </summary>
public class DeviceInfoResponse
{
/// <summary>
/// 设备序列号(SN)
/// </summary>
public string SerialNumber { get; set; } = string.Empty;
/// <summary>
/// 获取时间
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
}

2
src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs

@ -13,11 +13,13 @@ public class CellularDeviceConfiguration : IEntityTypeConfiguration<CellularDevi
// 配置索引 // 配置索引
builder.HasIndex(d => d.SerialNumber).IsUnique().HasDatabaseName("IX_CellularDevices_SerialNumber"); builder.HasIndex(d => d.SerialNumber).IsUnique().HasDatabaseName("IX_CellularDevices_SerialNumber");
builder.HasIndex(d => d.DeviceCode).IsUnique().HasDatabaseName("IX_CellularDevices_DeviceCode");
// 配置属性 // 配置属性
builder.Property(d => d.Id).HasComment("设备ID"); builder.Property(d => d.Id).HasComment("设备ID");
builder.Property(d => d.Name).IsRequired().HasMaxLength(100).HasComment("设备名称"); builder.Property(d => d.Name).IsRequired().HasMaxLength(100).HasComment("设备名称");
builder.Property(d => d.SerialNumber).IsRequired().HasMaxLength(50).HasComment("序列号"); builder.Property(d => d.SerialNumber).IsRequired().HasMaxLength(50).HasComment("序列号");
builder.Property(d => d.DeviceCode).IsRequired().HasMaxLength(50).HasComment("设备编码");
builder.Property(d => d.Description).HasMaxLength(500).HasComment("设备描述"); builder.Property(d => d.Description).HasMaxLength(500).HasComment("设备描述");
builder.Property(d => d.AgentPort).IsRequired().HasComment("Agent端口"); builder.Property(d => d.AgentPort).IsRequired().HasComment("Agent端口");
builder.Property(d => d.IpAddress).IsRequired().HasMaxLength(45).HasComment("IP地址"); builder.Property(d => d.IpAddress).IsRequired().HasMaxLength(45).HasComment("IP地址");

16
src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs

@ -144,4 +144,20 @@ public class CellularDeviceRepository : BaseRepository<CellularDevice>, ICellula
{ {
return await QueryRepository.AnyAsync(d => d.SerialNumber == serialNumber, cancellationToken: cancellationToken); return await QueryRepository.AnyAsync(d => d.SerialNumber == serialNumber, cancellationToken: cancellationToken);
} }
/// <summary>
/// 检查设备编码是否存在
/// </summary>
public async Task<bool> DeviceCodeExistsAsync(string deviceCode, CancellationToken cancellationToken = default)
{
return await QueryRepository.AnyAsync(d => d.DeviceCode == deviceCode, cancellationToken: cancellationToken);
}
/// <summary>
/// 获取设备总数
/// </summary>
public async Task<int> GetDeviceCountAsync(CancellationToken cancellationToken = default)
{
return await QueryRepository.CountAsync(d => true, cancellationToken: cancellationToken);
}
} }

3
src/X1.WebAPI/Program.cs

@ -21,6 +21,7 @@ using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
using CellularManagement.Domain.Options; using CellularManagement.Domain.Options;
using X1.WebAPI.Extensions; using X1.WebAPI.Extensions;
using X1.DynamicClientCore.Extensions;
// 创建 Web 应用程序构建器 // 创建 Web 应用程序构建器
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -40,6 +41,8 @@ builder.Services.AddInfrastructure(builder.Configuration);
// 包括命令/查询处理、验证等服务 // 包括命令/查询处理、验证等服务
builder.Services.AddApplication(builder.Configuration); builder.Services.AddApplication(builder.Configuration);
builder.Services.AddDynamicServiceClient();
// 注册表现层服务 // 注册表现层服务
// 包括控制器、视图、中间件等表现层相关服务 // 包括控制器、视图、中间件等表现层相关服务
builder.Services.AddPresentation(); builder.Services.AddPresentation();

142
src/X1.WebUI/src/pages/instruments/DeviceForm.tsx

@ -3,12 +3,11 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import { CreateDeviceRequest, UpdateDeviceRequest } from '@/services/instrumentService'; import { CreateDeviceRequest, UpdateDeviceRequest } from '@/services/instrumentService';
interface DeviceFormProps { interface DeviceFormProps {
onSubmit: (data: CreateDeviceRequest | UpdateDeviceRequest) => void; onSubmit: (data: CreateDeviceRequest | UpdateDeviceRequest) => void;
initialData?: Partial<CreateDeviceRequest>; initialData?: Partial<CreateDeviceRequest> & { deviceId?: string };
isEdit?: boolean; isEdit?: boolean;
isSubmitting?: boolean; isSubmitting?: boolean;
} }
@ -16,18 +15,31 @@ interface DeviceFormProps {
export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: DeviceFormProps) { export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: DeviceFormProps) {
const [formData, setFormData] = React.useState<CreateDeviceRequest>({ const [formData, setFormData] = React.useState<CreateDeviceRequest>({
deviceName: initialData?.deviceName || '', deviceName: initialData?.deviceName || '',
serialNumber: initialData?.serialNumber || '',
description: initialData?.description || '', description: initialData?.description || '',
ipAddress: initialData?.ipAddress || '', ipAddress: initialData?.ipAddress || '',
agentPort: initialData?.agentPort || 8080, agentPort: initialData?.agentPort || 8080,
isEnabled: initialData?.isEnabled ?? true, isEnabled: true,
isRunning: initialData?.isRunning ?? false isRunning: false
}); });
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (isSubmitting) return; // 防止重复提交 if (isSubmitting) return; // 防止重复提交
onSubmit(formData);
if (isEdit) {
// 编辑模式:只提交可修改的字段
const updateData: UpdateDeviceRequest = {
deviceId: initialData?.deviceId || '',
deviceName: formData.deviceName,
description: formData.description,
isEnabled: formData.isEnabled,
isRunning: formData.isRunning
};
onSubmit(updateData);
} else {
// 创建模式:提交所有字段
onSubmit(formData);
}
}; };
return ( return (
@ -44,17 +56,65 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu
/> />
</div> </div>
<div className="space-y-2"> {!isEdit && (
<Label htmlFor="serialNumber"></Label> <>
<Input <div className="space-y-2">
id="serialNumber" <Label htmlFor="ipAddress">IP地址</Label>
value={formData.serialNumber} <Input
onChange={e => setFormData({ ...formData, serialNumber: e.target.value })} id="ipAddress"
placeholder="请输入设备序列号" type="text"
required value={formData.ipAddress}
disabled={isSubmitting} onChange={e => setFormData({ ...formData, ipAddress: e.target.value })}
/> placeholder="例如: 192.168.1.100"
</div> required
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label htmlFor="agentPort">Agent端口</Label>
<Input
id="agentPort"
type="number"
value={formData.agentPort}
onChange={e => setFormData({ ...formData, agentPort: parseInt(e.target.value) || 0 })}
placeholder="例如: 8080"
min="1"
max="65535"
required
disabled={isSubmitting}
/>
</div>
</>
)}
{isEdit && (
<>
<div className="space-y-2">
<Label htmlFor="ipAddress">IP地址</Label>
<Input
id="ipAddress"
type="text"
value={formData.ipAddress}
disabled
className="bg-gray-100"
/>
<p className="text-sm text-gray-500">IP地址不可修改</p>
</div>
<div className="space-y-2">
<Label htmlFor="agentPort">Agent端口</Label>
<Input
id="agentPort"
type="number"
value={formData.agentPort}
disabled
className="bg-gray-100"
/>
<p className="text-sm text-gray-500">Agent端口不可修改</p>
</div>
</>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="description"></Label> <Label htmlFor="description"></Label>
@ -68,53 +128,7 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="ipAddress">IP地址</Label>
<Input
id="ipAddress"
type="text"
value={formData.ipAddress}
onChange={e => setFormData({ ...formData, ipAddress: e.target.value })}
placeholder="例如: 192.168.1.100"
required
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label htmlFor="agentPort">Agent端口</Label>
<Input
id="agentPort"
type="number"
value={formData.agentPort}
onChange={e => setFormData({ ...formData, agentPort: parseInt(e.target.value) || 0 })}
placeholder="例如: 8080"
min="1"
max="65535"
required
disabled={isSubmitting}
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="isEnabled"
checked={formData.isEnabled}
onCheckedChange={(checked) => setFormData({ ...formData, isEnabled: checked as boolean })}
disabled={isSubmitting}
/>
<Label htmlFor="isEnabled"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="isRunning"
checked={formData.isRunning}
onCheckedChange={(checked) => setFormData({ ...formData, isRunning: checked as boolean })}
disabled={isSubmitting}
/>
<Label htmlFor="isRunning"></Label>
</div>
<Button type="submit" className="w-full" disabled={isSubmitting}> <Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : (isEdit ? '更新设备' : '创建设备')} {isSubmitting ? '提交中...' : (isEdit ? '更新设备' : '创建设备')}

60
src/X1.WebUI/src/pages/instruments/DevicesView.tsx

@ -159,40 +159,43 @@ export default function DevicesView() {
} }
}; };
const handleCreate = async (data: CreateDeviceRequest) => { const handleCreate = async (data: CreateDeviceRequest | UpdateDeviceRequest) => {
if (isSubmitting) return; // 防止重复提交 if (isSubmitting) return; // 防止重复提交
console.log('开始创建设备:', data); // 确保是创建设备请求
setIsSubmitting(true); if ('ipAddress' in data && 'agentPort' in data) {
try { console.log('开始创建设备:', data);
const result = await createDevice(data); setIsSubmitting(true);
console.log('创建设备结果:', result); try {
const result = await createDevice(data as CreateDeviceRequest);
if (result.isSuccess) { console.log('创建设备结果:', result);
toast({
title: "创建成功", if (result.isSuccess) {
description: `设备 "${data.deviceName}" 创建成功`, toast({
}); title: "创建成功",
setOpen(false); description: `设备 "${data.deviceName}" 创建成功`,
fetchDevices(); });
} else { setOpen(false);
const errorMessage = result.errorMessages?.join(', ') || "创建设备时发生错误"; fetchDevices();
console.error('创建设备失败:', errorMessage, result); } else {
const errorMessage = result.errorMessages?.join(', ') || "创建设备时发生错误";
console.error('创建设备失败:', errorMessage, result);
toast({
title: "创建失败",
description: errorMessage,
variant: "destructive",
});
}
} catch (error) {
console.error('创建设备异常:', error);
toast({ toast({
title: "创建失败", title: "创建失败",
description: errorMessage, description: "网络错误,请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} finally {
setIsSubmitting(false);
} }
} catch (error) {
console.error('创建设备异常:', error);
toast({
title: "创建失败",
description: "网络错误,请稍后重试",
variant: "destructive",
});
} finally {
setIsSubmitting(false);
} }
}; };
@ -385,10 +388,9 @@ export default function DevicesView() {
<DeviceForm <DeviceForm
onSubmit={(data) => handleUpdate(data as UpdateDeviceRequest)} onSubmit={(data) => handleUpdate(data as UpdateDeviceRequest)}
initialData={selectedDevice ? { initialData={selectedDevice ? {
deviceId: selectedDevice.deviceId,
deviceName: selectedDevice.deviceName, deviceName: selectedDevice.deviceName,
serialNumber: selectedDevice.serialNumber,
description: selectedDevice.description, description: selectedDevice.description,
agentPort: selectedDevice.agentPort,
isEnabled: selectedDevice.isEnabled, isEnabled: selectedDevice.isEnabled,
isRunning: selectedDevice.isRunning isRunning: selectedDevice.isRunning
} : undefined} } : undefined}

7
src/X1.WebUI/src/services/instrumentService.ts

@ -13,6 +13,7 @@ export interface Device {
deviceId: string; deviceId: string;
deviceName: string; deviceName: string;
serialNumber: string; serialNumber: string;
deviceCode: string;
description: string; description: string;
agentPort: number; agentPort: number;
isEnabled: boolean; isEnabled: boolean;
@ -42,7 +43,6 @@ export interface GetDevicesResponse {
// 创建设备请求接口 - 匹配后端CreateDeviceCommand // 创建设备请求接口 - 匹配后端CreateDeviceCommand
export interface CreateDeviceRequest { export interface CreateDeviceRequest {
deviceName: string; deviceName: string;
serialNumber: string;
description?: string; description?: string;
ipAddress: string; ipAddress: string;
agentPort: number; agentPort: number;
@ -55,6 +55,7 @@ export interface CreateDeviceResponse {
deviceId: string; deviceId: string;
deviceName: string; deviceName: string;
serialNumber: string; serialNumber: string;
deviceCode: string;
description: string; description: string;
agentPort: number; agentPort: number;
isEnabled: boolean; isEnabled: boolean;
@ -66,10 +67,7 @@ export interface CreateDeviceResponse {
export interface UpdateDeviceRequest { export interface UpdateDeviceRequest {
deviceId: string; deviceId: string;
deviceName: string; deviceName: string;
serialNumber: string;
description?: string; description?: string;
ipAddress: string;
agentPort: number;
isEnabled?: boolean; isEnabled?: boolean;
isRunning?: boolean; isRunning?: boolean;
} }
@ -79,6 +77,7 @@ export interface UpdateDeviceResponse {
deviceId: string; deviceId: string;
deviceName: string; deviceName: string;
serialNumber: string; serialNumber: string;
deviceCode: string;
description: string; description: string;
agentPort: number; agentPort: number;
isEnabled: boolean; isEnabled: boolean;

863
src/modify.md

@ -1,5 +1,132 @@
# 修改记录 # 修改记录
## 2024-12-19 仪器客户端接口规范化重构
### 修改文件
- `X1.DynamicClientCore/Features/IBaseInstrumentClient.cs` - 规范化基础仪器客户端接口
- `X1.DynamicClientCore/Features/IInstrumentProtocolClient.cs` - 规范化仪器协议客户端接口
- `X1.DynamicClientCore/Features/IInstrumentHttpClient.cs` - 创建仪器HTTP客户端接口
- `X1.DynamicClientCore/Features/Service/InstrumentProtocolClient.cs` - 更新实现类方法名
### 修改内容
#### 1. IBaseInstrumentClient 接口规范化
- **添加完整注释**:为接口和方法添加详细的XML文档注释
- **方法命名规范化**:`GetDeviceSerialNumber` → `GetDeviceSerialNumberAsync`
- **参数文档**:为所有参数添加详细的说明文档
- **异常文档**:添加可能抛出的异常类型说明
- **接口说明**:添加接口用途和设计理念的说明
#### 2. IInstrumentProtocolClient 接口规范化
- **添加完整注释**:为接口和所有方法添加详细的XML文档注释
- **方法命名规范化**
- `StartNetwork``StartNetworkAsync`
- `StopNetwork``StopNetworkAsync`
- **参数命名规范化**:`InstrumentNumber` → `instrumentNumber`(遵循C#命名规范)
- **参数文档**:为所有参数添加详细的说明文档
- **异常文档**:添加可能抛出的异常类型说明
- **接口说明**:添加接口用途和设计理念的说明
#### 3. IInstrumentHttpClient 接口创建
- **接口设计**:创建组合接口,继承自 `IBaseInstrumentClient``IInstrumentProtocolClient`
- **功能组合**:提供完整的仪器设备通信服务
- **接口说明**:添加详细的接口用途和设计理念说明
- **方法继承**:明确说明继承的方法和功能
#### 4. InstrumentProtocolClient 实现类更新
- **方法名更新**:更新所有方法名以匹配接口定义
- **接口实现**:改为实现 `IInstrumentHttpClient` 接口
- **方法签名**:确保方法签名与接口定义完全一致
### 技术特性
- **命名规范**:遵循C#异步方法命名规范(Async后缀)
- **接口设计**:使用接口组合模式,提供灵活的功能组合
- **文档完整**:为所有接口和方法提供完整的XML文档注释
- **类型安全**:确保接口定义和实现的一致性
- **异常处理**:明确定义可能抛出的异常类型
### 设计理念
- **职责分离**:基础功能、协议功能、HTTP功能分别定义
- **接口组合**:通过接口继承实现功能组合
- **命名一致性**:统一的命名规范和异步方法约定
- **文档驱动**:完整的文档注释,便于理解和使用
### 用户体验改进
- **代码可读性**:清晰的接口定义和方法命名
- **开发体验**:完整的IntelliSense支持和文档提示
- **维护性**:清晰的接口层次和职责划分
- **扩展性**:灵活的接口组合,便于功能扩展
## 2024-12-19 InstrumentProtocolClient 规范化重构
### 修改文件
- `X1.DynamicClientCore/Features/Service/InstrumentProtocolClient.cs` - 规范化仪器协议客户端实现
### 修改内容
#### 1. 代码结构优化
- **构造函数规范化**:修复参数命名不一致问题,使用统一的命名规范
- **字段重命名**:`_dynamicHttp` → `_dynamicHttpClient`,`_endpointManager` → `_serviceEndpointManager`
- **参数验证**:添加空值检查和异常抛出,确保依赖注入的正确性
- **代码格式**:统一代码格式和缩进,提高可读性
#### 2. 方法规范化
- **GetDeviceSerialNumber方法**
- 添加完整的XML文档注释
- 改进错误处理逻辑,区分不同类型的错误
- 优化日志记录,使用结构化日志
- 修复ApiActionResult字段访问错误(ErrorMessage → Message)
- 添加参数验证和异常处理
- **StartNetwork方法**
- 添加完整的XML文档注释
- 添加参数验证(仪器编号不能为空)
- 添加异常处理和日志记录
- 添加TODO注释,说明需要实现的业务逻辑
- **StopNetwork方法**
- 添加完整的XML文档注释
- 添加参数验证(仪器编号不能为空)
- 添加异常处理和日志记录
- 添加TODO注释,说明需要实现的业务逻辑
#### 3. 错误处理改进
- **分层异常处理**:区分WebSocket异常和其他异常
- **详细错误日志**:提供更详细的错误信息和上下文
- **参数验证**:添加输入参数的空值检查和验证
- **异常传播**:合理处理异常传播,避免不必要的异常捕获
#### 4. 日志记录优化
- **结构化日志**:使用结构化日志记录,提高日志可读性
- **日志级别**:合理使用Debug、Info、Warning、Error级别
- **上下文信息**:添加连接ID、端点名称等上下文信息
- **性能监控**:添加关键操作的性能监控日志
#### 5. 代码质量提升
- **XML文档注释**:为类、方法、参数添加完整的XML文档注释
- **命名规范**:统一变量和方法命名规范
- **代码组织**:优化代码结构,提高可维护性
- **类型安全**:确保类型安全和空值处理
### 技术特性
- **依赖注入**:正确使用依赖注入模式
- **异步编程**:使用async/await模式,支持取消令牌
- **错误处理**:完整的异常处理和错误恢复机制
- **日志记录**:结构化日志记录,便于监控和调试
- **参数验证**:输入参数验证,确保数据完整性
### 用户体验改进
- **错误提示**:提供更清晰的错误信息和处理建议
- **性能监控**:通过日志监控系统性能和响应时间
- **调试支持**:详细的调试日志,便于问题排查
- **代码可读性**:清晰的代码结构和注释,便于维护
### 后续开发建议
- **业务逻辑实现**:根据具体需求实现StartNetwork和StopNetwork方法
- **单元测试**:为重构后的方法添加单元测试
- **集成测试**:测试与其他组件的集成
- **性能优化**:根据实际使用情况优化性能
## 2024-12-19 RAN配置列表状态开关优化 ## 2024-12-19 RAN配置列表状态开关优化
### 修改文件 ### 修改文件
@ -2153,74 +2280,684 @@ chore: 更新.gitignore忽略日志文件
- 添加X1.WebAPI/logs/目录到忽略列表 - 添加X1.WebAPI/logs/目录到忽略列表
- 添加*.log文件模式到忽略列表 - 添加*.log文件模式到忽略列表
- 从版本控制中移除已跟踪的日志文件 - 从版本控制中移除已跟踪的日志文件
```
### 状态 ## 2024-12-19 添加设备编码字段
✅ 完成 - 所有代码已成功提交、推送和同步到远程仓库
## 2024-12-19 修复网络栈配置更新命令处理器绑定关系更新逻辑 ### 修改文件
- `X1.Domain/Entities/Device/CellularDevice.cs` - 在蜂窝设备实体中添加设备编码字段
### 修改概述 ### 修改内容
修复了 `UpdateNetworkStackConfigCommandHandler` 中绑定关系更新逻辑的问题。原代码总是创建新的绑定关系,但实际上应该根据索引是否存在来决定是更新现有绑定关系还是创建新的绑定关系,并且应该跳过无变化的绑定关系。同时移除了响应类中不必要的时间字段。
### 修改的文件 #### 1. 添加设备编码属性
- **字段名称**:`DeviceCode`
- **字段类型**:`string`
- **约束条件**
- `[Required]` - 必填字段
- `[MaxLength(50)]` - 最大长度50个字符
- **访问修饰符**:`private set` - 只读属性,通过方法修改
#### 2. 更新创建方法
- **方法签名**:`Create` 方法添加 `deviceCode` 参数
- **参数位置**:在 `serialNumber` 参数之后,`description` 参数之前
- **参数验证**:通过 `[Required]``[MaxLength(50)]` 进行验证
- **属性赋值**:在创建设备时设置 `DeviceCode` 属性
#### 3. 更新更新方法
- **方法签名**:`Update` 方法添加 `deviceCode` 参数
- **参数位置**:在 `serialNumber` 参数之后,`description` 参数之前
- **属性赋值**:在更新设备时设置 `DeviceCode` 属性
#### 1. `X1.Application/Features/NetworkStackConfigs/Commands/UpdateNetworkStackConfig/UpdateNetworkStackConfigCommandHandler.cs` ### 技术特性
- **问题描述**:原代码总是创建新的绑定关系,无法处理用户只想修改现有绑定关系的 `cnId``imsId` 的场景,也没有检查是否有实际变化 - **数据完整性**:设备编码作为必填字段,确保每个设备都有唯一标识
- **修复内容** - **长度限制**:最大50个字符,适合各种编码格式
- 检查是否存在相同索引的现有绑定关系 - **封装性**:使用私有setter,确保数据只能通过方法修改
- 如果存在,检查是否有实际变化(`cnId` 和 `imsId` 是否相同) - **一致性**:与现有字段(如SerialNumber)保持相同的设计模式
- 如果无变化,跳过更新并记录日志
- 如果有变化,则更新现有的绑定关系(使用 `Update` 方法)
- 如果不存在,则创建新的绑定关系
- 修复了 `CreatedAt` 属性访问错误(实体没有该属性)
#### 2. `X1.Application/Features/NetworkStackConfigs/Commands/UpdateNetworkStackConfig/UpdateNetworkStackConfigResponse.cs`
- **问题描述**:响应类中包含不必要的时间字段
- **修复内容**
- 移除了 `UpdateNetworkStackConfigResponse` 中的 `UpdatedAt` 字段
- 移除了 `UpdateStackCoreIMSBindingResponseItem` 中的 `CreatedAt``UpdatedAt` 字段
#### 3. `X1.Application/Features/NetworkStackConfigs/Commands/CreateNetworkStackConfig/CreateNetworkStackConfigResponse.cs`
- **问题描述**:响应类中包含不必要的时间字段
- **修复内容**
- 移除了 `CreateStackCoreIMSBindingResponseItem` 中的 `CreatedAt` 字段
#### 4. `X1.Application/Features/NetworkStackConfigs/Commands/CreateNetworkStackConfig/CreateNetworkStackConfigCommandHandler.cs`
- **问题描述**:命令处理器中设置了不必要的时间字段
- **修复内容**
- 移除了构建响应项时的 `CreatedAt` 字段设置
#### 5. `X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsResponse.cs`
- **问题描述**:响应类中包含不必要的时间字段
- **修复内容**
- 移除了 `GetNetworkStackConfigsBindingResponseItem` 中的 `CreatedAt``UpdatedAt` 字段
#### 6. `X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQueryHandler.cs`
- **问题描述**:查询处理器中设置了不必要的时间字段
- **修复内容**
- 移除了构建响应项时的 `CreatedAt` 字段设置
#### 7. `X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdResponse.cs`
- **问题描述**:响应类中包含不必要的时间字段
- **修复内容**
- 移除了 `GetNetworkStackConfigByIdBindingResponseItem` 中的 `CreatedAt``UpdatedAt` 字段
#### 8. `X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdQueryHandler.cs`
- **问题描述**:查询处理器中设置了不必要的时间字段
- **修复内容**
- 移除了构建响应项时的 `CreatedAt` 字段设置
### 技术细节 ### 业务价值
- **更新策略**:根据索引是否存在决定更新或创建 - **设备标识**:提供额外的设备标识方式,便于设备管理
- **变化检测**:比较现有绑定关系的 `cnId``imsId` 与请求中的值 - **系统集成**:支持与其他系统的设备编码对接
- **业务逻辑**:支持修改现有绑定关系的核心网配置ID或IMS配置ID - **数据追溯**:增强设备数据的可追溯性
- **数据完整性**:保持索引唯一性约束,避免不必要的更新操作 - **管理便利**:提供更灵活的设备识别和管理方式
- **响应优化**:移除不必要的时间字段,简化响应结构
### 影响范围
- **实体模型**:CellularDevice实体增加了DeviceCode字段
- **创建操作**:所有创建设备的操作都需要提供设备编码
- **更新操作**:所有更新设备信息的操作都需要提供设备编码
- **数据验证**:确保设备编码的完整性和有效性
### 后续工作建议
1. **数据库迁移**:需要创建数据库迁移来添加DeviceCode字段
2. **API更新**:更新相关的API接口以支持设备编码字段
3. **前端界面**:更新前端界面以显示和编辑设备编码
4. **数据验证**:在应用层添加设备编码的业务验证逻辑
5. **测试用例**:为设备编码字段添加相应的测试用例
## 2024-12-19 修复设备命令处理器中的deviceCode字段
### 修改文件
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs` - 添加设备编码字段
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs` - 添加设备编码字段
- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs` - 添加设备编码字段
- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceResponse.cs` - 添加设备编码字段
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修复设备编码字段处理
- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs` - 修复设备编码字段处理
### 修改内容
#### 1. 命令类修复
- **CreateDeviceCommand**
- 添加 `DeviceCode` 属性
- 添加 `[Required]``[MaxLength(50)]` 验证特性
- 位置:在 `SerialNumber` 字段之后,`Description` 字段之前
- **UpdateDeviceCommand**
- 添加 `DeviceCode` 属性
- 添加 `[Required]``[MaxLength(50)]` 验证特性
- 位置:在 `SerialNumber` 字段之后,`Description` 字段之前
#### 2. 响应类修复
- **CreateDeviceResponse**
- 添加 `DeviceCode` 属性
- 位置:在 `SerialNumber` 字段之后,`Description` 字段之前
- **UpdateDeviceResponse**
- 添加 `DeviceCode` 属性
- 位置:在 `SerialNumber` 字段之后,`Description` 字段之前
#### 3. 命令处理器修复
- **CreateDeviceCommandHandler**
- 在 `CellularDevice.Create()` 方法调用中添加 `deviceCode: request.DeviceCode` 参数
- 在响应构建中添加 `DeviceCode = device.DeviceCode` 字段
- **UpdateDeviceCommandHandler**
- 在 `existingDevice.Update()` 方法调用中添加 `deviceCode: request.DeviceCode` 参数
- 在响应构建中添加 `DeviceCode = existingDevice.DeviceCode` 字段
### 技术特性
- **数据验证**:设备编码字段为必填,最大长度50个字符
- **参数传递**:正确传递设备编码参数到实体创建和更新方法
- **响应映射**:在响应中包含设备编码字段
- **一致性**:与实体模型中的DeviceCode字段保持一致
### 业务价值
- **完整性**:确保设备编码在整个创建和更新流程中正确传递
- **验证**:在应用层提供设备编码的验证
- **API一致性**:确保API接口与实体模型保持一致
- **用户体验**:提供完整的设备编码管理功能
### 影响范围
- **创建操作**:设备创建API现在支持设备编码字段
- **更新操作**:设备更新API现在支持设备编码字段
- **数据验证**:应用层验证确保设备编码的完整性
- **响应数据**:API响应包含设备编码信息
### 测试建议
1. 测试设备创建API,验证设备编码字段的处理
2. 测试设备更新API,验证设备编码字段的处理
3. 验证设备编码字段的验证规则
4. 确认API响应包含正确的设备编码信息
5. 测试设备编码字段的边界值(空值、最大长度等)
## 2025-07-28 - 自动生成设备编号,移除用户手动输入
### 修改原因
根据用户需求,设备编号应该自动生成,格式为 `DEV-001-SN`(其中001是序号,SN是序列号),序号从001开始根据数据库中的设备数量自动递增,不需要用户手动输入。
### 修改文件
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs` - 移除DeviceCode字段
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 添加自动生成设备编号逻辑
- `X1.Domain/Repositories/Device/ICellularDeviceRepository.cs` - 添加GetDeviceCountAsync方法
- `X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs` - 实现GetDeviceCountAsync方法
- `X1.WebUI/src/services/instrumentService.ts` - 移除DeviceCode字段
- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 移除设备编码输入字段
- `X1.WebUI/src/pages/instruments/DevicesView.tsx` - 更新类型检查逻辑
### 修改内容
#### 1. 后端命令类修改
- **CreateDeviceCommand**
- 删除 `DeviceCode` 属性及其验证特性
- 用户不再需要手动输入设备编号
#### 2. 后端命令处理器修改
- **CreateDeviceCommandHandler**
- 添加 `GenerateDeviceCodeAsync` 方法,自动生成设备编号
- 设备编号格式:`DEV-{序号}-{序列号}`,序号为3位数字(001, 002, ...)
- 序号根据数据库中的设备总数自动递增
- 在获取序列号成功后,移除并重新添加endpointManager
- 修改 `CreateAndSaveDeviceAsync` 方法签名,添加deviceCode参数
#### 3. 仓储接口扩展
- **ICellularDeviceRepository**
- 添加 `GetDeviceCountAsync` 方法
- 用于获取当前设备总数,计算下一个序号
#### 4. 仓储实现
- **CellularDeviceRepository**
- 实现 `GetDeviceCountAsync` 方法
- 使用 `QueryRepository.CountAsync` 获取设备总数
#### 5. 前端接口修改
- **CreateDeviceRequest**
- 删除 `deviceCode` 字段
- 用户不再需要提供设备编号
#### 6. 前端表单修改
- **DeviceForm**
- 移除设备编码输入字段
- 更新表单状态管理,移除deviceCode相关代码
#### 7. 前端页面修改
- **DevicesView**
- 更新类型检查逻辑,移除对deviceCode的检查
- 修复语法错误
### 技术特性
- **自动编号生成**:根据设备总数自动生成序号
- **格式规范**:设备编号格式为 `DEV-001-SN`
- **序号递增**:序号从001开始,每次递增1
- **endpointManager管理**:在获取序列号成功后重新管理endpoint
- **用户友好**:用户无需手动输入设备编号
### 业务价值
- **简化操作**:用户无需手动输入设备编号
- **避免重复**:自动生成的编号确保唯一性
- **规范管理**:统一的编号格式便于管理
- **减少错误**:避免用户输入错误
### 影响范围
- **创建操作**:设备创建时自动生成设备编号
- **前端界面**:移除设备编码输入字段
- **API接口**:更新请求模型,移除deviceCode字段
- **数据库操作**:添加获取设备总数的方法
### 设备编号生成逻辑
1. **获取设备总数**:调用 `GetDeviceCountAsync` 获取当前设备数量
2. **计算序号**:设备总数 + 1
3. **格式化序号**:使用 `ToString("D3")` 格式化为3位数字
4. **组装编号**:`DEV-{序号}-{序列号}`
### 注意事项
- 设备编号格式:`DEV-001-SN`、`DEV-002-SN` 等
- 序号从001开始,最大到999
- 如果设备数量超过999,需要扩展序号位数
- 设备编号在创建时生成,后续不可修改
## 2025-07-28 - 添加设备编号重复检查
### 修改原因
根据用户需求,在创建设备时不仅需要检查序列号是否重复,还需要检查设备编号(DeviceCode)是否重复,确保设备编号的唯一性。
### 修改文件
- `X1.Domain/Repositories/Device/ICellularDeviceRepository.cs` - 添加DeviceCodeExistsAsync方法
- `X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs` - 实现DeviceCodeExistsAsync方法
- `X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs` - 添加DeviceCode唯一索引
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 添加设备编号重复检查逻辑
### 修改内容
#### 1. 仓储接口修改
- **ICellularDeviceRepository**
- 添加 `DeviceCodeExistsAsync` 方法
- 用于检查设备编号是否已存在
#### 2. 仓储实现修改
- **CellularDeviceRepository**
- 实现 `DeviceCodeExistsAsync` 方法
- 使用 `QueryRepository.AnyAsync` 检查设备编号是否存在
#### 3. 数据库配置修改
- **CellularDeviceConfiguration**
- 添加 `DeviceCode` 字段的唯一索引
- 添加 `DeviceCode` 字段的属性配置
- 确保数据库层面的唯一性约束
#### 4. 命令处理器修改
- **CreateDeviceCommandHandler**
- 在检查序列号重复后,添加设备编号重复检查
- 如果设备编号已存在,返回相应的错误信息
- 确保创建设备时两个字段都是唯一的
### 技术特性
- **双重唯一性检查**:确保序列号和设备编号都是唯一的
- **数据库约束**:在数据库层面添加唯一索引
- **应用层验证**:在应用层进行重复性检查
- **错误处理**:提供清晰的错误信息
### 业务价值
- **数据完整性**:确保设备编号的唯一性
- **业务逻辑**:符合设备管理的业务规则
- **用户体验**:提供明确的错误提示
- **数据一致性**:防止重复设备编号的创建
### 影响范围
- **创建操作**:设备创建时检查设备编号重复
- **数据库结构**:添加设备编号唯一索引
- **错误处理**:增加设备编号重复的错误处理
- **数据验证**:确保设备编号的唯一性
### 检查顺序
1. **序列号检查**:首先检查序列号是否重复
2. **设备编号检查**:然后检查设备编号是否重复
3. **创建设备**:只有两个检查都通过才创建设备
### 注意事项
- 需要创建数据库迁移来添加新的唯一索引
- 如果数据库中已存在重复的设备编号,迁移可能会失败
- 建议在应用此修改前清理重复数据
## 2025-07-28 - 限制UpdateDeviceResponse只能修改特定字段
### 修改原因
根据用户需求,设备更新时只能修改 `IsEnabled`、`IsRunning`、`Description` 和 `DeviceName` 这四个字段,其他字段(如序列号、设备编码、IP地址、端口等)不能修改。
### 修改文件
- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs` - 只保留可修改的字段
- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs` - 只更新允许修改的字段
- `X1.WebUI/src/services/instrumentService.ts` - 更新前端接口定义
- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 编辑模式下只显示可修改的字段
- `X1.WebUI/src/pages/instruments/DevicesView.tsx` - 更新编辑设备时的数据传递
### 修改内容
#### 1. 后端命令类修改
- **UpdateDeviceCommand**
- 只保留 `DeviceId`、`DeviceName`、`Description`、`IsEnabled`、`IsRunning` 字段
- 删除 `DeviceCode`、`IpAddress`、`AgentPort` 字段
- 确保只有这四个字段可以被用户修改
#### 2. 后端命令处理器修改
- **UpdateDeviceCommandHandler**
- 在更新设备时,只更新允许修改的字段
- 其他字段(序列号、设备编码、IP地址、端口)保持原有值不变
- 添加注释说明哪些字段保持不变
#### 3. 前端接口修改
- **UpdateDeviceRequest**
- 只包含 `deviceId`、`deviceName`、`description`、`isEnabled`、`isRunning` 字段
- 删除 `deviceCode`、`ipAddress`、`agentPort` 字段
#### 4. 前端表单修改
- **DeviceForm**
- 在编辑模式下,只显示可修改的字段
- 创建设备时显示所有字段
- 根据 `isEdit` 属性动态显示不同的字段
- 更新表单提交逻辑,区分创建和编辑模式
#### 5. 前端页面修改
- **DevicesView**
- 更新编辑设备时的初始数据传递
- 只传递可修改的字段给表单组件
### 技术特性
- **字段限制**:严格限制只能修改指定的四个字段
- **数据保护**:防止用户意外修改关键字段(如序列号、IP地址等)
- **界面适配**:根据操作模式动态显示不同的表单字段
- **类型安全**:更新所有相关的TypeScript接口定义
### 业务价值
- **数据安全**:防止用户误操作修改关键设备信息
- **用户体验**:简化编辑界面,只显示可修改的字段
- **业务逻辑**:符合设备管理的业务规则
- **一致性**:确保设备关键信息不被意外修改
### 影响范围
- **更新操作**:设备更新时只能修改指定的四个字段
- **前端界面**:编辑模式下只显示可修改的字段
- **API接口**:更新请求模型,只包含可修改的字段
- **数据保护**:确保设备关键信息的安全性
### 可修改字段
1. **DeviceName** - 设备名称
2. **Description** - 设备描述
3. **IsEnabled** - 是否启用
4. **IsRunning** - 设备运行状态
### 不可修改字段
1. **SerialNumber** - 设备序列号(通过IP自动获取)
2. **DeviceCode** - 设备编码
3. **IpAddress** - IP地址
4. **AgentPort** - Agent端口
### 注意事项
- 创建设备时仍然需要提供所有必要字段
- 更新设备时只允许修改指定的四个字段
- 其他字段在更新时会保持原有值不变
- 前端界面会根据操作模式动态调整显示内容
## 2025-07-28 - 删除CreateDeviceCommand中的SerialNumber字段,改为通过IP地址自动获取
### 修改原因
根据用户需求,设备序列号不应该由用户手动填写,而应该通过设备的IP地址自动获取。
### 修改文件
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs` - 删除SerialNumber字段
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 添加通过IP获取序列号的逻辑
- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs` - 删除SerialNumber字段
- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs` - 移除序列号验证和更新逻辑
- `X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs` - 注册IBaseInstrumentClient服务
- `X1.WebUI/src/services/instrumentService.ts` - 更新前端接口定义
- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 更新前端表单
- `X1.WebUI/src/pages/instruments/DevicesView.tsx` - 更新前端页面
### 修改内容
#### 1. 后端命令类修改
- **CreateDeviceCommand**
- 删除 `SerialNumber` 属性及其验证特性
- 保留 `DeviceCode` 属性用于设备标识
- **UpdateDeviceCommand**
- 删除 `SerialNumber` 属性及其验证特性
- 保留 `DeviceCode` 属性用于设备标识
#### 2. 后端命令处理器修改
- **CreateDeviceCommandHandler**
- 添加 `IBaseInstrumentClient` 依赖注入
- 在创建设备前通过IP地址和端口获取设备序列号
- 使用 `ServiceEndpoint` 配置设备连接信息
- 调用 `GetDeviceSerialNumberAsync` 方法获取序列号
- 添加序列号获取失败的错误处理
- 更新日志记录,移除对SerialNumber的引用
- **UpdateDeviceCommandHandler**
- 移除序列号验证逻辑
- 在更新设备时保持原有序列号不变
- 更新日志记录,移除对SerialNumber的引用
#### 3. 前端接口修改
- **CreateDeviceRequest**
- 删除 `serialNumber` 字段
- 保留 `deviceCode` 字段
- **UpdateDeviceRequest**
- 删除 `serialNumber` 字段
- 添加 `deviceCode` 字段
- **Device**
- 添加 `deviceCode` 字段
- **CreateDeviceResponse**
- 添加 `deviceCode` 字段
- **UpdateDeviceResponse**
- 添加 `deviceCode` 字段
#### 4. 前端表单修改
- **DeviceForm**
- 删除序列号输入字段
- 添加设备编码输入字段
- 更新表单状态管理
#### 5. 服务注册修改
- **ServiceCollectionExtensions**
- 注册 `IBaseInstrumentClient` 服务
- 注册 `IInstrumentHttpClient` 服务
- 使用 `InstrumentProtocolClient` 作为实现类
#### 6. 前端页面修改
- **DevicesView**
- 更新编辑设备时的初始数据传递
- 移除对序列号字段的引用
### 技术特性
- **自动序列号获取**:通过设备的IP地址和端口自动获取序列号
- **错误处理**:当无法获取序列号时提供友好的错误信息
- **服务集成**:集成 `IBaseInstrumentClient` 服务进行设备通信
- **类型安全**:更新所有相关的TypeScript接口定义
### 业务价值
- **用户体验**:用户无需手动输入序列号,减少输入错误
- **数据准确性**:序列号直接从设备获取,确保数据准确性
- **自动化**:简化设备创建流程,提高效率
- **一致性**:确保序列号与设备实际状态一致
### 影响范围
- **创建操作**:设备创建时自动获取序列号
- **更新操作**:设备更新时保持序列号不变
- **前端界面**:移除序列号输入字段,添加设备编码字段
- **API接口**:更新请求和响应模型
- **错误处理**:添加序列号获取失败的处理逻辑
- **服务注册**:注册设备通信相关服务
### 依赖服务
- `IBaseInstrumentClient`:用于与设备通信获取序列号
- `ServiceEndpoint`:配置设备连接信息
- `GetDeviceSerialNumberAsync`:获取设备序列号的方法
### 注意事项
- 设备必须能够通过HTTP协议访问
- 设备需要提供 `/System/serial-number` 接口
- 网络连接必须正常才能获取序列号
- 如果获取失败,会返回相应的错误信息
## 2025-07-28 - 修复endpointManager重新添加时机bug
### 修改原因
在获取设备序列号成功后,原来的代码立即重新添加了服务端点,但此时设备数据还没有保存到数据库。这可能导致服务端点与数据库中的设备数据不一致的问题。
### 修改文件
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修复endpointManager重新添加时机
### 修改内容
#### 1. GetDeviceSerialNumberAsync方法修改
- **移除立即重新添加逻辑**
- 在获取序列号成功后,只移除临时服务端点
- 不再立即重新添加服务端点
- 确保临时端点被正确清理
#### 2. CreateAndSaveDeviceAsync方法修改
- **添加延迟重新添加逻辑**
- 在设备成功保存到数据库后,重新添加服务端点
- 使用设备编号(deviceCode)作为端点名称
- 确保服务端点与数据库中的设备数据一致
### 技术特性
- **正确的执行顺序**:先保存数据,再添加服务端点
- **数据一致性**:确保服务端点与数据库中的设备信息一致
- **错误处理**:如果设备保存失败,不会添加服务端点
- **日志记录**:详细记录服务端点的添加过程
### 业务价值
- **数据完整性**:确保服务端点与设备数据的一致性
- **系统稳定性**:避免因数据不一致导致的问题
- **可维护性**:清晰的执行流程便于调试和维护
### 影响范围
- **创建流程**:修改了设备创建时的endpointManager管理逻辑
- **数据一致性**:确保服务端点与设备数据同步
- **错误处理**:改进了异常情况下的资源清理
### 执行流程
1. **获取序列号**:使用临时服务端点获取设备序列号
2. **清理临时端点**:移除临时服务端点
3. **保存设备数据**:将设备信息保存到数据库
4. **添加正式端点**:使用设备编号作为名称添加正式服务端点
### 注意事项
- 临时服务端点使用时间戳生成名称,避免冲突
- 正式服务端点使用设备编号作为名称,便于管理
- 只有在设备数据保存成功后才会添加正式服务端点
## 2025-07-28 - 修复设备编号生成逻辑,从000开始
### 修改原因
根据用户需求,设备编号应该从000开始,类似001、002的格式,而不是从1开始。
### 修改文件
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修改CalculateRequiredDigits方法和GenerateDeviceCodeAsync方法
### 修改内容
#### 1. CalculateRequiredDigits方法修改
- **最小位数设置**:将最小位数从1改为3,确保从000开始
- **逻辑优化**:使用 `Math.Max(calculatedDigits, 3)` 确保至少3位数
- **注释更新**:更新注释说明从000开始,至少3位数
#### 2. GenerateDeviceCodeAsync方法修改
- **注释更新**:更新注释说明从000开始,确保至少3位数
- **格式说明**:更新设备编号格式说明为 DEV-000-SN, DEV-001-SN, DEV-002-SN 等
### 技术特性
- **从000开始**:设备编号从DEV-000-SN开始,而不是DEV-1-SN
- **至少3位数**:确保序号至少有3位数字,格式统一
- **动态扩展**:当设备数量超过999时,自动扩展为4位数(1000、1001等)
- **格式一致**:所有设备编号都遵循统一的格式规范
### 业务价值
- **格式规范**:统一的设备编号格式,便于管理和识别
- **用户友好**:从000开始的编号更符合用户习惯
- **可扩展性**:支持大量设备的编号需求
- **一致性**:所有设备编号都遵循相同的格式规则
### 设备编号格式示例
- 第1个设备:DEV-000-SN
- 第2个设备:DEV-001-SN
- 第3个设备:DEV-002-SN
- ...
- 第1000个设备:DEV-1000-SN
- 第1001个设备:DEV-1001-SN
### 影响范围
- **设备创建**:新创建的设备将使用从000开始的编号格式
- **编号生成**:设备编号生成逻辑已更新
- **格式统一**:所有设备编号都遵循新的格式规范
### 注意事项
- 现有设备的编号不会自动更新
- 新创建的设备将使用新的编号格式
- 编号格式支持扩展到更多位数(当设备数量超过999时)
## 2025-07-28 - 优化临时服务端点名称生成,使用GUID替代时间戳
### 修改原因
使用时间戳生成临时服务端点名称可能存在冲突风险,特别是在高并发场景下。使用GUID可以确保端点名称的唯一性。
### 修改文件
- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修改CreateServiceEndpoint方法
### 修改内容
#### 1. CreateServiceEndpoint方法修改
- **名称生成方式**:从时间戳改为GUID
- **格式变更**:`DEV-{DateTime.Now:yyyyMMddHHmmss}` → `DEV-{Guid.NewGuid():N}`
- **唯一性保证**:GUID确保每个临时端点名称都是唯一的
### 技术特性
- **唯一性保证**:GUID提供全局唯一性,避免名称冲突
- **并发安全**:在高并发场景下不会出现重复名称
- **格式简洁**:使用 `:N` 格式去除GUID中的连字符,生成32位字符串
- **性能优化**:GUID生成比时间戳更高效
### 业务价值
- **系统稳定性**:避免因端点名称冲突导致的问题
- **并发支持**:支持多用户同时创建设备
- **可靠性提升**:确保每个临时端点都能正确创建和管理
### 临时端点名称示例
- 修改前:`DEV-20250728143022`
- 修改后:`DEV-a1b2c3d4e5f678901234567890123456`
### 影响范围 ### 影响范围
- **功能修复**:修复了网络栈配置更新功能中的绑定关系更新逻辑 - **临时端点创建**:临时服务端点名称生成方式已更新
- **性能优化**:避免不必要的数据库更新操作 - **并发处理**:支持更好的并发创建设备场景
- **业务逻辑**:支持部分更新绑定关系的场景 - **系统稳定性**:提高了端点管理的可靠性
- **用户体验**:允许用户只修改绑定关系的部分字段
- **响应优化**:简化了响应结构,移除了不必要的时间字段 ## 2025-07-28 - 移除设备表单中的启用和启动复选框
### 修改原因
根据用户需求,设备表单中不需要显示"启用设备"和"启动设备"这两个复选框,这些状态应该通过其他方式管理。
### 修改文件
- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 移除启用和启动复选框
### 修改内容
#### 1. 表单状态管理修改
- **初始状态设置**
- `isEnabled` 默认设置为 `true`
- `isRunning` 默认设置为 `false`
- 这些值不再从 `initialData` 中获取
#### 2. 表单界面修改
- **移除复选框组件**
- 删除"启用设备"复选框及其相关代码
- 删除"启动设备"复选框及其相关代码
- 保留其他表单字段不变
#### 3. 表单提交逻辑
- **创建模式**:提交所有字段,包括默认的启用和启动状态
- **编辑模式**:只提交可修改的字段,启用和启动状态保持默认值
### 技术特性
- **默认状态**:设备创建时默认启用,默认不启动
- **界面简化**:移除不必要的复选框,简化用户界面
- **状态管理**:启用和启动状态通过默认值管理
- **功能保持**:其他表单功能保持不变
### 业务价值
- **用户体验**:简化设备创建和编辑界面
- **操作简化**:减少用户需要选择的选项
- **界面清晰**:专注于设备的基本信息配置
- **一致性**:所有设备使用统一的默认状态
### 影响范围
- **设备创建**:新设备默认启用,默认不启动
- **设备编辑**:编辑时不再显示启用和启动选项
- **界面布局**:表单界面更加简洁
- **状态管理**:启用和启动状态通过系统默认值管理
### 注意事项
- 设备创建后默认启用状态为true
- 设备创建后默认启动状态为false
- 用户无法通过表单界面修改这些状态
- 如果需要修改这些状态,需要通过其他方式(如设备管理列表)
## 2025-07-28 - 限制设备编辑时只能修改名称和描述
### 修改原因
根据用户需求,在编辑设备时,只能修改设备名称和描述,而IP地址和端口不能修改,因为这些是设备的关键连接信息。
### 修改文件
- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 添加编辑模式下的字段限制
### 修改内容
#### 1. 编辑模式字段显示
- **IP地址字段**
- 在编辑模式下显示为禁用状态
- 添加灰色背景样式 `bg-gray-100`
- 显示提示文本"IP地址不可修改"
- **Agent端口字段**
- 在编辑模式下显示为禁用状态
- 添加灰色背景样式 `bg-gray-100`
- 显示提示文本"Agent端口不可修改"
#### 2. 条件渲染逻辑
- **创建模式**:显示可编辑的IP地址和端口输入框
- **编辑模式**:显示禁用的IP地址和端口字段,并添加说明文本
#### 3. 用户体验优化
- **视觉区分**:禁用的字段使用灰色背景,明确表示不可编辑
- **提示信息**:添加说明文本,告知用户为什么不能修改
- **保持布局**:字段位置和布局保持一致
### 技术特性
- **条件渲染**:根据 `isEdit` 属性显示不同的字段状态
- **样式区分**:使用不同的样式区分可编辑和不可编辑字段
- **用户提示**:提供清晰的说明文本
- **数据保护**:防止用户意外修改关键连接信息
### 业务价值
- **数据安全**:防止用户误操作修改设备连接信息
- **用户体验**:明确告知用户哪些字段可以修改
- **系统稳定性**:确保设备连接信息的稳定性
- **操作规范**:符合设备管理的业务规则
### 影响范围
- **设备编辑**:编辑设备时IP地址和端口不可修改
- **界面显示**:编辑模式下显示禁用的字段和提示信息
- **用户操作**:用户只能修改设备名称和描述
- **数据完整性**:确保设备连接信息不被意外修改
### 注意事项
- 创建设备时仍然需要提供IP地址和端口
- 编辑设备时IP地址和端口显示为只读状态
- 用户可以通过提示文本了解为什么不能修改这些字段
- 如果需要修改IP地址或端口,需要重新创建设备
Loading…
Cancel
Save