# 数据字典设计文档
## 一、设计背景
前端开发中,大量存在"字段值 → 展示值 → 颜色标签"的映射需求。例如表格中状态列的 `0/1` 需要显示为"禁用/启用",下拉选择器需要选项列表,标签需要根据值显示不同颜色。
在没有字典机制时,这些映射以硬编码形式散落在各个页面的函数、map 对象、模板三元表达式中,带来以下问题:
- **重复定义**:同一个 `user_status`(0=禁用, 1=启用)在多个页面中重复硬编码
- **不一致风险**:不同页面可能定义不同的展示文本(如"禁用"/"停用"/"无效")
- **修改成本高**:新增一个状态或修改展示文本需要改动多处代码并重新发布
- **魔法值泛滥**:代码中到处是 `=== 0 ? '禁用' : '启用'`,可读性和可维护性差
字典功能的设计初衷是:**将值转换映射统一收口到数据库,前端组件自动读取并渲染,消除硬编码**。
## 二、数据模型
### 2.1 数据库表结构
表名:`t_data_dict`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | BIGINT | 主键 |
| `company_id` | BIGINT | 租户ID(多租户隔离) |
| `code` | VARCHAR(50) | 字典编码,作为分组标识 |
| `name` | VARCHAR(100) | 展示名称 |
| `value` | VARCHAR(200) | 字典值 |
| `weight` | INT | 排序权重,控制前端展示顺序 |
| `color` | VARCHAR(20) | 颜色标记(如 success/danger/warning/info/primary) |
| `remark` | VARCHAR(255) | 备注说明 |
| `create_time` | DATETIME | 创建时间 |
| `update_time` | DATETIME | 更新时间 |
### 2.2 设计特点
**无字典类型表**:不设独立的"字典类型"实体。`code` 字段即类型标识,共享同一 `code` 的多条记录组成一个字典分组。这种扁平化设计降低了查询复杂度。
**code 命名约定**:字典 code 采用 `类名.属性名` 格式(如 `Order.status`、`WashStation.type`),与业务实体类属性直接对应,前端开发者可直观地从字段名推断对应的字典 code。
### 2.3 唯一性约束
同一 `code` 分组内,`name` 和 `value` 均不可重复。后端 `DataDictServiceImpl.saveOrUpdate` 方法在写入前执行校验:
```
字典[code]取值异常,请核对
```
## 三、架构设计
### 3.1 整体架构
```
┌─────────────────────────────────────────────────┐
│ 前端 │
│ ExtDSelect / ExtDLabel / dictUtil │
│ ↓ 读取 │
│ sessionStorage / uni.Storage (key: "dicts") │
│ ↑ 登录/启动时一次性拉取 │
├─────────────────────────────────────────────────┤
│ 后端 │
│ DataDictController │
│ ↓ │
│ DataDictService │
│ ↓ │
│ t_data_dict (MySQL) │
└─────────────────────────────────────────────────┘
```
### 3.2 缓存策略
| 层级 | 缓存方式 | 生命周期 | 说明 |
|---|---|---|---|
| 后端 | 无缓存 | — | 每次请求直接查库,字典变更低频,无缓存失效问题 |
| 前端 | sessionStorage / uni.Storage | 登录会话 | 登录后调用 `POST /dataDict/list (pageSize=1024)` 全量拉取,按 `code` 分组存储 |
**为什么后端不缓存**:字典数据变更频率极低(设计理念为"定义后不可修改")。前端 sessionStorage 缓存的生命周期恰好是一个登录会话。免去了 Redis 同步/失效的运维复杂度。
**为什么全量拉取**:字典数据量小(通常 50-200 条),一次请求全量获取后,后续所有字典组件均为纯内存操作,零网络延迟。
### 3.3 后端 API
| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| POST | `/dataDict/list` | 管理端需登录 | 按 code/name 模糊查询,前端以 pageSize=1024 拉取全量 |
| POST | `/dataDict/saveOrUpdate` | 需 `dict.add/edit` 权限 | 批量保存/更新,含唯一性校验 |
| POST | `/dict/list` | 无需认证 | 小程序端公开接口 |
## 四、前端实现
### 4.1 admin-web-new (Vue 3)
**工具模块**:`src/utils/dict.ts`
| 函数 | 说明 |
|---|---|
| `dictUtil.loadDicts()` | 从服务端拉取全量字典并缓存到 sessionStorage |
| `dictUtil.getDicts()` | 获取完整的 `{ [code]: DictItem[] }` 结构 |
| `dictUtil.getDictList(code)` | 获取指定 code 下的字典条目列表 |
| `dictUtil.getDictLabel(code, value)` | 根据 code 和 value 获取展示名称(输出:`"--"` / label) |
| `dictUtil.getDictValue(code, name)` | 根据 code 和 name 反查值 |
| `getDictOptions(code)` | 返回 `[{label, value}]` 格式,用于 el-select 的 options |
| `formatDict(code, value)` | `getDictLabel` 的便捷导出 |
| `getDictColor(code, value)` | 根据 code 和 value 获取颜色标记 |
**字典组件**:
| 组件 | 路径 | 用法 |
|---|---|---|
| `ExtDSelect` | `components/ExtForm/ExtDSelect.vue` | `` |
| `ExtDLabel` | `components/ExtForm/ExtDLabel.vue` | `` |
### 4.2 admin-web (Vue 2)
**工具模块**:`src/utils/u.ts`
| 函数 | 说明 |
|---|---|
| `u.fmt.fmtDict(value, code)` | 根据 code 和 value 获取展示名称(输出:`"--"` / label) |
| `u.fmt.fmtDictColor(value, code)` | 根据 code 和 value 获取颜色标记 |
原始数据存储在 `Session.get("dicts")`(与 admin-web-new 相同的结构)。
**字典组件**:
| 组件 | 路径 | 用法 |
|---|---|---|
| `ExtDSelect` | `components/form/ExtDSelect.vue` | `` |
| `ExtDLabel` | `components/form/ExtDLabel.vue` | `` |
| `ExtDRadio` | `components/form/ExtDRadio.vue` | `` |
| `ExtBoolean` | `components/form/ExtBoolean.vue` | 布尔值选择器,使用 `yes_no` 字典 |
表格列和查询表单列在配置中指定 `type: 'dict'` + `conf: {dict: 'xxx'}` 即可自动渲染为对应字典组件。
### 4.3 car-wash-mp (UniApp 小程序)
**工具函数**:`src/utils/common.ts`
| 函数 | 说明 |
|---|---|
| `fmtDictName(code, value)` | 根据 code 和 value 获取展示名称 |
| `getServicePhone()` | 从 `Service.phone` 字典获取客服电话 |
字典数据在 `App.vue` 的 `onLaunch` 中通过 `POST /dict/list`(无需认证)全量拉取,使用 `uni.setStorage({key: 'dict'})` 缓存。
## 五、字典条目清单
### 5.1 种子数据(init.sql)
| code | 条目 |
|---|---|
| `user_status` | 0=禁用(danger), 1=启用(success) |
| `message_type` | 1=系统通知(primary), 2=站内信(success), 3=待办事项(warning), 4=公告通知(info) |
| `message_status` | 0=未读(danger), 1=已读(success), 2=已删除(info) |
| `priority` | 0=普通, 1=重要(warning), 2=紧急(danger) |
| `notice_status` | 0=未开始(info), 1=生效中(success), 2=已结束, 3=已取消(danger) |
| `yes_no` | 0=否(info), 1=是(success) |
| `Settlement.status` | 0=待结算(info), 1=已结算(success), 2=异常结算(danger) |
| `OptLog.operationType` | CREATE=新增(success), UPDATE=修改(warning), DELETE=删除(danger), QUERY=查询(info), LOGIN=登录(primary), LOGOUT=登出, OTHER=其他 |
| `OptLog.httpMethod` | GET(success), POST(primary), PUT(warning), DELETE(danger) |
| `Menu.type` | 0=菜单(primary), 1=iframe(warning), 2=外链(danger), 3=按钮(info) |
| `AdminUser.sex` | 0=男, 1=女 |
| `WalletDetail.type` | 1=积分, 2=红包, 3=消费 |
### 5.2 业务字典(通过管理界面维护,部分)
| code | 业务含义 |
|---|---|
| `AdminUser.status` | 管理员用户状态 |
| `Order.status` | 订单状态 |
| `Order.pay` | 支付状态 |
| `Order.openType` | 开单方式 |
| `Order.closeType` | 关单方式 |
| `Order.feeType` | 费用类型 |
| `OrderCard.type` | 订单卡类型 |
| `WashStation.status` | 站点运营状态 |
| `WashStation.type` | 站点类型 |
| `WashDevice.status` | 设备状态 |
| `WashDevice.foam` | 泡沫能力 |
| `WashDevice.water` | 水能力 |
| `Activity.discountType` | 活动优惠类型 |
| `Banner.status` | 横幅状态 |
| `Investor.status` | 投资人状态 |
| `Department.status` | 部门状态 |
| `Invoice.status` | 发票状态 |
| `Feedback.type` | 反馈类型 |
| `SplitRecord.type` | 分账类型 |
| `SplitRecord.status` | 分账状态 |
| `WithdrawnRecord.status` | 提现审核状态 |
| `WithdrawnRecord.paymentStatus` | 提现打款状态 |
| `RefundLog.status` | 退款状态 |
| `RefundLog.fundsAccount` | 退款资金账户 |
| `Faq.status` | 常见问题状态 |
| `Object.type` | 通用对象类型 |
| `Service.phone` | 客服电话 |
## 六、设计理念与约束
### 核心原则
> 字典以常量形式定义在业务实体类中,业务逻辑以常量替代,取消魔法值
### 运营约束
- 字典**只提供查询接口**,修改和新增由数据库 DML 执行实现
- 字典定义后**不可修改 code**,不可删除,可废弃
- 为保持与前端及关联系统一致,字典条目需全量维护
### 适用场景
字典机制适合以下场景:
- 状态/类型等有限枚举值的展示
- 需要下拉选择的枚举值列表
- 带颜色标记的状态标签
不适合的场景:
- 动态变化频繁的数据
- 数据量巨大的列表(字典全量缓存在前端,条目过多影响性能)
## 七、最佳实践
### 前端开发
```typescript
// admin-web-new: 表格列中使用 dict 函数
{{ formatDict('Order.status', row.status) }}
// admin-web-new: 查询表单中使用字典下拉
// admin-web (Vue 2): 使用字典组件
// admin-web (Vue 2): 使用工具函数
const label = u.fmt.fmtDict(value, 'Order.status');
const color = u.fmt.fmtDictColor(value, 'Order.status');
```
### 禁止的写法
```javascript
// 禁止:硬编码 map 对象
const statusMap = { 0: '禁用', 1: '启用' };
// 禁止:模板内三元表达式
{{ row.status === 1 ? '启用' : '禁用' }}
// 禁止:硬编码下拉选项
```
### 新增字典步骤
1. 执行 SQL 插入字典数据到 `t_data_dict` 表
2. 前端更新后会自动加载新字典
3. 在页面中使用字典工具函数或组件引用新的字典 code