由ruoyi的字典表勾出的一堆伪架构 —— 为了维护而维护不但不优雅, 还显得很笨.
先讲个低代码的笑话:
客户: 开发这个项目要多久?
程序员: 1个月
客户: 如果用开源项目改一改呢?
程序员: 不到1个月
客户: 如果用低代码开发呢?
程序员: 1年.
引子:当“配置即编码”变成了“维护即业务” 📦
低代码、后台脚手架、字典表、代码生成器,本意是降本增效。现实里,很多团队却把“字典表”用成了万能遥控器:
- 枚举、标签、列名、校验规则、流程状态、权限点……统统塞进 sys_dict。
- 页面上“字典翻译”“字典标签”到处都是,缓存一层一层,刷新又怕脏。
- 变更全靠“改表+热更新”,发布越来越少,偶发事故越来越多。
最后产物是一套“为了维护而维护”的伪架构:所有复杂性都被转移到了运行时和配置数据里,既不优雅,也不稳健。
一、字典表的合理边界到底在哪?
在像 RuoYi 这样的模板里,字典表通常承担这些合理职责:
- 下拉选项与显示文本映射(如性别、简单标签)
- 可本地化的展示文案(配合 i18n)
- 极小变化频率、低风险的显示层参数
⚠️ 不合理扩张的常见表现:
- 用字典来定义“业务域模型”的状态机与行为
- 用字典替代代码中的枚举与校验规则
- 用字典驱动接口入参/出参结构
- 用字典存放权限、流程编排、收费策略等高风险逻辑
一句话:字典表适合“显示层的映射与轻配置”,不适合“领域模型与行为”。
二、伪架构的五大症状 🧩
- 类型安全退化
- 代码里没有 enum,只有字符串/数字 + 字典翻译
- 静态检查、IDE 补全、编译期约束全没了
- 读写复杂度激增
- 每次查询都要做字典联表或翻译
- 缓存层需要处理多维度失效与一致性
- 配置热更新成了悬崖
- 运维“热改字典”= 改契约,导致历史数据与新逻辑不兼容
- 回滚困难,无可审计的“业务行为变更”
- 测试不可重复
- 一套系统依赖一堆初始字典数据
- 单测/集成测试难以构造确定性环境
- 实施与交付的“黑箱化”
- 现场实施人员改字典救火,但团队内无人知道系统真实可接受的取值与边界
- 知识沉淀失败,问题复现依赖“环境快照”
三、为什么团队会走到这一步?🧭
- 业务压力:要快,最好“明天就能改”
- 组织分工:把变更从研发发布转移到实施/运维
- 销售话术:全可配是卖点,变成“全都配”
- 技术债滚雪球:一次妥协带来更多妥协
低代码不是原罪,把“设计期决策”挪到“运行时”才是风险根源。
四、正确姿势:清边界,做分层 ✅
- 字典表只做“显示层映射”和“可本地化标签”
- 领域状态、业务规则、结构约束放在代码(类型系统)里
- 配置中心区分“运行时配置”和“设计期模型”,不混用
- 翻译层承担“值→文案”的责任,不反向驱动领域行为
📌 基本原则:
- 代码里的 enum 决定“允许值域”;字典只决定“这些值如何展示”
- 改显示可以热;改值域必须走发布与数据迁移
五、落地方案(可操作清单)🛠️
- 枚举规范(后端)
- 用 Java enum 定义值域,并在持久化层显式映射
- 提供统一“枚举字典”只读接口给前端取 options
// 领域枚举(值域由代码定义)
public enum OrderStatus {
CREATED(0), PAID(1), SHIPPED(2), DONE(3), CANCELED(4);
private final int code;
// getter & fromCode(...)
}
- 翻译层与 i18n
- 字典表或资源文件仅承担“code → label”的映射
- 允许多语言,禁止改变值域
- 缓存策略
- 只缓存“dictType → [ {value,label} ]”
- 变更通过事件广播/版本号兜底;失效粒度按 dictType
- 前端用法统一化
- 统一组件从
/dict/{type}
取 options - 表格列渲染只做显示翻译,不参与逻辑判断
// 前端统一获取 options
const opts = await api.getDict('order_status')
// 业务逻辑判断用后端返回的 code,不依赖 label
- 代码生成器的“边界约束”
- 允许在字段上标注一个枚举类型注解,生成展示层翻译
- 禁止生成“可配置字段/可配置结构”的动态模型
- 数据迁移治理
- 用 Flyway/Liquibase 管理“初始字典数据”和升级脚本
- 对“值域变化”必须走版本化迁移与灰度
六、案例对比:订单状态 🔄
反例(典型“伪架构”)
- 表字段存字符串 'created'/'paid'…
- 页面 everywhere 用 dictTag 翻译
- 实施可在 sys_dict 改/删状态项
问题:接口契约被现场热改,历史数据、报表、流程分支全部不可靠。
正例(推荐)
- 表字段存 tinyint code,代码 enum 限定值域
- sys_dict 仅提供 code 的 label,i18n 由资源文件/字典管理
- 前后端逻辑基于 code,展示基于 label
SQL 示例:
ALTER TABLE t_order
MODIFY COLUMN status TINYINT NOT NULL COMMENT 'OrderStatus code';
前端渲染:
<el-tag :type="statusTagMap[row.status]">
{{ dictLabel('order_status', row.status) }}
</el-tag>
七、如何从泥潭抽身:迁移路线图 🧭
- 资产审计
- 导出所有 dict_type 与业务引用点,区分“展示字典”和“值域字典”
- 分级治理
- 对 SLA 关键枚举先“代码化”(引入 enum、校验、迁移脚本)
- 低风险项保持字典,仅做显示
- 兼容过渡
- 在 API 层加版本:v1 继续返回 label,v2 返回 code + label
- 前端逐页迁移到 code 逻辑判断
- 自动化护栏
- CI 加 schema 规则:禁止在运行时新增/删除“值域字典”的取值
- 引入合规校验:字典只允许改 label,不允许改 value
- 债务清单与时间盒
- 列出“字典驱动领域”的模块,按收益/风险排期,每期限定工时
八、结语:工具为人服务,而不是反过来 💡
把“设计期决策”塞进“运行时配置”看似灵活,实际是把复杂性与风险转嫁到了最脆弱的环节。
- 架构的优雅来自清晰边界与约束
- 维护的成本来自不必要的自由
低代码不是“全都能配”,而是“把该配的配好,把不该配的收回”。当我们把字典从“万能遥控器”收回到“展示层映射”,系统才会重新变得简单、稳定、可演进。
附:快速自查清单 ✅
- 是否存在用字典控制值域/流程/权限的场景?
- 是否能在代码里找到所有“允许值”的单一真相来源(enum)?
- 测试环境是否能在不导入“现场字典”的情况下稳定运行?
- 字典变更是否有版本化、审计、回滚策略?
- 代码生成器是否越界生成了“动态结构/动态枚举”的能力?
如果以上有两条以上回答“不确定/否”,你大概率正被“为了维护而维护”的伪架构拖累。现在,就是把复杂性还给设计期、把确定性还给代码的时候。