字典模块设计文档
一、概述
字典模块为系统提供统一的字典数据管理能力,核心设计思想是双源字典 + 统一树形结构——将编译时确定的系统枚举字典与运行时动态创建的业务字典无缝融合为一致的树形数据,上层业务无需感知数据来源。
二、核心架构
┌──────────────────────────────────────────────────────────────────────┐
│ DictController (REST) │
│ /tree/system /tree/business /search-all │
└──────────────────────────────┬───────────────────────────────────────┘
│
┌──────────▼──────────┐
│ DictService │
│ (合并 & 去重 & 建树) │
└─────────┬───────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌─────────▼─────────┐ ┌───────▼───────┐ ┌─────────▼─────────┐
│ EnumCollector │ │ DictMapper │ │ DictUtil │
│ (系统枚举扫描器) │ │ (数据库访问层) │ │ (缓存 & 树构建) │
└─────────┬─────────┘ └───────────────┘ └───────────────────┘
│
┌─────────▼─────────┐
│ SystemEnum 枚举 │ ← 约22个枚举类,通过 @DictParent 声明层级
│ (编译时确定) │
└───────────────────┘
三、双源字典设计
3.1 系统字典(System Dictionary)
- 来源:实现
SystemEnum接口的 Java 枚举类 - 特点:编译时确定,不可在运行时增删改
- 存储:Caffeine 缓存(永不过期,启动时一次性构建)
- 层级定义:通过
@DictParent注解声明父子关系
3.2 业务字典(Business Dictionary)
- 来源:数据库表
sys_dict - 特点:运行时动态创建、修改、删除
- 存储:数据库 + Caffeine 缓存(1 分钟过期)
- 层级定义:通过
parentId/parentPath字段
3.3 为什么不统一存储?
| 维度 | 系统字典(枚举) | 业务字典(数据库) |
|---|---|---|
| 确定性 | 编译时保证,不会丢失 | 依赖数据库状态 |
| 维护成本 | 代码即文档,零运维 | 需要后台管理页面 |
| 变更灵活性 | 需要发版 | 运行时可改 |
| 适用场景 | 固定的系统常量分类 | 用户自定义分类/标签 |
四、枚举扫描与树构建
4.1 扫描机制
EnumCollector 实现 CommandLineRunner,在 Spring 启动时执行:
- 包扫描 — 通过
ClassPathScanningCandidateComponentProvider扫描com.sie.imom包下所有实现SystemEnum的枚举类 - 层级排序 —
sortEnumClassesByHierarchyDepth()按@DictParent链深度排序,保证处理子节点时父节点已完成 - 反射解析 — 遍历每个枚举常量,通过反射获取
code和name,从@DictParent获取父级信息 - 路径拼接 — 构建完整路径
parentPath + "." + code - 缓存存储 — 扁平列表存入
DICT_INIT_CACHE,树形结构存入DICT_TREE_CACHE
4.2 层级定义示例
// 根节点
@DictParent() // parent=EmptyEnum,表示根节点
public enum SysRootEnum implements SystemEnum {
SysSysEnum("sysSys", "系统");
}
// 一级子节点
@DictParent(parent = SysRootEnum.class)
public enum SysSysEnum implements SystemEnum {
TaskEnum("task", "异步任务"),
PrintEnum("print", "打印");
}
// 二级子节点
@DictParent(parent = SysSysEnum.class)
public enum TaskEnum implements SystemEnum {
TaskStatusEnum("taskStatus", "异步任务状态"),
TaskTagEnum("taskTag", "异步任务标识");
}
// 三级叶子节点
@DictParent(parent = TaskEnum.class)
public enum TaskStatusEnum implements SystemEnum {
PENDING("PENDING", "待执行"),
RUNNING("RUNNING", "运行中"),
SUCCESS("SUCCESS", "成功"),
FAILED("FAILED", "失败");
}
4.3 路径模型
所有字典节点使用 path 字段表示完整层级路径:
sysSys → 系统
sysSys.task → 系统 > 异步任务
sysSys.task.taskStatus → 系统 > 异步任务 > 异步任务状态
sysSys.task.taskStatus.PENDING → 系统 > 异步任务 > 异步任务状态 > 待执行
parentPath 为最后一节之前的路径部分,parentCode 为最后一节的值。这种设计使得树构建可以通过按 parentPath 分组实现 O(n) 复杂度。
五、缓存策略
DictUtil 维护三层 Caffeine 缓存:
| 缓存名 | 用途 | 过期策略 | 最大条目 |
|---|---|---|---|
DICT_INIT_CACHE |
启动时构建的系统/业务字典扁平列表 | 永不过期 | 100 |
DICT_CACHE |
运行时查询的业务字典 | 1 分钟过期 | 100 |
DICT_TREE_CACHE |
系统枚举的树形结构 | 永不过期 | 50 |
缓存更新时机:
- 业务字典 CRUD 操作后立即清除相关缓存
- 系统枚举树缓存启动时构建一次,之后不变
六、发布机制(DictMeta → Dict)
业务字典的发布通过 DictMeta 中间表实现两步同步:
DictEnum (init cache) → DictMeta (sys_dict_meta) → Dict (sys_dict)
① 差异同步 ② 逐条发布
- 差异同步 —
DictInvokeService.publish()将 init cache 中所有业务字典与sys_dict_meta对比,新增内容写入 DictMeta - 逐条发布 —
publishDict()将isPublish=false的 DictMeta 记录按 path 长度升序处理(保证父节点先于子节点),解析 parentId 后写入sys_dict
七、导出/导入设计
- 导出:
DictExportHandler通过异步任务机制处理 Excel 导出 - 导入:
DictImportHandler接收 Excel 数据,importEnforce()校验 path 冲突并解析父节点,importExcel()执行批量插入/更新 - 安全约束:系统字典(
category=SYS)不允许通过 API 创建/修改/删除
八、关键设计决策
- 路径而非 ID 做关联 — 枚举没有 ID,使用 path 字符串做层级标识,业务字典同样沿用以保持一致
- 启动时预构建 — 系统枚举扫描和树构建在启动时完成并缓存,后续请求直接返回缓存结果
- 合并而非替代 —
/search-all返回系统字典 + 业务字典的合集,保证上层 API 透明 - 精确路径匹配防冲突 — 当多个枚举有相同 code 值时(如不同枚举中都有
"type"),通过"ClassName:code"精确 key 避免歧义匹配