From 83942627be79a1cc1748219bb37530b339f668a9 Mon Sep 17 00:00:00 2001 From: hyh Date: Tue, 29 Jul 2025 18:17:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=99=90=E5=88=B6=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=97=B6=E5=8F=AA=E8=83=BD=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=E5=92=8C=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除设备表单中的启用和启动复选框 - 移除未使用的Checkbox导入语句 - 限制编辑模式下只能修改设备名称和描述 - IP地址和端口在编辑模式下显示为只读状态 - 添加灰色背景和提示文本,明确表示不可修改 - 更新修改记录文档 --- .../CreateDevice/CreateDeviceCommand.cs | 8 +- .../CreateDeviceCommandHandler.cs | 284 +++++- .../CreateDevice/CreateDeviceResponse.cs | 5 + .../UpdateDevice/UpdateDeviceCommand.cs | 20 - .../UpdateDeviceCommandHandler.cs | 22 +- .../UpdateDevice/UpdateDeviceResponse.cs | 5 + src/X1.Application/X1.Application.csproj | 1 + .../Entities/Device/CellularDevice.cs | 11 + .../Device/ICellularDeviceRepository.cs | 10 + .../Extensions/ServiceCollectionExtensions.cs | 4 + .../Features/IBaseInstrumentClient.cs | 34 + .../Features/IInstrumentHttpClient.cs | 30 + .../Features/IInstrumentProtocolClient.cs | 48 + .../Service/InstrumentProtocolClient.cs | 163 ++++ .../Models/ApiActionResult.cs | 104 +++ .../Models/DeviceInfoResponse.cs | 24 + .../Device/CellularDeviceConfiguration.cs | 2 + .../Device/CellularDeviceRepository.cs | 16 + src/X1.WebAPI/Program.cs | 3 + .../src/pages/instruments/DeviceForm.tsx | 142 +-- .../src/pages/instruments/DevicesView.tsx | 60 +- .../src/services/instrumentService.ts | 7 +- src/modify.md | 863 ++++++++++++++++-- 23 files changed, 1627 insertions(+), 239 deletions(-) create mode 100644 src/X1.DynamicClientCore/Features/IBaseInstrumentClient.cs create mode 100644 src/X1.DynamicClientCore/Features/IInstrumentHttpClient.cs create mode 100644 src/X1.DynamicClientCore/Features/IInstrumentProtocolClient.cs create mode 100644 src/X1.DynamicClientCore/Features/Service/InstrumentProtocolClient.cs create mode 100644 src/X1.DynamicClientCore/Models/ApiActionResult.cs create mode 100644 src/X1.DynamicClientCore/Models/DeviceInfoResponse.cs diff --git a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs index 86634c1..2cae6e4 100644 --- a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs +++ b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs @@ -17,12 +17,7 @@ public class CreateDeviceCommand : IRequest - /// 序列号 - /// - [Required(ErrorMessage = "序列号不能为空")] - [MaxLength(50, ErrorMessage = "序列号不能超过50个字符")] - public string SerialNumber { get; set; } + /// /// 设备描述 @@ -30,7 +25,6 @@ public class CreateDeviceCommand : IRequest /// IP地址 /// diff --git a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs index 16f53be..0305c60 100644 --- a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs +++ b/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.Base; using CellularManagement.Domain.Services; +using X1.DynamicClientCore.Features; +using X1.DynamicClientCore.Models; +using X1.DynamicClientCore.Interfaces; namespace CellularManagement.Application.Features.Devices.Commands.CreateDevice; @@ -18,6 +21,8 @@ public class CreateDeviceCommandHandler : IRequestHandler _logger; private readonly IUnitOfWork _unitOfWork; private readonly ICurrentUserService _currentUserService; + private readonly IBaseInstrumentClient _instrumentClient; + private readonly IServiceEndpointManager _endpointManager; /// /// 初始化命令处理器 @@ -26,12 +31,16 @@ public class CreateDeviceCommandHandler : IRequestHandler logger, IUnitOfWork unitOfWork, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + IBaseInstrumentClient instrumentClient, + IServiceEndpointManager endpointManager) { _deviceRepository = deviceRepository; _logger = logger; _unitOfWork = unitOfWork; _currentUserService = currentUserService; + _instrumentClient = instrumentClient; + _endpointManager = endpointManager; } /// @@ -39,30 +48,251 @@ public class CreateDeviceCommandHandler : IRequestHandler public async Task> Handle(CreateDeviceCommand request, CancellationToken cancellationToken) { + // 验证请求参数 + var validationResult = ValidateRequest(request); + if (!validationResult.IsSuccess) + { + return OperationResult.CreateFailure(validationResult.ErrorMessages); + } + + _logger.LogInformation("开始创建设备,设备名称: {DeviceName}, IP地址: {IpAddress}", + request.DeviceName, request.IpAddress); + + // 验证用户认证 + var currentUserId = ValidateUserAuthentication(); + if (!currentUserId.IsSuccess) + { + return OperationResult.CreateFailure(currentUserId.ErrorMessages); + } + + // 创建服务端点 + var serviceEndpoint = CreateServiceEndpoint(request); + + // 获取设备序列号 + var serialNumberResult = await GetDeviceSerialNumberAsync(request, serviceEndpoint, cancellationToken); + if (!serialNumberResult.IsSuccess) + { + return OperationResult.CreateFailure(serialNumberResult.ErrorMessages); + } + + var serialNumber = serialNumberResult.Data; + + // 验证序列号 + if (string.IsNullOrWhiteSpace(serialNumber)) + { + _logger.LogError("获取到的序列号为空"); + return OperationResult.CreateFailure("无法获取设备序列号"); + } + + // 检查序列号是否已存在 + if (await _deviceRepository.SerialNumberExistsAsync(serialNumber, cancellationToken)) + { + _logger.LogWarning("设备序列号已存在: {SerialNumber}", serialNumber); + return OperationResult.CreateFailure($"设备序列号 {serialNumber} 已存在"); + } + + // 生成设备编号 + var deviceCode = await GenerateDeviceCodeAsync(serialNumber, cancellationToken); + + // 验证设备编号 + if (string.IsNullOrWhiteSpace(deviceCode)) + { + _logger.LogError("生成的设备编号为空"); + return OperationResult.CreateFailure("生成设备编号失败"); + } + + // 创建设备并保存 + var deviceResult = await CreateAndSaveDeviceAsync(request, serialNumber, deviceCode, currentUserId.Data, cancellationToken); + if (!deviceResult.IsSuccess) + { + return OperationResult.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.CreateSuccess(response); + } + + /// + /// 验证请求参数 + /// + private OperationResult ValidateRequest(CreateDeviceCommand request) + { + if (request == null) + { + _logger.LogError("请求参数为空"); + return OperationResult.CreateFailure("请求参数不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.DeviceName)) + { + _logger.LogError("设备名称为空"); + return OperationResult.CreateFailure("设备名称不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.IpAddress)) + { + _logger.LogError("IP地址为空"); + return OperationResult.CreateFailure("IP地址不能为空"); + } + + if (request.AgentPort <= 0 || request.AgentPort > 65535) + { + _logger.LogError("Agent端口无效: {AgentPort}", request.AgentPort); + return OperationResult.CreateFailure("Agent端口必须在1-65535之间"); + } + + return OperationResult.CreateSuccess(true); + } + + /// + /// 验证用户认证 + /// + private OperationResult ValidateUserAuthentication() + { + var currentUserId = _currentUserService.GetCurrentUserId(); + if (string.IsNullOrEmpty(currentUserId)) + { + _logger.LogError("无法获取当前用户ID,用户可能未认证"); + return OperationResult.CreateFailure("用户未认证,无法创建设备"); + } + return OperationResult.CreateSuccess(currentUserId); + } + + /// + /// 获取设备序列号 + /// + private async Task> GetDeviceSerialNumberAsync(CreateDeviceCommand request, ServiceEndpoint serviceEndpoint, CancellationToken cancellationToken) + { + 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); - return OperationResult.CreateFailure($"设备序列号 {request.SerialNumber} 已存在"); + _logger.LogWarning("无法通过IP地址获取设备序列号,IP地址: {IpAddress}", request.IpAddress); + // 获取序列号失败时清理服务端点 + _endpointManager.RemoveEndpoint(serviceEndpoint.Name); + _logger.LogDebug("已清理服务端点: {EndpointName}", serviceEndpoint.Name); + return OperationResult.CreateFailure($"无法通过IP地址 {request.IpAddress} 获取设备序列号,请检查设备连接状态"); } - // 获取当前用户ID - var currentUserId = _currentUserService.GetCurrentUserId(); - if (string.IsNullOrEmpty(currentUserId)) - { - _logger.LogError("无法获取当前用户ID,用户可能未认证"); - return OperationResult.CreateFailure("用户未认证,无法创建设备"); - } + _logger.LogInformation("成功获取设备序列号: {SerialNumber}, IP地址: {IpAddress}", serialNumber, request.IpAddress); + + // 获取序列号成功后,移除临时服务端点 + _endpointManager.RemoveEndpoint(serviceEndpoint.Name); + _logger.LogDebug("已移除临时服务端点: {EndpointName}", serviceEndpoint.Name); + + return OperationResult.CreateSuccess(serialNumber); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取设备序列号时发生异常,IP地址: {IpAddress}", request.IpAddress); + // 发生异常时清理服务端点 + _endpointManager.RemoveEndpoint(serviceEndpoint.Name); + _logger.LogDebug("已清理服务端点: {EndpointName}", serviceEndpoint.Name); + return OperationResult.CreateFailure($"获取设备序列号时发生错误: {ex.Message}"); + } + } + + /// + /// 生成设备编号 + /// + private async Task 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; + } + + /// + /// 计算需要的位数 + /// + 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位数 + } + + /// + /// 创建服务端点配置 + /// + 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 + }; + } + /// + /// 创建设备并保存到数据库 + /// + private async Task> CreateAndSaveDeviceAsync( + CreateDeviceCommand request, + string serialNumber, + string deviceCode, + string currentUserId, + CancellationToken cancellationToken) + { + try + { // 创建设备实体 var device = CellularDevice.Create( name: request.DeviceName, - serialNumber: request.SerialNumber, + serialNumber: serialNumber, + deviceCode: deviceCode, description: request.Description, agentPort: request.AgentPort, ipAddress: request.IpAddress, @@ -76,30 +306,12 @@ public class CreateDeviceCommandHandler : IRequestHandler.CreateSuccess(response); + return OperationResult.CreateSuccess(device); } catch (Exception ex) { - _logger.LogError(ex, "创建设备时发生错误,设备名称: {DeviceName}, 序列号: {SerialNumber}", - request.DeviceName, request.SerialNumber); - return OperationResult.CreateFailure($"创建设备时发生错误: {ex.Message}"); + _logger.LogError(ex, "创建设备实体时发生错误,设备名称: {DeviceName}", request.DeviceName); + return OperationResult.CreateFailure($"创建设备时发生错误: {ex.Message}"); } } - - } \ No newline at end of file diff --git a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs index c74c029..1d6ee6f 100644 --- a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs +++ b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs @@ -20,6 +20,11 @@ public class CreateDeviceResponse /// public string SerialNumber { get; set; } + /// + /// 设备编码 + /// + public string DeviceCode { get; set; } + /// /// 设备描述 /// diff --git a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs index d7716c9..1847c22 100644 --- a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs +++ b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs @@ -23,32 +23,12 @@ public class UpdateDeviceCommand : IRequest - /// 序列号 - /// - [Required] - [MaxLength(50)] - public string SerialNumber { get; set; } - /// /// 设备描述 /// [MaxLength(500)] public string Description { get; set; } - - /// - /// IP地址 - /// - [Required] - [MaxLength(45)] - public string IpAddress { get; set; } = null!; - /// - /// Agent端口 - /// - [Required] - public int AgentPort { get; set; } - /// /// 是否启用 /// diff --git a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs index f470c8e..2eb5894 100644 --- a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs +++ b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs @@ -54,16 +54,6 @@ public class UpdateDeviceCommandHandler : IRequestHandler.CreateFailure($"未找到ID为 {request.DeviceId} 的设备"); } - // 如果序列号发生变化,检查新序列号是否已存在 - if (existingDevice.SerialNumber != request.SerialNumber) - { - if (await _deviceRepository.SerialNumberExistsAsync(request.SerialNumber, cancellationToken)) - { - _logger.LogWarning("设备序列号已存在: {SerialNumber}", request.SerialNumber); - return OperationResult.CreateFailure($"设备序列号 {request.SerialNumber} 已存在"); - } - } - // 获取当前用户ID var currentUserId = _currentUserService.GetCurrentUserId(); if (string.IsNullOrEmpty(currentUserId)) @@ -72,13 +62,14 @@ public class UpdateDeviceCommandHandler : IRequestHandler.CreateFailure("用户未认证,无法更新设备"); } - // 更新设备属性 + // 只更新允许修改的字段,其他字段保持不变 existingDevice.Update( name: request.DeviceName, - serialNumber: request.SerialNumber, + serialNumber: existingDevice.SerialNumber, // 保持原有序列号不变 + deviceCode: existingDevice.DeviceCode, // 保持原有设备编码不变 description: request.Description, - agentPort: request.AgentPort, - ipAddress: request.IpAddress, + agentPort: existingDevice.AgentPort, // 保持原有端口不变 + ipAddress: existingDevice.IpAddress, // 保持原有IP地址不变 updatedBy: currentUserId, isEnabled: request.IsEnabled, isRunning: request.IsRunning); @@ -94,6 +85,7 @@ public class UpdateDeviceCommandHandler : IRequestHandler.CreateFailure($"更新设备时发生错误: {ex.Message}"); } } - - } \ No newline at end of file diff --git a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceResponse.cs b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceResponse.cs index 27d5c3b..b6ccf4a 100644 --- a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceResponse.cs +++ b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceResponse.cs @@ -20,6 +20,11 @@ public class UpdateDeviceResponse /// public string SerialNumber { get; set; } + /// + /// 设备编码 + /// + public string DeviceCode { get; set; } + /// /// 设备描述 /// diff --git a/src/X1.Application/X1.Application.csproj b/src/X1.Application/X1.Application.csproj index ec15c49..15aa001 100644 --- a/src/X1.Application/X1.Application.csproj +++ b/src/X1.Application/X1.Application.csproj @@ -3,6 +3,7 @@ + diff --git a/src/X1.Domain/Entities/Device/CellularDevice.cs b/src/X1.Domain/Entities/Device/CellularDevice.cs index 6caa921..eb76e9d 100644 --- a/src/X1.Domain/Entities/Device/CellularDevice.cs +++ b/src/X1.Domain/Entities/Device/CellularDevice.cs @@ -25,6 +25,13 @@ public class CellularDevice : AuditableEntity [MaxLength(50)] public string SerialNumber { get; private set; } = null!; + /// + /// 设备编码 + /// + [Required] + [MaxLength(50)] + public string DeviceCode { get; private set; } = null!; + /// /// 设备描述 /// @@ -67,6 +74,7 @@ public class CellularDevice : AuditableEntity public static CellularDevice Create( string name, string serialNumber, + string deviceCode, string description, int agentPort, string ipAddress, @@ -79,6 +87,7 @@ public class CellularDevice : AuditableEntity Id = Guid.NewGuid().ToString(), Name = name, SerialNumber = serialNumber, + DeviceCode = deviceCode, Description = description, AgentPort = agentPort, IpAddress = ipAddress, @@ -99,6 +108,7 @@ public class CellularDevice : AuditableEntity public void Update( string name, string serialNumber, + string deviceCode, string description, int agentPort, string ipAddress, @@ -108,6 +118,7 @@ public class CellularDevice : AuditableEntity { Name = name; SerialNumber = serialNumber; + DeviceCode = deviceCode; Description = description; AgentPort = agentPort; IpAddress = ipAddress; diff --git a/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs b/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs index dd7ac86..cd55476 100644 --- a/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs +++ b/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs @@ -65,4 +65,14 @@ public interface ICellularDeviceRepository : IBaseRepository /// 检查序列号是否存在 /// Task SerialNumberExistsAsync(string serialNumber, CancellationToken cancellationToken = default); + + /// + /// 检查设备编码是否存在 + /// + Task DeviceCodeExistsAsync(string deviceCode, CancellationToken cancellationToken = default); + + /// + /// 获取设备总数 + /// + Task GetDeviceCountAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs b/src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs index 953182f..96af0d9 100644 --- a/src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs @@ -45,6 +45,10 @@ namespace X1.DynamicClientCore.Extensions services.AddSingleton(); services.AddSingleton(); + // 注册仪器协议客户端(单例) + services.AddSingleton(); + services.AddSingleton(); + return services; } diff --git a/src/X1.DynamicClientCore/Features/IBaseInstrumentClient.cs b/src/X1.DynamicClientCore/Features/IBaseInstrumentClient.cs new file mode 100644 index 0000000..b019b72 --- /dev/null +++ b/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 +{ + /// + /// 基础仪器客户端接口 + /// 提供仪器设备的基础通信功能,包括设备信息获取等通用操作 + /// + /// + /// 作为仪器客户端的基础接口,定义了所有仪器客户端都应该支持的基本功能 + /// 主要用于设备信息查询和状态获取等基础操作 + /// + public interface IBaseInstrumentClient + { + /// + /// 获取设备序列号 + /// + /// 服务端点配置,包含设备的连接信息 + /// 请求选项,包含超时、请求头等配置 + /// 取消令牌,用于取消异步操作 + /// 设备序列号,如果获取失败则返回空字符串 + /// 当endpoint为null时抛出 + /// 当endpoint配置无效时抛出 + Task GetDeviceSerialNumberAsync( + string instrumentNumber, + RequestOptions? options = null, + CancellationToken cancellationToken = default); + } +} diff --git a/src/X1.DynamicClientCore/Features/IInstrumentHttpClient.cs b/src/X1.DynamicClientCore/Features/IInstrumentHttpClient.cs new file mode 100644 index 0000000..85cf2d3 --- /dev/null +++ b/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 +{ + /// + /// 仪器HTTP客户端接口 + /// 组合基础仪器客户端和协议客户端的功能,提供完整的仪器设备通信服务 + /// + /// + /// 该接口继承自 IBaseInstrumentClient 和 IInstrumentProtocolClient, + /// 提供仪器设备的基础信息获取和网络控制功能 + /// 主要用于通过HTTP协议与仪器设备进行通信 + /// + public interface IInstrumentHttpClient : IBaseInstrumentClient, IInstrumentProtocolClient + { + // 继承自 IBaseInstrumentClient 的方法: + // - GetDeviceSerialNumberAsync: 获取设备序列号 + + // 继承自 IInstrumentProtocolClient 的方法: + // - StartNetworkAsync: 启动网络连接 + // - StopNetworkAsync: 停止网络连接 + + // 可以在这里添加仪器HTTP客户端特有的方法 + } +} diff --git a/src/X1.DynamicClientCore/Features/IInstrumentProtocolClient.cs b/src/X1.DynamicClientCore/Features/IInstrumentProtocolClient.cs new file mode 100644 index 0000000..2f9a71c --- /dev/null +++ b/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 +{ + /// + /// 仪器协议客户端接口 + /// 提供仪器设备的网络控制功能,包括网络启动、停止等协议相关操作 + /// + /// + /// 作为仪器客户端的协议接口,定义了仪器设备网络控制相关的功能 + /// 主要用于网络连接的管理和控制操作 + /// + public interface IInstrumentProtocolClient + { + /// + /// 启动网络连接 + /// + /// 仪器编号,用于标识特定的仪器设备 + /// 请求选项,包含超时、请求头等配置 + /// 取消令牌,用于取消异步操作 + /// 异步任务,表示网络启动操作的完成状态 + /// 当instrumentNumber为null或空时抛出 + /// 当instrumentNumber格式无效时抛出 + Task StartNetworkAsync( + string instrumentNumber, + RequestOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// 停止网络连接 + /// + /// 仪器编号,用于标识特定的仪器设备 + /// 请求选项,包含超时、请求头等配置 + /// 取消令牌,用于取消异步操作 + /// 异步任务,表示网络停止操作的完成状态 + /// 当instrumentNumber为null或空时抛出 + /// 当instrumentNumber格式无效时抛出 + Task StopNetworkAsync( + string instrumentNumber, + RequestOptions? options = null, + CancellationToken cancellationToken = default); + } +} diff --git a/src/X1.DynamicClientCore/Features/Service/InstrumentProtocolClient.cs b/src/X1.DynamicClientCore/Features/Service/InstrumentProtocolClient.cs new file mode 100644 index 0000000..75c573d --- /dev/null +++ b/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 +{ + /// + /// 设备协议客户端实现类 + /// 提供与设备设备通信的协议服务,包括设备信息获取、网络控制等功能 + /// + /// + /// 实现 IInstrumentProtocolClient 和 IBaseInstrumentClient 接口 + /// 支持动态服务端点管理和HTTP通信 + /// + public class InstrumentProtocolClient : IInstrumentHttpClient + { + private readonly ILogger _logger; + private readonly IDynamicHttpClient _dynamicHttpClient; + private readonly IServiceEndpointManager _serviceEndpointManager; + + /// + /// 初始化设备协议客户端 + /// + /// 动态HTTP客户端 + /// 服务端点管理器 + /// 日志记录器 + /// 当任何必需参数为null时抛出 + public InstrumentProtocolClient( + IDynamicHttpClient dynamicHttpClient, + IServiceEndpointManager serviceEndpointManager, + ILogger logger) + { + _dynamicHttpClient = dynamicHttpClient ?? throw new ArgumentNullException(nameof(dynamicHttpClient)); + _serviceEndpointManager = serviceEndpointManager ?? throw new ArgumentNullException(nameof(serviceEndpointManager)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 获取设备序列号 + /// + /// 服务端点配置 + /// 请求选项 + /// 取消令牌 + /// 设备序列号,如果获取失败则返回空字符串 + /// 当endpoint为null时抛出 + public async Task 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>( + 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; + } + } + + /// + /// 启动网络连接 + /// + /// 设备编号 + /// 请求选项 + /// 取消令牌 + /// 异步任务 + /// 当instrumentNumber为空或null时抛出 + 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; + } + } + + /// + /// 停止网络连接 + /// + /// 设备编号 + /// 请求选项 + /// 取消令牌 + /// 异步任务 + /// 当instrumentNumber为空或null时抛出 + 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; + } + } + } +} diff --git a/src/X1.DynamicClientCore/Models/ApiActionResult.cs b/src/X1.DynamicClientCore/Models/ApiActionResult.cs new file mode 100644 index 0000000..0a738a8 --- /dev/null +++ b/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 +{ + /// + /// API操作结果 + /// + public class ApiActionResult + { + /// + /// 是否成功 + /// + public bool IsSuccess { get; set; } + + /// + /// 消息 + /// + public string Message { get; set; } + + /// + /// 错误代码 + /// + public string ErrorCode { get; set; } + + /// + /// HTTP状态码 + /// + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK; + + /// + /// 创建成功结果 + /// + /// 消息 + /// API操作结果 + public static ApiActionResult Ok(string message = "操作成功") + { + return new ApiActionResult + { + IsSuccess = true, + Message = message, + ErrorCode = string.Empty, + StatusCode = HttpStatusCode.OK + }; + } + } + + + /// + /// 带数据的API操作结果 + /// + /// 数据类型 + public class ApiActionResult : ApiActionResult + { + /// + /// 数据 + /// + public T Data { get; set; } + + /// + /// 创建成功结果 + /// + /// 数据 + /// 消息 + /// API操作结果 + public static ApiActionResult Ok(T data, string message = "操作成功") + { + return new ApiActionResult + { + IsSuccess = true, + Message = message, + ErrorCode = string.Empty, + StatusCode = HttpStatusCode.OK, + Data = data + }; + } + + /// + /// 创建失败结果 + /// + /// 错误消息 + /// 错误代码 + /// HTTP状态码 + /// API操作结果 + public static ApiActionResult Error(string message, string errorCode = "ERROR", HttpStatusCode statusCode = HttpStatusCode.BadRequest) + { + return new ApiActionResult + { + IsSuccess = false, + Message = message, + ErrorCode = errorCode, + StatusCode = statusCode, + Data = default + }; + } + } + + + +} diff --git a/src/X1.DynamicClientCore/Models/DeviceInfoResponse.cs b/src/X1.DynamicClientCore/Models/DeviceInfoResponse.cs new file mode 100644 index 0000000..274dae2 --- /dev/null +++ b/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 +{ + /// + /// 设备信息响应模型(用于API响应) + /// + public class DeviceInfoResponse + { + /// + /// 设备序列号(SN) + /// + public string SerialNumber { get; set; } = string.Empty; + + /// + /// 获取时间 + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } +} diff --git a/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs b/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs index c230ad1..b9e46ce 100644 --- a/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs +++ b/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs @@ -13,11 +13,13 @@ public class CellularDeviceConfiguration : IEntityTypeConfiguration 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.Name).IsRequired().HasMaxLength(100).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.AgentPort).IsRequired().HasComment("Agent端口"); builder.Property(d => d.IpAddress).IsRequired().HasMaxLength(45).HasComment("IP地址"); diff --git a/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs b/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs index 712c9ea..16d202f 100644 --- a/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs +++ b/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs @@ -144,4 +144,20 @@ public class CellularDeviceRepository : BaseRepository, ICellula { return await QueryRepository.AnyAsync(d => d.SerialNumber == serialNumber, cancellationToken: cancellationToken); } + + /// + /// 检查设备编码是否存在 + /// + public async Task DeviceCodeExistsAsync(string deviceCode, CancellationToken cancellationToken = default) + { + return await QueryRepository.AnyAsync(d => d.DeviceCode == deviceCode, cancellationToken: cancellationToken); + } + + /// + /// 获取设备总数 + /// + public async Task GetDeviceCountAsync(CancellationToken cancellationToken = default) + { + return await QueryRepository.CountAsync(d => true, cancellationToken: cancellationToken); + } } \ No newline at end of file diff --git a/src/X1.WebAPI/Program.cs b/src/X1.WebAPI/Program.cs index a6dc1b1..68b66c4 100644 --- a/src/X1.WebAPI/Program.cs +++ b/src/X1.WebAPI/Program.cs @@ -21,6 +21,7 @@ using Microsoft.IdentityModel.Tokens; using System.Text; using CellularManagement.Domain.Options; using X1.WebAPI.Extensions; +using X1.DynamicClientCore.Extensions; // 创建 Web 应用程序构建器 var builder = WebApplication.CreateBuilder(args); @@ -40,6 +41,8 @@ builder.Services.AddInfrastructure(builder.Configuration); // 包括命令/查询处理、验证等服务 builder.Services.AddApplication(builder.Configuration); +builder.Services.AddDynamicServiceClient(); + // 注册表现层服务 // 包括控制器、视图、中间件等表现层相关服务 builder.Services.AddPresentation(); diff --git a/src/X1.WebUI/src/pages/instruments/DeviceForm.tsx b/src/X1.WebUI/src/pages/instruments/DeviceForm.tsx index 5a019ae..1f1ad35 100644 --- a/src/X1.WebUI/src/pages/instruments/DeviceForm.tsx +++ b/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 { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; -import { Checkbox } from '@/components/ui/checkbox'; import { CreateDeviceRequest, UpdateDeviceRequest } from '@/services/instrumentService'; interface DeviceFormProps { onSubmit: (data: CreateDeviceRequest | UpdateDeviceRequest) => void; - initialData?: Partial; + initialData?: Partial & { deviceId?: string }; isEdit?: boolean; isSubmitting?: boolean; } @@ -16,18 +15,31 @@ interface DeviceFormProps { export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: DeviceFormProps) { const [formData, setFormData] = React.useState({ deviceName: initialData?.deviceName || '', - serialNumber: initialData?.serialNumber || '', description: initialData?.description || '', ipAddress: initialData?.ipAddress || '', agentPort: initialData?.agentPort || 8080, - isEnabled: initialData?.isEnabled ?? true, - isRunning: initialData?.isRunning ?? false + isEnabled: true, + isRunning: false }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); 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 ( @@ -44,17 +56,65 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu /> -
- - setFormData({ ...formData, serialNumber: e.target.value })} - placeholder="请输入设备序列号" - required - disabled={isSubmitting} - /> -
+ {!isEdit && ( + <> +
+ + setFormData({ ...formData, ipAddress: e.target.value })} + placeholder="例如: 192.168.1.100" + required + disabled={isSubmitting} + /> +
+ +
+ + setFormData({ ...formData, agentPort: parseInt(e.target.value) || 0 })} + placeholder="例如: 8080" + min="1" + max="65535" + required + disabled={isSubmitting} + /> +
+ + )} + + {isEdit && ( + <> +
+ + +

IP地址不可修改

+
+ +
+ + +

Agent端口不可修改

+
+ + )}
@@ -68,53 +128,7 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu />
-
- - setFormData({ ...formData, ipAddress: e.target.value })} - placeholder="例如: 192.168.1.100" - required - disabled={isSubmitting} - /> -
- -
- - setFormData({ ...formData, agentPort: parseInt(e.target.value) || 0 })} - placeholder="例如: 8080" - min="1" - max="65535" - required - disabled={isSubmitting} - /> -
- -
- setFormData({ ...formData, isEnabled: checked as boolean })} - disabled={isSubmitting} - /> - -
- -
- setFormData({ ...formData, isRunning: checked as boolean })} - disabled={isSubmitting} - /> - -
+