From 47d9a8bf0a94edaa5e5fcd81c3867ca21a3fa9d2 Mon Sep 17 00:00:00 2001 From: root <295172551@qq.com> Date: Wed, 30 Jul 2025 00:20:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E6=A0=88=E9=85=8D=E7=BD=AE=E6=9F=A5=E8=AF=A2=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8DPostgreSQL=E8=AF=AD=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 性能优化: - 使用原生SQL JOIN查询替代N+1查询问题 - 添加NetworkStackConfigWithBindingNamesDto用于扁平化查询结果 - 优化GetNetworkStackConfigById和SearchNetworkStackConfigs查询 2. PostgreSQL语法修复: - 修复表名和列名使用双引号包围 - 修复参数占位符语法(@p0, @p1等) - 修复分页语法(LIMIT/OFFSET) - 移除SQL Server特有的[Index]语法 3. 前端界面优化: - 显示NetworkStackCode、RanName、CoreNetworkConfigNames、IMSConfigNames - 优化表格列显示和搜索功能 4. 设备管理优化: - SerialNumber改为DeviceCode显示 - 更新相关查询处理器和前端组件 5. 仓储层重构: - 添加ExecuteSqlQueryAsync泛型方法 - 移除泛型约束where TResult : class - 优化参数传递方式 --- .../CreateDeviceCommandHandler.cs | 1 - .../CreateDevice/CreateDeviceResponse.cs | 4 - .../GetDeviceByIdQueryHandler.cs | 2 +- .../GetDeviceById/GetDeviceByIdResponse.cs | 4 +- .../GetDevices/GetDevicesQueryHandler.cs | 2 +- .../GetNetworkStackConfigByIdQueryHandler.cs | 58 +- .../GetNetworkStackConfigByIdResponse.cs | 20 + .../GetNetworkStackConfigsQueryHandler.cs | 55 +- .../GetNetworkStackConfigsResponse.cs | 20 + .../NetworkStackConfigWithBindingNamesDto.cs | 99 ++++ .../Repositories/Base/IQueryRepository.cs | 15 + .../INetworkStackConfigRepository.cs | 17 + .../Repositories/Base/BaseRepository.cs | 8 + .../Repositories/CQRS/QueryRepository.cs | 9 + .../NetworkStackConfigRepository.cs | 129 ++++ src/X1.WebUI/src/config/core/env.config.ts | 2 +- .../src/pages/instruments/DevicesTable.tsx | 6 +- .../src/pages/instruments/DevicesView.tsx | 6 +- .../NetworkStackConfigsTable.tsx | 36 +- .../NetworkStackConfigsView.tsx | 5 +- .../src/services/instrumentService.ts | 3 - .../src/services/networkStackConfigService.ts | 3 + src/modify.md | 554 +++++++++++++----- 23 files changed, 846 insertions(+), 212 deletions(-) create mode 100644 src/X1.Domain/Models/NetworkStackConfigWithBindingNamesDto.cs diff --git a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs index 218af9d..b654d58 100644 --- a/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs +++ b/src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs @@ -120,7 +120,6 @@ public class CreateDeviceCommandHandler : IRequestHandler public string DeviceName { get; set; } - /// - /// 序列号 - /// - public string SerialNumber { 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 e13f42f..5e1765d 100644 --- a/src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs +++ b/src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs @@ -47,7 +47,7 @@ public class GetDeviceByIdQueryHandler : IRequestHandler - /// 序列号 + /// 设备编码 /// - public string SerialNumber { get; set; } + public string DeviceCode { get; set; } /// /// 设备描述 diff --git a/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs b/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs index 554e82e..0b045b3 100644 --- a/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs +++ b/src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs @@ -69,7 +69,7 @@ public class GetDevicesQueryHandler : IRequestHandler.CreateFailure($"网络栈配置 {request.NetworkStackConfigId} 不存在"); } - // 构建响应,使用导航属性获取绑定关系 + // 获取第一条记录作为主配置信息 + var firstRecord = queryResults.First(); + + // 构建绑定关系列表 + var bindingItems = queryResults + .Where(r => r.StackCoreIMSBindingId != null) + .Select(r => new GetNetworkStackConfigByIdBindingResponseItem + { + StackCoreIMSBindingId = r.StackCoreIMSBindingId!, + NetworkStackConfigId = r.NetworkStackConfigId, + Index = r.Index ?? 0, + CoreNetworkConfigId = r.CoreNetworkConfigId ?? string.Empty, + CoreNetworkConfigName = r.CoreNetworkConfigName ?? "未知配置", + IMSConfigId = r.IMSConfigId ?? string.Empty, + IMSConfigName = r.IMSConfigName ?? "未知配置" + }) + .ToList(); + + // 构建响应 var response = new GetNetworkStackConfigByIdResponse { - NetworkStackConfigId = networkStackConfig.Id, - NetworkStackName = networkStackConfig.NetworkStackName, - RanId = networkStackConfig.RanId, - Description = networkStackConfig.Description, - IsActive = networkStackConfig.IsActive, - CreatedAt = networkStackConfig.CreatedAt, - UpdatedAt = networkStackConfig.UpdatedAt, - CreatedBy = networkStackConfig.CreatedBy, - UpdatedBy = networkStackConfig.UpdatedBy, - StackCoreIMSBindings = networkStackConfig.StackCoreIMSBindings.Select(binding => new GetNetworkStackConfigByIdBindingResponseItem - { - StackCoreIMSBindingId = binding.Id, - NetworkStackConfigId = binding.NetworkStackConfigId, - Index = binding.Index, - CoreNetworkConfigId = binding.CnId, - IMSConfigId = binding.ImsId - }).ToList() + NetworkStackConfigId = firstRecord.NetworkStackConfigId, + NetworkStackName = firstRecord.NetworkStackName, + NetworkStackCode = firstRecord.NetworkStackCode, + RanId = firstRecord.RanId, + RanName = firstRecord.RanName, + Description = firstRecord.Description, + IsActive = firstRecord.IsActive, + CreatedAt = firstRecord.CreatedAt, + UpdatedAt = firstRecord.UpdatedAt, + CreatedBy = firstRecord.CreatedBy, + UpdatedBy = firstRecord.UpdatedBy, + StackCoreIMSBindings = bindingItems }; _logger.LogInformation("网络栈配置查询成功,配置ID: {NetworkStackConfigId}, 网络栈名称: {NetworkStackName}, 绑定关系数量: {BindingCount}", - networkStackConfig.Id, networkStackConfig.NetworkStackName, response.StackCoreIMSBindings.Count); + firstRecord.NetworkStackConfigId, firstRecord.NetworkStackName, bindingItems.Count); return OperationResult.CreateSuccess(response); } catch (Exception ex) diff --git a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdResponse.cs b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdResponse.cs index f18a305..05db8bd 100644 --- a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdResponse.cs +++ b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdResponse.cs @@ -15,11 +15,21 @@ public class GetNetworkStackConfigByIdResponse /// public string NetworkStackName { get; set; } = null!; + /// + /// 网络栈编码 + /// + public string NetworkStackCode { get; set; } = null!; + /// /// RAN配置ID(外键,可为空) /// public string? RanId { get; set; } + /// + /// RAN配置名称 + /// + public string? RanName { get; set; } + /// /// 描述 /// @@ -81,8 +91,18 @@ public class GetNetworkStackConfigByIdBindingResponseItem /// public string CoreNetworkConfigId { get; set; } = null!; + /// + /// 核心网配置名称 + /// + public string CoreNetworkConfigName { get; set; } = null!; + /// /// IMS配置ID(外键) /// public string IMSConfigId { get; set; } = null!; + + /// + /// IMS配置名称 + /// + public string IMSConfigName { get; set; } = null!; } \ No newline at end of file diff --git a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQueryHandler.cs b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQueryHandler.cs index ef5019c..9010213 100644 --- a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQueryHandler.cs +++ b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQueryHandler.cs @@ -33,8 +33,8 @@ public class GetNetworkStackConfigsQueryHandler : IRequestHandler 1; var hasNextPage = request.PageNumber < totalPages; - // 构建DTO列表,使用导航属性获取绑定关系 - var networkStackConfigDtos = networkStackConfigs.Select(config => new NetworkStackConfigDto + // 按网络栈配置ID分组,构建DTO列表 + var networkStackConfigGroups = queryResults + .GroupBy(r => r.NetworkStackConfigId) + .ToList(); + + var networkStackConfigDtos = networkStackConfigGroups.Select(group => { - NetworkStackConfigId = config.Id, - NetworkStackName = config.NetworkStackName, - RanId = config.RanId, - Description = config.Description, - IsActive = config.IsActive, - CreatedAt = config.CreatedAt, - UpdatedAt = config.UpdatedAt, - CreatedBy = config.CreatedBy, - UpdatedBy = config.UpdatedBy, - StackCoreIMSBindings = config.StackCoreIMSBindings.Select(binding => new GetNetworkStackConfigsBindingResponseItem + var firstRecord = group.First(); + return new NetworkStackConfigDto { - StackCoreIMSBindingId = binding.Id, - NetworkStackConfigId = binding.NetworkStackConfigId, - Index = binding.Index, - CoreNetworkConfigId = binding.CnId, - IMSConfigId = binding.ImsId - }).ToList() + NetworkStackConfigId = firstRecord.NetworkStackConfigId, + NetworkStackName = firstRecord.NetworkStackName, + NetworkStackCode = firstRecord.NetworkStackCode, + RanId = firstRecord.RanId, + RanName = firstRecord.RanName, + Description = firstRecord.Description, + IsActive = firstRecord.IsActive, + CreatedAt = firstRecord.CreatedAt, + UpdatedAt = firstRecord.UpdatedAt, + CreatedBy = firstRecord.CreatedBy, + UpdatedBy = firstRecord.UpdatedBy, + StackCoreIMSBindings = group + .Where(r => r.StackCoreIMSBindingId != null) + .Select(r => new GetNetworkStackConfigsBindingResponseItem + { + StackCoreIMSBindingId = r.StackCoreIMSBindingId!, + NetworkStackConfigId = r.NetworkStackConfigId, + Index = r.Index ?? 0, + CoreNetworkConfigId = r.CoreNetworkConfigId ?? string.Empty, + CoreNetworkConfigName = r.CoreNetworkConfigName ?? "未知配置", + IMSConfigId = r.IMSConfigId ?? string.Empty, + IMSConfigName = r.IMSConfigName ?? "未知配置" + }) + .ToList() + }; }).ToList(); // 构建响应 diff --git a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsResponse.cs b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsResponse.cs index 7c8f4e2..e15935e 100644 --- a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsResponse.cs +++ b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsResponse.cs @@ -56,11 +56,21 @@ public class NetworkStackConfigDto /// public string NetworkStackName { get; set; } = null!; + /// + /// 网络栈编码 + /// + public string NetworkStackCode { get; set; } = null!; + /// /// RAN配置ID(外键,可为空) /// public string? RanId { get; set; } + /// + /// RAN配置名称 + /// + public string? RanName { get; set; } + /// /// 描述 /// @@ -122,8 +132,18 @@ public class GetNetworkStackConfigsBindingResponseItem /// public string CoreNetworkConfigId { get; set; } = null!; + /// + /// 核心网配置名称 + /// + public string CoreNetworkConfigName { get; set; } = null!; + /// /// IMS配置ID(外键) /// public string IMSConfigId { get; set; } = null!; + + /// + /// IMS配置名称 + /// + public string IMSConfigName { get; set; } = null!; } \ No newline at end of file diff --git a/src/X1.Domain/Models/NetworkStackConfigWithBindingNamesDto.cs b/src/X1.Domain/Models/NetworkStackConfigWithBindingNamesDto.cs new file mode 100644 index 0000000..71725a6 --- /dev/null +++ b/src/X1.Domain/Models/NetworkStackConfigWithBindingNamesDto.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace X1.Domain.Models +{ + /// + /// 网络栈配置查询结果DTO(用于仓储层查询) + /// + public class NetworkStackConfigWithBindingNamesDto + { + /// + /// 网络栈配置ID + /// + public string NetworkStackConfigId { get; set; } = null!; + + /// + /// 网络栈名称 + /// + public string NetworkStackName { get; set; } = null!; + + /// + /// 网络栈编码 + /// + public string NetworkStackCode { get; set; } = null!; + + /// + /// RAN配置ID + /// + public string? RanId { get; set; } + + /// + /// RAN配置名称 + /// + public string? RanName { get; set; } + + /// + /// 描述 + /// + public string? Description { get; set; } + + /// + /// 是否激活 + /// + public bool IsActive { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 创建者 + /// + public string CreatedBy { get; set; } = null!; + + /// + /// 更新者 + /// + public string? UpdatedBy { get; set; } + + /// + /// 绑定关系ID + /// + public string? StackCoreIMSBindingId { get; set; } + + /// + /// 索引 + /// + public int? Index { get; set; } + + /// + /// 核心网配置ID + /// + public string? CoreNetworkConfigId { get; set; } + + /// + /// 核心网配置名称 + /// + public string? CoreNetworkConfigName { get; set; } + + /// + /// IMS配置ID + /// + public string? IMSConfigId { get; set; } + + /// + /// IMS配置名称 + /// + public string? IMSConfigName { get; set; } + } +} diff --git a/src/X1.Domain/Repositories/Base/IQueryRepository.cs b/src/X1.Domain/Repositories/Base/IQueryRepository.cs index 1e4034f..13c6bd3 100644 --- a/src/X1.Domain/Repositories/Base/IQueryRepository.cs +++ b/src/X1.Domain/Repositories/Base/IQueryRepository.cs @@ -144,4 +144,19 @@ public interface IQueryRepository where T : class /// 返回的结果会被映射到实体类型T /// Task> ExecuteSqlQueryAsync(string sql, object[] parameters, CancellationToken cancellationToken = default); + + /// + /// 执行SQL查询并映射到自定义类型 + /// + /// 结果类型 + /// SQL查询语句 + /// SQL参数 + /// 取消令牌,用于取消异步操作 + /// 查询结果集合 + /// + /// 这是一个异步操作,用于执行自定义SQL查询并映射到任意类型 + /// 支持参数化查询,防止SQL注入 + /// 返回的结果会被映射到指定的结果类型 + /// + Task> ExecuteSqlQueryAsync(string sql, object[] parameters, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/X1.Domain/Repositories/NetworkProfile/INetworkStackConfigRepository.cs b/src/X1.Domain/Repositories/NetworkProfile/INetworkStackConfigRepository.cs index 802dfdd..d9331b7 100644 --- a/src/X1.Domain/Repositories/NetworkProfile/INetworkStackConfigRepository.cs +++ b/src/X1.Domain/Repositories/NetworkProfile/INetworkStackConfigRepository.cs @@ -1,5 +1,6 @@ using CellularManagement.Domain.Entities.NetworkProfile; using CellularManagement.Domain.Repositories.Base; +using X1.Domain.Models; namespace CellularManagement.Domain.Repositories.NetworkProfile; @@ -79,4 +80,20 @@ public interface INetworkStackConfigRepository : IBaseRepository + /// 根据ID获取网络栈配置(包含绑定关系和关联配置名称) + /// + Task> GetNetworkStackConfigByIdWithBindingNamesAsync(string id, CancellationToken cancellationToken = default); + + /// + /// 搜索网络栈配置(包含绑定关系和关联配置名称,分页) + /// + Task<(int TotalCount, IList Items)> SearchNetworkStackConfigsWithBindingNamesAsync( + string? networkStackName = null, + string? ranId = null, + bool? isActive = null, + int pageNumber = 1, + int pageSize = 10, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/X1.Infrastructure/Repositories/Base/BaseRepository.cs b/src/X1.Infrastructure/Repositories/Base/BaseRepository.cs index 6f082f7..a8e9e4c 100644 --- a/src/X1.Infrastructure/Repositories/Base/BaseRepository.cs +++ b/src/X1.Infrastructure/Repositories/Base/BaseRepository.cs @@ -180,5 +180,13 @@ public abstract class BaseRepository : IBaseRepository where T : class return await QueryRepository.ExecuteSqlQueryAsync(sql, parameters, cancellationToken); } + /// + /// 执行SQL查询并映射到自定义类型 + /// + public virtual async Task> ExecuteSqlQueryAsync(string sql, object[] parameters, CancellationToken cancellationToken = default) + { + return await QueryRepository.ExecuteSqlQueryAsync(sql, parameters, cancellationToken); + } + #endregion } \ No newline at end of file diff --git a/src/X1.Infrastructure/Repositories/CQRS/QueryRepository.cs b/src/X1.Infrastructure/Repositories/CQRS/QueryRepository.cs index 18d4915..067a655 100644 --- a/src/X1.Infrastructure/Repositories/CQRS/QueryRepository.cs +++ b/src/X1.Infrastructure/Repositories/CQRS/QueryRepository.cs @@ -147,4 +147,13 @@ public class QueryRepository : IQueryRepository where T : class _logger.LogInformation("执行SQL查询: {Sql}", sql); return await _dbSet.FromSqlRaw(sql, parameters).ToListAsync(cancellationToken); } + + /// + /// 执行SQL查询并映射到自定义类型 + /// + public async Task> ExecuteSqlQueryAsync(string sql, object[] parameters, CancellationToken cancellationToken = default) + { + _logger.LogInformation("执行SQL查询并映射到类型 {ResultType}: {Sql}", typeof(TResult).Name, sql); + return await _context.Database.SqlQueryRaw(sql, parameters).ToListAsync(cancellationToken); + } } \ No newline at end of file diff --git a/src/X1.Infrastructure/Repositories/NetworkProfile/NetworkStackConfigRepository.cs b/src/X1.Infrastructure/Repositories/NetworkProfile/NetworkStackConfigRepository.cs index 7e4370c..200b55a 100644 --- a/src/X1.Infrastructure/Repositories/NetworkProfile/NetworkStackConfigRepository.cs +++ b/src/X1.Infrastructure/Repositories/NetworkProfile/NetworkStackConfigRepository.cs @@ -5,6 +5,7 @@ using CellularManagement.Domain.Entities.NetworkProfile; using CellularManagement.Domain.Repositories.Base; using CellularManagement.Domain.Repositories.NetworkProfile; using CellularManagement.Infrastructure.Repositories.Base; +using X1.Domain.Models; namespace CellularManagement.Infrastructure.Repositories.NetworkProfile; @@ -259,4 +260,132 @@ public class NetworkStackConfigRepository : BaseRepository, return (result.TotalCount, result.Items.OrderBy(x => x.NetworkStackName).ToList()); } + + /// + /// 根据ID获取网络栈配置(包含绑定关系和关联配置名称) + /// + public async Task> GetNetworkStackConfigByIdWithBindingNamesAsync(string id, CancellationToken cancellationToken = default) + { + // 使用原生SQL查询来一次性获取所有需要的数据 - PostgreSQL语法 + var sql = @" + SELECT + nsc.""Id"" AS ""NetworkStackConfigId"", + nsc.""NetworkStackName"", + nsc.""NetworkStackCode"", + nsc.""RanId"", + ran.""Name"" AS ""RanName"", + nsc.""Description"", + nsc.""IsActive"", + nsc.""CreatedAt"", + nsc.""UpdatedAt"", + nsc.""CreatedBy"", + nsc.""UpdatedBy"", + binding.""Id"" AS ""StackCoreIMSBindingId"", + binding.""Index"", + binding.""CnId"" AS ""CoreNetworkConfigId"", + cnc.""Name"" AS ""CoreNetworkConfigName"", + binding.""ImsId"" AS ""IMSConfigId"", + ims.""Name"" AS ""IMSConfigName"" + FROM ""NetworkStackConfigs"" nsc + LEFT JOIN ""RAN_Configurations"" ran ON nsc.""RanId"" = ran.""Id"" + LEFT JOIN ""Stack_CoreIMS_Bindings"" binding ON nsc.""Id"" = binding.""NetworkStackConfigId"" + LEFT JOIN ""CoreNetworkConfigs"" cnc ON binding.""CnId"" = cnc.""Id"" + LEFT JOIN ""IMS_Configurations"" ims ON binding.""ImsId"" = ims.""Id"" + WHERE nsc.""Id"" = @p0 + ORDER BY binding.""Index"""; + + var parameters = new object[] { id }; + + // 执行查询 + var results = await ExecuteSqlQueryAsync(sql, parameters, cancellationToken); + return results.ToList(); + } + + /// + /// 搜索网络栈配置(包含绑定关系和关联配置名称,分页) + /// + public async Task<(int TotalCount, IList Items)> SearchNetworkStackConfigsWithBindingNamesAsync( + string? networkStackName = null, + string? ranId = null, + bool? isActive = null, + int pageNumber = 1, + int pageSize = 10, + CancellationToken cancellationToken = default) + { + // 构建WHERE条件和参数 + var whereConditions = new List(); + var parameters = new List(); + var paramIndex = 0; + + if (!string.IsNullOrWhiteSpace(networkStackName)) + { + whereConditions.Add($"nsc.\"NetworkStackName\" LIKE @p{paramIndex}"); + parameters.Add($"%{networkStackName}%"); + paramIndex++; + } + + if (!string.IsNullOrWhiteSpace(ranId)) + { + whereConditions.Add($"nsc.\"RanId\" = @p{paramIndex}"); + parameters.Add(ranId); + paramIndex++; + } + + if (isActive.HasValue) + { + whereConditions.Add($"nsc.\"IsActive\" = @p{paramIndex}"); + parameters.Add(isActive.Value); + paramIndex++; + } + + var whereClause = whereConditions.Any() ? "WHERE " + string.Join(" AND ", whereConditions) : ""; + + // 计算总记录数的SQL - PostgreSQL语法 + var countSql = $@" + SELECT COUNT(DISTINCT nsc.""Id"") + FROM ""NetworkStackConfigs"" nsc + {whereClause}"; + + // 获取总记录数 + var totalCount = await ExecuteSqlQueryAsync(countSql, parameters.ToArray(), cancellationToken); + var count = totalCount.FirstOrDefault(); + + // 分页查询SQL - PostgreSQL语法 + var offset = (pageNumber - 1) * pageSize; + var sql = $@" + SELECT + nsc.""Id"" AS ""NetworkStackConfigId"", + nsc.""NetworkStackName"", + nsc.""NetworkStackCode"", + nsc.""RanId"", + ran.""Name"" AS ""RanName"", + nsc.""Description"", + nsc.""IsActive"", + nsc.""CreatedAt"", + nsc.""UpdatedAt"", + nsc.""CreatedBy"", + nsc.""UpdatedBy"", + binding.""Id"" AS ""StackCoreIMSBindingId"", + binding.""Index"", + binding.""CnId"" AS ""CoreNetworkConfigId"", + cnc.""Name"" AS ""CoreNetworkConfigName"", + binding.""ImsId"" AS ""IMSConfigId"", + ims.""Name"" AS ""IMSConfigName"" + FROM ""NetworkStackConfigs"" nsc + LEFT JOIN ""RAN_Configurations"" ran ON nsc.""RanId"" = ran.""Id"" + LEFT JOIN ""Stack_CoreIMS_Bindings"" binding ON nsc.""Id"" = binding.""NetworkStackConfigId"" + LEFT JOIN ""CoreNetworkConfigs"" cnc ON binding.""CnId"" = cnc.""Id"" + LEFT JOIN ""IMS_Configurations"" ims ON binding.""ImsId"" = ims.""Id"" + {whereClause} + ORDER BY nsc.""NetworkStackName"", binding.""Index"" + LIMIT @p{paramIndex} OFFSET @p{paramIndex + 1}"; + + parameters.Add(pageSize); + parameters.Add(offset); + + // 执行查询 + var results = await ExecuteSqlQueryAsync(sql, parameters.ToArray(), cancellationToken); + + return (count, results.ToList()); + } } \ No newline at end of file diff --git a/src/X1.WebUI/src/config/core/env.config.ts b/src/X1.WebUI/src/config/core/env.config.ts index 34fa412..a0a4b89 100644 --- a/src/X1.WebUI/src/config/core/env.config.ts +++ b/src/X1.WebUI/src/config/core/env.config.ts @@ -4,7 +4,7 @@ import type { ApiConfig, AuthConfig, AppConfig, MockConfig, Environment } from ' // 默认配置 const DEFAULT_CONFIG = { // API配置 - VITE_API_BASE_URL: 'https://192.168.2.142:7268/api', + VITE_API_BASE_URL: 'https://localhost:7268/api', VITE_API_TIMEOUT: '30000', VITE_API_VERSION: 'v1', VITE_API_MAX_RETRIES: '3', diff --git a/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx b/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx index c3df239..f550f40 100644 --- a/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx +++ b/src/X1.WebUI/src/pages/instruments/DevicesTable.tsx @@ -12,7 +12,7 @@ import { DensityType } from '@/components/ui/TableToolbar'; * 表格字段映射关系(基于后端GetDeviceByIdResponse): * - deviceId: 设备ID * - deviceName: 设备名称 - * - serialNumber: 序列号 + * - deviceCode: 设备编码 * - description: 设备描述 * - agentPort: Agent端口 * - isEnabled: 是否启用 @@ -98,8 +98,8 @@ export default function DevicesTable({ {device.deviceName} ); - case 'serialNumber': - return {device.serialNumber}; + case 'deviceCode': + return {device.deviceCode}; case 'description': return (
diff --git a/src/X1.WebUI/src/pages/instruments/DevicesView.tsx b/src/X1.WebUI/src/pages/instruments/DevicesView.tsx index 7cde8f2..d8d1567 100644 --- a/src/X1.WebUI/src/pages/instruments/DevicesView.tsx +++ b/src/X1.WebUI/src/pages/instruments/DevicesView.tsx @@ -13,7 +13,7 @@ import { useToast } from '@/components/ui/use-toast'; const defaultColumns = [ { key: 'deviceId', title: '设备ID', visible: false }, { key: 'deviceName', title: '设备名称', visible: true }, - { key: 'serialNumber', title: '序列号', visible: true }, + { key: 'deviceCode', title: '设备编码', visible: true }, { key: 'description', title: '描述', visible: true }, { key: 'agentPort', title: 'Agent端口', visible: true }, { key: 'isEnabled', title: '状态', visible: true }, @@ -29,7 +29,7 @@ type SearchField = // 第一行字段(收起时只显示这3个) const firstRowFields: SearchField[] = [ - { key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入设备名称或序列号' }, + { key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入设备名称或设备编码' }, { key: 'isEnabled', label: '状态', type: 'select', options: [ { value: '', label: '请选择' }, { value: 'true', label: '启用' }, @@ -53,7 +53,7 @@ const advancedFields: SearchField[] = [ * 表格字段映射关系(基于后端GetDeviceByIdResponse): * - deviceId: 设备ID * - deviceName: 设备名称 - * - serialNumber: 序列号 + * - deviceCode: 设备编码 * - description: 设备描述 * - agentPort: Agent端口 * - isEnabled: 是否启用 diff --git a/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx b/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx index 9fd9b47..2df3db0 100644 --- a/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx +++ b/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx @@ -73,10 +73,40 @@ export default function NetworkStackConfigsTable({ {config.networkStackName}
); - case 'ranId': + case 'networkStackCode': return ( -
- {config.ranId || '-'} +
+ {config.networkStackCode} +
+ ); + case 'ranName': + return ( +
+ {config.ranName || '-'} +
+ ); + case 'coreNetworkConfigNames': + const coreNetworkNames = config.stackCoreIMSBindings + ?.map(binding => binding.coreNetworkConfigName) + .filter(name => name) || []; + const coreNetworkNamesText = coreNetworkNames.length > 0 + ? coreNetworkNames.join(', ') + : '-'; + return ( +
+ {coreNetworkNamesText} +
+ ); + case 'imsConfigNames': + const imsNames = config.stackCoreIMSBindings + ?.map(binding => binding.imsConfigName) + .filter(name => name) || []; + const imsNamesText = imsNames.length > 0 + ? imsNames.join(', ') + : '-'; + return ( +
+ {imsNamesText}
); case 'description': diff --git a/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx b/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx index 333ab88..247a3e2 100644 --- a/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx +++ b/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx @@ -12,7 +12,10 @@ import { useToast } from '@/components/ui/use-toast'; const defaultColumns = [ { key: 'networkStackName', title: '网络栈名称', visible: true }, - { key: 'ranId', title: 'RAN ID', visible: true }, + { key: 'networkStackCode', title: '网络栈编码', visible: true }, + { key: 'ranName', title: 'RAN配置', visible: true }, + { key: 'coreNetworkConfigNames', title: '核心网配置', visible: true }, + { key: 'imsConfigNames', title: 'IMS配置', visible: true }, { key: 'description', title: '描述', visible: true }, { key: 'isActive', title: '状态', visible: true }, { key: 'createdAt', title: '创建时间', visible: true }, diff --git a/src/X1.WebUI/src/services/instrumentService.ts b/src/X1.WebUI/src/services/instrumentService.ts index 3ca4816..eebfd66 100644 --- a/src/X1.WebUI/src/services/instrumentService.ts +++ b/src/X1.WebUI/src/services/instrumentService.ts @@ -12,7 +12,6 @@ export type DeviceRunningStatus = 'running' | 'stopped'; export interface Device { deviceId: string; deviceName: string; - serialNumber: string; deviceCode: string; description: string; agentPort: number; @@ -54,7 +53,6 @@ export interface CreateDeviceRequest { export interface CreateDeviceResponse { deviceId: string; deviceName: string; - serialNumber: string; deviceCode: string; description: string; agentPort: number; @@ -76,7 +74,6 @@ export interface UpdateDeviceRequest { export interface UpdateDeviceResponse { deviceId: string; deviceName: string; - serialNumber: string; deviceCode: string; description: string; agentPort: number; diff --git a/src/X1.WebUI/src/services/networkStackConfigService.ts b/src/X1.WebUI/src/services/networkStackConfigService.ts index e4b0351..d86b2e8 100644 --- a/src/X1.WebUI/src/services/networkStackConfigService.ts +++ b/src/X1.WebUI/src/services/networkStackConfigService.ts @@ -8,6 +8,7 @@ export interface NetworkStackConfig { networkStackName: string; networkStackCode: string; ranId?: string; + ranName?: string; description?: string; isActive: boolean; createdAt: string; @@ -23,7 +24,9 @@ export interface StackCoreIMSBinding { networkStackConfigId: string; index: number; coreNetworkConfigId: string; + coreNetworkConfigName: string; imsConfigId: string; + imsConfigName: string; createdAt: string; } diff --git a/src/modify.md b/src/modify.md index 0052d62..f1f36e3 100644 --- a/src/modify.md +++ b/src/modify.md @@ -441,7 +441,7 @@ ### 用户体验 - **统一体验**:所有配置表单现在都有相同的编辑体验 -- **搜索功能**:支持在配置内容中搜索和高亮 +- **搜索功能**:支持在配置内容中搜索和高亮显示匹配项 - **格式灵活**:不再强制要求JSON格式,支持任意格式的配置内容 ## 2024-12-19 RAN配置表格Key属性修复 @@ -2478,152 +2478,9 @@ chore: 更新.gitignore忽略日志文件 4. **组装编号**:`DEV-{序号}-{序列号}` ### 注意事项 -- 设备编号格式:`DEV-001-SN`、`DEV-002-SN` 等 -- 序号从001开始,最大到999 +- 设备编号格式:`DEV-000-SN`、`DEV-001-SN` 等 +- 序号从000开始,最大到999 - 如果设备数量超过999,需要扩展序号位数 -- 设备编号在创建时生成,后续不可修改 - -## 2025-07-28 - 添加设备编号重复检查 - -### 修改原因 -根据用户需求,在创建设备时不仅需要检查序列号是否重复,还需要检查设备编号(DeviceCode)是否重复,确保设备编号的唯一性。 - -### 修改文件 -- `X1.Domain/Repositories/Device/ICellularDeviceRepository.cs` - 添加DeviceCodeExistsAsync方法 -- `X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs` - 实现DeviceCodeExistsAsync方法 -- `X1.Infrastructure/Configurations/Device/CellularDeviceConfiguration.cs` - 添加DeviceCode唯一索引 -- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 添加设备编号重复检查逻辑 - -### 修改内容 - -#### 1. 仓储接口修改 -- **ICellularDeviceRepository**: - - 添加 `DeviceCodeExistsAsync` 方法 - - 用于检查设备编号是否已存在 - -#### 2. 仓储实现修改 -- **CellularDeviceRepository**: - - 实现 `DeviceCodeExistsAsync` 方法 - - 使用 `QueryRepository.AnyAsync` 检查设备编号是否存在 - -#### 3. 数据库配置修改 -- **CellularDeviceConfiguration**: - - 添加 `DeviceCode` 字段的唯一索引 - - 添加 `DeviceCode` 字段的属性配置 - - 确保数据库层面的唯一性约束 - -#### 4. 命令处理器修改 -- **CreateDeviceCommandHandler**: - - 在检查序列号重复后,添加设备编号重复检查 - - 如果设备编号已存在,返回相应的错误信息 - - 确保创建设备时两个字段都是唯一的 - -### 技术特性 -- **双重唯一性检查**:确保序列号和设备编号都是唯一的 -- **数据库约束**:在数据库层面添加唯一索引 -- **应用层验证**:在应用层进行重复性检查 -- **错误处理**:提供清晰的错误信息 - -### 业务价值 -- **数据完整性**:确保设备编号的唯一性 -- **业务逻辑**:符合设备管理的业务规则 -- **用户体验**:提供明确的错误提示 -- **数据一致性**:防止重复设备编号的创建 - -### 影响范围 -- **创建操作**:设备创建时检查设备编号重复 -- **数据库结构**:添加设备编号唯一索引 -- **错误处理**:增加设备编号重复的错误处理 -- **数据验证**:确保设备编号的唯一性 - -### 检查顺序 -1. **序列号检查**:首先检查序列号是否重复 -2. **设备编号检查**:然后检查设备编号是否重复 -3. **创建设备**:只有两个检查都通过才创建设备 - -### 注意事项 -- 需要创建数据库迁移来添加新的唯一索引 -- 如果数据库中已存在重复的设备编号,迁移可能会失败 -- 建议在应用此修改前清理重复数据 - -## 2025-07-28 - 限制UpdateDeviceResponse只能修改特定字段 - -### 修改原因 -根据用户需求,设备更新时只能修改 `IsEnabled`、`IsRunning`、`Description` 和 `DeviceName` 这四个字段,其他字段(如序列号、设备编码、IP地址、端口等)不能修改。 - -### 修改文件 -- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs` - 只保留可修改的字段 -- `X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs` - 只更新允许修改的字段 -- `X1.WebUI/src/services/instrumentService.ts` - 更新前端接口定义 -- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 编辑模式下只显示可修改的字段 -- `X1.WebUI/src/pages/instruments/DevicesView.tsx` - 更新编辑设备时的数据传递 - -### 修改内容 - -#### 1. 后端命令类修改 -- **UpdateDeviceCommand**: - - 只保留 `DeviceId`、`DeviceName`、`Description`、`IsEnabled`、`IsRunning` 字段 - - 删除 `DeviceCode`、`IpAddress`、`AgentPort` 字段 - - 确保只有这四个字段可以被用户修改 - -#### 2. 后端命令处理器修改 -- **UpdateDeviceCommandHandler**: - - 在更新设备时,只更新允许修改的字段 - - 其他字段(序列号、设备编码、IP地址、端口)保持原有值不变 - - 添加注释说明哪些字段保持不变 - -#### 3. 前端接口修改 -- **UpdateDeviceRequest**: - - 只包含 `deviceId`、`deviceName`、`description`、`isEnabled`、`isRunning` 字段 - - 删除 `deviceCode`、`ipAddress`、`agentPort` 字段 - -#### 4. 前端表单修改 -- **DeviceForm**: - - 在编辑模式下,只显示可修改的字段 - - 创建设备时显示所有字段 - - 根据 `isEdit` 属性动态显示不同的字段 - - 更新表单提交逻辑,区分创建和编辑模式 - -#### 5. 前端页面修改 -- **DevicesView**: - - 更新编辑设备时的初始数据传递 - - 只传递可修改的字段给表单组件 - -### 技术特性 -- **字段限制**:严格限制只能修改指定的四个字段 -- **数据保护**:防止用户意外修改关键字段(如序列号、IP地址等) -- **界面适配**:根据操作模式动态显示不同的表单字段 -- **类型安全**:更新所有相关的TypeScript接口定义 - -### 业务价值 -- **数据安全**:防止用户误操作修改关键设备信息 -- **用户体验**:简化编辑界面,只显示可修改的字段 -- **业务逻辑**:符合设备管理的业务规则 -- **一致性**:确保设备关键信息不被意外修改 - -### 影响范围 -- **更新操作**:设备更新时只能修改指定的四个字段 -- **前端界面**:编辑模式下只显示可修改的字段 -- **API接口**:更新请求模型,只包含可修改的字段 -- **数据保护**:确保设备关键信息的安全性 - -### 可修改字段 -1. **DeviceName** - 设备名称 -2. **Description** - 设备描述 -3. **IsEnabled** - 是否启用 -4. **IsRunning** - 设备运行状态 - -### 不可修改字段 -1. **SerialNumber** - 设备序列号(通过IP自动获取) -2. **DeviceCode** - 设备编码 -3. **IpAddress** - IP地址 -4. **AgentPort** - Agent端口 - -### 注意事项 -- 创建设备时仍然需要提供所有必要字段 -- 更新设备时只允许修改指定的四个字段 -- 其他字段在更新时会保持原有值不变 -- 前端界面会根据操作模式动态调整显示内容 ## 2025-07-28 - 删除CreateDeviceCommand中的SerialNumber字段,改为通过IP地址自动获取 @@ -2960,4 +2817,407 @@ chore: 更新.gitignore忽略日志文件 - 创建设备时仍然需要提供IP地址和端口 - 编辑设备时IP地址和端口显示为只读状态 - 用户可以通过提示文本了解为什么不能修改这些字段 -- 如果需要修改IP地址或端口,需要重新创建设备 \ No newline at end of file +- 如果需要修改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接口定义 + +### 业务价值 +- **用户体验**:用户无需手动输入序列号,减少输入错误 +- **数据准确性**:序列号直接从设备获取,确保数据准确性 +- **自动化**:简化设备创建流程,提高效率 +- **一致性**:确保序列号与设备实际状态一致 + +### 影响范围 +- **创建操作**:设备创建时自动获取序列号 +- **更新操作**:设备更新时保持序列号不变 +- **前端界面**:移除序列号输入字段,添加设备编码字段 +- **API接口**:更新请求和响应模型 +- **错误处理**:添加序列号获取失败的处理逻辑 +- **服务注册**:注册设备通信相关服务 + +### 依赖服务 +- `IBaseInstrumentClient`:用于与设备通信获取序列号 +- `ServiceEndpoint`:配置设备连接信息 +- `GetDeviceSerialNumberAsync`:获取设备序列号的方法 + +### 注意事项 +- 设备必须能够通过HTTP协议访问 +- 设备需要提供 `/System/serial-number` 接口 +- 网络连接必须正常才能获取序列号 +- 如果获取失败,会返回相应的错误信息 + +## 2025-07-28 - 修复endpointManager重新添加时机bug + +### 修改原因 +在获取设备序列号成功后,原来的代码立即重新添加了服务端点,但此时设备数据还没有保存到数据库。这可能导致服务端点与数据库中的设备数据不一致的问题。 + +### 修改文件 +- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修复endpointManager重新添加时机 + +### 修改内容 + +#### 1. GetDeviceSerialNumberAsync方法修改 +- **移除立即重新添加逻辑**: + - 在获取序列号成功后,只移除临时服务端点 + - 不再立即重新添加服务端点 + - 确保临时端点被正确清理 + +#### 2. CreateAndSaveDeviceAsync方法修改 +- **添加延迟重新添加逻辑**: + - 在设备成功保存到数据库后,重新添加服务端点 + - 使用设备编号(deviceCode)作为端点名称 + - 确保服务端点与数据库中的设备数据一致 + +### 技术特性 +- **正确的执行顺序**:先保存数据,再添加服务端点 +- **数据一致性**:确保服务端点与数据库中的设备信息一致 +- **错误处理**:如果设备保存失败,不会添加服务端点 +- **日志记录**:详细记录服务端点的添加过程 + +### 业务价值 +- **数据完整性**:确保服务端点与设备数据的一致性 +- **系统稳定性**:避免因数据不一致导致的问题 +- **可维护性**:清晰的执行流程便于调试和维护 + +### 影响范围 +- **创建流程**:修改了设备创建时的endpointManager管理逻辑 +- **数据一致性**:确保服务端点与设备数据同步 +- **错误处理**:改进了异常情况下的资源清理 + +### 执行流程 +1. **获取序列号**:使用临时服务端点获取设备序列号 +2. **清理临时端点**:移除临时服务端点 +3. **保存设备数据**:将设备信息保存到数据库 +4. **添加正式端点**:使用设备编号作为名称添加正式服务端点 + +### 注意事项 +- 临时服务端点使用时间戳生成名称,避免冲突 +- 正式服务端点使用设备编号作为名称,便于管理 +- 只有在设备数据保存成功后才会添加正式服务端点 + +## 2025-07-28 - 修复设备编号生成逻辑,从000开始 + +### 修改原因 +根据用户需求,设备编号应该从000开始,类似001、002的格式,而不是从1开始。 + +### 修改文件 +- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修改CalculateRequiredDigits方法和GenerateDeviceCodeAsync方法 + +### 修改内容 + +#### 1. CalculateRequiredDigits方法修改 +- **最小位数设置**:将最小位数从1改为3,确保从000开始 +- **逻辑优化**:使用 `Math.Max(calculatedDigits, 3)` 确保至少3位数 +- **注释更新**:更新注释说明从000开始,至少3位数 + +#### 2. GenerateDeviceCodeAsync方法修改 +- **注释更新**:更新注释说明从000开始,确保至少3位数 +- **格式说明**:更新设备编号格式说明为 DEV-000-SN, DEV-001-SN, DEV-002-SN 等 + +### 技术特性 +- **从000开始**:设备编号从DEV-000-SN开始,而不是DEV-1-SN +- **至少3位数**:确保序号至少有3位数字,格式统一 +- **动态扩展**:当设备数量超过999时,自动扩展为4位数(1000、1001等) +- **格式一致**:所有设备编号都遵循统一的格式规范 + +### 业务价值 +- **格式规范**:统一的设备编号格式,便于管理和识别 +- **用户友好**:从000开始的编号更符合用户习惯 +- **可扩展性**:支持大量设备的编号需求 +- **一致性**:所有设备编号都遵循相同的格式规则 + +### 设备编号格式示例 +- 第1个设备:DEV-000-SN +- 第2个设备:DEV-001-SN +- 第3个设备:DEV-002-SN +- ... +- 第1000个设备:DEV-1000-SN +- 第1001个设备:DEV-1001-SN + +### 影响范围 +- **设备创建**:新创建的设备将使用从000开始的编号格式 +- **编号生成**:设备编号生成逻辑已更新 +- **格式统一**:所有设备编号都遵循新的格式规范 + +### 注意事项 +- 现有设备的编号不会自动更新 +- 新创建的设备将使用新的编号格式 +- 编号格式支持扩展到更多位数(当设备数量超过999时) + +## 2025-07-28 - 优化临时服务端点名称生成,使用GUID替代时间戳 + +### 修改原因 +使用时间戳生成临时服务端点名称可能存在冲突风险,特别是在高并发场景下。使用GUID可以确保端点名称的唯一性。 + +### 修改文件 +- `X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs` - 修改CreateServiceEndpoint方法 + +### 修改内容 + +#### 1. CreateServiceEndpoint方法修改 +- **名称生成方式**:从时间戳改为GUID +- **格式变更**:`DEV-{DateTime.Now:yyyyMMddHHmmss}` → `DEV-{Guid.NewGuid():N}` +- **唯一性保证**:GUID确保每个临时端点名称都是唯一的 + +### 技术特性 +- **唯一性保证**:GUID提供全局唯一性,避免名称冲突 +- **并发安全**:在高并发场景下不会出现重复名称 +- **格式简洁**:使用 `:N` 格式去除GUID中的连字符,生成32位字符串 +- **性能优化**:GUID生成比时间戳更高效 + +### 业务价值 +- **系统稳定性**:避免因端点名称冲突导致的问题 +- **并发支持**:支持多用户同时创建设备 +- **可靠性提升**:确保每个临时端点都能正确创建和管理 + +### 临时端点名称示例 +- 修改前:`DEV-20250728143022` +- 修改后:`DEV-a1b2c3d4e5f678901234567890123456` + +### 影响范围 +- **临时端点创建**:临时服务端点名称生成方式已更新 +- **并发处理**:支持更好的并发创建设备场景 +- **系统稳定性**:提高了端点管理的可靠性 + +## 2025-07-28 - 移除设备表单中的启用和启动复选框 + +### 修改原因 +根据用户需求,设备表单中不需要显示"启用设备"和"启动设备"这两个复选框,这些状态应该通过其他方式管理。 + +### 修改文件 +- `X1.WebUI/src/pages/instruments/DeviceForm.tsx` - 移除启用和启动复选框 + +### 修改内容 + +#### 1. 表单状态管理修改 +- **初始状态设置**: + - `isEnabled` 默认设置为 `true` + - `isRunning` 默认设置为 `false` + - 这些值不再从 `initialData` 中获取 + +#### 2. 表单界面修改 +- **移除复选框组件**: + - 删除"启用设备"复选框及其相关代码 + - 删除"启动设备"复选框及其相关代码 + - 保留其他表单字段不变 + +#### 3. 表单提交逻辑 +- **创建模式**:提交所有字段,包括默认的启用和启动状态 +- **编辑模式**:只提交可修改的字段,启用和启动状态保持默认值 + +### 技术特性 +- **默认状态**:设备创建时默认启用,默认不启动 +- **界面简化**:移除不必要的复选框,简化用户界面 +- **状态管理**:启用和启动状态通过默认值管理 +- **功能保持**:其他表单功能保持不变 + +### 业务价值 +- **用户体验**:简化设备创建和编辑界面 +- **操作简化**:减少用户需要选择的选项 + +## 2024-12-19 前端网络栈配置界面更新 + +### 修改原因 +根据后端API的更新,前端需要显示新的字段信息,包括网络栈编码、RAN配置名称和核心网配置名称,以提供更好的用户体验。 + +### 修改文件 +- `X1.WebUI/src/services/networkStackConfigService.ts` - 更新接口定义 +- `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx` - 更新列配置 +- `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx` - 更新表格渲染 + +### 修改内容 + +#### 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(', ')` 方法将多个名称连接 + - 处理空值情况,显示"-" + +### 技术特性 +- **数据展示优化**:显示有意义的名称而不是ID +- **多值处理**:支持显示多个关联配置的名称 +- **空值处理**:优雅处理空值情况 +- **向后兼容**:保留原有字段,确保兼容性 + +### 用户体验改进 +- **信息丰富度**:用户可以直接看到配置名称,无需记忆ID +- **可读性提升**:使用名称比ID更直观易懂 +- **关联信息展示**:清楚显示网络栈配置与核心网配置的关联关系 +- **界面一致性**:与其他模块保持一致的显示风格 + +### 影响范围 +- **表格显示**:网络栈配置列表表格显示更多有用信息 +- **用户操作**:用户可以更直观地识别和操作配置项 +- **数据理解**:减少用户对ID的记忆负担,提高工作效率 + +## 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