diff --git a/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommand.cs b/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommand.cs new file mode 100644 index 0000000..aafc5ee --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommand.cs @@ -0,0 +1,46 @@ +using CellularManagement.Domain.Common; +using MediatR; +using System.ComponentModel.DataAnnotations; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Commands.StartDeviceRuntime; + +/// +/// 启动设备运行时状态命令 +/// +public class StartDeviceRuntimeCommand : IRequest> +{ + /// + /// 设备启动请求列表 + /// + [Required(ErrorMessage = "设备启动请求列表不能为空")] + public List DeviceRequests { get; set; } = null!; + + /// + /// 验证命令参数 + /// + public bool IsValid() + { + return DeviceRequests != null && DeviceRequests.Any() && + DeviceRequests.All(d => !string.IsNullOrEmpty(d.DeviceCode) && !string.IsNullOrEmpty(d.NetworkStackCode)); + } +} + +/// +/// 设备启动请求 +/// +public class DeviceStartRequest +{ + /// + /// 设备编号 + /// + [Required(ErrorMessage = "设备编号不能为空")] + [MaxLength(50, ErrorMessage = "设备编号不能超过50个字符")] + public string DeviceCode { get; set; } = null!; + + /// + /// 网络栈配置编号 + /// + [Required(ErrorMessage = "网络栈配置编号不能为空")] + [MaxLength(50, ErrorMessage = "网络栈配置编号不能超过50个字符")] + public string NetworkStackCode { get; set; } = null!; +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommandHandler.cs b/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommandHandler.cs new file mode 100644 index 0000000..c71b514 --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommandHandler.cs @@ -0,0 +1,208 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using CellularManagement.Domain.Common; +using CellularManagement.Domain.Entities.Device; +using CellularManagement.Domain.Repositories.Device; +using CellularManagement.Domain.Repositories.Base; +using CellularManagement.Domain.Services; +using X1.DynamicClientCore.Features; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Commands.StartDeviceRuntime; + +/// +/// 启动设备运行时状态命令处理器 +/// +public class StartDeviceRuntimeCommandHandler : IRequestHandler> +{ + private readonly ICellularDeviceRuntimeRepository _deviceRuntimeRepository; + private readonly ICellularDeviceRepository _deviceRepository; + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + private readonly ICurrentUserService _currentUserService; + private readonly IInstrumentProtocolClient _protocolClient; + + /// + /// 初始化命令处理器 + /// + public StartDeviceRuntimeCommandHandler( + ICellularDeviceRuntimeRepository deviceRuntimeRepository, + ICellularDeviceRepository deviceRepository, + ILogger logger, + IUnitOfWork unitOfWork, + ICurrentUserService currentUserService, + IInstrumentProtocolClient protocolClient) + { + _deviceRuntimeRepository = deviceRuntimeRepository; + _deviceRepository = deviceRepository; + _logger = logger; + _unitOfWork = unitOfWork; + _currentUserService = currentUserService; + _protocolClient= protocolClient; + } + + /// + /// 处理启动设备运行时状态命令 + /// + public async Task> Handle(StartDeviceRuntimeCommand request, CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("开始批量启动设备运行时状态,设备数量: {DeviceCount}", request.DeviceRequests.Count); + + // 验证命令参数 + if (!request.IsValid()) + { + _logger.LogWarning("命令参数验证失败"); + return OperationResult.CreateFailure("命令参数验证失败"); + } + + // 获取当前用户 + var currentUser = _currentUserService.GetCurrentUserId(); + if (string.IsNullOrEmpty(currentUser)) + { + _logger.LogWarning("当前用户未登录"); + return OperationResult.CreateFailure("用户未登录"); + } + + var deviceResults = new List(); + var successCount = 0; + var failureCount = 0; + + // 批量处理设备启动 + foreach (var deviceRequest in request.DeviceRequests) + { + try + { + _logger.LogInformation("开始启动设备,设备编号: {DeviceCode}, 网络栈配置编号: {NetworkStackCode}", + deviceRequest.DeviceCode, deviceRequest.NetworkStackCode); + + // 获取设备运行时状态 + var deviceRuntime = await _deviceRuntimeRepository.GetRuntimeByDeviceCodeAsync(deviceRequest.DeviceCode, cancellationToken); + if (deviceRuntime == null) + { + _logger.LogWarning("设备运行时状态不存在,设备编号: {DeviceCode}", deviceRequest.DeviceCode); + deviceResults.Add(new DeviceStartResult + { + DeviceCode = deviceRequest.DeviceCode, + IsSuccess = false, + ErrorMessage = $"设备编号 {deviceRequest.DeviceCode} 的运行时状态不存在" + }); + failureCount++; + continue; + } + + // 检查设备是否已经在运行 + if (deviceRuntime.RuntimeStatus == DeviceRuntimeStatus.Running) + { + _logger.LogWarning("设备已经在运行中,设备编号: {DeviceCode}", deviceRequest.DeviceCode); + deviceResults.Add(new DeviceStartResult + { + DeviceCode = deviceRequest.DeviceCode, + IsSuccess = false, + ErrorMessage = $"设备编号 {deviceRequest.DeviceCode} 已经在运行中" + }); + failureCount++; + continue; + } + + // 生成运行编码 + var runtimeCode = await GenerateRuntimeCodeAsync(deviceRequest.DeviceCode, cancellationToken); + + // 启动设备 + deviceRuntime.Start(deviceRequest.NetworkStackCode, runtimeCode); + + // 更新到数据库 + _deviceRuntimeRepository.UpdateRuntime(deviceRuntime); + + _logger.LogInformation("设备启动成功,设备编号: {DeviceCode}, 运行编码: {RuntimeCode}", + deviceRuntime.DeviceCode, deviceRuntime.RuntimeCode); + + // 添加到结果列表 + deviceResults.Add(new DeviceStartResult + { + DeviceCode = deviceRuntime.DeviceCode, + Id = deviceRuntime.Id, + RuntimeStatus = deviceRuntime.RuntimeStatus.ToString(), + NetworkStackCode = deviceRuntime.NetworkStackCode, + UpdatedAt = deviceRuntime.UpdatedAt ?? DateTime.UtcNow, + IsSuccess = true + }); + successCount++; + } + catch (Exception ex) + { + _logger.LogError(ex, "启动设备失败,设备编号: {DeviceCode}", deviceRequest.DeviceCode); + deviceResults.Add(new DeviceStartResult + { + DeviceCode = deviceRequest.DeviceCode, + IsSuccess = false, + ErrorMessage = $"启动设备失败: {ex.Message}" + }); + failureCount++; + } + } + + // 保存所有更改 + await _unitOfWork.SaveChangesAsync(cancellationToken); + + // 构建响应 + var response = new StartDeviceRuntimeResponse + { + DeviceResults = deviceResults, + Summary = new BatchOperationSummary + { + TotalCount = request.DeviceRequests.Count, + SuccessCount = successCount, + FailureCount = failureCount + } + }; + + _logger.LogInformation("批量启动设备完成,总数量: {TotalCount}, 成功: {SuccessCount}, 失败: {FailureCount}", + response.Summary.TotalCount, response.Summary.SuccessCount, response.Summary.FailureCount); + + return OperationResult.CreateSuccess(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "批量启动设备运行时状态失败"); + return OperationResult.CreateFailure($"批量启动设备运行时状态失败: {ex.Message}"); + } + } + + /// + /// 生成运行编码 + /// + private async Task GenerateRuntimeCodeAsync(string deviceCode, CancellationToken cancellationToken) + { + // 获取当前设备运行时总数 + var runtimeCount = await _deviceRuntimeRepository.GetRuntimeCountAsync(cancellationToken); + var nextNumber = runtimeCount + 1; + + // 计算需要的位数,确保至少3位数 + var digitCount = CalculateRequiredDigits(nextNumber); + + // 格式化序号,动态补0,确保从000开始 + var formattedNumber = nextNumber.ToString($"D{digitCount}"); + + // 生成运行编码格式:RT-000-DEVCODE, RT-001-DEVCODE 等 + var runtimeCode = $"RT-{formattedNumber}-{deviceCode}"; + + _logger.LogDebug("生成运行编码: {RuntimeCode}, 运行时总数: {RuntimeCount}, 位数: {DigitCount}", + runtimeCode, runtimeCount, digitCount); + + return runtimeCode; + } + + /// + /// 计算需要的位数 + /// + 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位数 + } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeResponse.cs b/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeResponse.cs new file mode 100644 index 0000000..e8f1e36 --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeResponse.cs @@ -0,0 +1,113 @@ +namespace CellularManagement.Application.Features.DeviceRuntimes.Commands.StartDeviceRuntime; + +/// +/// 启动设备运行时状态响应 +/// +public class StartDeviceRuntimeResponse +{ + /// + /// 运行时状态ID(单个设备启动时使用) + /// + public string? Id { get; set; } + + /// + /// 设备编号(单个设备启动时使用) + /// + public string? DeviceCode { get; set; } + + /// + /// 运行时状态(单个设备启动时使用) + /// + public string? RuntimeStatus { get; set; } + + /// + /// 网络栈配置编号 + /// + public string? NetworkStackCode { get; set; } + + + + /// + /// 更新时间 + /// + public DateTime UpdatedAt { get; set; } + + /// + /// 设备启动结果列表(批量启动时使用) + /// + public List? DeviceResults { get; set; } + + /// + /// 批量操作统计 + /// + public BatchOperationSummary? Summary { get; set; } +} + +/// +/// 设备启动结果 +/// +public class DeviceStartResult +{ + /// + /// 设备编号 + /// + public string DeviceCode { get; set; } = null!; + + /// + /// 运行时状态ID + /// + public string Id { get; set; } = null!; + + /// + /// 运行时状态 + /// + public string RuntimeStatus { get; set; } = null!; + + /// + /// 网络栈配置编号 + /// + public string? NetworkStackCode { get; set; } + + + + /// + /// 更新时间 + /// + public DateTime UpdatedAt { get; set; } + + /// + /// 是否成功 + /// + public bool IsSuccess { get; set; } + + /// + /// 错误信息 + /// + public string? ErrorMessage { get; set; } +} + +/// +/// 批量操作统计 +/// +public class BatchOperationSummary +{ + /// + /// 总设备数 + /// + public int TotalCount { get; set; } + + /// + /// 成功数量 + /// + public int SuccessCount { get; set; } + + /// + /// 失败数量 + /// + public int FailureCount { get; set; } + + /// + /// 成功率 + /// + public double SuccessRate => TotalCount > 0 ? (double)SuccessCount / TotalCount * 100 : 0; +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommand.cs b/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommand.cs new file mode 100644 index 0000000..f4e11b2 --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommand.cs @@ -0,0 +1,18 @@ +using CellularManagement.Domain.Common; +using MediatR; +using System.ComponentModel.DataAnnotations; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Commands.StopDeviceRuntime; + +/// +/// 停止设备运行时状态命令 +/// +public class StopDeviceRuntimeCommand : IRequest> +{ + /// + /// 设备编号 + /// + [Required(ErrorMessage = "设备编号不能为空")] + [MaxLength(50, ErrorMessage = "设备编号不能超过50个字符")] + public string DeviceCode { get; set; } = null!; +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommandHandler.cs b/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommandHandler.cs new file mode 100644 index 0000000..f144dcd --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommandHandler.cs @@ -0,0 +1,97 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using CellularManagement.Domain.Common; +using CellularManagement.Domain.Entities.Device; +using CellularManagement.Domain.Repositories.Device; +using CellularManagement.Domain.Repositories.Base; +using CellularManagement.Domain.Services; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Commands.StopDeviceRuntime; + +/// +/// 停止设备运行时状态命令处理器 +/// +public class StopDeviceRuntimeCommandHandler : IRequestHandler> +{ + private readonly ICellularDeviceRuntimeRepository _deviceRuntimeRepository; + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + private readonly ICurrentUserService _currentUserService; + + /// + /// 初始化命令处理器 + /// + public StopDeviceRuntimeCommandHandler( + ICellularDeviceRuntimeRepository deviceRuntimeRepository, + ILogger logger, + IUnitOfWork unitOfWork, + ICurrentUserService currentUserService) + { + _deviceRuntimeRepository = deviceRuntimeRepository; + _logger = logger; + _unitOfWork = unitOfWork; + _currentUserService = currentUserService; + } + + /// + /// 处理停止设备运行时状态命令 + /// + public async Task> Handle(StopDeviceRuntimeCommand request, CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("开始停止设备运行时状态,设备编号: {DeviceCode}", request.DeviceCode); + + // 获取设备运行时状态 + var deviceRuntime = await _deviceRuntimeRepository.GetRuntimeByDeviceCodeAsync(request.DeviceCode, cancellationToken); + if (deviceRuntime == null) + { + _logger.LogWarning("设备运行时状态不存在,设备编号: {DeviceCode}", request.DeviceCode); + return OperationResult.CreateFailure($"设备编号 {request.DeviceCode} 的运行时状态不存在"); + } + + // 检查设备是否已经在停止或未运行 + if (deviceRuntime.RuntimeStatus != DeviceRuntimeStatus.Running) + { + _logger.LogWarning("设备未在运行中,设备编号: {DeviceCode}, 当前状态: {RuntimeStatus}", + request.DeviceCode, deviceRuntime.RuntimeStatus); + return OperationResult.CreateFailure($"设备编号 {request.DeviceCode} 未在运行中,当前状态: {deviceRuntime.RuntimeStatus}"); + } + + // 获取当前用户 + var currentUser = _currentUserService.GetCurrentUserId(); + if (string.IsNullOrEmpty(currentUser)) + { + _logger.LogWarning("当前用户未登录"); + return OperationResult.CreateFailure("用户未登录"); + } + + // 停止设备 + deviceRuntime.Stop(); + + // 更新到数据库 + _deviceRuntimeRepository.UpdateRuntime(deviceRuntime); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + _logger.LogInformation("设备停止成功,设备编号: {DeviceCode}", deviceRuntime.DeviceCode); + + // 构建响应 + var response = new StopDeviceRuntimeResponse + { + Id = deviceRuntime.Id, + DeviceCode = deviceRuntime.DeviceCode, + RuntimeStatus = deviceRuntime.RuntimeStatus.ToString(), + RuntimeCode = deviceRuntime.RuntimeCode, + NetworkStackCode = deviceRuntime.NetworkStackCode, + UpdatedAt = deviceRuntime.UpdatedAt ?? DateTime.UtcNow + }; + + return OperationResult.CreateSuccess(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "停止设备运行时状态失败,设备编号: {DeviceCode}", request.DeviceCode); + return OperationResult.CreateFailure($"停止设备运行时状态失败: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeResponse.cs b/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeResponse.cs new file mode 100644 index 0000000..193b2f6 --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeResponse.cs @@ -0,0 +1,39 @@ +namespace CellularManagement.Application.Features.DeviceRuntimes.Commands.StopDeviceRuntime; + +/// +/// 停止设备运行时状态响应 +/// +public class StopDeviceRuntimeResponse +{ + /// + /// 运行时状态ID + /// + public string Id { get; set; } = null!; + + /// + /// 设备编号 + /// + public string DeviceCode { get; set; } = null!; + + /// + /// 运行时状态 + /// + public string RuntimeStatus { get; set; } = null!; + + /// + /// 运行编码 + /// + public string? RuntimeCode { get; set; } + + /// + /// 网络栈配置编号 + /// + public string? NetworkStackCode { get; set; } + + + + /// + /// 更新时间 + /// + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQuery.cs b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQuery.cs new file mode 100644 index 0000000..07dea16 --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQuery.cs @@ -0,0 +1,26 @@ +using CellularManagement.Domain.Common; +using MediatR; +using System.ComponentModel.DataAnnotations; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimeStatus; + +/// +/// 获取设备运行时状态查询 +/// +public class GetDeviceRuntimeStatusQuery : IRequest> +{ + /// + /// 设备编号 + /// + [Required(ErrorMessage = "设备编号不能为空")] + [MaxLength(50, ErrorMessage = "设备编号不能超过50个字符")] + public string DeviceCode { get; } + + /// + /// 初始化查询 + /// + public GetDeviceRuntimeStatusQuery(string deviceCode) + { + DeviceCode = deviceCode; + } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQueryHandler.cs b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQueryHandler.cs new file mode 100644 index 0000000..a61456f --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQueryHandler.cs @@ -0,0 +1,66 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using CellularManagement.Domain.Common; +using CellularManagement.Domain.Entities.Device; +using CellularManagement.Domain.Repositories.Device; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimeStatus; + +/// +/// 获取设备运行时状态查询处理器 +/// +public class GetDeviceRuntimeStatusQueryHandler : IRequestHandler> +{ + private readonly ICellularDeviceRuntimeRepository _deviceRuntimeRepository; + private readonly ILogger _logger; + + /// + /// 初始化查询处理器 + /// + public GetDeviceRuntimeStatusQueryHandler( + ICellularDeviceRuntimeRepository deviceRuntimeRepository, + ILogger logger) + { + _deviceRuntimeRepository = deviceRuntimeRepository; + _logger = logger; + } + + /// + /// 处理查询请求 + /// + public async Task> Handle(GetDeviceRuntimeStatusQuery request, CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("开始查询设备运行时状态,设备编号: {DeviceCode}", request.DeviceCode); + + // 获取设备运行时状态 + var deviceRuntime = await _deviceRuntimeRepository.GetRuntimeByDeviceCodeAsync(request.DeviceCode, cancellationToken); + + if (deviceRuntime == null) + { + _logger.LogWarning("未找到设备运行时状态,设备编号: {DeviceCode}", request.DeviceCode); + return OperationResult.CreateFailure($"未找到设备编号为 {request.DeviceCode} 的运行时状态"); + } + + // 构建响应 + var response = new GetDeviceRuntimeStatusResponse + { + DeviceCode = deviceRuntime.DeviceCode, + RuntimeStatus = deviceRuntime.RuntimeStatus.ToString(), + RuntimeCode = deviceRuntime.RuntimeCode, + NetworkStackCode = deviceRuntime.NetworkStackCode + }; + + _logger.LogInformation("查询设备运行时状态成功,设备编号: {DeviceCode}, 运行时状态: {RuntimeStatus}", + deviceRuntime.DeviceCode, deviceRuntime.RuntimeStatus); + + return OperationResult.CreateSuccess(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "查询设备运行时状态失败,设备编号: {DeviceCode}", request.DeviceCode); + return OperationResult.CreateFailure($"查询设备运行时状态失败: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusResponse.cs b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusResponse.cs new file mode 100644 index 0000000..1d98518 --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusResponse.cs @@ -0,0 +1,27 @@ +namespace CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimeStatus; + +/// +/// 获取设备运行时状态响应 +/// +public class GetDeviceRuntimeStatusResponse +{ + /// + /// 设备编号 + /// + public string DeviceCode { get; set; } = null!; + + /// + /// 运行时状态 + /// + public string RuntimeStatus { get; set; } = null!; + + /// + /// 运行编码 + /// + public string? RuntimeCode { get; set; } + + /// + /// 网络栈配置编号 + /// + public string? NetworkStackCode { get; set; } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQuery.cs b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQuery.cs new file mode 100644 index 0000000..142638f --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQuery.cs @@ -0,0 +1,40 @@ +using CellularManagement.Domain.Common; +using MediatR; +using System.ComponentModel.DataAnnotations; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimes; + +/// +/// 获取设备运行时状态列表查询 +/// +public class GetDeviceRuntimesQuery : IRequest> +{ + /// + /// 页码,从1开始 + /// + [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] + public int PageNumber { get; set; } = 1; + + /// + /// 每页记录数 + /// + [Range(1, 100, ErrorMessage = "每页记录数必须在1-100之间")] + public int PageSize { get; set; } = 10; + + /// + /// 搜索关键词 + /// + [MaxLength(100, ErrorMessage = "搜索关键词不能超过100个字符")] + public string? SearchTerm { get; set; } + + /// + /// 运行时状态过滤 + /// + public string? RuntimeStatus { get; set; } + + /// + /// 网络栈配置编号过滤 + /// + [MaxLength(50, ErrorMessage = "网络栈配置编号不能超过50个字符")] + public string? NetworkStackCode { get; set; } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQueryHandler.cs b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQueryHandler.cs new file mode 100644 index 0000000..1c17ce6 --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQueryHandler.cs @@ -0,0 +1,97 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using CellularManagement.Domain.Common; +using CellularManagement.Domain.Entities.Device; +using CellularManagement.Domain.Repositories.Device; + +namespace CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimes; + +/// +/// 获取设备运行时状态列表查询处理器 +/// +public class GetDeviceRuntimesQueryHandler : IRequestHandler> +{ + private readonly ICellularDeviceRuntimeRepository _deviceRuntimeRepository; + private readonly ILogger _logger; + + /// + /// 初始化查询处理器 + /// + public GetDeviceRuntimesQueryHandler( + ICellularDeviceRuntimeRepository deviceRuntimeRepository, + ILogger logger) + { + _deviceRuntimeRepository = deviceRuntimeRepository; + _logger = logger; + } + + /// + /// 处理查询请求 + /// + public async Task> Handle(GetDeviceRuntimesQuery request, CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("开始查询设备运行时状态列表,页码: {PageNumber}, 每页数量: {PageSize}, 搜索关键词: {SearchTerm}, 运行时状态: {RuntimeStatus}, 网络栈配置编号: {NetworkStackCode}", + request.PageNumber, request.PageSize, request.SearchTerm, request.RuntimeStatus, request.NetworkStackCode); + + // 执行分页查询 + var (totalCount, items) = await _deviceRuntimeRepository.SearchRuntimesAsync( + request.SearchTerm, + request.PageNumber, + request.PageSize, + cancellationToken); + + // 计算总页数 + var totalPages = (int)Math.Ceiling((double)totalCount / request.PageSize); + + // 构建响应 + var response = new GetDeviceRuntimesResponse + { + TotalCount = totalCount, + PageNumber = request.PageNumber, + PageSize = request.PageSize, + TotalPages = totalPages, + Items = items.Select(MapToDto).ToList() + }; + + _logger.LogInformation("查询设备运行时状态列表成功,总记录数: {TotalCount}, 总页数: {TotalPages}", totalCount, totalPages); + + return OperationResult.CreateSuccess(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "查询设备运行时状态列表失败"); + return OperationResult.CreateFailure($"查询设备运行时状态列表失败: {ex.Message}"); + } + } + + /// + /// 映射到数据传输对象 + /// + private static DeviceRuntimeDto MapToDto(CellularDeviceRuntime runtime) + { + return new DeviceRuntimeDto + { + Id = runtime.Id, + DeviceCode = runtime.DeviceCode, + RuntimeStatus = runtime.RuntimeStatus.ToString(), + RuntimeCode = runtime.RuntimeCode, + NetworkStackCode = runtime.NetworkStackCode, + + CreatedAt = runtime.CreatedAt, + UpdatedAt = runtime.UpdatedAt ?? DateTime.UtcNow, + Device = runtime.Device != null ? new DeviceInfoDto + { + Id = runtime.Device.Id, + Name = runtime.Device.Name, + SerialNumber = runtime.Device.SerialNumber, + DeviceCode = runtime.Device.DeviceCode, + Description = runtime.Device.Description, + IpAddress = runtime.Device.IpAddress, + AgentPort = runtime.Device.AgentPort, + IsEnabled = runtime.Device.IsEnabled + } : null + }; + } +} \ No newline at end of file diff --git a/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesResponse.cs b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesResponse.cs new file mode 100644 index 0000000..52edfaf --- /dev/null +++ b/src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesResponse.cs @@ -0,0 +1,126 @@ +namespace CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimes; + +/// +/// 获取设备运行时状态列表响应 +/// +public class GetDeviceRuntimesResponse +{ + /// + /// 总记录数 + /// + public int TotalCount { get; set; } + + /// + /// 当前页码 + /// + public int PageNumber { get; set; } + + /// + /// 每页记录数 + /// + public int PageSize { get; set; } + + /// + /// 总页数 + /// + public int TotalPages { get; set; } + + /// + /// 设备运行时状态列表 + /// + public List Items { get; set; } = new(); +} + +/// +/// 设备运行时状态数据传输对象 +/// +public class DeviceRuntimeDto +{ + /// + /// 运行时状态ID + /// + public string Id { get; set; } = null!; + + /// + /// 设备编号 + /// + public string DeviceCode { get; set; } = null!; + + /// + /// 运行时状态 + /// + public string RuntimeStatus { get; set; } = null!; + + /// + /// 运行编码 + /// + public string? RuntimeCode { get; set; } + + /// + /// 网络栈配置编号 + /// + public string? NetworkStackCode { get; set; } + + + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 更新时间 + /// + public DateTime UpdatedAt { get; set; } + + /// + /// 设备信息 + /// + public DeviceInfoDto? Device { get; set; } +} + +/// +/// 设备信息数据传输对象 +/// +public class DeviceInfoDto +{ + /// + /// 设备ID + /// + public string Id { get; set; } = null!; + + /// + /// 设备名称 + /// + public string Name { get; set; } = null!; + + /// + /// 设备序列号 + /// + public string SerialNumber { get; set; } = null!; + + /// + /// 设备编码 + /// + public string DeviceCode { get; set; } = null!; + + /// + /// 设备描述 + /// + public string Description { get; set; } = null!; + + /// + /// IP地址 + /// + public string IpAddress { get; set; } = null!; + + /// + /// Agent端口 + /// + public int AgentPort { get; set; } + + /// + /// 是否启用 + /// + public bool IsEnabled { get; set; } +} \ No newline at end of file diff --git a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs index 2cae6e4..eccae39 100644 --- a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs +++ b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs @@ -17,8 +17,6 @@ public class CreateDeviceCommand : IRequest /// 设备描述 /// @@ -42,9 +40,4 @@ public class CreateDeviceCommand : IRequest public bool IsEnabled { get; set; } = true; - - /// - /// 设备状态(启动/未启动) - /// - public bool IsRunning { get; set; } = false; } \ No newline at end of file diff --git a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs index b654d58..b69c93e 100644 --- a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs +++ b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs @@ -18,6 +18,7 @@ namespace CellularManagement.Application.Features.Devices.Commands.CreateDevice; public class CreateDeviceCommandHandler : IRequestHandler> { private readonly ICellularDeviceRepository _deviceRepository; + private readonly ICellularDeviceRuntimeRepository _deviceRuntimeRepository; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly ICurrentUserService _currentUserService; @@ -29,6 +30,7 @@ public class CreateDeviceCommandHandler : IRequestHandler public CreateDeviceCommandHandler( ICellularDeviceRepository deviceRepository, + ICellularDeviceRuntimeRepository deviceRuntimeRepository, ILogger logger, IUnitOfWork unitOfWork, ICurrentUserService currentUserService, @@ -36,6 +38,7 @@ public class CreateDeviceCommandHandler : IRequestHandler.CreateFailure("生成设备编号失败"); } - // 创建设备并保存 - var deviceResult = await CreateAndSaveDeviceAsync(request, serialNumber, deviceCode, currentUserId.Data, cancellationToken); + // 创建设备和运行时状态并保存(原子性操作) + var deviceResult = await CreateAndSaveDeviceWithRuntimeAsync(request, serialNumber, deviceCode, currentUserId.Data, cancellationToken); if (!deviceResult.IsSuccess) { return OperationResult.CreateFailure(deviceResult.ErrorMessages); @@ -124,7 +127,6 @@ public class CreateDeviceCommandHandler : IRequestHandler - /// 创建设备并保存到数据库 + /// 创建设备和运行时状态并保存到数据库(原子性操作) /// - private async Task> CreateAndSaveDeviceAsync( + private async Task> CreateAndSaveDeviceWithRuntimeAsync( CreateDeviceCommand request, string serialNumber, string deviceCode, @@ -296,21 +298,32 @@ public class CreateDeviceCommandHandler : IRequestHandler.CreateSuccess(device); } catch (Exception ex) { - _logger.LogError(ex, "创建设备实体时发生错误,设备名称: {DeviceName}", request.DeviceName); - 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 74be1f4..27eda3a 100644 --- a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs +++ b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs @@ -38,11 +38,6 @@ public class CreateDeviceResponse /// public bool IsEnabled { get; set; } - /// - /// 设备状态(启动/未启动) - /// - public bool IsRunning { get; set; } - /// /// 创建时间 /// diff --git a/src/X1.Application/Features/Devices/Commands/DeleteDevice/DeleteDeviceCommandHandler.cs b/src/X1.Application/Features/Devices/Commands/DeleteDevice/DeleteDeviceCommandHandler.cs index 95a960c..a6be11d 100644 --- a/src/X1.Application/Features/Devices/Commands/DeleteDevice/DeleteDeviceCommandHandler.cs +++ b/src/X1.Application/Features/Devices/Commands/DeleteDevice/DeleteDeviceCommandHandler.cs @@ -13,6 +13,7 @@ namespace CellularManagement.Application.Features.Devices.Commands.DeleteDevice; public class DeleteDeviceCommandHandler : IRequestHandler> { private readonly ICellularDeviceRepository _deviceRepository; + private readonly ICellularDeviceRuntimeRepository _runtimeRepository; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; @@ -21,10 +22,12 @@ public class DeleteDeviceCommandHandler : IRequestHandler public DeleteDeviceCommandHandler( ICellularDeviceRepository deviceRepository, + ICellularDeviceRuntimeRepository runtimeRepository, ILogger logger, IUnitOfWork unitOfWork) { _deviceRepository = deviceRepository; + _runtimeRepository = runtimeRepository; _logger = logger; _unitOfWork = unitOfWork; } @@ -46,6 +49,10 @@ public class DeleteDeviceCommandHandler : IRequestHandler.CreateFailure($"未找到ID为 {request.DeviceId} 的设备"); } + // 软删除相关的设备运行时状态记录 + await _runtimeRepository.DeleteRuntimeByDeviceCodeAsync(device.DeviceCode, cancellationToken); + _logger.LogInformation("已软删除设备运行时状态记录,设备编号: {DeviceCode}", device.DeviceCode); + // 删除设备 await _deviceRepository.DeleteDeviceAsync(request.DeviceId, cancellationToken); diff --git a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs index 1847c22..2687e13 100644 --- a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs +++ b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs @@ -33,9 +33,4 @@ public class UpdateDeviceCommand : IRequest public bool IsEnabled { get; set; } = true; - - /// - /// 设备状态(启动/未启动) - /// - public bool IsRunning { get; set; } = false; } \ No newline at end of file diff --git a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs index 2eb5894..606418e 100644 --- a/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs +++ b/src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs @@ -71,8 +71,7 @@ public class UpdateDeviceCommandHandler : IRequestHandler public bool IsEnabled { get; set; } - /// - /// 设备状态(启动/未启动) - /// - public bool IsRunning { get; set; } - /// /// 更新时间 /// diff --git a/src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs b/src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs index 5e1765d..398c74e 100644 --- a/src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs +++ b/src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs @@ -35,7 +35,7 @@ public class GetDeviceByIdQueryHandler : IRequestHandler - /// 设备状态(启动/未启动) + /// 创建时间 /// - public bool IsRunning { get; set; } + public DateTime CreatedAt { get; set; } /// - /// 创建时间 + /// 运行时状态(-1表示未知状态) /// - public DateTime CreatedAt { get; set; } + public int RuntimeStatus { get; set; } } \ No newline at end of file diff --git a/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs b/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs index 0b045b3..18721ba 100644 --- a/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs +++ b/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs @@ -49,8 +49,8 @@ public class GetDevicesQueryHandler : IRequestHandler 1, HasNextPage = request.PageNumber < (int)Math.Ceiling(devices.TotalCount / (double)request.PageSize), - Items = devices.Items.Select(device => new GetDeviceByIdResponse + Items = devices.Items.Select(item => new GetDeviceByIdResponse { - DeviceId = device.Id, - DeviceName = device.Name, - DeviceCode = device.DeviceCode, - Description = device.Description, - AgentPort = device.AgentPort, - IsEnabled = device.IsEnabled, - IsRunning = device.IsRunning, - CreatedAt = device.CreatedAt + DeviceId = item.Device.Id, + DeviceName = item.Device.Name, + DeviceCode = item.Device.DeviceCode, + Description = item.Device.Description, + AgentPort = item.Device.AgentPort, + IsEnabled = item.Device.IsEnabled, + CreatedAt = item.Device.CreatedAt, + RuntimeStatus = item.Runtime?.RuntimeStatus != null ? (int)item.Runtime.RuntimeStatus : -1 }).ToList() }; diff --git a/src/X1.Domain/Entities/Common/BaseEntity.cs b/src/X1.Domain/Entities/Common/BaseEntity.cs index 76055ca..2873453 100644 --- a/src/X1.Domain/Entities/Common/BaseEntity.cs +++ b/src/X1.Domain/Entities/Common/BaseEntity.cs @@ -1,11 +1,12 @@ using System.ComponentModel.DataAnnotations; +using CellularManagement.Domain.Abstractions; namespace CellularManagement.Domain.Entities.Common; /// /// 基础实体类 /// -public abstract class BaseEntity +public abstract class BaseEntity : ISoftDelete { /// /// 实体ID diff --git a/src/X1.Domain/Entities/Device/CellularDevice.cs b/src/X1.Domain/Entities/Device/CellularDevice.cs index eb76e9d..4f0937f 100644 --- a/src/X1.Domain/Entities/Device/CellularDevice.cs +++ b/src/X1.Domain/Entities/Device/CellularDevice.cs @@ -64,9 +64,9 @@ public class CellularDevice : AuditableEntity public bool IsEnabled { get; private set; } = true; /// - /// 设备状态(启动/未启动) + /// 设备运行时状态 /// - public bool IsRunning { get; private set; } = false; + public virtual CellularDeviceRuntime? Runtime { get; private set; } /// /// 创建设备 @@ -79,8 +79,7 @@ public class CellularDevice : AuditableEntity int agentPort, string ipAddress, string createdBy, - bool isEnabled = true, - bool isRunning = false) + bool isEnabled = true) { var device = new CellularDevice { @@ -92,7 +91,6 @@ public class CellularDevice : AuditableEntity AgentPort = agentPort, IpAddress = ipAddress, IsEnabled = isEnabled, - IsRunning = isRunning, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, CreatedBy = createdBy, @@ -113,8 +111,7 @@ public class CellularDevice : AuditableEntity int agentPort, string ipAddress, string updatedBy, - bool isEnabled = true, - bool isRunning = false) + bool isEnabled = true) { Name = name; SerialNumber = serialNumber; @@ -123,7 +120,6 @@ public class CellularDevice : AuditableEntity AgentPort = agentPort; IpAddress = ipAddress; IsEnabled = isEnabled; - IsRunning = isRunning; UpdatedAt = DateTime.UtcNow; UpdatedBy = updatedBy; } @@ -146,21 +142,5 @@ public class CellularDevice : AuditableEntity UpdatedAt = DateTime.UtcNow; } - /// - /// 启动设备 - /// - public void Start() - { - IsRunning = true; - UpdatedAt = DateTime.UtcNow; - } - /// - /// 停止设备 - /// - public void Stop() - { - IsRunning = false; - UpdatedAt = DateTime.UtcNow; - } } \ No newline at end of file diff --git a/src/X1.Domain/Entities/Device/CellularDeviceRuntime.cs b/src/X1.Domain/Entities/Device/CellularDeviceRuntime.cs new file mode 100644 index 0000000..aa14bf5 --- /dev/null +++ b/src/X1.Domain/Entities/Device/CellularDeviceRuntime.cs @@ -0,0 +1,84 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using CellularManagement.Domain.Entities.Common; + +namespace CellularManagement.Domain.Entities.Device; + +/// +/// 蜂窝设备运行时状态实体 +/// +public class CellularDeviceRuntime : BaseEntity +{ + private CellularDeviceRuntime() { } + + /// + /// 设备编号 + /// + [Required] + [MaxLength(50)] + public string DeviceCode { get; private set; } = null!; + + /// + /// 运行时状态 + /// + public DeviceRuntimeStatus RuntimeStatus { get; private set; } = DeviceRuntimeStatus.Initializing; + + /// + /// 运行编码(仅在Running状态时生成) + /// + [MaxLength(50)] + public string? RuntimeCode { get; private set; } + + /// + /// 网络栈配置编号 + /// + [MaxLength(50)] + public string? NetworkStackCode { get; private set; } + + /// + /// 设备实例 + /// + public virtual CellularDevice Device { get; private set; } = null!; + + /// + /// 创建设备运行时状态 + /// + public static CellularDeviceRuntime Create( + string deviceCode, + string createdBy, + DeviceRuntimeStatus runtimeStatus = DeviceRuntimeStatus.Initializing, + string? networkStackCode = null) + { + var runtime = new CellularDeviceRuntime + { + Id = Guid.NewGuid().ToString(), + DeviceCode = deviceCode, + RuntimeStatus = runtimeStatus, + RuntimeCode = null, // 运行编码由业务层设置 + NetworkStackCode = networkStackCode, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + return runtime; + } + + /// + /// 启动设备 + /// + public void Start(string networkStackCode, string runtimeCode) + { + RuntimeStatus = DeviceRuntimeStatus.Running; + NetworkStackCode = networkStackCode; + RuntimeCode = runtimeCode; + } + + /// + /// 停止设备 + /// + public void Stop() + { + RuntimeStatus = DeviceRuntimeStatus.Stopping; + RuntimeCode = null; + } +} \ No newline at end of file diff --git a/src/X1.Domain/Entities/Device/CellularDeviceRuntimeDetail.cs b/src/X1.Domain/Entities/Device/CellularDeviceRuntimeDetail.cs new file mode 100644 index 0000000..a32fc47 --- /dev/null +++ b/src/X1.Domain/Entities/Device/CellularDeviceRuntimeDetail.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.DataAnnotations; +using CellularManagement.Domain.Abstractions; + +namespace CellularManagement.Domain.Entities.Device; + +/// +/// 蜂窝设备运行时明细表实体 +/// +public class CellularDeviceRuntimeDetail : Entity +{ + private CellularDeviceRuntimeDetail() { } + + /// + /// 运行编码 + /// + [Required] + [MaxLength(50)] + public string RuntimeCode { get; private set; } = null!; + + /// + /// 运行时状态(true=运行中,false=已停止) + /// + public bool RuntimeStatus { get; private set; } = false; + + /// + /// 创建人ID + /// + [MaxLength(450)] + public string? CreatedBy { get; private set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; private set; } + + /// + /// 创建设备运行时明细记录 + /// + public static CellularDeviceRuntimeDetail Create( + string runtimeCode, + string createdBy, + bool runtimeStatus = false) + { + var detail = new CellularDeviceRuntimeDetail + { + Id = Guid.NewGuid().ToString(), + RuntimeCode = runtimeCode, + RuntimeStatus = runtimeStatus, + CreatedBy = createdBy, + CreatedAt = DateTime.UtcNow + }; + + return detail; + } +} \ No newline at end of file diff --git a/src/X1.Domain/Entities/Device/CellularDeviceRuntimeHistory.cs b/src/X1.Domain/Entities/Device/CellularDeviceRuntimeHistory.cs new file mode 100644 index 0000000..59c843c --- /dev/null +++ b/src/X1.Domain/Entities/Device/CellularDeviceRuntimeHistory.cs @@ -0,0 +1,106 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using CellularManagement.Domain.Entities.Common; + +namespace CellularManagement.Domain.Entities.Device; + +/// +/// 蜂窝设备运行时历史记录实体 +/// +public class CellularDeviceRuntimeHistory : BaseEntity +{ + private CellularDeviceRuntimeHistory() { } + + /// + /// 运行时ID(外键) + /// + [Required] + [MaxLength(450)] + public string RuntimeId { get; private set; } = null!; + + /// + /// 设备编号 + /// + [Required] + [MaxLength(50)] + public string DeviceCode { get; private set; } = null!; + + /// + /// 操作前状态 + /// + public DeviceRuntimeStatus PreviousStatus { get; private set; } + + /// + /// 操作后状态 + /// + public DeviceRuntimeStatus NewStatus { get; private set; } + + /// + /// 运行编码 + /// + [MaxLength(50)] + public string? RuntimeCode { get; private set; } + + /// + /// 网络栈配置编号 + /// + [MaxLength(50)] + public string? NetworkStackCode { get; private set; } + + /// + /// 操作人ID + /// + [Required] + [MaxLength(450)] + public string OperatorId { get; private set; } = null!; + + /// + /// 操作描述 + /// + [Required] + [MaxLength(200)] + public string OperationDescription { get; private set; } = null!; + + /// + /// 操作时间 + /// + public DateTime OperationAt { get; private set; } + + /// + /// 运行时实例 + /// + public virtual CellularDeviceRuntime Runtime { get; private set; } = null!; + + /// + /// 创建设备运行时历史记录 + /// + public static CellularDeviceRuntimeHistory Create( + string runtimeId, + string deviceCode, + DeviceRuntimeStatus previousStatus, + DeviceRuntimeStatus newStatus, + string? runtimeCode, + string? networkStackCode, + string operatorId, + string operationDescription, + DateTime operationAt) + { + var history = new CellularDeviceRuntimeHistory + { + Id = Guid.NewGuid().ToString(), + RuntimeId = runtimeId, + DeviceCode = deviceCode, + PreviousStatus = previousStatus, + NewStatus = newStatus, + RuntimeCode = runtimeCode, + NetworkStackCode = networkStackCode, + OperatorId = operatorId, + OperationDescription = operationDescription, + OperationAt = operationAt, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + return history; + } +} \ No newline at end of file diff --git a/src/X1.Domain/Entities/Device/DeviceConnectionStatus.cs b/src/X1.Domain/Entities/Device/DeviceConnectionStatus.cs new file mode 100644 index 0000000..c9bd0d0 --- /dev/null +++ b/src/X1.Domain/Entities/Device/DeviceConnectionStatus.cs @@ -0,0 +1,22 @@ +namespace CellularManagement.Domain.Entities.Device; + +/// +/// 设备连接状态枚举 +/// +public enum DeviceConnectionStatus +{ + /// + /// 初始化 + /// + Initializing = 0, + + /// + /// 未连接 + /// + Disconnected = 1, + + /// + /// 已连接 + /// + Connected = 2 +} \ No newline at end of file diff --git a/src/X1.Domain/Entities/Device/DeviceRuntimeStatus.cs b/src/X1.Domain/Entities/Device/DeviceRuntimeStatus.cs new file mode 100644 index 0000000..8ece37b --- /dev/null +++ b/src/X1.Domain/Entities/Device/DeviceRuntimeStatus.cs @@ -0,0 +1,22 @@ +namespace CellularManagement.Domain.Entities.Device; + +/// +/// 设备运行时状态枚举 +/// +public enum DeviceRuntimeStatus +{ + /// + /// 初始化 + /// + Initializing = 0, + + /// + /// 运行中 + /// + Running = 1, + + /// + /// 停止中 + /// + Stopping = 2 +} \ No newline at end of file diff --git a/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs b/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs index cd55476..f59408e 100644 --- a/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs +++ b/src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs @@ -35,6 +35,11 @@ public interface ICellularDeviceRepository : IBaseRepository /// Task GetDeviceByIdAsync(string id, CancellationToken cancellationToken = default); + /// + /// 根据ID获取蜂窝设备(包含运行时状态) + /// + Task GetDeviceByIdWithRuntimeAsync(string id, CancellationToken cancellationToken = default); + /// /// 根据序列号获取蜂窝设备 /// @@ -56,6 +61,15 @@ public interface ICellularDeviceRepository : IBaseRepository int pageSize, CancellationToken cancellationToken = default); + /// + /// 搜索蜂窝设备(包含运行时状态) + /// + Task<(int TotalCount, IList<(CellularDevice Device, CellularDeviceRuntime? Runtime)> Items)> SearchDevicesWithRuntimeAsync( + string? keyword, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default); + /// /// 检查蜂窝设备是否存在 /// diff --git a/src/X1.Domain/Repositories/Device/ICellularDeviceRuntimeDetailRepository.cs b/src/X1.Domain/Repositories/Device/ICellularDeviceRuntimeDetailRepository.cs new file mode 100644 index 0000000..55a3cd7 --- /dev/null +++ b/src/X1.Domain/Repositories/Device/ICellularDeviceRuntimeDetailRepository.cs @@ -0,0 +1,71 @@ +using CellularManagement.Domain.Entities.Device; +using CellularManagement.Domain.Repositories.Base; + +namespace CellularManagement.Domain.Repositories.Device; + +/// +/// 蜂窝设备运行时明细仓储接口 +/// +public interface ICellularDeviceRuntimeDetailRepository : IBaseRepository +{ + /// + /// 根据运行编码获取运行时明细列表 + /// + /// 运行编码 + /// 取消令牌 + /// 运行时明细列表 + Task> GetDetailsByRuntimeCodeAsync(string runtimeCode, CancellationToken cancellationToken = default); + + /// + /// 根据创建人获取运行时明细列表 + /// + /// 创建人ID + /// 取消令牌 + /// 运行时明细列表 + Task> GetDetailsByCreatedByAsync(string createdBy, CancellationToken cancellationToken = default); + + /// + /// 根据运行时状态获取运行时明细列表 + /// + /// 运行时状态(true=运行中,false=已停止) + /// 取消令牌 + /// 运行时明细列表 + Task> GetDetailsByRuntimeStatusAsync(bool runtimeStatus, CancellationToken cancellationToken = default); + + /// + /// 分页查询运行时明细 + /// + /// 运行编码(可选) + /// 创建人(可选) + /// 运行时状态(可选,true=运行中,false=已停止) + /// 开始日期(可选) + /// 结束日期(可选) + /// 页码 + /// 每页数量 + /// 取消令牌 + /// 分页结果 + Task<(int TotalCount, List Items)> SearchDetailsAsync( + string? runtimeCode = null, + string? createdBy = null, + bool? runtimeStatus = null, + DateTime? startDate = null, + DateTime? endDate = null, + int pageNumber = 1, + int pageSize = 10, + CancellationToken cancellationToken = default); + + /// + /// 获取运行时明细总数 + /// + /// 取消令牌 + /// 总数 + Task GetDetailCountAsync(CancellationToken cancellationToken = default); + + /// + /// 批量添加运行时明细 + /// + /// 明细列表 + /// 取消令牌 + /// 添加后的明细列表 + Task> AddDetailsRangeAsync(IEnumerable details, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/X1.Domain/Repositories/Device/ICellularDeviceRuntimeRepository.cs b/src/X1.Domain/Repositories/Device/ICellularDeviceRuntimeRepository.cs new file mode 100644 index 0000000..5ca8d04 --- /dev/null +++ b/src/X1.Domain/Repositories/Device/ICellularDeviceRuntimeRepository.cs @@ -0,0 +1,107 @@ +using CellularManagement.Domain.Entities.Device; +using CellularManagement.Domain.Repositories.Base; + +namespace CellularManagement.Domain.Repositories.Device; + +/// +/// 蜂窝设备运行时状态仓储接口 +/// 组合命令和查询仓储接口 +/// +public interface ICellularDeviceRuntimeRepository : IBaseRepository +{ + /// + /// 添加设备运行时状态 + /// + Task AddRuntimeAsync(CellularDeviceRuntime runtime, CancellationToken cancellationToken = default); + + /// + /// 更新设备运行时状态 + /// + void UpdateRuntime(CellularDeviceRuntime runtime); + + /// + /// 删除设备运行时状态 + /// + Task DeleteRuntimeAsync(string id, CancellationToken cancellationToken = default); + + /// + /// 获取所有设备运行时状态 + /// + Task> GetAllRuntimesAsync(CancellationToken cancellationToken = default); + + /// + /// 根据ID获取设备运行时状态 + /// + Task GetRuntimeByIdAsync(string id, CancellationToken cancellationToken = default); + + /// + /// 根据设备编号获取运行时状态 + /// + Task GetRuntimeByDeviceCodeAsync(string deviceCode, CancellationToken cancellationToken = default); + + /// + /// 根据设备编号获取运行时状态(包含设备信息) + /// + Task GetRuntimeByDeviceCodeWithDeviceAsync(string deviceCode, CancellationToken cancellationToken = default); + + /// + /// 根据运行编码获取运行时状态 + /// + Task GetRuntimeByRuntimeCodeAsync(string runtimeCode, CancellationToken cancellationToken = default); + + /// + /// 获取正在运行的设备运行时状态 + /// + Task> GetRunningRuntimesAsync(CancellationToken cancellationToken = default); + + /// + /// 根据运行时状态获取设备运行时状态 + /// + Task> GetRuntimesByStatusAsync(DeviceRuntimeStatus runtimeStatus, CancellationToken cancellationToken = default); + + /// + /// 根据网络栈配置编号获取运行时状态 + /// + Task> GetRuntimesByNetworkStackCodeAsync(string networkStackCode, CancellationToken cancellationToken = default); + + /// + /// 搜索设备运行时状态 + /// + Task> SearchRuntimesAsync( + string? keyword, + CancellationToken cancellationToken = default); + + /// + /// 搜索设备运行时状态(分页) + /// + Task<(int TotalCount, IList Items)> SearchRuntimesAsync( + string? keyword, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 检查设备运行时状态是否存在 + /// + Task ExistsAsync(string id, CancellationToken cancellationToken = default); + + /// + /// 检查设备编号是否有运行时状态 + /// + Task DeviceCodeExistsAsync(string deviceCode, CancellationToken cancellationToken = default); + + /// + /// 检查运行编码是否存在 + /// + Task RuntimeCodeExistsAsync(string runtimeCode, CancellationToken cancellationToken = default); + + /// + /// 获取运行时状态总数 + /// + Task GetRuntimeCountAsync(CancellationToken cancellationToken = default); + + /// + /// 根据设备编号删除运行时状态 + /// + Task DeleteRuntimeByDeviceCodeAsync(string deviceCode, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs b/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs index b9e46ce..f1b9940 100644 --- a/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs +++ b/src/X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs @@ -24,7 +24,6 @@ public class CellularDeviceConfiguration : IEntityTypeConfiguration d.AgentPort).IsRequired().HasComment("Agent端口"); builder.Property(d => d.IpAddress).IsRequired().HasMaxLength(45).HasComment("IP地址"); builder.Property(d => d.IsEnabled).IsRequired().HasComment("是否启用"); - builder.Property(d => d.IsRunning).IsRequired().HasComment("设备状态(启动/未启动)"); builder.Property(d => d.CreatedAt).IsRequired().HasColumnType("timestamp with time zone").HasComment("创建时间"); builder.Property(d => d.UpdatedAt).IsRequired().HasColumnType("timestamp with time zone").HasComment("更新时间"); diff --git a/src/X1.Infrastructure/Configurations/Device/CellularDeviceRuntimeConfiguration.cs b/src/X1.Infrastructure/Configurations/Device/CellularDeviceRuntimeConfiguration.cs new file mode 100644 index 0000000..4caaba4 --- /dev/null +++ b/src/X1.Infrastructure/Configurations/Device/CellularDeviceRuntimeConfiguration.cs @@ -0,0 +1,66 @@ +using CellularManagement.Domain.Entities.Device; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CellularManagement.Infrastructure.Configurations.Device; + +/// +/// 蜂窝设备运行时状态配置 +/// +public class CellularDeviceRuntimeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("CellularDeviceRuntimes"); + + builder.HasKey(x => x.Id); + + // 基础属性配置 + builder.Property(x => x.DeviceCode) + .IsRequired() + .HasMaxLength(50); + + builder.Property(x => x.RuntimeStatus) + .IsRequired() + .HasConversion() + .HasDefaultValue(DeviceRuntimeStatus.Initializing); + + builder.Property(x => x.RuntimeCode) + .HasMaxLength(50); + + builder.Property(x => x.NetworkStackCode) + .HasMaxLength(50); + + + + // BaseEntity 属性配置 + builder.Property(x => x.CreatedAt) + .IsRequired(); + + builder.Property(x => x.UpdatedAt); + + builder.Property(x => x.IsDeleted) + .HasDefaultValue(false); + + // 配置与 CellularDevice 的关系 + builder.HasOne(x => x.Device) + .WithOne(x => x.Runtime) + .HasForeignKey(x => x.DeviceCode) + .HasPrincipalKey(x => x.DeviceCode) + .OnDelete(DeleteBehavior.Cascade); + + // 创建索引 + builder.HasIndex(x => x.DeviceCode) + .IsUnique(); + + // 复合索引:优化按设备编号和创建时间排序的查询 + builder.HasIndex(x => new { x.DeviceCode, x.CreatedAt }) + .HasDatabaseName("IX_CellularDeviceRuntimes_DeviceCode_CreatedAt"); + + builder.HasIndex(x => x.RuntimeStatus); + builder.HasIndex(x => x.RuntimeCode); + builder.HasIndex(x => x.NetworkStackCode); + + builder.HasIndex(x => x.IsDeleted); + } +} \ No newline at end of file diff --git a/src/X1.Infrastructure/Configurations/Device/CellularDeviceRuntimeDetailConfiguration.cs b/src/X1.Infrastructure/Configurations/Device/CellularDeviceRuntimeDetailConfiguration.cs new file mode 100644 index 0000000..4cc4206 --- /dev/null +++ b/src/X1.Infrastructure/Configurations/Device/CellularDeviceRuntimeDetailConfiguration.cs @@ -0,0 +1,55 @@ +using CellularManagement.Domain.Entities.Device; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CellularManagement.Infrastructure.Configurations.Device; + +/// +/// 蜂窝设备运行时明细配置 +/// +public class CellularDeviceRuntimeDetailConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("CellularDeviceRuntimeDetails"); + + builder.HasKey(x => x.Id); + + // 基础属性配置 + builder.Property(x => x.RuntimeCode) + .IsRequired() + .HasMaxLength(50); + + builder.Property(x => x.RuntimeStatus) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(x => x.CreatedBy) + .HasMaxLength(450); + + // Entity 属性配置 + builder.Property(x => x.Id) + .IsRequired() + .HasMaxLength(450); + + // 创建时间配置 + builder.Property(x => x.CreatedAt) + .IsRequired(); + + // 创建索引 + builder.HasIndex(x => x.RuntimeCode) + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_RuntimeCode"); + + builder.HasIndex(x => x.CreatedBy) + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_CreatedBy"); + + builder.HasIndex(x => x.RuntimeStatus) + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_RuntimeStatus"); + + // 复合索引:优化按创建时间排序的查询 + builder.HasIndex(x => x.CreatedAt) + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_CreatedAt"); + + + } +} \ No newline at end of file diff --git a/src/X1.Infrastructure/Context/AppDbContext.cs b/src/X1.Infrastructure/Context/AppDbContext.cs index 127bc15..efdfb31 100644 --- a/src/X1.Infrastructure/Context/AppDbContext.cs +++ b/src/X1.Infrastructure/Context/AppDbContext.cs @@ -47,7 +47,10 @@ public class AppDbContext : IdentityDbContext /// public DbSet ProtocolVersions { get; set; } = null!; - + /// + /// 蜂窝设备运行时状态集合 + /// + public DbSet CellularDeviceRuntimes { get; set; } = null!; /// /// RAN配置集合 diff --git a/src/X1.Infrastructure/DependencyInjection.cs b/src/X1.Infrastructure/DependencyInjection.cs index 550efc6..abc45c6 100644 --- a/src/X1.Infrastructure/DependencyInjection.cs +++ b/src/X1.Infrastructure/DependencyInjection.cs @@ -176,6 +176,7 @@ public static class DependencyInjection // 注册设备相关仓储 services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs b/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs index 16d202f..687c630 100644 --- a/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs +++ b/src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs @@ -75,6 +75,17 @@ public class CellularDeviceRepository : BaseRepository, ICellula return await QueryRepository.GetByIdAsync(id, cancellationToken: cancellationToken); } + /// + /// 根据ID获取蜂窝设备(包含运行时状态) + /// + public async Task GetDeviceByIdWithRuntimeAsync(string id, CancellationToken cancellationToken = default) + { + return await QueryRepository.FirstOrDefaultAsync( + d => d.Id == id, + include: q => q.Include(d => d.Runtime), + cancellationToken: cancellationToken); + } + /// /// 根据序列号获取蜂窝设备 /// @@ -97,6 +108,7 @@ public class CellularDeviceRepository : BaseRepository, ICellula query = query.Where(d => d.Name.Contains(keyword) || d.SerialNumber.Contains(keyword) || + d.DeviceCode.Contains(keyword) || d.Description.Contains(keyword)); } @@ -120,6 +132,7 @@ public class CellularDeviceRepository : BaseRepository, ICellula { predicate = d => d.Name.Contains(keyword) || d.SerialNumber.Contains(keyword) || + d.DeviceCode.Contains(keyword) || d.Description.Contains(keyword); } @@ -129,6 +142,40 @@ public class CellularDeviceRepository : BaseRepository, ICellula return (result.TotalCount, result.Items.ToList()); } + /// + /// 搜索蜂窝设备(包含运行时状态) + /// + public async Task<(int TotalCount, IList<(CellularDevice Device, CellularDeviceRuntime? Runtime)> Items)> SearchDevicesWithRuntimeAsync( + string? keyword, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default) + { + // 构建查询条件 + Expression> predicate = d => true; + + if (!string.IsNullOrWhiteSpace(keyword)) + { + predicate = d => d.Name.Contains(keyword) || + d.SerialNumber.Contains(keyword) || + d.DeviceCode.Contains(keyword) || + d.Description.Contains(keyword); + } + + // 执行分页查询,包含运行时状态 + var result = await QueryRepository.GetPagedAsync( + predicate, + pageNumber, + pageSize, + include: q => q.Include(d => d.Runtime), + cancellationToken: cancellationToken); + + // 构建结果 + var items = result.Items.Select(d => (Device: d, Runtime: d.Runtime)).ToList(); + + return (result.TotalCount, items); + } + /// /// 检查蜂窝设备是否存在 /// diff --git a/src/X1.Infrastructure/Repositories/Device/CellularDeviceRuntimeRepository.cs b/src/X1.Infrastructure/Repositories/Device/CellularDeviceRuntimeRepository.cs new file mode 100644 index 0000000..4667019 --- /dev/null +++ b/src/X1.Infrastructure/Repositories/Device/CellularDeviceRuntimeRepository.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Text.RegularExpressions; +using CellularManagement.Domain.Entities.Device; +using CellularManagement.Domain.Repositories.Device; +using CellularManagement.Domain.Repositories.Base; +using CellularManagement.Infrastructure.Repositories.Base; + +namespace CellularManagement.Infrastructure.Repositories.Device; + +/// +/// 蜂窝设备运行时状态仓储实现类 +/// +public class CellularDeviceRuntimeRepository : BaseRepository, ICellularDeviceRuntimeRepository +{ + private readonly ILogger _logger; + + /// + /// 初始化仓储 + /// + public CellularDeviceRuntimeRepository( + ICommandRepository commandRepository, + IQueryRepository queryRepository, + ILogger logger) + : base(commandRepository, queryRepository, logger) + { + _logger = logger; + } + + /// + /// 添加设备运行时状态 + /// + public async Task AddRuntimeAsync(CellularDeviceRuntime runtime, CancellationToken cancellationToken = default) + { + return await CommandRepository.AddAsync(runtime, cancellationToken); + } + + /// + /// 更新设备运行时状态 + /// + public void UpdateRuntime(CellularDeviceRuntime runtime) + { + CommandRepository.Update(runtime); + } + + /// + /// 删除设备运行时状态 + /// + public async Task DeleteRuntimeAsync(string id, CancellationToken cancellationToken = default) + { + await CommandRepository.DeleteByIdAsync(id, cancellationToken); + } + + /// + /// 获取所有设备运行时状态 + /// + public async Task> GetAllRuntimesAsync(CancellationToken cancellationToken = default) + { + var runtimes = await QueryRepository.GetAllAsync(cancellationToken: cancellationToken); + return runtimes.ToList(); + } + + /// + /// 根据ID获取设备运行时状态 + /// + public async Task GetRuntimeByIdAsync(string id, CancellationToken cancellationToken = default) + { + return await QueryRepository.GetByIdAsync(id, cancellationToken: cancellationToken); + } + + /// + /// 根据设备编号获取运行时状态(最新一条按创建时间排序) + /// + public async Task GetRuntimeByDeviceCodeAsync(string deviceCode, CancellationToken cancellationToken = default) + { + // 使用queryRepository获取所有匹配的记录,然后在内存中排序获取最新一条 + // 注意:由于queryRepository的限制,我们无法直接在数据库层面进行排序 + // 但通过添加的复合索引(DeviceCode, CreatedAt)可以提升查询性能 + var runtimes = await QueryRepository.FindAsync(r => r.DeviceCode == deviceCode, cancellationToken: cancellationToken); + return runtimes.OrderByDescending(r => r.CreatedAt).FirstOrDefault(); + } + + /// + /// 根据设备编号获取运行时状态(包含设备信息) + /// + public async Task GetRuntimeByDeviceCodeWithDeviceAsync(string deviceCode, CancellationToken cancellationToken = default) + { + return await QueryRepository.FirstOrDefaultAsync( + r => r.DeviceCode == deviceCode, + include: q => q.Include(r => r.Device), + cancellationToken: cancellationToken); + } + + /// + /// 根据运行编码获取运行时状态 + /// + public async Task GetRuntimeByRuntimeCodeAsync(string runtimeCode, CancellationToken cancellationToken = default) + { + return await QueryRepository.FirstOrDefaultAsync( + r => r.RuntimeCode == runtimeCode, + include: q => q.Include(r => r.Device), + cancellationToken: cancellationToken); + } + + /// + /// 获取正在运行的设备运行时状态 + /// + public async Task> GetRunningRuntimesAsync(CancellationToken cancellationToken = default) + { + var runtimes = await QueryRepository.FindAsync( + r => r.RuntimeStatus == DeviceRuntimeStatus.Running, + include: q => q.Include(r => r.Device), + cancellationToken: cancellationToken); + return runtimes.ToList(); + } + + /// + /// 根据运行时状态获取设备运行时状态 + /// + public async Task> GetRuntimesByStatusAsync(DeviceRuntimeStatus runtimeStatus, CancellationToken cancellationToken = default) + { + var runtimes = await QueryRepository.FindAsync( + r => r.RuntimeStatus == runtimeStatus, + include: q => q.Include(r => r.Device), + cancellationToken: cancellationToken); + return runtimes.ToList(); + } + + /// + /// 根据网络栈配置编号获取运行时状态 + /// + public async Task> GetRuntimesByNetworkStackCodeAsync(string networkStackCode, CancellationToken cancellationToken = default) + { + var runtimes = await QueryRepository.FindAsync( + r => r.NetworkStackCode == networkStackCode, + include: q => q.Include(r => r.Device), + cancellationToken: cancellationToken); + return runtimes.ToList(); + } + + /// + /// 搜索设备运行时状态 + /// + public async Task> SearchRuntimesAsync( + string? keyword, + CancellationToken cancellationToken = default) + { + var query = await QueryRepository.FindAsync(r => true, cancellationToken: cancellationToken); + + if (!string.IsNullOrWhiteSpace(keyword)) + { + query = query.Where(r => + r.DeviceCode.Contains(keyword) || + (r.RuntimeCode != null && r.RuntimeCode.Contains(keyword)) || + (r.NetworkStackCode != null && r.NetworkStackCode.Contains(keyword))); + } + + var runtimes = query; + return runtimes.ToList(); + } + + /// + /// 搜索设备运行时状态(分页) + /// + public async Task<(int TotalCount, IList Items)> SearchRuntimesAsync( + string? keyword, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default) + { + // 构建查询条件 + Expression> predicate = r => true; + + if (!string.IsNullOrWhiteSpace(keyword)) + { + predicate = r => r.DeviceCode.Contains(keyword) || + (r.RuntimeCode != null && r.RuntimeCode.Contains(keyword)) || + (r.NetworkStackCode != null && r.NetworkStackCode.Contains(keyword)); + } + + // 执行分页查询 + var result = await QueryRepository.GetPagedAsync(predicate, pageNumber, pageSize, null, cancellationToken); + + return (result.TotalCount, result.Items.ToList()); + } + + /// + /// 检查设备运行时状态是否存在 + /// + public async Task ExistsAsync(string id, CancellationToken cancellationToken = default) + { + return await QueryRepository.AnyAsync(r => r.Id == id, cancellationToken: cancellationToken); + } + + /// + /// 检查设备编号是否有运行时状态 + /// + public async Task DeviceCodeExistsAsync(string deviceCode, CancellationToken cancellationToken = default) + { + return await QueryRepository.AnyAsync(r => r.DeviceCode == deviceCode, cancellationToken: cancellationToken); + } + + /// + /// 检查运行编码是否存在 + /// + public async Task RuntimeCodeExistsAsync(string runtimeCode, CancellationToken cancellationToken = default) + { + return await QueryRepository.AnyAsync(r => r.RuntimeCode == runtimeCode, cancellationToken: cancellationToken); + } + + /// + /// 获取运行时状态总数 + /// + public async Task GetRuntimeCountAsync(CancellationToken cancellationToken = default) + { + return await QueryRepository.CountAsync(r => true, cancellationToken: cancellationToken); + } + + /// + /// 根据设备编号删除运行时状态 + /// + public async Task DeleteRuntimeByDeviceCodeAsync(string deviceCode, CancellationToken cancellationToken = default) + { + await CommandRepository.DeleteWhereAsync(r => r.DeviceCode == deviceCode, cancellationToken); + } +} \ No newline at end of file diff --git a/src/X1.Presentation/Controllers/DeviceRuntimesController.cs b/src/X1.Presentation/Controllers/DeviceRuntimesController.cs new file mode 100644 index 0000000..e370f9b --- /dev/null +++ b/src/X1.Presentation/Controllers/DeviceRuntimesController.cs @@ -0,0 +1,127 @@ +using CellularManagement.Application.Features.DeviceRuntimes.Commands.StartDeviceRuntime; +using CellularManagement.Application.Features.DeviceRuntimes.Commands.StopDeviceRuntime; +using CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimes; +using CellularManagement.Application.Features.DeviceRuntimes.Queries.GetDeviceRuntimeStatus; +using CellularManagement.Domain.Common; +using CellularManagement.Presentation.Abstractions; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace CellularManagement.Presentation.Controllers; + +/// +/// 设备运行时状态管理控制器 +/// +[Route("api/device-runtimes")] +[ApiController] +[Authorize] +public class DeviceRuntimesController : ApiController +{ + private readonly ILogger _logger; + + /// + /// 初始化设备运行时状态控制器 + /// + public DeviceRuntimesController(IMediator mediator, ILogger logger) + : base(mediator) + { + _logger = logger; + } + + /// + /// 获取设备运行时状态列表 + /// + [HttpGet] + public async Task> GetAll([FromQuery] GetDeviceRuntimesQuery query) + { + _logger.LogInformation("开始获取设备运行时状态列表,页码: {PageNumber}, 每页数量: {PageSize}, 搜索关键词: {SearchTerm}", + query.PageNumber, query.PageSize, query.SearchTerm); + + var result = await mediator.Send(query); + + if (result.IsSuccess) + { + _logger.LogInformation("获取设备运行时状态列表成功,总记录数: {TotalCount}", result.Data?.TotalCount); + } + else + { + _logger.LogWarning("获取设备运行时状态列表失败: {Error}", result.ErrorMessages?.FirstOrDefault()); + } + + return result; + } + + /// + /// 根据设备编号获取设备运行时状态 + /// + [HttpGet("{deviceCode}")] + public async Task> GetByDeviceCode(string deviceCode) + { + _logger.LogInformation("开始获取设备运行时状态,设备编号: {DeviceCode}", deviceCode); + + var query = new GetDeviceRuntimeStatusQuery(deviceCode); + var result = await mediator.Send(query); + + if (result.IsSuccess) + { + _logger.LogInformation("获取设备运行时状态成功,设备编号: {DeviceCode}, 运行时状态: {RuntimeStatus}", + deviceCode, result.Data?.RuntimeStatus); + } + else + { + _logger.LogWarning("获取设备运行时状态失败,设备编号: {DeviceCode}, 错误: {Error}", + deviceCode, result.ErrorMessages?.FirstOrDefault()); + } + + return result; + } + + /// + /// 启动设备 + /// + [HttpPost("start")] + public async Task> StartDevices([FromBody] StartDeviceRuntimeCommand command) + { + _logger.LogInformation("开始批量启动设备,设备数量: {DeviceCount}", command.DeviceRequests.Count); + + var result = await mediator.Send(command); + + if (result.IsSuccess) + { + _logger.LogInformation("批量启动设备成功,总数量: {TotalCount}, 成功: {SuccessCount}, 失败: {FailureCount}", + result.Data?.Summary?.TotalCount, result.Data?.Summary?.SuccessCount, result.Data?.Summary?.FailureCount); + } + else + { + _logger.LogWarning("批量启动设备失败: {Error}", result.ErrorMessages?.FirstOrDefault()); + } + + return result; + } + + /// + /// 停止设备 + /// + [HttpPost("{deviceCode}/stop")] + public async Task> StopDevice(string deviceCode) + { + _logger.LogInformation("开始停止设备,设备编号: {DeviceCode}", deviceCode); + + var command = new StopDeviceRuntimeCommand { DeviceCode = deviceCode }; + var result = await mediator.Send(command); + + if (result.IsSuccess) + { + _logger.LogInformation("设备停止成功,设备编号: {DeviceCode}", deviceCode); + } + else + { + _logger.LogWarning("设备停止失败,设备编号: {DeviceCode}, 错误: {Error}", + deviceCode, result.ErrorMessages?.FirstOrDefault()); + } + + return result; + } +} \ No newline at end of file diff --git a/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx b/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx index f550f40..3a38467 100644 --- a/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx +++ b/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { Device, DeviceStatus, DeviceRunningStatus } from '@/services/instrumentService'; +import { Device } from '@/services/instrumentService'; import { Badge } from '@/components/ui/badge'; import { Pencil1Icon, TrashIcon } from '@radix-ui/react-icons'; import { DensityType } from '@/components/ui/TableToolbar'; @@ -16,7 +16,6 @@ import { DensityType } from '@/components/ui/TableToolbar'; * - description: 设备描述 * - agentPort: Agent端口 * - isEnabled: 是否启用 - * - isRunning: 设备状态(启动/未启动) * - createdAt: 创建时间 * * 注意:IpAddress字段在创建/更新时使用,但查询响应中不返回 @@ -45,15 +44,6 @@ function DeviceStatusBadge({ isEnabled }: { isEnabled: boolean }) { ); } -// 设备运行状态徽章组件 -function DeviceRunningStatusBadge({ isRunning }: { isRunning: boolean }) { - return ( - - {isRunning ? '运行中' : '已停止'} - - ); -} - // 日期显示组件 function DateDisplay({ date }: { date: string }) { return ( @@ -110,8 +100,6 @@ export default function DevicesTable({ return {device.agentPort}; case 'isEnabled': return ; - case 'isRunning': - return ; case 'createdAt': return ; case 'actions': diff --git a/src/X1.WebUI/src/pages/instruments/DevicesView.tsx b/src/X1.WebUI/src/pages/instruments/DevicesView.tsx index d8d1567..a0485d3 100644 --- a/src/X1.WebUI/src/pages/instruments/DevicesView.tsx +++ b/src/X1.WebUI/src/pages/instruments/DevicesView.tsx @@ -17,7 +17,6 @@ const defaultColumns = [ { key: 'description', title: '描述', visible: true }, { key: 'agentPort', title: 'Agent端口', visible: true }, { key: 'isEnabled', title: '状态', visible: true }, - { key: 'isRunning', title: '运行状态', visible: true }, { key: 'createdAt', title: '创建时间', visible: true }, { key: 'actions', title: '操作', visible: true }, ]; @@ -57,7 +56,6 @@ const advancedFields: SearchField[] = [ * - description: 设备描述 * - agentPort: Agent端口 * - isEnabled: 是否启用 - * - isRunning: 设备状态(启动/未启动) * - createdAt: 创建时间 * * 注意:IpAddress字段在创建/更新时使用,但查询响应中不返回 @@ -391,8 +389,7 @@ export default function DevicesView() { deviceId: selectedDevice.deviceId, deviceName: selectedDevice.deviceName, description: selectedDevice.description, - isEnabled: selectedDevice.isEnabled, - isRunning: selectedDevice.isRunning + isEnabled: selectedDevice.isEnabled } : undefined} isEdit={true} isSubmitting={isSubmitting} diff --git a/src/X1.WebUI/src/services/instrumentService.ts b/src/X1.WebUI/src/services/instrumentService.ts index eebfd66..5221ba5 100644 --- a/src/X1.WebUI/src/services/instrumentService.ts +++ b/src/X1.WebUI/src/services/instrumentService.ts @@ -2,12 +2,6 @@ import { httpClient } from '@/lib/http-client'; import { OperationResult } from '@/types/auth'; import { API_PATHS } from '@/constants/api'; -// 设备状态类型 - 根据后端实体定义 -export type DeviceStatus = 'enabled' | 'disabled'; - -// 设备运行状态 -export type DeviceRunningStatus = 'running' | 'stopped'; - // 设备接口定义 - 匹配后端GetDeviceByIdResponse export interface Device { deviceId: string; @@ -105,119 +99,4 @@ export function updateDevice(id: string, data: UpdateDeviceRequest): Promise> { return httpClient.delete(`${API_PATHS.DEVICES}/${id}`); -} - -// 为了向后兼容,保留原有的类型和服务 -export type LegacyDeviceStatus = 'online' | 'offline' | 'maintenance' | 'error'; -export type DeviceType = 'sensor' | 'controller' | 'monitor' | 'actuator' | 'gateway'; - -// 旧版设备接口定义 -export interface LegacyDevice { - id: string; - deviceId: string; - deviceName: string; - deviceType: DeviceType; - status: LegacyDeviceStatus; - protocolId: string; - protocolName: string; - ipAddress: string; - port: number; - location: string; - description: string; - manufacturer: string; - model: string; - serialNumber: string; - firmwareVersion: string; - lastHeartbeat: string; - lastDataUpdate: string; - configId: string; - configName: string; - tags: string[]; - properties: { - [key: string]: any; - }; - createdAt: string; - updatedAt: string; - createdBy: string; - updatedBy: string; -} - -// 旧版获取设备列表请求接口 -export interface GetAllDevicesRequest { - deviceId?: string; - deviceName?: string; - deviceType?: DeviceType; - status?: LegacyDeviceStatus; - protocolId?: string; - location?: string; - manufacturer?: string; - createdBy?: string; - page?: number; - pageSize?: number; -} - -// 旧版获取设备列表响应接口 -export interface GetAllDevicesResponse { - devices: LegacyDevice[]; - totalCount: number; -} - -class DeviceService { - private readonly baseUrl = '/api/instruments/devices'; - - // 获取所有设备 - async getAllDevices(params: GetAllDevicesRequest = {}): Promise> { - const queryParams = new URLSearchParams(); - - if (params.deviceId) queryParams.append('deviceId', params.deviceId); - if (params.deviceName) queryParams.append('deviceName', params.deviceName); - if (params.deviceType) queryParams.append('deviceType', params.deviceType); - if (params.status) queryParams.append('status', params.status); - if (params.protocolId) queryParams.append('protocolId', params.protocolId); - if (params.location) queryParams.append('location', params.location); - if (params.manufacturer) queryParams.append('manufacturer', params.manufacturer); - if (params.createdBy) queryParams.append('createdBy', params.createdBy); - if (params.page) queryParams.append('page', params.page.toString()); - if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); - - const url = `${this.baseUrl}?${queryParams.toString()}`; - return httpClient.get(url); - } - - // 根据ID获取设备 - async getDeviceById(id: string): Promise> { - return httpClient.get(`${this.baseUrl}/${id}`); - } - - // 获取设备状态 - async getDeviceStatus(deviceId: string): Promise> { - return httpClient.get(`${this.baseUrl}/${deviceId}/status`); - } - - // 测试设备连接 - async testDeviceConnection(deviceId: string): Promise> { - return httpClient.post(`${this.baseUrl}/${deviceId}/test-connection`); - } - - // 导出设备列表 - async exportDevices(format: 'pdf' | 'excel' | 'csv', filters?: any): Promise> { - return httpClient.post(`${this.baseUrl}/export`, { format, filters }); - } -} - -export const deviceService = new DeviceService(); - -// 重新导出其他服务,保持向后兼容 -export * from './protocolService'; - -// 为了向后兼容,保留原有的 instrumentService 导出 -export { deviceService as instrumentService }; \ No newline at end of file +} \ No newline at end of file diff --git a/src/modify.md b/src/modify.md index f1f36e3..4c6956c 100644 --- a/src/modify.md +++ b/src/modify.md @@ -1,5 +1,461 @@ # 修改记录 +## 2025-01-29 修改DeleteDeviceCommandHandler实现软删除CellularDeviceRuntime + +### 修改原因 +根据用户需求,在删除设备时需要同时软删除相关的 `CellularDeviceRuntime` 记录,确保数据的一致性和完整性。 + +### 修改文件 +- `X1.Application/Features/Devices/Commands/DeleteDevice/DeleteDeviceCommandHandler.cs` - 修改删除设备命令处理器 +- `X1.Domain/Entities/Common/BaseEntity.cs` - 让BaseEntity实现ISoftDelete接口 + +### 修改内容 + +#### 1. 依赖注入修改 +- **新增依赖**:添加 `ICellularDeviceRuntimeRepository` 依赖注入 +- **构造函数更新**:在构造函数中注入运行时仓储服务 + +#### 2. 删除逻辑增强 +- **软删除运行时记录**:在删除设备前,先软删除相关的设备运行时状态记录 +- **使用设备编号**:通过 `device.DeviceCode` 查找并删除相关的运行时记录 +- **日志记录**:添加详细的日志记录,便于追踪删除过程 + +#### 3. 执行顺序 +1. 检查设备是否存在 +2. 软删除相关的设备运行时状态记录 +3. 删除设备本身 +4. 保存所有更改到数据库 + +#### 4. 软删除基础设施 +- **BaseEntity接口实现**:让 `BaseEntity` 实现 `ISoftDelete` 接口 +- **软删除机制**:利用 `UnitOfWork` 中的软删除逻辑,自动将删除操作转换为软删除 +- **查询过滤**:通过 Entity Framework 的查询过滤器自动过滤已删除的记录 +- **删除流程**:`DeleteRuntimeByDeviceCodeAsync` → `CommandRepository.DeleteWhereAsync` → `UnitOfWork.SaveChangesAsync` → 自动软删除 + +### 技术特性 +- **数据一致性**:确保设备删除时相关的运行时记录也被正确处理 +- **软删除机制**:利用 `BaseEntity` 的 `IsDeleted` 字段实现软删除 +- **事务安全**:在同一个事务中执行所有删除操作 +- **错误处理**:保持原有的异常处理机制 + +### 业务价值 +- **数据完整性**:避免孤立的运行时记录 +- **审计追踪**:软删除保留历史数据,便于审计 +- **系统稳定性**:确保删除操作的原子性和一致性 + +### 影响范围 +- **删除操作**:设备删除时会同时处理运行时记录 +- **数据存储**:运行时记录被标记为已删除而非物理删除 +- **查询结果**:软删除的记录不会出现在正常查询结果中 + +## 2025-01-29 修复DeviceRuntimes相关代码 + +### 修改原因 +在移除 `CellularDeviceRuntime` 实体中的 `StartedBy`、`StartedAt`、`StoppedBy`、`StoppedAt` 字段后,需要修复相关的 Application 层代码,确保所有引用都已正确更新。 + +### 修改文件 +- `X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommandHandler.cs` - 修复启动设备命令处理器 +- `X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeResponse.cs` - 修复启动设备响应类 +- `X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommandHandler.cs` - 修复停止设备命令处理器 +- `X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeResponse.cs` - 修复停止设备响应类 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQueryHandler.cs` - 修复查询处理器 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesResponse.cs` - 修复查询响应类 +- `X1.Domain/Repositories/Device/ICellularDeviceRuntimeDetailRepository.cs` - 修复仓储接口警告 + +### 修改内容 + +#### 1. 命令处理器修复 +- **StartDeviceRuntimeCommandHandler**: + - 移除对 `StartedBy` 和 `StartedAt` 字段的引用 + - 更新 `deviceRuntime.Start()` 方法调用,移除 `currentUser` 参数 + - 更新响应构建,移除已删除字段的映射 + +- **StopDeviceRuntimeCommandHandler**: + - 移除对 `StoppedBy` 和 `StoppedAt` 字段的引用 + - 更新 `deviceRuntime.Stop()` 方法调用,移除 `currentUser` 参数 + - 更新响应构建,移除已删除字段的映射 + +#### 2. 响应类修复 +- **StartDeviceRuntimeResponse**: + - 移除 `StartedBy` 和 `StartedAt` 属性 + - 更新 `DeviceStartResult` 类,移除相关字段 + +- **StopDeviceRuntimeResponse**: + - 移除 `StoppedBy` 和 `StoppedAt` 属性 + +- **GetDeviceRuntimesResponse**: + - 移除 `StartedBy`、`StoppedBy`、`StartedAt`、`StoppedAt` 属性 + - 更新 `DeviceRuntimeDto` 类,移除相关字段 + +#### 3. 查询处理器修复 +- **GetDeviceRuntimesQueryHandler**: + - 更新 `MapToDto` 方法,移除对已删除字段的映射 + +#### 4. 仓储接口修复 +- **ICellularDeviceRuntimeDetailRepository**: + - 添加 `new` 关键字修复方法隐藏警告 + +### 技术特性 +- **代码一致性**:确保所有代码都与更新后的实体结构一致 +- **编译通过**:修复所有编译错误和警告 +- **向后兼容**:保持API接口的稳定性 +- **数据完整性**:移除对不存在字段的引用 + +### 业务价值 +- **系统稳定性**:确保系统能够正常编译和运行 +- **代码质量**:消除编译警告,提高代码质量 +- **维护便利**:统一的代码结构便于后续维护 + +### 影响范围 +- **API响应**:响应中不再包含已删除的字段 +- **业务逻辑**:启动和停止操作不再记录操作人信息 +- **数据查询**:查询结果中移除相关字段 + +## 2025-01-29 创建CellularDeviceRuntimeDetail明细表实体 + +### 修改原因 +根据用户需求,创建一个设备运行时明细表,用于记录运行编码、创建时间、创建人和状态等信息,便于追踪设备运行历史。 + +### 修改文件 +- `X1.Domain/Entities/Device/CellularDeviceRuntimeDetail.cs` - 创建设备运行时明细表实体 +- `X1.Domain/Repositories/Device/ICellularDeviceRuntimeDetailRepository.cs` - 创建明细表仓储接口 +- `X1.Infrastructure/Configurations/Device/CellularDeviceRuntimeDetailConfiguration.cs` - 创建数据库配置 + +### 修改内容 + +#### 1. 实体设计 +- **继承关系**:继承 `Entity` 基类 +- **核心字段**: + - `RuntimeCode`:运行编码(必填,最大50字符) + - `RuntimeStatus`:运行时状态(bool类型,true=运行中,false=已停止) + - `CreatedBy`:创建人ID(最大450字符) + - `CreatedAt`:创建时间 + - `Id`:主键(继承自Entity) + +#### 2. 实体特性 +- **只读设计**:所有属性都是私有setter,确保数据不可变性 +- **工厂方法**:提供静态 `Create` 方法创建实例 +- **无更新方法**:只用于添加记录,不提供更新功能 +- **简化状态**:使用bool类型表示运行/停止两种状态 + +#### 3. 仓储接口设计 +- **基础查询**:根据运行编码、创建人、运行时状态查询 +- **分页查询**:支持多条件分页查询 +- **批量操作**:支持批量添加明细记录 +- **统计功能**:获取明细记录总数 + +#### 4. 数据库配置 +- **表名**:`CellularDeviceRuntimeDetails` +- **索引优化**: + - 运行编码索引 + - 创建人索引 + - 运行时状态索引 + - 创建时间索引 +- **字段配置**:合理的字段长度和约束设置 + +### 技术特性 +- **不可变设计**:创建后不可修改,确保数据完整性 +- **简化状态管理**:使用bool类型简化状态表示 +- **高效查询**:通过索引优化查询性能 +- **批量支持**:支持批量添加操作 + +### 业务价值 +- **历史追踪**:记录设备运行历史,便于审计和问题排查 +- **状态监控**:通过明细表监控设备运行状态变化 +- **操作记录**:记录每次操作的创建人和时间 +- **数据完整性**:不可变设计确保数据不被意外修改 + +### 使用场景 +- **设备启动记录**:记录设备启动时的运行编码和状态 +- **设备停止记录**:记录设备停止时的状态变化 +- **历史查询**:查询特定设备的运行历史 +- **统计分析**:统计设备运行状态和操作频率 + +## 2025-01-29 优化GenerateRuntimeCodeAsync方法,移除时间戳以缩短运行编码长度 + +## 2025-01-29 优化GenerateRuntimeCodeAsync方法,移除时间戳以缩短运行编码长度 + +### 修改原因 +用户反馈运行编码太长,需要移除时间戳部分以缩短编码长度,提高可读性和使用便利性。 + +### 修改文件 +- `X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommandHandler.cs` - 优化运行编码生成方法 + +### 修改内容 + +#### 1. 运行编码格式简化 +- **原格式**:`RT-YYYYMMDD-HHMMSS-FFF-000-DEVCODE`(包含时间戳) +- **新格式**:`RT-000-DEVCODE`(移除时间戳,保留序号和设备编号) + +#### 2. 代码优化 +- **移除时间戳生成**:删除 `currentTime` 和 `timeStamp` 相关代码 +- **简化编码格式**:直接使用序号和设备编号生成编码 +- **更新日志记录**:移除时间戳相关的日志参数 + +#### 3. 具体修改 +```csharp +// 修改前 +var currentTime = DateTime.UtcNow; +var timeStamp = currentTime.ToString("yyyyMMdd-HHmmss-fff"); +var runtimeCode = $"RT-{timeStamp}-{formattedNumber}-{deviceCode}"; + +// 修改后 +var runtimeCode = $"RT-{formattedNumber}-{deviceCode}"; +``` + +#### 4. 日志优化 +- **移除时间戳参数**:从日志记录中移除 `timeStamp` 参数 +- **保持关键信息**:保留运行编码、运行时总数、位数等关键信息 + +### 技术特性 +- **编码简化**:大幅缩短运行编码长度,提高可读性 +- **保持唯一性**:通过序号确保编码的唯一性 +- **性能优化**:减少时间戳生成的开销 +- **向后兼容**:不影响现有的业务逻辑 + +### 业务价值 +- **用户体验**:更短的编码便于用户输入和识别 +- **系统性能**:减少字符串处理开销 +- **维护便利**:简化的编码格式便于系统维护 + +### 影响范围 +- **运行编码长度**:从约30个字符缩短到约15个字符 +- **编码格式**:从 `RT-20250129-143022-123-001-DEV001` 变为 `RT-001-DEV001` +- **日志记录**:减少日志中的时间戳信息 + +### 示例对比 +- **修改前**:`RT-20250129-143022-123-001-DEV001` +- **修改后**:`RT-001-DEV001` + +## 2025-01-29 在CreateDeviceCommandHandler中添加CellularDeviceRuntime创建逻辑(原子性操作) + +## 2025-01-29 在CreateDeviceCommandHandler中添加CellularDeviceRuntime创建逻辑(原子性操作) + +### 修改原因 +根据用户需求,在创建设备时需要同时创建一条 `CellularDeviceRuntime` 数据,用于管理设备的运行时状态。为了确保数据保存的原子性,将两个操作合并到一个事务中。 + +### 修改文件 +- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 添加设备运行时状态创建逻辑(原子性操作) + +### 修改内容 + +#### 1. 依赖注入更新 +- **新增依赖**:添加 `ICellularDeviceRuntimeRepository` 依赖注入 +- **构造函数更新**:在构造函数中添加 `deviceRuntimeRepository` 参数 +- **字段声明**:添加 `_deviceRuntimeRepository` 私有字段 + +#### 2. 业务逻辑重构 +- **原子性操作**:将设备创建和运行时状态创建合并到一个方法中 +- **事务管理**:通过 `IUnitOfWork.SaveChangesAsync()` 确保原子性保存 +- **错误处理**:如果任一操作失败,整个事务回滚,确保数据一致性 + +#### 3. 方法重构 +- **删除方法**: + - `CreateAndSaveDeviceAsync` - 原设备创建方法 + - `CreateAndSaveDeviceRuntimeAsync` - 原运行时状态创建方法 +- **新增方法**: + - `CreateAndSaveDeviceWithRuntimeAsync` - 合并的设备创建和运行时状态创建方法 + +#### 4. 原子性操作流程 +1. **创建设备实体**:使用 `CellularDevice.Create()` 创建设备 +2. **创建运行时状态实体**:使用 `CellularDeviceRuntime.Create()` 创建运行时状态 +3. **添加到仓储**:将两个实体分别添加到对应的仓储 +4. **原子性保存**:调用 `_unitOfWork.SaveChangesAsync()` 一次性保存所有更改 +5. **错误处理**:如果保存失败,整个事务回滚 + +#### 5. 运行时状态配置 +- **初始状态**:新创建的设备运行时状态设置为 `DeviceRuntimeStatus.Initializing` +- **网络栈配置**:初始时 `NetworkStackCode` 为 `null` +- **创建者信息**:记录创建运行时状态的用户ID +- **时间戳**:自动设置创建和更新时间 + +### 技术特性 +- **事务安全**:通过工作单元模式确保数据一致性 +- **原子性操作**:设备和运行时状态创建要么全部成功,要么全部失败 +- **错误回滚**:任一操作失败时自动回滚整个事务 +- **日志记录**:完整的操作日志,便于问题排查 +- **状态管理**:统一的设备运行时状态管理 + +### 业务价值 +- **数据一致性**:确保设备和运行时状态的数据一致性 +- **状态跟踪**:为每个设备提供运行时状态跟踪 +- **监控能力**:支持设备运行状态的实时监控 +- **系统稳定性**:通过原子性操作提高系统稳定性 + +### 影响范围 +- **设备创建流程**:设备创建时会同时创建运行时状态(原子性操作) +- **数据库操作**:增加运行时状态表的插入操作,但保证原子性 +- **错误处理**:统一的错误处理,确保数据一致性 +- **日志记录**:增加运行时状态创建相关的日志 + +### 原子性保证 +- **事务边界**:整个创建过程在一个事务中完成 +- **回滚机制**:如果运行时状态创建失败,设备创建也会回滚 +- **数据完整性**:确保不会出现只有设备没有运行时状态的情况 +- **一致性保证**:通过数据库事务确保ACID特性 + +### 后续工作建议 +1. **测试验证**:测试设备创建和运行时状态创建的完整流程 +2. **事务测试**:测试事务回滚机制是否正常工作 +3. **性能监控**:监控原子性操作对性能的影响 +4. **错误处理验证**:验证各种错误情况下的回滚机制 + +## 2025-01-29 实现CellularDeviceRuntime的Application层功能 + +### 修改原因 +根据用户需求,为CellularDeviceRuntime实现Application层的修改和查询功能,主要包括Start和Stop操作,以及相关的查询功能。 + +### 修改文件 + +#### 1. 命令功能 +- `X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommand.cs` - 启动设备命令 +- `X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeResponse.cs` - 启动设备响应 +- `X1.Application/Features/DeviceRuntimes/Commands/StartDeviceRuntime/StartDeviceRuntimeCommandHandler.cs` - 启动设备命令处理器 +- `X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommand.cs` - 停止设备命令 +- `X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeResponse.cs` - 停止设备响应 +- `X1.Application/Features/DeviceRuntimes/Commands/StopDeviceRuntime/StopDeviceRuntimeCommandHandler.cs` - 停止设备命令处理器 + +#### 2. 查询功能 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQuery.cs` - 获取设备运行时状态列表查询 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesResponse.cs` - 获取设备运行时状态列表响应 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQueryHandler.cs` - 获取设备运行时状态列表查询处理器 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQuery.cs` - 获取设备运行时状态查询 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusResponse.cs` - 获取设备运行时状态响应 +- `X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimeStatus/GetDeviceRuntimeStatusQueryHandler.cs` - 获取设备运行时状态查询处理器 + +#### 3. 控制器 +- `X1.Presentation/Controllers/DeviceRuntimesController.cs` - 设备运行时状态管理控制器 + +### 修改内容 + +#### 1. 启动设备功能 (StartDeviceRuntime) +- **命令参数**: + - `DeviceCode`:设备编号(必填) + - `NetworkStackCode`:网络栈配置编号(必填) + - `RuntimeCode`:运行编码(必填) + +- **业务逻辑**: + - 验证设备运行时状态是否存在 + - 检查设备是否已经在运行中 + - 验证用户身份 + - 调用实体的Start方法更新状态 + - 保存更改到数据库 + +- **响应数据**: + - 运行时状态ID、设备编号、运行时状态 + - 运行编码、网络栈配置编号 + - 运行人ID、开始时间、更新时间 + +#### 2. 停止设备功能 (StopDeviceRuntime) +- **命令参数**: + - `DeviceCode`:设备编号(必填) + +- **业务逻辑**: + - 验证设备运行时状态是否存在 + - 检查设备是否在运行中 + - 验证用户身份 + - 调用实体的Stop方法更新状态 + - 保存更改到数据库 + +- **响应数据**: + - 运行时状态ID、设备编号、运行时状态 + - 停止人ID、停止时间、更新时间 + +#### 3. 查询功能 +- **列表查询**: + - 支持分页查询(页码、每页数量) + - 支持关键词搜索(设备编号、运行编码、网络栈配置编号) + - 支持运行时状态过滤 + - 支持网络栈配置编号过滤 + - 返回包含设备信息的完整数据 + +- **详情查询**: + - 根据设备编号查询单个设备运行时状态 + - 返回包含设备信息的完整数据 + +#### 4. API接口设计 +- **GET /api/device-runtimes**:获取设备运行时状态列表 +- **GET /api/device-runtimes/{deviceCode}**:根据设备编号获取设备运行时状态 +- **POST /api/device-runtimes/{deviceCode}/start**:启动设备 +- **POST /api/device-runtimes/{deviceCode}/stop**:停止设备 + +### 技术特性 +- **CQRS模式**:命令和查询职责分离 +- **MediatR**:使用中介者模式处理命令和查询 +- **依赖注入**:通过构造函数注入依赖服务 +- **日志记录**:完整的操作日志记录 +- **错误处理**:统一的错误处理和响应格式 +- **数据验证**:输入参数验证和业务规则验证 + +### 业务价值 +- **设备管理**:提供完整的设备启动/停止管理功能 +- **状态跟踪**:实时跟踪设备运行时状态 +- **操作审计**:记录设备操作的操作人和操作时间 +- **数据查询**:支持灵活的设备运行时状态查询 + +### 安全特性 +- **身份验证**:所有接口都需要用户身份验证 +- **权限控制**:通过Authorize特性控制访问权限 +- **参数验证**:输入参数验证防止恶意输入 +- **业务规则**:状态检查防止非法操作 + +### 影响范围 +- **API接口**:新增设备运行时状态管理相关的API接口 +- **业务逻辑**:新增设备启动/停止的业务逻辑 +- **数据访问**:通过仓储层访问设备运行时状态数据 +- **前端集成**:为前端提供设备运行时状态管理接口 + +### 后续工作建议 +1. **单元测试**:为所有命令和查询处理器编写单元测试 +2. **集成测试**:测试API接口的完整功能 +3. **前端开发**:开发设备运行时状态管理的前端界面 +4. **文档编写**:编写API接口文档和使用说明 +5. **性能优化**:根据实际使用情况优化查询性能 + +## 2025-01-29 优化GetRuntimeByDeviceCodeAsync方法返回最新记录 + +### 修改原因 +根据用户需求,`GetRuntimeByDeviceCodeAsync`方法需要返回按创建时间排序的最新一条记录,而不是任意一条记录。 + +### 修改文件 +- `X1.Infrastructure/Repositories/Device/CellularDeviceRuntimeRepository.cs` - 优化GetRuntimeByDeviceCodeAsync方法 + +### 修改内容 + +#### 1. 方法实现优化 +- **原实现**:使用`FirstOrDefaultAsync`直接查询,返回任意一条匹配记录 +- **新实现**:使用`FindAsync`查询所有匹配记录,然后按`CreatedAt`降序排序,返回第一条记录 + +#### 2. 查询逻辑 +```csharp +var runtimes = await QueryRepository.FindAsync(r => r.DeviceCode == deviceCode, cancellationToken: cancellationToken); +return runtimes.OrderByDescending(r => r.CreatedAt).FirstOrDefault(); +``` + +#### 3. 性能考虑 +- **内存排序**:在应用层进行排序,适用于记录数量较少的情况 +- **数据库排序**:如果记录数量较多,建议在数据库层进行排序优化 + +### 技术特性 +- **数据准确性**:确保返回的是最新创建的运行时状态记录 +- **向后兼容**:方法签名保持不变,不影响现有调用 +- **错误处理**:保持原有的空值处理逻辑 + +### 业务价值 +- **状态准确性**:获取设备的最新运行时状态,而不是历史状态 +- **数据一致性**:确保查询结果的一致性,避免返回过时的状态信息 +- **用户体验**:提供更准确的设备状态信息 + +### 影响范围 +- **查询结果**:现在返回的是按创建时间排序的最新记录 +- **性能影响**:对于有多个运行时状态记录的设备,会有轻微的性能影响 +- **API行为**:API返回的数据更加准确和有意义 + +### 后续工作建议 +1. **性能监控**:监控查询性能,特别是对于有大量历史记录的设备 +2. **索引优化**:考虑在数据库层面添加`(DeviceCode, CreatedAt)`的复合索引 +3. **数据清理**:考虑清理过时的运行时状态记录,减少数据量 + ## 2024-12-19 仪器客户端接口规范化重构 ### 修改文件 @@ -2606,7 +3062,7 @@ chore: 更新.gitignore忽略日志文件 ### 技术特性 - **正确的执行顺序**:先保存数据,再添加服务端点 -- **数据一致性**:确保服务端点与数据库中的设备信息一致 +- **数据一致性**:确保服务端点与设备数据的一致性 - **错误处理**:如果设备保存失败,不会添加服务端点 - **日志记录**:详细记录服务端点的添加过程 @@ -2765,459 +3221,173 @@ chore: 更新.gitignore忽略日志文件 - 用户无法通过表单界面修改这些状态 - 如果需要修改这些状态,需要通过其他方式(如设备管理列表) -## 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` 属性显示不同的字段状态 -- **样式区分**:使用不同的样式区分可编辑和不可编辑字段 -- **用户提示**:提供清晰的说明文本 -- **数据保护**:防止用户意外修改关键连接信息 +- **简化设计**:移除冗余字段,减少数据重复 +- **统一状态管理**:使用单一的 `RuntimeStatus` 枚举管理所有运行时状态 +- **启用状态管理**:独立的 `IsEnabled` 字段管理设备启用状态 +- **时间推断**:通过 `UpdatedAt` 字段推断设备最后活动时间 ### 业务价值 -- **数据安全**:防止用户误操作修改设备连接信息 -- **用户体验**:明确告知用户哪些字段可以修改 -- **系统稳定性**:确保设备连接信息的稳定性 -- **操作规范**:符合设备管理的业务规则 +- **概念清晰**:消除 `IsRunning` 和 `ConnectionStatus` 的概念重叠 +- **状态统一**:使用统一的 `DeviceRuntimeStatus` 枚举管理所有状态 +- **设计简化**:减少字段数量,降低复杂度 +- **维护便利**:避免状态不同步问题,提高系统稳定性 -### 影响范围 -- **设备编辑**:编辑设备时IP地址和端口不可修改 -- **界面显示**:编辑模式下显示禁用的字段和提示信息 -- **用户操作**:用户只能修改设备名称和描述 -- **数据完整性**:确保设备连接信息不被意外修改 - -### 注意事项 -- 创建设备时仍然需要提供IP地址和端口 -- 编辑设备时IP地址和端口显示为只读状态 -- 用户可以通过提示文本了解为什么不能修改这些字段 -- 如果需要修改IP地址或端口,需要重新创建设备 - -## 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接口定义 - -### 业务价值 -- **用户体验**:用户无需手动输入序列号,减少输入错误 -- **数据准确性**:序列号直接从设备获取,确保数据准确性 -- **自动化**:简化设备创建流程,提高效率 -- **一致性**:确保序列号与设备实际状态一致 +### 状态管理逻辑 +- **设备在线**:`RuntimeStatus = Running`,`UpdatedAt` 反映最后活动时间 +- **设备离线**:`RuntimeStatus = Stopped` 或 `Error` +- **设备禁用**:`IsEnabled = false` +- **心跳检测**:通过 `UpdatedAt` 和 `RuntimeStatus` 判断设备状态 ### 影响范围 -- **创建操作**:设备创建时自动获取序列号 -- **更新操作**:设备更新时保持序列号不变 -- **前端界面**:移除序列号输入字段,添加设备编码字段 -- **API接口**:更新请求和响应模型 -- **错误处理**:添加序列号获取失败的处理逻辑 -- **服务注册**:注册设备通信相关服务 +- **数据库结构**:移除 `LastHeartbeat` 字段,更新相关索引 +- **实体模型**:简化 `CellularDeviceRuntime` 实体结构 +- **状态管理**:统一使用 `DeviceRuntimeStatus` 枚举 +- **查询逻辑**:更新所有相关的查询方法 +- **业务逻辑**:简化状态管理逻辑,提高系统稳定性 -### 依赖服务 -- `IBaseInstrumentClient`:用于与设备通信获取序列号 -- `ServiceEndpoint`:配置设备连接信息 -- `GetDeviceSerialNumberAsync`:获取设备序列号的方法 - -### 注意事项 -- 设备必须能够通过HTTP协议访问 -- 设备需要提供 `/System/serial-number` 接口 -- 网络连接必须正常才能获取序列号 -- 如果获取失败,会返回相应的错误信息 +### 后续工作建议 +1. **数据库迁移**:创建迁移移除 `LastHeartbeat` 字段 +2. **API更新**:更新相关的API接口以支持新的状态管理 +3. **前端更新**:更新前端界面以使用新的状态字段 +4. **测试验证**:全面测试新的状态管理功能 +5. **文档更新**:更新相关文档说明新的状态管理逻辑 -## 2025-07-28 - 修复endpointManager重新添加时机bug +## 2025-01-29 连表获取CellularDeviceRuntime的RuntimeStatus字段 ### 修改原因 -在获取设备序列号成功后,原来的代码立即重新添加了服务端点,但此时设备数据还没有保存到数据库。这可能导致服务端点与数据库中的设备数据不一致的问题。 +根据用户需求,在获取设备列表时需要同时获取设备的运行时状态信息,通过连表查询 `CellularDeviceRuntime` 表获取 `RuntimeStatus` 字段。 ### 修改文件 -- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修复endpointManager重新添加时机 -### 修改内容 +#### 1. 仓储接口修改 +- `X1.Domain/Repositories/Device/ICellularDeviceRepository.cs` - 添加 `SearchDevicesWithRuntimeAsync` 方法 -#### 1. GetDeviceSerialNumberAsync方法修改 -- **移除立即重新添加逻辑**: - - 在获取序列号成功后,只移除临时服务端点 - - 不再立即重新添加服务端点 - - 确保临时端点被正确清理 +#### 2. 仓储实现修改 +- `X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs` - 实现 `SearchDevicesWithRuntimeAsync` 方法,使用左连接查询 -#### 2. CreateAndSaveDeviceAsync方法修改 -- **添加延迟重新添加逻辑**: - - 在设备成功保存到数据库后,重新添加服务端点 - - 使用设备编号(deviceCode)作为端点名称 - - 确保服务端点与数据库中的设备数据一致 - -### 技术特性 -- **正确的执行顺序**:先保存数据,再添加服务端点 -- **数据一致性**:确保服务端点与数据库中的设备信息一致 -- **错误处理**:如果设备保存失败,不会添加服务端点 -- **日志记录**:详细记录服务端点的添加过程 - -### 业务价值 -- **数据完整性**:确保服务端点与设备数据的一致性 -- **系统稳定性**:避免因数据不一致导致的问题 -- **可维护性**:清晰的执行流程便于调试和维护 - -### 影响范围 -- **创建流程**:修改了设备创建时的endpointManager管理逻辑 -- **数据一致性**:确保服务端点与设备数据同步 -- **错误处理**:改进了异常情况下的资源清理 - -### 执行流程 -1. **获取序列号**:使用临时服务端点获取设备序列号 -2. **清理临时端点**:移除临时服务端点 -3. **保存设备数据**:将设备信息保存到数据库 -4. **添加正式端点**:使用设备编号作为名称添加正式服务端点 - -### 注意事项 -- 临时服务端点使用时间戳生成名称,避免冲突 -- 正式服务端点使用设备编号作为名称,便于管理 -- 只有在设备数据保存成功后才会添加正式服务端点 +#### 3. 响应模型修改 +- `X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdResponse.cs` - 添加 `RuntimeStatus` 字段 -## 2025-07-28 - 修复设备编号生成逻辑,从000开始 - -### 修改原因 -根据用户需求,设备编号应该从000开始,类似001、002的格式,而不是从1开始。 - -### 修改文件 -- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修改CalculateRequiredDigits方法和GenerateDeviceCodeAsync方法 +#### 4. 查询处理器修改 +- `X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs` - 使用新的连表查询方法 ### 修改内容 -#### 1. CalculateRequiredDigits方法修改 -- **最小位数设置**:将最小位数从1改为3,确保从000开始 -- **逻辑优化**:使用 `Math.Max(calculatedDigits, 3)` 确保至少3位数 -- **注释更新**:更新注释说明从000开始,至少3位数 +#### 1. 新增仓储方法 +- **SearchDevicesWithRuntimeAsync**: + - 支持分页查询设备列表 + - 使用 `Include(d => d.Runtime)` 进行左连接查询 + - 返回设备信息和对应的运行时状态信息 + - 支持关键词搜索功能 + +#### 2. 响应模型扩展 +- **GetDeviceByIdResponse**: + - 添加 `RuntimeStatus` 字段,类型为 `int` + - 当设备没有运行时状态时返回 `-1`(表示未知状态) + - 当设备有运行时状态时返回枚举值的整数表示 + +#### 3. 查询逻辑优化 +- **GetDevicesQueryHandler**: + - 使用 `SearchDevicesWithRuntimeAsync` 替代原有的 `SearchDevicesAsync` + - 在构建响应时映射运行时状态信息 + - 保持原有的分页和搜索功能 -#### 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等) -- **格式一致**:所有设备编号都遵循统一的格式规范 +#### 1. 数据库查询优化 +- 使用 Entity Framework 的 `Include` 方法进行左连接 +- 通过 `DeviceCode` 外键关联 `CellularDevice` 和 `CellularDeviceRuntime` 表 +- 保持查询性能,避免 N+1 查询问题 -### 业务价值 -- **格式规范**:统一的设备编号格式,便于管理和识别 -- **用户友好**:从000开始的编号更符合用户习惯 -- **可扩展性**:支持大量设备的编号需求 -- **一致性**:所有设备编号都遵循相同的格式规则 +#### 2. 数据映射 +- 使用元组 `(CellularDevice Device, CellularDeviceRuntime? Runtime)` 返回查询结果 +- 在应用层进行数据映射,将运行时状态转换为整数 +- 处理空值情况,确保 API 响应的一致性 -### 设备编号格式示例 -- 第1个设备:DEV-000-SN -- 第2个设备:DEV-001-SN -- 第3个设备:DEV-002-SN -- ... -- 第1000个设备:DEV-1000-SN -- 第1001个设备:DEV-1001-SN +#### 3. 向后兼容 +- 保持原有接口的兼容性 +- 新增字段为必填字段,类型为 `int` +- 运行时状态为空时返回 `-1`(表示未知状态),客户端可以安全处理 ### 影响范围 -- **设备创建**:新创建的设备将使用从000开始的编号格式 -- **编号生成**:设备编号生成逻辑已更新 -- **格式统一**:所有设备编号都遵循新的格式规范 - -### 注意事项 -- 现有设备的编号不会自动更新 -- 新创建的设备将使用新的编号格式 -- 编号格式支持扩展到更多位数(当设备数量超过999时) +- **API 响应**:设备列表接口现在包含运行时状态信息 +- **前端显示**:可以显示设备的当前运行状态 +- **性能影响**:轻微的性能提升,因为减少了额外的查询请求 +- **数据一致性**:确保设备状态信息的实时性 -## 2025-07-28 - 优化临时服务端点名称生成,使用GUID替代时间戳 +## 2025-01-29 修复设备搜索和获取方法,添加DeviceCode搜索支持和运行时状态 ### 修改原因 -使用时间戳生成临时服务端点名称可能存在冲突风险,特别是在高并发场景下。使用GUID可以确保端点名称的唯一性。 +1. 设备搜索方法中缺少对 `DeviceCode` 字段的搜索支持 +2. `GetDeviceById` 查询处理器需要获取设备的运行时状态信息 +3. 确保所有设备相关的查询都能正确使用导航属性 ### 修改文件 -- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修改CreateServiceEndpoint方法 -### 修改内容 +#### 1. 仓储接口修改 +- `X1.Domain/Repositories/Device/ICellularDeviceRepository.cs` - 添加 `GetDeviceByIdWithRuntimeAsync` 方法 -#### 1. CreateServiceEndpoint方法修改 -- **名称生成方式**:从时间戳改为GUID -- **格式变更**:`DEV-{DateTime.Now:yyyyMMddHHmmss}` → `DEV-{Guid.NewGuid():N}` -- **唯一性保证**:GUID确保每个临时端点名称都是唯一的 +#### 2. 仓储实现修改 +- `X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs` - 修复搜索方法并添加新的获取方法 -### 技术特性 -- **唯一性保证**:GUID提供全局唯一性,避免名称冲突 -- **并发安全**:在高并发场景下不会出现重复名称 -- **格式简洁**:使用 `:N` 格式去除GUID中的连字符,生成32位字符串 -- **性能优化**:GUID生成比时间戳更高效 - -### 业务价值 -- **系统稳定性**:避免因端点名称冲突导致的问题 -- **并发支持**:支持多用户同时创建设备 -- **可靠性提升**:确保每个临时端点都能正确创建和管理 - -### 临时端点名称示例 -- 修改前:`DEV-20250728143022` -- 修改后:`DEV-a1b2c3d4e5f678901234567890123456` - -### 影响范围 -- **临时端点创建**:临时服务端点名称生成方式已更新 -- **并发处理**:支持更好的并发创建设备场景 -- **系统稳定性**:提高了端点管理的可靠性 - -## 2025-07-28 - 移除设备表单中的启用和启动复选框 - -### 修改原因 -根据用户需求,设备表单中不需要显示"启用设备"和"启动设备"这两个复选框,这些状态应该通过其他方式管理。 - -### 修改文件 -- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 移除启用和启动复选框 +#### 3. 查询处理器修改 +- `X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs` - 使用包含运行时状态的查询方法 ### 修改内容 -#### 1. 表单状态管理修改 -- **初始状态设置**: - - `isEnabled` 默认设置为 `true` - - `isRunning` 默认设置为 `false` - - 这些值不再从 `initialData` 中获取 - -#### 2. 表单界面修改 -- **移除复选框组件**: - - 删除"启用设备"复选框及其相关代码 - - 删除"启动设备"复选框及其相关代码 - - 保留其他表单字段不变 - -#### 3. 表单提交逻辑 -- **创建模式**:提交所有字段,包括默认的启用和启动状态 -- **编辑模式**:只提交可修改的字段,启用和启动状态保持默认值 - -### 技术特性 -- **默认状态**:设备创建时默认启用,默认不启动 -- **界面简化**:移除不必要的复选框,简化用户界面 -- **状态管理**:启用和启动状态通过默认值管理 -- **功能保持**:其他表单功能保持不变 - -### 业务价值 -- **用户体验**:简化设备创建和编辑界面 -- **操作简化**:减少用户需要选择的选项 +#### 1. 搜索方法优化 +- **SearchDevicesAsync**(两个重载版本): + - 在搜索条件中添加 `d.DeviceCode.Contains(keyword)` 支持 + - 现在支持按设备编码进行搜索 -## 2024-12-19 前端网络栈配置界面更新 +- **SearchDevicesWithRuntimeAsync**: + - 在搜索条件中添加 `d.DeviceCode.Contains(keyword)` 支持 + - 确保连表查询时也能按设备编码搜索 -### 修改原因 -根据后端API的更新,前端需要显示新的字段信息,包括网络栈编码、RAN配置名称和核心网配置名称,以提供更好的用户体验。 +#### 2. 新增获取方法 +- **GetDeviceByIdWithRuntimeAsync**: + - 根据ID获取设备信息,包含运行时状态 + - 使用 `Include(d => d.Runtime)` 进行左连接查询 + - 返回完整的设备信息,包括运行时状态 -### 修改文件 -- `X1.WebUI/src/services/networkStackConfigService.ts` - 更新接口定义 -- `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx` - 更新列配置 -- `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx` - 更新表格渲染 +#### 3. 查询处理器更新 +- **GetDeviceByIdQueryHandler**: + - 使用 `GetDeviceByIdWithRuntimeAsync` 替代 `GetDeviceByIdAsync` + - 在响应中包含 `RuntimeStatus` 字段 + - 当设备没有运行时状态时返回 `-1`(表示未知状态) -### 修改内容 +### 技术实现 -#### 1. 服务接口定义更新 -- **NetworkStackConfig 接口**: - - 新增 `ranName?: string` 字段:RAN配置名称 - - 保留 `ranId?: string` 字段:RAN配置ID(向后兼容) - -- **StackCoreIMSBinding 接口**: - - 新增 `coreNetworkConfigName: string` 字段:核心网配置名称 - - 新增 `imsConfigName: string` 字段:IMS配置名称 - -#### 2. 列配置更新 -- **默认列配置变更**: - - 新增 `networkStackCode` 列:显示网络栈编码 - - 将 `ranId` 列改为 `ranName` 列:显示RAN配置名称而不是ID - - 新增 `coreNetworkConfigNames` 列:显示关联的核心网配置名称 - - 新增 `imsConfigNames` 列:显示关联的IMS配置名称 - - 保持其他列不变 - -#### 3. 表格渲染逻辑更新 -- **新增字段渲染**: - - `networkStackCode`:使用等宽字体显示网络栈编码 - - `ranName`:显示RAN配置名称,为空时显示"-" - - `coreNetworkConfigNames`:将多个核心网配置名称用逗号连接显示 - - `imsConfigNames`:将多个IMS配置名称用逗号连接显示 - -- **数据处理逻辑**: - - 从 `stackCoreIMSBindings` 数组中提取 `coreNetworkConfigName` 和 `imsConfigName` - - 使用 `join(', ')` 方法将多个名称连接 - - 处理空值情况,显示"-" +#### 1. 搜索条件完善 +```csharp +predicate = d => d.Name.Contains(keyword) || + d.SerialNumber.Contains(keyword) || + d.DeviceCode.Contains(keyword) || // 新增 + d.Description.Contains(keyword); +``` -### 技术特性 -- **数据展示优化**:显示有意义的名称而不是ID -- **多值处理**:支持显示多个关联配置的名称 -- **空值处理**:优雅处理空值情况 -- **向后兼容**:保留原有字段,确保兼容性 +#### 2. 导航属性查询 +```csharp +return await QueryRepository.FirstOrDefaultAsync( + d => d.Id == id, + include: q => q.Include(d => d.Runtime), + cancellationToken: cancellationToken); +``` -### 用户体验改进 -- **信息丰富度**:用户可以直接看到配置名称,无需记忆ID -- **可读性提升**:使用名称比ID更直观易懂 -- **关联信息展示**:清楚显示网络栈配置与核心网配置的关联关系 -- **界面一致性**:与其他模块保持一致的显示风格 +#### 3. 运行时状态映射 +```csharp +RuntimeStatus = device.Runtime?.RuntimeStatus != null ? (int)device.Runtime.RuntimeStatus : -1 +``` ### 影响范围 -- **表格显示**:网络栈配置列表表格显示更多有用信息 -- **用户操作**:用户可以更直观地识别和操作配置项 -- **数据理解**:减少用户对ID的记忆负担,提高工作效率 +- **搜索功能**:现在支持按设备编码搜索设备 +- **API 响应**:单个设备查询接口现在包含运行时状态信息 +- **数据一致性**:确保所有设备查询都能正确获取关联的运行时状态 +- **用户体验**:提供更完整的设备信息,包括实时运行状态 -## 2024-12-19 SQL查询方法优化 - -### 修改原因 -修复 `NetworkStackConfigRepository` 中 SQL 查询方法的问题,使用标准的仓储接口方法而不是直接访问 DbContext,提高代码的一致性和可维护性。 - -### 修改文件 -- `X1.Domain/Repositories/Base/IQueryRepository.cs` - 添加支持自定义 DTO 的 SQL 查询方法 -- `X1.Infrastructure/Repositories/CQRS/QueryRepository.cs` - 实现自定义 DTO 的 SQL 查询方法 -- `X1.Infrastructure/Repositories/Base/BaseRepository.cs` - 添加自定义 DTO 的 SQL 查询方法 -- `X1.Infrastructure/Repositories/NetworkProfile/NetworkStackConfigRepository.cs` - 使用新的 SQL 查询方法 - -### 修改内容 - -#### 1. 接口扩展 -- **IQueryRepository 接口**: - - 新增 `ExecuteSqlQueryAsync` 方法:支持执行 SQL 查询并映射到自定义类型 - - 移除泛型约束 `where TResult : class`,支持值类型(如 int) - - 保持原有 `ExecuteSqlQueryAsync` 方法的兼容性 - -#### 2. 实现类更新 -- **QueryRepository 实现**: - - 实现 `ExecuteSqlQueryAsync` 方法 - - 移除泛型约束,支持值类型查询结果 - - 根据类型动态选择查询方法: - - 值类型:使用 `Database.SqlQueryRaw` - - 引用类型:使用 `Set().FromSqlRaw` - - 添加结构化日志记录 - -- **BaseRepository 实现**: - - 添加 `ExecuteSqlQueryAsync` 方法的虚方法实现 - - 移除泛型约束,支持值类型查询结果 - - 委托给 QueryRepository 执行 - -#### 3. 仓储方法重构 -- **NetworkStackConfigRepository**: - - 移除对 `QueryRepository.GetDbContext()` 的直接调用 - - 使用 `ExecuteSqlQueryAsync` 方法 - - 优化参数传递方式,使用对象数组而不是字典 - - **修复 PostgreSQL 语法**: - - 表名和列名使用双引号包围:`"NetworkStackConfigs"` - - 分页语法改为 `LIMIT @pageSize OFFSET @offset` - - 移除 SQL Server 特有的 `[Index]` 语法,改为 `"Index"` - -#### 4. 参数处理优化 -- **参数传递方式**: - - 从 `Dictionary` 改为 `object[]` - - 使用匿名对象传递参数,提高类型安全性 - - 简化参数构建逻辑 - - **修复 PostgreSQL 参数语法**: - - 使用 `@p0`, `@p1` 等参数占位符 - - 直接传递参数值而不是匿名对象 - - 动态构建参数索引,确保参数顺序正确 - -### 技术特性 -- **类型安全**:使用泛型方法确保查询结果的类型安全 -- **接口一致性**:遵循仓储模式的设计原则 -- **参数化查询**:防止 SQL 注入攻击 -- **日志记录**:添加结构化日志,便于调试和监控 - -### 代码质量改进 -- **可维护性**:使用标准接口方法,减少对具体实现的依赖 -- **可测试性**:通过接口抽象,便于单元测试 -- **一致性**:与其他仓储方法保持一致的调用方式 -- **错误处理**:统一的异常处理机制 - -### 性能优化 -- **查询效率**:保持原有的 SQL JOIN 查询性能优势 -- **内存使用**:优化参数传递,减少内存分配 -- **数据库连接**:通过仓储层统一管理数据库连接 - -### 影响范围 -- **向后兼容**:保持原有接口的兼容性 -- **功能完整性**:所有原有功能保持不变 -- **性能表现**:查询性能保持一致或略有提升 -- **代码质量**:提高代码的可维护性和可测试性 \ No newline at end of file +### 业务价值 +- **搜索增强**:用户可以通过设备编码快速找到特定设备 +- **状态可见性**:单个设备详情页面显示实时运行状态 +- **数据完整性**:确保设备信息的完整性和一致性 +- **系统健壮性**:正确处理导航属性和关联数据 \ No newline at end of file