# 数据字典设计文档

## 一、设计背景

前端开发中，大量存在"字段值 → 展示值 → 颜色标签"的映射需求。例如表格中状态列的 `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` | `<ext-d-select type="Order.status" v-model="..." />` |
| `ExtDLabel` | `components/ExtForm/ExtDLabel.vue` | `<ext-d-label type="Order.status" :value="row.status" />` |

### 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` | `<ext-d-select type="Order.status" v-model="..." />` |
| `ExtDLabel` | `components/form/ExtDLabel.vue` | `<ext-d-label type="Order.status" :value="row.status" />` |
| `ExtDRadio` | `components/form/ExtDRadio.vue` | `<ext-d-radio type="user_status" v-model="..." />` |
| `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 函数
<el-table-column label="状态">
  <template #default="{ row }">
    <el-tag :type="getDictColor('Order.status', row.status)" size="small">
      {{ formatDict('Order.status', row.status) }}
    </el-tag>
  </template>
</el-table-column>

// admin-web-new: 查询表单中使用字典下拉
<el-form-item label="状态">
  <el-select v-model="query.status">
    <el-option v-for="opt in getDictOptions('Order.status')"
      :key="opt.value" :label="opt.label" :value="opt.value" />
  </el-select>
</el-form-item>

// admin-web (Vue 2): 使用字典组件
<ext-d-select type="Order.status" v-model="query.status" />
<ext-d-label type="Order.status" :value="row.status" />

// 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 ? '启用' : '禁用' }}

// 禁止：硬编码下拉选项
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
```

### 新增字典步骤

1. 执行 SQL 插入字典数据到 `t_data_dict` 表
2. 前端更新后会自动加载新字典
3. 在页面中使用字典工具函数或组件引用新的字典 code
