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