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

71 lines
5.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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 与用户确认中敲定)