# refactor(product-v2): 餐饮选项 meal_option 由硬编码改为系统字典 + 新增「含（正餐）」

**日期**：2026-05-21
**PR**：#2783（已合 dev → 已合 dev-v3 → 已部署测试服并验证）
**工单**：#2778
**影响端**：管理后台前端（hl-ui）

---

## 背景

原「餐饮安排」下拉框 4 个选项（含酒店 / 含营地 / 含特色餐 / 自理）在前端 `MealConfig.vue` 和后端 `ProductItineraryAggregateFacade` 都是**硬编码**。每次加一个选项都要前后端同时改代码、走 PR、上线。

本次改造：
- 后端 switch 硬编码 → 改为读 `meal_option` 字典
- 字典数据库里新加一条 `REGULAR` = 「含（正餐）」
- 以后再加选项只在字典管理后台点一下即可，**前后端代码无需任何改动**

---

## 字典数据（meal_option）

测试服已落库，可通过 `GET /admin/dict/data/meal_option` 拉到：

| sortOrder | dictValue | dictLabel | 说明 |
|-----------|-----------|-----------|------|
| 1 | `HOTEL` | 含（酒店） | 历史值 |
| 2 | `CAMP` | 含（营地） | 历史值 |
| 3 | `SPECIAL` | 含（特色餐） | 历史值 |
| 4 | `REGULAR` | **含（正餐）** | **本次新增** |
| 5 | `SELF` | 自理 | 历史值（默认值） |

---

## 🚨 前端需要做的事

### 1. 删除硬编码

文件：`hl-ui/src/views/product/edit/components/itinerary/MealConfig.vue`

把第 22-27 行的硬编码数组**删掉**：

```js
// 删掉这段
const mealOptions = [
  { label: '含（酒店）', value: 'HOTEL' },
  { label: '含（营地）', value: 'CAMP' },
  { label: '含（特色餐）', value: 'SPECIAL' },
  { label: '自理', value: 'SELF' },
]
```

### 2. 改成调字典接口

调用：`GET /admin/dict/data/meal_option`（已存在的字典 API）

**Header**：需要 admin token

**响应结构**：

```json
{
  "code": 200,
  "message": "成功",
  "data": [
    { "dictValue": "HOTEL",   "dictLabel": "含（酒店）",   "sortOrder": 1, "status": "ACTIVE" },
    { "dictValue": "CAMP",    "dictLabel": "含（营地）",   "sortOrder": 2, "status": "ACTIVE" },
    { "dictValue": "SPECIAL", "dictLabel": "含（特色餐）", "sortOrder": 3, "status": "ACTIVE" },
    { "dictValue": "REGULAR", "dictLabel": "含（正餐）",   "sortOrder": 4, "status": "ACTIVE" },
    { "dictValue": "SELF",    "dictLabel": "自理",         "sortOrder": 5, "status": "ACTIVE" }
  ]
}
```

### 3. 下拉绑定动态数据

`n-popselect` 的 `:options` 改为从接口返回的数组映射（注意现有字典 store 可能已经封装好，可以复用）：

```js
// 推荐：直接复用全局 dictStore（如果已有）
const mealOptions = computed(() =>
  dictStore.getDictData('meal_option').map(d => ({
    label: d.dictLabel,
    value: d.dictValue
  }))
)
```

如果暂时没全局字典 store，组件级别 `onMounted` 拉一次即可（字典本身后端有 5 分钟 Redis 缓存，不会打到 DB）。

### 4. 校验逻辑无变化

`isSelf(value)` 仍然按 `value === 'SELF' || !value` 判断 — `SELF` 是业务语义的「自理」，前端这层判断不动。

---

## 兼容性

- ✅ 历史数据（HOTEL/CAMP/SPECIAL/SELF）**全兼容**，已上线的产品数据无需迁移
- ✅ 字典服务挂掉时后端会 fallback 到 dictValue 字面值（订单详情不会显示空白）
- ✅ 字典本身有 Redis 5 分钟缓存，QPS 无压力

---

## 测试服验证记录

- 测试服 https://web.test.1814.love:9443 已部署 commit `b28cef1c7`（dev-v3）
- 字典查询接口返回 5 条数据 ✓
- 产品详情接口对 5 种 meal_option（HOTEL/CAMP/SPECIAL/REGULAR/SELF）全部返回正确 dictLabel ✓
- 无 token 调用返 `code: 401` ✓

---

## 联系人

后端：wx（呼籁旅行）
前端：mmg

如有疑问或字典接口路径需要确认，可在 [#2778](https://git.1814.love:8443/wx/HL/issues/2778) 评论区留言。
