Browse Source

feat: 优化网络栈配置查询性能并修复PostgreSQL语法

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
   - 优化参数传递方式
feature/x1-web-request
root 5 days ago
parent
commit
47d9a8bf0a
  1. 1
      src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs
  2. 4
      src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceResponse.cs
  3. 2
      src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs
  4. 4
      src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdResponse.cs
  5. 2
      src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs
  6. 58
      src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdQueryHandler.cs
  7. 20
      src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdResponse.cs
  8. 55
      src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQueryHandler.cs
  9. 20
      src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsResponse.cs
  10. 99
      src/X1.Domain/Models/NetworkStackConfigWithBindingNamesDto.cs
  11. 15
      src/X1.Domain/Repositories/Base/IQueryRepository.cs
  12. 17
      src/X1.Domain/Repositories/NetworkProfile/INetworkStackConfigRepository.cs
  13. 8
      src/X1.Infrastructure/Repositories/Base/BaseRepository.cs
  14. 9
      src/X1.Infrastructure/Repositories/CQRS/QueryRepository.cs
  15. 129
      src/X1.Infrastructure/Repositories/NetworkProfile/NetworkStackConfigRepository.cs
  16. 2
      src/X1.WebUI/src/config/core/env.config.ts
  17. 6
      src/X1.WebUI/src/pages/instruments/DevicesTable.tsx
  18. 6
      src/X1.WebUI/src/pages/instruments/DevicesView.tsx
  19. 36
      src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx
  20. 5
      src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx
  21. 3
      src/X1.WebUI/src/services/instrumentService.ts
  22. 3
      src/X1.WebUI/src/services/networkStackConfigService.ts
  23. 552
      src/modify.md

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

@ -120,7 +120,6 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
{
DeviceId = device.Id,
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
DeviceCode = device.DeviceCode,
Description = device.Description,
AgentPort = device.AgentPort,

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

@ -15,10 +15,6 @@ public class CreateDeviceResponse
/// </summary>
public string DeviceName { get; set; }
/// <summary>
/// 序列号
/// </summary>
public string SerialNumber { get; set; }
/// <summary>
/// 设备编码

2
src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs

@ -47,7 +47,7 @@ public class GetDeviceByIdQueryHandler : IRequestHandler<GetDeviceByIdQuery, Ope
{
DeviceId = device.Id,
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
DeviceCode = device.DeviceCode,
Description = device.Description,
AgentPort = device.AgentPort,
IsEnabled = device.IsEnabled,

4
src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdResponse.cs

@ -16,9 +16,9 @@ public class GetDeviceByIdResponse
public string DeviceName { get; set; }
/// <summary>
/// 序列号
/// 设备编码
/// </summary>
public string SerialNumber { get; set; }
public string DeviceCode { get; set; }
/// <summary>
/// 设备描述

2
src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs

@ -69,7 +69,7 @@ public class GetDevicesQueryHandler : IRequestHandler<GetDevicesQuery, Operation
{
DeviceId = device.Id,
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
DeviceCode = device.DeviceCode,
Description = device.Description,
AgentPort = device.AgentPort,
IsEnabled = device.IsEnabled,

58
src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdQueryHandler.cs

@ -33,38 +33,52 @@ public class GetNetworkStackConfigByIdQueryHandler : IRequestHandler<GetNetworkS
{
_logger.LogInformation("开始查询网络栈配置,配置ID: {NetworkStackConfigId}", request.NetworkStackConfigId);
// 获取网络栈配置(包含导航属性)
var networkStackConfig = await _networkStackConfigRepository.GetNetworkStackConfigByIdWithBindingsAsync(request.NetworkStackConfigId, cancellationToken);
if (networkStackConfig == null)
// 使用优化的查询方法一次性获取所有数据
var queryResults = await _networkStackConfigRepository.GetNetworkStackConfigByIdWithBindingNamesAsync(request.NetworkStackConfigId, cancellationToken);
if (!queryResults.Any())
{
_logger.LogWarning("网络栈配置不存在: {NetworkStackConfigId}", request.NetworkStackConfigId);
return OperationResult<GetNetworkStackConfigByIdResponse>.CreateFailure($"网络栈配置 {request.NetworkStackConfigId} 不存在");
}
// 构建响应,使用导航属性获取绑定关系
var response = new GetNetworkStackConfigByIdResponse
// 获取第一条记录作为主配置信息
var firstRecord = queryResults.First();
// 构建绑定关系列表
var bindingItems = queryResults
.Where(r => r.StackCoreIMSBindingId != null)
.Select(r => new GetNetworkStackConfigByIdBindingResponseItem
{
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 = 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
{
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<GetNetworkStackConfigByIdResponse>.CreateSuccess(response);
}
catch (Exception ex)

20
src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigById/GetNetworkStackConfigByIdResponse.cs

@ -15,11 +15,21 @@ public class GetNetworkStackConfigByIdResponse
/// </summary>
public string NetworkStackName { get; set; } = null!;
/// <summary>
/// 网络栈编码
/// </summary>
public string NetworkStackCode { get; set; } = null!;
/// <summary>
/// RAN配置ID(外键,可为空)
/// </summary>
public string? RanId { get; set; }
/// <summary>
/// RAN配置名称
/// </summary>
public string? RanName { get; set; }
/// <summary>
/// 描述
/// </summary>
@ -81,8 +91,18 @@ public class GetNetworkStackConfigByIdBindingResponseItem
/// </summary>
public string CoreNetworkConfigId { get; set; } = null!;
/// <summary>
/// 核心网配置名称
/// </summary>
public string CoreNetworkConfigName { get; set; } = null!;
/// <summary>
/// IMS配置ID(外键)
/// </summary>
public string IMSConfigId { get; set; } = null!;
/// <summary>
/// IMS配置名称
/// </summary>
public string IMSConfigName { get; set; } = null!;
}

55
src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQueryHandler.cs

@ -33,8 +33,8 @@ public class GetNetworkStackConfigsQueryHandler : IRequestHandler<GetNetworkStac
{
_logger.LogInformation("开始查询网络栈配置列表,页码: {PageNumber}, 页大小: {PageSize}", request.PageNumber, request.PageSize);
// 获取网络栈配置列表(包含导航属性)
var (totalCount, networkStackConfigs) = await _networkStackConfigRepository.SearchNetworkStackConfigsWithBindingsAsync(
// 使用优化的查询方法一次性获取所有数据
var (totalCount, queryResults) = await _networkStackConfigRepository.SearchNetworkStackConfigsWithBindingNamesAsync(
networkStackName: request.NetworkStackName,
ranId: request.RanId,
isActive: request.IsActive,
@ -47,26 +47,41 @@ public class GetNetworkStackConfigsQueryHandler : IRequestHandler<GetNetworkStac
var hasPreviousPage = request.PageNumber > 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 =>
{
var firstRecord = group.First();
return new NetworkStackConfigDto
{
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
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 = binding.Id,
NetworkStackConfigId = binding.NetworkStackConfigId,
Index = binding.Index,
CoreNetworkConfigId = binding.CnId,
IMSConfigId = binding.ImsId
}).ToList()
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();
// 构建响应

20
src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsResponse.cs

@ -56,11 +56,21 @@ public class NetworkStackConfigDto
/// </summary>
public string NetworkStackName { get; set; } = null!;
/// <summary>
/// 网络栈编码
/// </summary>
public string NetworkStackCode { get; set; } = null!;
/// <summary>
/// RAN配置ID(外键,可为空)
/// </summary>
public string? RanId { get; set; }
/// <summary>
/// RAN配置名称
/// </summary>
public string? RanName { get; set; }
/// <summary>
/// 描述
/// </summary>
@ -122,8 +132,18 @@ public class GetNetworkStackConfigsBindingResponseItem
/// </summary>
public string CoreNetworkConfigId { get; set; } = null!;
/// <summary>
/// 核心网配置名称
/// </summary>
public string CoreNetworkConfigName { get; set; } = null!;
/// <summary>
/// IMS配置ID(外键)
/// </summary>
public string IMSConfigId { get; set; } = null!;
/// <summary>
/// IMS配置名称
/// </summary>
public string IMSConfigName { get; set; } = null!;
}

99
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
{
/// <summary>
/// 网络栈配置查询结果DTO(用于仓储层查询)
/// </summary>
public class NetworkStackConfigWithBindingNamesDto
{
/// <summary>
/// 网络栈配置ID
/// </summary>
public string NetworkStackConfigId { get; set; } = null!;
/// <summary>
/// 网络栈名称
/// </summary>
public string NetworkStackName { get; set; } = null!;
/// <summary>
/// 网络栈编码
/// </summary>
public string NetworkStackCode { get; set; } = null!;
/// <summary>
/// RAN配置ID
/// </summary>
public string? RanId { get; set; }
/// <summary>
/// RAN配置名称
/// </summary>
public string? RanName { get; set; }
/// <summary>
/// 描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 是否激活
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// 创建者
/// </summary>
public string CreatedBy { get; set; } = null!;
/// <summary>
/// 更新者
/// </summary>
public string? UpdatedBy { get; set; }
/// <summary>
/// 绑定关系ID
/// </summary>
public string? StackCoreIMSBindingId { get; set; }
/// <summary>
/// 索引
/// </summary>
public int? Index { get; set; }
/// <summary>
/// 核心网配置ID
/// </summary>
public string? CoreNetworkConfigId { get; set; }
/// <summary>
/// 核心网配置名称
/// </summary>
public string? CoreNetworkConfigName { get; set; }
/// <summary>
/// IMS配置ID
/// </summary>
public string? IMSConfigId { get; set; }
/// <summary>
/// IMS配置名称
/// </summary>
public string? IMSConfigName { get; set; }
}
}

15
src/X1.Domain/Repositories/Base/IQueryRepository.cs

@ -144,4 +144,19 @@ public interface IQueryRepository<T> where T : class
/// 返回的结果会被映射到实体类型T
/// </remarks>
Task<IEnumerable<T>> ExecuteSqlQueryAsync(string sql, object[] parameters, CancellationToken cancellationToken = default);
/// <summary>
/// 执行SQL查询并映射到自定义类型
/// </summary>
/// <typeparam name="TResult">结果类型</typeparam>
/// <param name="sql">SQL查询语句</param>
/// <param name="parameters">SQL参数</param>
/// <param name="cancellationToken">取消令牌,用于取消异步操作</param>
/// <returns>查询结果集合</returns>
/// <remarks>
/// 这是一个异步操作,用于执行自定义SQL查询并映射到任意类型
/// 支持参数化查询,防止SQL注入
/// 返回的结果会被映射到指定的结果类型
/// </remarks>
Task<IEnumerable<TResult>> ExecuteSqlQueryAsync<TResult>(string sql, object[] parameters, CancellationToken cancellationToken = default);
}

17
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<NetworkStackCon
int pageNumber = 1,
int pageSize = 10,
CancellationToken cancellationToken = default);
/// <summary>
/// 根据ID获取网络栈配置(包含绑定关系和关联配置名称)
/// </summary>
Task<IList<NetworkStackConfigWithBindingNamesDto>> GetNetworkStackConfigByIdWithBindingNamesAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// 搜索网络栈配置(包含绑定关系和关联配置名称,分页)
/// </summary>
Task<(int TotalCount, IList<NetworkStackConfigWithBindingNamesDto> Items)> SearchNetworkStackConfigsWithBindingNamesAsync(
string? networkStackName = null,
string? ranId = null,
bool? isActive = null,
int pageNumber = 1,
int pageSize = 10,
CancellationToken cancellationToken = default);
}

8
src/X1.Infrastructure/Repositories/Base/BaseRepository.cs

@ -180,5 +180,13 @@ public abstract class BaseRepository<T> : IBaseRepository<T> where T : class
return await QueryRepository.ExecuteSqlQueryAsync(sql, parameters, cancellationToken);
}
/// <summary>
/// 执行SQL查询并映射到自定义类型
/// </summary>
public virtual async Task<IEnumerable<TResult>> ExecuteSqlQueryAsync<TResult>(string sql, object[] parameters, CancellationToken cancellationToken = default)
{
return await QueryRepository.ExecuteSqlQueryAsync<TResult>(sql, parameters, cancellationToken);
}
#endregion
}

9
src/X1.Infrastructure/Repositories/CQRS/QueryRepository.cs

@ -147,4 +147,13 @@ public class QueryRepository<T> : IQueryRepository<T> where T : class
_logger.LogInformation("执行SQL查询: {Sql}", sql);
return await _dbSet.FromSqlRaw(sql, parameters).ToListAsync(cancellationToken);
}
/// <summary>
/// 执行SQL查询并映射到自定义类型
/// </summary>
public async Task<IEnumerable<TResult>> ExecuteSqlQueryAsync<TResult>(string sql, object[] parameters, CancellationToken cancellationToken = default)
{
_logger.LogInformation("执行SQL查询并映射到类型 {ResultType}: {Sql}", typeof(TResult).Name, sql);
return await _context.Database.SqlQueryRaw<TResult>(sql, parameters).ToListAsync(cancellationToken);
}
}

129
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<NetworkStackConfig>,
return (result.TotalCount, result.Items.OrderBy(x => x.NetworkStackName).ToList());
}
/// <summary>
/// 根据ID获取网络栈配置(包含绑定关系和关联配置名称)
/// </summary>
public async Task<IList<NetworkStackConfigWithBindingNamesDto>> 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<NetworkStackConfigWithBindingNamesDto>(sql, parameters, cancellationToken);
return results.ToList();
}
/// <summary>
/// 搜索网络栈配置(包含绑定关系和关联配置名称,分页)
/// </summary>
public async Task<(int TotalCount, IList<NetworkStackConfigWithBindingNamesDto> 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<string>();
var parameters = new List<object>();
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<int>(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<NetworkStackConfigWithBindingNamesDto>(sql, parameters.ToArray(), cancellationToken);
return (count, results.ToList());
}
}

2
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',

6
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}
</div>
);
case 'serialNumber':
return <span className="font-mono text-sm">{device.serialNumber}</span>;
case 'deviceCode':
return <span className="font-mono text-sm">{device.deviceCode}</span>;
case 'description':
return (
<div className="max-w-xs truncate text-gray-600" title={device.description || ''}>

6
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: 是否启用

36
src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx

@ -73,10 +73,40 @@ export default function NetworkStackConfigsTable({
{config.networkStackName}
</div>
);
case 'ranId':
case 'networkStackCode':
return (
<div className="max-w-xs truncate text-gray-600" title={config.ranId || ''}>
{config.ranId || '-'}
<div className="max-w-xs truncate font-mono text-sm" title={config.networkStackCode}>
{config.networkStackCode}
</div>
);
case 'ranName':
return (
<div className="max-w-xs truncate text-gray-600" title={config.ranName || ''}>
{config.ranName || '-'}
</div>
);
case 'coreNetworkConfigNames':
const coreNetworkNames = config.stackCoreIMSBindings
?.map(binding => binding.coreNetworkConfigName)
.filter(name => name) || [];
const coreNetworkNamesText = coreNetworkNames.length > 0
? coreNetworkNames.join(', ')
: '-';
return (
<div className="max-w-xs truncate text-gray-600" title={coreNetworkNamesText}>
{coreNetworkNamesText}
</div>
);
case 'imsConfigNames':
const imsNames = config.stackCoreIMSBindings
?.map(binding => binding.imsConfigName)
.filter(name => name) || [];
const imsNamesText = imsNames.length > 0
? imsNames.join(', ')
: '-';
return (
<div className="max-w-xs truncate text-gray-600" title={imsNamesText}>
{imsNamesText}
</div>
);
case 'description':

5
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 },

3
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;

3
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;
}

552
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地址自动获取
@ -2961,3 +2818,406 @@ chore: 更新.gitignore忽略日志文件
- 编辑设备时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接口定义
### 业务价值
- **用户体验**:用户无需手动输入序列号,减少输入错误
- **数据准确性**:序列号直接从设备获取,确保数据准确性
- **自动化**:简化设备创建流程,提高效率
- **一致性**:确保序列号与设备实际状态一致
### 影响范围
- **创建操作**:设备创建时自动获取序列号
- **更新操作**:设备更新时保持序列号不变
- **前端界面**:移除序列号输入字段,添加设备编码字段
- **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<TResult>` 方法:支持执行 SQL 查询并映射到自定义类型
- 移除泛型约束 `where TResult : class`,支持值类型(如 int)
- 保持原有 `ExecuteSqlQueryAsync` 方法的兼容性
#### 2. 实现类更新
- **QueryRepository 实现**
- 实现 `ExecuteSqlQueryAsync<TResult>` 方法
- 移除泛型约束,支持值类型查询结果
- 根据类型动态选择查询方法:
- 值类型:使用 `Database.SqlQueryRaw<TResult>`
- 引用类型:使用 `Set<TResult>().FromSqlRaw`
- 添加结构化日志记录
- **BaseRepository 实现**
- 添加 `ExecuteSqlQueryAsync<TResult>` 方法的虚方法实现
- 移除泛型约束,支持值类型查询结果
- 委托给 QueryRepository 执行
#### 3. 仓储方法重构
- **NetworkStackConfigRepository**
- 移除对 `QueryRepository.GetDbContext()` 的直接调用
- 使用 `ExecuteSqlQueryAsync<NetworkStackConfigWithBindingNamesDto>` 方法
- 优化参数传递方式,使用对象数组而不是字典
- **修复 PostgreSQL 语法**
- 表名和列名使用双引号包围:`"NetworkStackConfigs"`
- 分页语法改为 `LIMIT @pageSize OFFSET @offset`
- 移除 SQL Server 特有的 `[Index]` 语法,改为 `"Index"`
#### 4. 参数处理优化
- **参数传递方式**
- 从 `Dictionary<string, object>` 改为 `object[]`
- 使用匿名对象传递参数,提高类型安全性
- 简化参数构建逻辑
- **修复 PostgreSQL 参数语法**
- 使用 `@p0`, `@p1` 等参数占位符
- 直接传递参数值而不是匿名对象
- 动态构建参数索引,确保参数顺序正确
### 技术特性
- **类型安全**:使用泛型方法确保查询结果的类型安全
- **接口一致性**:遵循仓储模式的设计原则
- **参数化查询**:防止 SQL 注入攻击
- **日志记录**:添加结构化日志,便于调试和监控
### 代码质量改进
- **可维护性**:使用标准接口方法,减少对具体实现的依赖
- **可测试性**:通过接口抽象,便于单元测试
- **一致性**:与其他仓储方法保持一致的调用方式
- **错误处理**:统一的异常处理机制
### 性能优化
- **查询效率**:保持原有的 SQL JOIN 查询性能优势
- **内存使用**:优化参数传递,减少内存分配
- **数据库连接**:通过仓储层统一管理数据库连接
### 影响范围
- **向后兼容**:保持原有接口的兼容性
- **功能完整性**:所有原有功能保持不变
- **性能表现**:查询性能保持一致或略有提升
- **代码质量**:提高代码的可维护性和可测试性
Loading…
Cancel
Save