5.4 KiB
Context
系统枚举的「值」目前是代码里的魔法字符串:后端收敛在 src/common/enums.ts(CommonStatus/UserStatus/ResourceType/DataScope/LoginStatus),前端则散布在 6+ 页面的 STATUS_MAP/TYPE_MAP/DATA_SCOPE_MAP。字典表(sys_dict_type / sys_dict_item)与字典管理页已存在但 seed 为空。见 proposal.md 动机。
关键约束:
- 枚举的 value 被业务逻辑分支依赖(登录校验
status==='enabled'、菜单树取type='menu'、resolveDataScope分支),因此 value 必须稳定,字典只承载「展示层」的 label/颜色/排序。 - 后端
enums.ts仍是代码逻辑唯一枚举值源;dict 是展示层映射,两者靠 value 字符串对齐。改值要两边同步,但本次只新增不改值。
Goals / Non-Goals
Goals:
- 让前端枚举渲染完全字典驱动,消除本地硬编码中文/颜色映射。
- 内置字典值受保护(不可删、value 不可改),label/排序/颜色可定制。
- 组件库支持
dictCode声明,降低页面配置成本。
Non-Goals:
- 不改任何枚举的现有 value(避免破坏既有登录/鉴权/菜单逻辑)。
- 不做「纯业务字典」(性别/学历等)——当前模型无对应字段,等业务来了再加。
http_method不进字典(固定标准、后端枚举校验)。- 不做字典版本号/增量同步/多级缓存失效,够用即可。
Decisions
D1. 保护标记:类型级 + 项级 built_in 各加一列
dict_type.built_in(Boolean,默认 false,内置类型不可删)+ dict_item.built_in(Boolean,默认 false,内置项 value 不可改、项不可删)。
- 备选:仅项级(空的内置类型会被误删)、仅硬编码保护清单(散在代码、不透明)。选两列,粒度最清晰,语义直白。
D2. 颜色存独立列 color_type
dict_item.color_type(String?,取值 primary/success/warning/danger/info,可空)。
- 备选:通用
extJSON 列(弱类型、字典页要做自定义编辑)、按 sort 猜颜色(不可控)。选独立列,语义化、前后端都简单,且复用前端既有TagType联合类型。
D3. 保护规则在后端 service 层强制,前端只做「降级展示 + 交互禁用」
dicts.service 的 update/delete 前置校验:内置类型删除、内置项删除、内置项改 value 一律拒绝(返回 4xx 明确错误);label/sort/color_type 放行。前端字典管理页展示「内置」徽标、禁用 value 输入与删除入口,但不把前端当安全边界,后端兜底。
D4. 聚合查询走新端点 GET /dicts
一次返回当前租户全部启用字典:{ list: [{ code, name, items: [{ label, value, colorType, sort }] }] },item 按 sort 升序、仅启用。登录后拉一次缓存。
- 备选:复用
listDictTypes+ 逐类型listDictItems(N+1 请求,5 个内置字典就 6 次)。选聚合端点,一次搞定。
D5. 前端字典 store + useDict
dict.tsPinia store:dicts: Record<code, DictItem[]>+loaded+fetch()(调 GET /dicts)+reset()(登出清空)。useDict(code):computed 返回该 code 的响应式字典项数组,附带tagMap(value→{label,type})与options(value→label,供下拉)两个衍生视图。- 加载时机:
layout/default.vue在菜单拉取后触发dictStore.fetch();main.ts的 auth 失效回调加dictStore.reset()。
D6. 组件 dictCode 声明与优先级
JsonSchemaProperty增dictCode?: string:ty-form / ty-search-form 的 select 字段若声明dictCode,忽略手写enum/enumNames,用useDict的 options 渲染下拉。TableColumn增dictCode?: string:ty-table 列若声明dictCode,忽略tagMap,用字典渲染标签(label + colorType)。- 优先级:
dictCode> 手写enum/tagMap。已声明dictCode的字段不再需要enum/enumNames。
D7. seed 幂等
按 (tenantCode, code) upsert dict_type、按 (tenantCode, dictTypeCode, value) upsert dict_item;内置项置 built_in=true。5 个字典项的值与 enums.ts 完全一致。
Risks / Trade-offs
- [内置项 value 被前端绕过] → 后端 service 层强制校验兜底(D3),前端禁用只是体验优化。
- [字典缓存滞后:改 label 后需刷新才生效] → 提供
useDict.refresh;字典管理页保存后调用dictStore.fetch()重拉。不追求实时推送。 - [停用某字典类型后 useDict 返回空] → 组件降级:无映射时直接展示原始 value 字符串,避免空白。
- [enums.ts 与 seed 取值漂移] → 两者值均由 spec(shared/data-model)锚定,本次不改值;后续改动需同步更新 seed 与 enums.ts,并在 spec 落一条。
- [加列迁移] → 均为可空/带默认值的新增列,
prisma migrate增量即可,无回滚风险;向后兼容(旧数据 built_in 默认 false)。
Migration Plan
- Prisma schema 加列 →
prisma migrate dev生成增量 migration。 - 更新 seed,跑
pnpm seed预置 5 个内置字典。 - 后端加保护校验 +
GET /dicts。 - 前端加 dict store/useDict + 组件 dictCode + 页面去硬编码。
- 回滚:加列可
prisma migrate resolve --rolled-back(无破坏性变更);前端无外部依赖,随代码回滚。
Open Questions
(无 —— 关键决策已在本 design 与用户确认中敲定)