liuyun 8743e68451 chore: archive dict-driven-enums change + sync specs to main specs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 14:44:56 +08:00

5.4 KiB
Raw Blame History

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,可空)。

  • 备选:通用 ext JSON 列(弱类型、字典页要做自定义编辑)、按 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.ts Pinia 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

  1. Prisma schema 加列 → prisma migrate dev 生成增量 migration。
  2. 更新 seed,跑 pnpm seed 预置 5 个内置字典。
  3. 后端加保护校验 + GET /dicts。
  4. 前端加 dict store/useDict + 组件 dictCode + 页面去硬编码。
  5. 回滚:加列可 prisma migrate resolve --rolled-back(无破坏性变更);前端无外部依赖,随代码回滚。

Open Questions

(无 —— 关键决策已在本 design 与用户确认中敲定)