feat: 新增字典类型元数据表与类型备注 API
支持字典类型备注维护及左侧类型列表查询,创建字典项时自动补齐类型元数据。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
dcf856568e
commit
93cf065165
16
db.sql
16
db.sql
@ -57,6 +57,7 @@ DROP TABLE IF EXISTS `t_system_other_config`;
|
||||
DROP TABLE IF EXISTS `t_warehouse_address`;
|
||||
DROP TABLE IF EXISTS `t_notice_read`;
|
||||
DROP TABLE IF EXISTS `t_notice`;
|
||||
DROP TABLE IF EXISTS `t_dict_type`;
|
||||
DROP TABLE IF EXISTS `t_dict`;
|
||||
DROP TABLE IF EXISTS `t_country`;
|
||||
DROP TABLE IF EXISTS `t_recharge_record`;
|
||||
@ -1032,6 +1033,21 @@ CREATE TABLE `t_dict` (
|
||||
KEY `idx_dict_type_sort` (`DictType`, `DictSort`, `Id`)
|
||||
) COMMENT='系统字典表';
|
||||
|
||||
CREATE TABLE `t_dict_type` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
||||
`DictType` VARCHAR(64) NOT NULL COMMENT '字典类型编码',
|
||||
`Remark` VARCHAR(512) NULL COMMENT '字典类型备注',
|
||||
`TypeSort` INT NOT NULL DEFAULT 0 COMMENT '左侧菜单排序',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_dict_type_code` (`DictType`)
|
||||
) COMMENT='字典类型元数据表';
|
||||
|
||||
CREATE TABLE `t_system_other_config` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
||||
`ConfigCode` VARCHAR(64) NOT NULL COMMENT '配置编码,如 track_service',
|
||||
|
||||
23
sql/migrate_t_dict_type.sql
Normal file
23
sql/migrate_t_dict_type.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- 已有库增量:字典类型元数据 t_dict_type(类型编码 + 备注)
|
||||
-- 可重复执行(IF NOT EXISTS)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_dict_type` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
||||
`DictType` VARCHAR(64) NOT NULL COMMENT '字典类型编码',
|
||||
`Remark` VARCHAR(512) NULL COMMENT '字典类型备注',
|
||||
`TypeSort` INT NOT NULL DEFAULT 0 COMMENT '左侧菜单排序',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_dict_type_code` (`DictType`)
|
||||
) COMMENT='字典类型元数据表';
|
||||
|
||||
-- 从现有字典项回填类型元数据(无备注)
|
||||
INSERT IGNORE INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `CreateUserId`)
|
||||
SELECT DISTINCT `DictType`, NULL, 0, 0
|
||||
FROM `t_dict`
|
||||
WHERE `IsDeleted` = 0 AND TRIM(`DictType`) <> '';
|
||||
32
src/Oms.Application/Dtos/System/DictTypeOutput.cs
Normal file
32
src/Oms.Application/Dtos/System/DictTypeOutput.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型列表项。
|
||||
/// </summary>
|
||||
public class DictTypeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 元数据主键;仅存在于 t_dict_type 时有值。
|
||||
/// </summary>
|
||||
public int? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型编码。
|
||||
/// </summary>
|
||||
public string DictType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 左侧菜单排序。
|
||||
/// </summary>
|
||||
public int TypeSort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 该类型下字典项数量。
|
||||
/// </summary>
|
||||
public int ItemCount { get; set; }
|
||||
}
|
||||
32
src/Oms.Application/Dtos/System/DictTypeSaveInput.cs
Normal file
32
src/Oms.Application/Dtos/System/DictTypeSaveInput.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 保存字典类型元数据入参(新增或更新备注)。
|
||||
/// </summary>
|
||||
public class DictTypeSaveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 元数据主键;更新时传入。
|
||||
/// </summary>
|
||||
public int? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型编码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string DictType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型备注。
|
||||
/// </summary>
|
||||
[StringLength(512)]
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 左侧菜单排序。
|
||||
/// </summary>
|
||||
public int TypeSort { get; set; }
|
||||
}
|
||||
@ -17,6 +17,8 @@ public class DictService : ApplicationService
|
||||
{
|
||||
private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService<IDictRepository>();
|
||||
|
||||
private IDictTypeRepository DictTypeRepository => LazyServiceProvider.GetRequiredService<IDictTypeRepository>();
|
||||
|
||||
/// <summary>
|
||||
/// 查询字典列表。
|
||||
/// </summary>
|
||||
@ -42,6 +44,138 @@ public class DictService : ApplicationService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询字典类型列表(含备注与项数)。
|
||||
/// </summary>
|
||||
/// <param name="keyword">可选:按类型编码或备注模糊搜索。</param>
|
||||
/// <returns>字典类型列表。</returns>
|
||||
[HttpGet("/system/dict/type/list")]
|
||||
public async Task<List<DictTypeOutput>> GetTypeListAsync([FromQuery] string? keyword)
|
||||
{
|
||||
var kw = keyword?.Trim();
|
||||
var metaRows = await DictTypeRepository.GetListAsync();
|
||||
var countRows = await DictRepository.GetItemCountByTypeAsync();
|
||||
var countMap = countRows.ToDictionary(x => x.DictType, x => x.ItemCount, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var merged = new Dictionary<string, DictTypeOutput>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var meta in metaRows)
|
||||
{
|
||||
var type = meta.DictType.Trim();
|
||||
if (string.IsNullOrWhiteSpace(type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
merged[type] = new DictTypeOutput
|
||||
{
|
||||
Id = meta.Id,
|
||||
DictType = type,
|
||||
Remark = meta.Remark,
|
||||
TypeSort = meta.TypeSort,
|
||||
ItemCount = countMap.TryGetValue(type, out var count) ? count : 0
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var (dictType, itemCount) in countRows)
|
||||
{
|
||||
var type = dictType.Trim();
|
||||
if (string.IsNullOrWhiteSpace(type) || merged.ContainsKey(type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
merged[type] = new DictTypeOutput
|
||||
{
|
||||
DictType = type,
|
||||
ItemCount = itemCount
|
||||
};
|
||||
}
|
||||
|
||||
IEnumerable<DictTypeOutput> result = merged.Values;
|
||||
if (!string.IsNullOrWhiteSpace(kw))
|
||||
{
|
||||
result = result.Where(x =>
|
||||
x.DictType.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||
(!string.IsNullOrWhiteSpace(x.Remark) && x.Remark.Contains(kw, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
return result
|
||||
.OrderBy(x => x.TypeSort)
|
||||
.ThenBy(x => x.DictType, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存字典类型元数据(新增或更新备注)。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:字典类型元数据。</param>
|
||||
/// <returns>无返回体。</returns>
|
||||
[HttpPost("/system/dict/type/save")]
|
||||
public async Task SaveTypeAsync(DictTypeSaveInput input)
|
||||
{
|
||||
var dictType = input.DictType?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(dictType))
|
||||
{
|
||||
throw new BusinessException("字典类型不能为空");
|
||||
}
|
||||
|
||||
var remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
|
||||
DictTypeMeta? entity = null;
|
||||
if (input.Id.HasValue && input.Id.Value > 0)
|
||||
{
|
||||
entity = await DictTypeRepository.FirstOrDefaultAsync(x => x.Id == input.Id.Value);
|
||||
}
|
||||
|
||||
entity ??= await DictTypeRepository.GetByDictTypeAsync(dictType);
|
||||
if (entity == null)
|
||||
{
|
||||
var existedType = await DictTypeRepository.GetByDictTypeAsync(dictType);
|
||||
if (existedType != null)
|
||||
{
|
||||
throw new BusinessException("字典类型已存在");
|
||||
}
|
||||
|
||||
await DictTypeRepository.InsertAsync(new DictTypeMeta
|
||||
{
|
||||
DictType = dictType,
|
||||
Remark = remark,
|
||||
TypeSort = input.TypeSort
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(entity.DictType, dictType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var duplicate = await DictTypeRepository.GetByDictTypeAsync(dictType);
|
||||
if (duplicate != null && duplicate.Id != entity.Id)
|
||||
{
|
||||
throw new BusinessException("字典类型已存在");
|
||||
}
|
||||
}
|
||||
|
||||
entity.DictType = dictType;
|
||||
entity.Remark = remark;
|
||||
entity.TypeSort = input.TypeSort;
|
||||
await DictTypeRepository.UpdateAsync(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典类型元数据(不删除字典项)。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:目标主键。</param>
|
||||
/// <returns>无返回体。</returns>
|
||||
[HttpPost("/system/dict/type/delete")]
|
||||
public async Task DeleteTypeAsync(IdInput input)
|
||||
{
|
||||
var entity = await DictTypeRepository.FirstOrDefaultAsync(x => x.Id == input.Id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException("字典类型不存在");
|
||||
}
|
||||
|
||||
await DictTypeRepository.DeleteAsync(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增字典。
|
||||
/// </summary>
|
||||
@ -51,6 +185,7 @@ public class DictService : ApplicationService
|
||||
public async Task CreateAsync(DictCreateInput input)
|
||||
{
|
||||
await ValidateDictInputAsync(input.DictType, input.DictCode, input.DictName, null);
|
||||
await EnsureDictTypeMetaAsync(input.DictType.Trim());
|
||||
await DictRepository.InsertAsync(new Dict
|
||||
{
|
||||
DictType = input.DictType.Trim(),
|
||||
@ -70,6 +205,7 @@ public class DictService : ApplicationService
|
||||
{
|
||||
var entity = await GetDictOrThrowAsync(input.Id);
|
||||
await ValidateDictInputAsync(input.DictType, input.DictCode, input.DictName, entity.Id);
|
||||
await EnsureDictTypeMetaAsync(input.DictType.Trim());
|
||||
entity.DictType = input.DictType.Trim();
|
||||
entity.DictSort = input.DictSort;
|
||||
entity.DictCode = input.DictCode.Trim();
|
||||
@ -148,4 +284,24 @@ public class DictService : ApplicationService
|
||||
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task EnsureDictTypeMetaAsync(string dictType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dictType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existed = await DictTypeRepository.GetByDictTypeAsync(dictType);
|
||||
if (existed != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DictTypeRepository.InsertAsync(new DictTypeMeta
|
||||
{
|
||||
DictType = dictType,
|
||||
TypeSort = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
27
src/Oms.Domain/SystemAggregate/Entities/DictTypeMeta.cs
Normal file
27
src/Oms.Domain/SystemAggregate/Entities/DictTypeMeta.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.SystemAggregate.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型元数据(类型编码 + 备注)。
|
||||
/// </summary>
|
||||
[SugarTable("t_dict_type")]
|
||||
public class DictTypeMeta : OmsAuditedAggregateRoot
|
||||
{
|
||||
/// <summary>
|
||||
/// 字典类型编码,与 t_dict.DictType 对应。
|
||||
/// </summary>
|
||||
public string DictType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型备注说明。
|
||||
/// </summary>
|
||||
[SugarColumn(IsNullable = true, Length = 512)]
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 左侧菜单排序,值越小越靠前。
|
||||
/// </summary>
|
||||
public int TypeSort { get; set; }
|
||||
}
|
||||
@ -6,4 +6,6 @@ namespace Oms.Domain.SystemAggregate.Repositories;
|
||||
public interface IDictRepository : IRepository<Dict>
|
||||
{
|
||||
Task<Dict?> GetByTypeAndCodeAsync(string dictType, string dictCode, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<(string DictType, int ItemCount)>> GetItemCountByTypeAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
using Fake.Domain.Repositories;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
|
||||
namespace Oms.Domain.SystemAggregate.Repositories;
|
||||
|
||||
public interface IDictTypeRepository : IRepository<DictTypeMeta>
|
||||
{
|
||||
Task<DictTypeMeta?> GetByDictTypeAsync(string dictType, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -2,6 +2,7 @@ using Fake.DependencyInjection;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
using Oms.SqlSugarCore.Repositories;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.SqlSugarCore.System;
|
||||
|
||||
@ -11,4 +12,19 @@ public class DictRepository : OmsSqlSugarRepository<Dict>, IDictRepository, ISco
|
||||
{
|
||||
return FirstOrDefaultAsync(x => x.DictType == dictType && x.DictCode == dictCode, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<(string DictType, int ItemCount)>> GetItemCountByTypeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
var rows = await db.Queryable<Dict>()
|
||||
.GroupBy(x => x.DictType)
|
||||
.Select(x => new
|
||||
{
|
||||
DictType = x.DictType,
|
||||
ItemCount = SqlFunc.AggregateCount(x.Id)
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return rows.Select(x => (x.DictType, x.ItemCount)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
14
src/Oms.SqlSugarCore/System/DictTypeRepository.cs
Normal file
14
src/Oms.SqlSugarCore/System/DictTypeRepository.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
using Oms.SqlSugarCore.Repositories;
|
||||
|
||||
namespace Oms.SqlSugarCore.System;
|
||||
|
||||
public class DictTypeRepository : OmsSqlSugarRepository<DictTypeMeta>, IDictTypeRepository, IScopedDependency
|
||||
{
|
||||
public Task<DictTypeMeta?> GetByDictTypeAsync(string dictType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return FirstOrDefaultAsync(x => x.DictType == dictType, cancellationToken);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user