13. 数据字典与常量
字典 = 运行时可变的枚举(存在库里,后台可维护);常量 = 编译期固定的取值(写在类里,前端直接下拉)。两者都带缓存,都给前端提供下拉数据源。
1. 字典表结构
SysDictType(字典类型,主库 [SysTable]) SysDictData(字典值,主库 [SysTable])
├─ Id ├─ Id
├─ Name 名称 ├─ DictTypeId → SysDictType.Id
├─ Code 编码 ★前端按它取 ├─ DictType [Navigate OneToOne]
├─ OrderNo 排序 ├─ Label 显示文本 ★
├─ Remark 备注 ├─ Value 值 ★(存库的值)
├─ Status 状态(StatusEnum) ├─ Code 编码
├─ SysFlag 是否内置(Y=仅超管可改) ├─ TagType 标签颜色(success/info/warning…)
└─ IsEnum 是否枚举自动生成的字典 ├─ StyleSetting / ClassSetting 样式
├─ OrderNo / Remark
├─ ExtData 拓展数据(大文本)
├─ Status 状态
└─ IsEnum 是否枚举转换字典缓存 Key:CacheConst.KeyDict = "sys_dict:" → sys_dict:{typeCode},存 List<SysDictData>。
2. SysDictTypeService(字典类型)
| 方法 | 特性 | 路由 |
|---|---|---|
Page(PageDictTypeInput) | [DisplayName] | POST /api/sysDictType/page |
GetList() | [DisplayName] | POST /api/sysDictType/getList |
GetDataList([FromQuery] GetDataDictTypeInput) | [DisplayName] | POST /api/sysDictType/getDataList |
AddDictType(AddDictTypeInput) | Add + HttpPost | POST /api/sysDictType/add |
UpdateDictType(UpdateDictTypeInput) | Update + HttpPost + [UnitOfWork] | POST /api/sysDictType/update |
DeleteDictType(DeleteDictTypeInput) | Delete + HttpPost + [UnitOfWork] | POST /api/sysDictType/delete |
GetDetail([FromQuery] DictTypeInput) | [DisplayName] | POST /api/sysDictType/getDetail |
SetStatus(DictTypeInput) | [DisplayName] + [UnitOfWork] | POST /api/sysDictType/setStatus |
GetAllDictList() | [DisplayName] | POST /api/sysDictType/getAllDictList |
// 禁止手建以 enum 结尾的编码(框架给枚举字典保留)
if (input.Code.ToLower().EndsWith("enum")) throw Oops.Oh(ErrorCodeEnum.D3006);
// SysFlag=Y 的字典只有超管能改/删
if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008);一次性拿全部字典(前端初始化常用):
[DisplayName("获取所有字典集合")]
public async Task<dynamic> GetAllDictList()
{
var ds = await _sysDictTypeRep.AsQueryable()
.InnerJoin<SysDictData>((u, a) => u.Id == a.DictTypeId).ClearFilter<ITenantIdFilter>()
.Where((u, a) => u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable)
.Select((u, a) => new { TypeCode = u.Code, a.Label, a.Value, a.Code, a.Remark, a.OrderNo, a.TagType, a.ExtData })
.ToListAsync();
return ds.OrderBy(u => u.OrderNo).GroupBy(u => u.TypeCode).ToDictionary(u => u.Key, u => u);
}动态 API 默认动词是 POST,表中未标
[HttpGet]的方法一律按 POST 调用;需要 GET 请自行给方法加[HttpGet]。
3. SysDictDataService(字典值)
| 方法 | 特性 | 路由 |
|---|---|---|
Page(PageDictDataInput) | [DisplayName] | POST /api/sysDictData/page |
GetList([FromQuery] GetDataDictDataInput) | [DisplayName] | POST /api/sysDictData/getList |
AddDictData(AddDictDataInput) | Add + HttpPost | POST /api/sysDictData/add |
UpdateDictData(UpdateDictDataInput) | Update + HttpPost + [UnitOfWork] | POST /api/sysDictData/update |
DeleteDictData(DeleteDictDataInput) | Delete + HttpPost + [UnitOfWork] | POST /api/sysDictData/delete |
GetDetail([FromQuery] DictDataInput) | [DisplayName] | POST /api/sysDictData/getDetail |
SetStatus(DictDataInput) | [DisplayName] + [UnitOfWork] | POST /api/sysDictData/setStatus |
GetDataList(string code) | [DisplayName] | ★ 按类型编码取值,业务代码用这个 |
GetDictDataListByDictTypeId(long) | [NonAction] | 按类型 Id 取值 |
GetDataListByIdOrCode(long?, string) | [NonAction] | 二选一(都传或都不传报 D3011) |
GetDataList([FromQuery] QueryDictDataInput) | [DisplayName] | 带状态过滤 |
DeleteDictData(long dictTypeId) | [NonAction] | 按类型 Id 批量删 |
4. 字典缓存与刷新
// 读取:先缓存后库(真实代码 SysDictDataService.GetDataListByIdOrCode)
var dictDataList = _sysCacheService.Get<List<SysDictData>>($"{CacheConst.KeyDict}{code}");
if (dictDataList == null)
{
dictDataList = await _sysDictDataRep.Change<SysDictType>().AsQueryable()
.LeftJoin<SysDictData>((u, a) => u.Id == a.DictTypeId).ClearFilter<ITenantIdFilter>()
.WhereIF(!string.IsNullOrWhiteSpace(code), u => u.Code == code)
.WhereIF(typeId != null && typeId > 0, u => u.Id == typeId)
.Where((u, a) => u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable)
.OrderBy((u, a) => new { a.OrderNo, a.Code })
.Select((u, a) => a).ToListAsync();
_sysCacheService.Set($"{CacheConst.KeyDict}{code}", dictDataList);
}
return dictDataList;任何增删改字典值/类型/状态,都必须先删缓存再写库:
_sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");服务里已经在 AddDictData/UpdateDictData/DeleteDictData/SetStatus/UpdateDictType/SetStatus/DeleteDictData(long) 全部处理好了;自己直接改表时必须手动清。
手动刷新示例:
_sysCacheService.Remove($"{CacheConst.KeyDict}leave_type");
// 下次调用 GetDataList("leave_type") 会重新查库并回填缓存5. [Dict] 校验特性
// Admin.NET.Core/Attribute/DictAttribute.cs
[Dict("leave_type")] // 单选
public string LeaveType { get; set; }
[Dict("biz_tag", multiple: true)] // 多选:逗号分隔字符串或 IList
public string Tags { get; set; }
[Dict("biz_tag", multiple: true, errorMessage = "标签不在允许范围内")]
public List<string> TagList { get; set; }行为:
| 场景 | 结果 |
|---|---|
| 值为 null / 空串 / 空集合 | 直接放行(不校验) |
| 属性是枚举类型 | 用 Enum.IsDefined 校验,不走字典 |
| 单选 | 值必须命中 dictData.Value 集合(忽略大小写) |
| 多选 | 逗号拆分或逐项校验,任一项不合法即报错 |
| 其他类型 | 报「不支持的字典值类型」 |
错误提示形如:"3" 不在字典【请假类型(leave_type)】允许的取值范围内,请重新选择。
6. 常量:[Const] + SysConstService
// Admin.NET.Core/Const/CommonConst.cs(真实代码)
[Const("平台配置")]
public class CommonConst
{
/// <summary>
/// 系统管理员角色编码
/// </summary>
[Description("系统管理员角色编码")]
public const string SysAdminRole = "sys_admin";
/// <summary>
/// 日志分组名称
/// </summary>
public const string SysLogCategoryName = "System.Logging.LoggingMonitor";
}插件里的写法(Plugins/*/Const/XxxConst.cs):[Const("Ai 相关常量")]、[Const("WorkFlow 相关常量")]、[Const("钉钉相关常量")] …
SysConstService 扫描所有贴了 [Const] 的类型并暴露两个接口:
[ApiDescriptionSettings(Order = 280, Description = "系统常量")]
public class SysConstService : IDynamicApiController, ITransient
{
// POST /api/sysConst/getList —— 取全部常量类(每个类的 Name = [Const] 的值,Code = 类名,Data = 成员列表)
[DisplayName("获取所有常量列表")]
public async Task<List<ConstOutput>> GetList();
// POST /api/sysConst/getData?typeName=CommonConst —— 取某个类的成员
[DisplayName("根据类名获取常量数据")]
public async Task<List<ConstOutput>> GetData([Required] string typeName);
}出参:
public class ConstOutput
{
public string Name { get; set; } // 成员名 或 [Const] 名称
public dynamic Code { get; set; } // 枚举→int,其它→实际值
public string Desc { get; set; } // [Description]
public dynamic Data { get; set; } // GetList 时为该类的成员列表
}缓存 Key:CacheConst.KeyConst = "sys_const:"
sys_const:ALL 全部常量类列表
sys_const:{TYPE_NAME} 某个类的成员列表(类名转大写)自定义常量类(业务层推荐):
// Admin.NET.Application/Const/BusinessModuleConst.cs
namespace Admin.NET.Application;
[Const("业务模块常量")]
public class BusinessModuleConst
{
[Description("请假-草稿")]
public const int LeaveStatusDraft = 0;
[Description("请假-审批中")]
public const int LeaveStatusApproving = 1;
[Description("请假-已通过")]
public const int LeaveStatusApproved = 2;
[Description("请假-已拒绝")]
public const int LeaveStatusRejected = 3;
}7. 前端消费方式
字典(运行时可变)
├─ 全部拉取:POST /api/sysDictType/getAllDictList → { leave_type: [{label, value, tagType, ...}], ... }
├─ 按编码取:POST /api/sysDictData/getDataList?value=leave_type
└─ 建议:登录后拉一次 getAllDictList 存 store,下拉框直接读;后台改字典后刷新页面
常量(编译期固定)
├─ 全部拉取:POST /api/sysConst/getList
├─ 按类名取:POST /api/sysConst/getData?typeName=BusinessModuleConst
└─ 建议:同样登录后拉一次存 store,下拉/枚举文案用 Name/Desc,值用 Code前端约定:
- 字典下拉用
Label(显示)+Value(提交值),TagType直接给标签组件用。 ExtData是 JSON 字符串,前端JSON.parse后取业务扩展配置。- 常量下拉用
Desc(显示)+Code(提交值),枚举常量Code是 int。 - 后端实体字段若贴了
[Dict("xxx")],前端下拉的候选必须与字典编码一致,否则提交会被校验拦截。
8. 步骤清单:新增一个业务字典
- 后台「系统管理 → 字典管理」新增字典类型,填
Name+Code(如leave_type),SysFlag选N - 在该类型下新增字典值,填
Label(显示)+Value(存库值)+OrderNo+TagType - 如需随代码发布,写
SeedData/SysDictTypeSeedData.cs/SysDictDataSeedData.cs(IsEnum = YesNoEnum.N) - 实体/DTO 字段贴
[Dict("leave_type")],提交时自动校验 - 前端用
getAllDictList拉leave_type渲染下拉 - 后台改字典值后务必确认缓存被清(走服务接口会自动清)
⚠️ 注意事项 / 常见坑
- 字典编码不要用
enum结尾:AddDictType会直接拒绝(D3006),这个后缀留给枚举自动生成的字典。 SysFlag = Y的字典只有超管能改删(D3008/D3009/D3010),业务字典建N。- 字典查询带了
ClearFilter<ITenantIdFilter>(),字典是全租户共享的;要租户级字典需自己扩展。 - 只有
Status = Enable的类型和值才会返回,禁用后记得清缓存。 [Dict]只校验入参,数据库里已有的脏数据不会被修正;导入场景配[ImportDict]做文本→值转换。- 直接改
SysDictData表(不走服务)必须手动Remove($"{CacheConst.KeyDict}{code}"),否则页面一直显示旧值。 GetDataListByIdOrCode(typeId, code)两个参数必须且只能传一个,否则抛D3011。- 常量类必须贴
[Const("名称")]才会被SysConstService扫描到;成员加[Description]才有中文说明。 - 常量的
GetData按类名精确匹配(u.Name == typeName),命名空间写错拿不到数据。
小结
字典管"会变的枚举":SysDictType + SysDictData,缓存 sys_dict:{code},校验用 [Dict],前端用 getAllDictList。常量管"不变的取值":[Const("名称")] 常量类 + [Description],SysConstService 扫描暴露,前端用 getList/getData。两者的增删改都要记得清缓存。