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);
+ }
+}