From 93cf0651652decd027869f820257fcc88281d01f Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Tue, 21 Jul 2026 17:48:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=85=83=E6=95=B0=E6=8D=AE=E8=A1=A8=E4=B8=8E?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=A4=87=E6=B3=A8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持字典类型备注维护及左侧类型列表查询,创建字典项时自动补齐类型元数据。 Co-authored-by: Cursor --- db.sql | 16 ++ sql/migrate_t_dict_type.sql | 23 +++ .../Dtos/System/DictTypeOutput.cs | 32 ++++ .../Dtos/System/DictTypeSaveInput.cs | 32 ++++ .../Services/System/DictService.cs | 156 ++++++++++++++++++ .../SystemAggregate/Entities/DictTypeMeta.cs | 27 +++ .../Repositories/IDictRepository.cs | 2 + .../Repositories/IDictTypeRepository.cs | 9 + src/Oms.SqlSugarCore/System/DictRepository.cs | 16 ++ .../System/DictTypeRepository.cs | 14 ++ 10 files changed, 327 insertions(+) create mode 100644 sql/migrate_t_dict_type.sql create mode 100644 src/Oms.Application/Dtos/System/DictTypeOutput.cs create mode 100644 src/Oms.Application/Dtos/System/DictTypeSaveInput.cs create mode 100644 src/Oms.Domain/SystemAggregate/Entities/DictTypeMeta.cs create mode 100644 src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs create mode 100644 src/Oms.SqlSugarCore/System/DictTypeRepository.cs diff --git a/db.sql b/db.sql index 32e2179..9df5e21 100644 --- a/db.sql +++ b/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', diff --git a/sql/migrate_t_dict_type.sql b/sql/migrate_t_dict_type.sql new file mode 100644 index 0000000..c3b2e3e --- /dev/null +++ b/sql/migrate_t_dict_type.sql @@ -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`) <> ''; diff --git a/src/Oms.Application/Dtos/System/DictTypeOutput.cs b/src/Oms.Application/Dtos/System/DictTypeOutput.cs new file mode 100644 index 0000000..d327e2b --- /dev/null +++ b/src/Oms.Application/Dtos/System/DictTypeOutput.cs @@ -0,0 +1,32 @@ +namespace Oms.Application.Dtos.System; + +/// +/// 字典类型列表项。 +/// +public class DictTypeOutput +{ + /// + /// 元数据主键;仅存在于 t_dict_type 时有值。 + /// + public int? Id { get; set; } + + /// + /// 字典类型编码。 + /// + public string DictType { get; set; } = string.Empty; + + /// + /// 字典类型备注。 + /// + public string? Remark { get; set; } + + /// + /// 左侧菜单排序。 + /// + public int TypeSort { get; set; } + + /// + /// 该类型下字典项数量。 + /// + public int ItemCount { get; set; } +} diff --git a/src/Oms.Application/Dtos/System/DictTypeSaveInput.cs b/src/Oms.Application/Dtos/System/DictTypeSaveInput.cs new file mode 100644 index 0000000..35f44b7 --- /dev/null +++ b/src/Oms.Application/Dtos/System/DictTypeSaveInput.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Oms.Application.Dtos.System; + +/// +/// 保存字典类型元数据入参(新增或更新备注)。 +/// +public class DictTypeSaveInput +{ + /// + /// 元数据主键;更新时传入。 + /// + public int? Id { get; set; } + + /// + /// 字典类型编码。 + /// + [Required] + [StringLength(64)] + public string DictType { get; set; } = string.Empty; + + /// + /// 字典类型备注。 + /// + [StringLength(512)] + public string? Remark { get; set; } + + /// + /// 左侧菜单排序。 + /// + public int TypeSort { get; set; } +} diff --git a/src/Oms.Application/Services/System/DictService.cs b/src/Oms.Application/Services/System/DictService.cs index d1bad20..a8f824f 100644 --- a/src/Oms.Application/Services/System/DictService.cs +++ b/src/Oms.Application/Services/System/DictService.cs @@ -17,6 +17,8 @@ public class DictService : ApplicationService { private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService(); + private IDictTypeRepository DictTypeRepository => LazyServiceProvider.GetRequiredService(); + /// /// 查询字典列表。 /// @@ -42,6 +44,138 @@ public class DictService : ApplicationService }; } + /// + /// 查询字典类型列表(含备注与项数)。 + /// + /// 可选:按类型编码或备注模糊搜索。 + /// 字典类型列表。 + [HttpGet("/system/dict/type/list")] + public async Task> 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(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 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(); + } + + /// + /// 保存字典类型元数据(新增或更新备注)。 + /// + /// 入参:字典类型元数据。 + /// 无返回体。 + [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); + } + + /// + /// 删除字典类型元数据(不删除字典项)。 + /// + /// 入参:目标主键。 + /// 无返回体。 + [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); + } + /// /// 新增字典。 /// @@ -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 + }); + } } diff --git a/src/Oms.Domain/SystemAggregate/Entities/DictTypeMeta.cs b/src/Oms.Domain/SystemAggregate/Entities/DictTypeMeta.cs new file mode 100644 index 0000000..2fcf84f --- /dev/null +++ b/src/Oms.Domain/SystemAggregate/Entities/DictTypeMeta.cs @@ -0,0 +1,27 @@ +using Oms.Domain.Shared; +using SqlSugar; + +namespace Oms.Domain.SystemAggregate.Entities; + +/// +/// 字典类型元数据(类型编码 + 备注)。 +/// +[SugarTable("t_dict_type")] +public class DictTypeMeta : OmsAuditedAggregateRoot +{ + /// + /// 字典类型编码,与 t_dict.DictType 对应。 + /// + public string DictType { get; set; } = string.Empty; + + /// + /// 字典类型备注说明。 + /// + [SugarColumn(IsNullable = true, Length = 512)] + public string? Remark { get; set; } + + /// + /// 左侧菜单排序,值越小越靠前。 + /// + public int TypeSort { get; set; } +} diff --git a/src/Oms.Domain/SystemAggregate/Repositories/IDictRepository.cs b/src/Oms.Domain/SystemAggregate/Repositories/IDictRepository.cs index 9384d44..4f9a6fa 100644 --- a/src/Oms.Domain/SystemAggregate/Repositories/IDictRepository.cs +++ b/src/Oms.Domain/SystemAggregate/Repositories/IDictRepository.cs @@ -6,4 +6,6 @@ namespace Oms.Domain.SystemAggregate.Repositories; public interface IDictRepository : IRepository { Task GetByTypeAndCodeAsync(string dictType, string dictCode, CancellationToken cancellationToken = default); + + Task> GetItemCountByTypeAsync(CancellationToken cancellationToken = default); } diff --git a/src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs b/src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs new file mode 100644 index 0000000..fb0e07f --- /dev/null +++ b/src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs @@ -0,0 +1,9 @@ +using Fake.Domain.Repositories; +using Oms.Domain.SystemAggregate.Entities; + +namespace Oms.Domain.SystemAggregate.Repositories; + +public interface IDictTypeRepository : IRepository +{ + Task GetByDictTypeAsync(string dictType, CancellationToken cancellationToken = default); +} diff --git a/src/Oms.SqlSugarCore/System/DictRepository.cs b/src/Oms.SqlSugarCore/System/DictRepository.cs index fd69ef8..8b0e4ba 100644 --- a/src/Oms.SqlSugarCore/System/DictRepository.cs +++ b/src/Oms.SqlSugarCore/System/DictRepository.cs @@ -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, IDictRepository, ISco { return FirstOrDefaultAsync(x => x.DictType == dictType && x.DictCode == dictCode, cancellationToken); } + + public async Task> GetItemCountByTypeAsync(CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + var rows = await db.Queryable() + .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(); + } } diff --git a/src/Oms.SqlSugarCore/System/DictTypeRepository.cs b/src/Oms.SqlSugarCore/System/DictTypeRepository.cs new file mode 100644 index 0000000..b52693e --- /dev/null +++ b/src/Oms.SqlSugarCore/System/DictTypeRepository.cs @@ -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, IDictTypeRepository, IScopedDependency +{ + public Task GetByDictTypeAsync(string dictType, CancellationToken cancellationToken = default) + { + return FirstOrDefaultAsync(x => x.DictType == dictType, cancellationToken); + } +}