# 前端 Bug 通知 — Step2 行程编排「住宿安排」档位显示错乱

- **日期**: 2026-04-17
- **类型**: 前端 Bug 修复指南（**不涉及后端代码改动，后端无需重启**）
- **涉及端**: 管理后台 hl-ui（产品编辑页 Step2）
- **后端服务**: hl-product-service-v2（**仅作接口说明，代码不变**）

---

## 一、问题描述

在产品编辑页 Step2「行程编排」→「住宿安排」tab 中：

- **预期**：显示 5 个档位 tab，名称依次为 `经济 / 舒适 / 轻奢 / 高端 / 豪华`（与 Step1 基础信息配置的 5 档一致）
- **实际**：只显示 **2 个档位 tab**，且名称为 `经奢 / 高端` —— 其中 **"经奢" 这个词在后端任何地方都不存在**

**复现路径**：
- 环境：测试服 `192.168.100.236`
- 产品 ID：`2045018534152478721`
- 页面：产品编辑 → Step2 行程编排 → 住宿安排

---

## 二、根因分析（后端已核实）

### 1. 后端返回的数据是完整正确的

调用 `GET /admin/product/item/{id}` 返回 `data.tiers` **完整包含 5 档**，每档都有 `tierSeq` / `tierName` / `tierDescription` 字段，数据无问题。

### 2. "经奢" 不是后端字典

在整个后端项目（包括 schema.sql、字典表、常量、VO、代码注释）`grep "经奢"` **零匹配**。该字符串**必然是前端代码内写死的**。

### 3. itinerary 内 hotels 只有部分档位是业务正常

后端 `data.itinerary[*].hotels` 里 `tierSeq` 只出现 `1` 和 `2`，是**用户在 Step2 只给档位 1、2 录入了酒店**的真实业务状态（业务允许不录满全部档位，不是 bug）。

前端**不应该**从 `itinerary[*].hotels` 反推档位 tab 列表 —— 这会导致：未录入酒店的档位 tab 直接消失；而一旦索引/字段错位，还会出现 "经奢" 这种幻觉档位名。

---

## 三、后端接口数据结构（示例）

接口：`GET /admin/product/item/{id}`

```json
{
  "code": 0,
  "data": {
    "id": 2045018534152478721,
    "name": "示例产品",

    "tiers": [
      {"tierSeq": 1, "tierName": "经济", "tierDescription": "快捷酒店/双床标间"},
      {"tierSeq": 2, "tierName": "舒适", "tierDescription": "四星酒店/河景房"},
      {"tierSeq": 3, "tierName": "轻奢", "tierDescription": "五星酒店/高级套房"},
      {"tierSeq": 4, "tierName": "高端", "tierDescription": "精品度假酒店/江景套房"},
      {"tierSeq": 5, "tierName": "豪华", "tierDescription": "奢华度假庄园/别墅"}
    ],

    "itinerary": [
      {
        "dayNumber": 1,
        "hotels": [
          {"tierSeq": 1, "hotelId": 111, "hotelName": "某快捷酒店"},
          {"tierSeq": 2, "hotelId": 222, "hotelName": "某四星酒店"}
        ]
      },
      {
        "dayNumber": 2,
        "hotels": []
      }
    ]
  }
}
```

字段说明：

| 字段 | 含义 | 说明 |
|------|------|------|
| `data.tiers` | **档位定义列表**（权威来源） | Step1 基础信息配置，**档位 tab 列表必须用它** |
| `data.tiers[*].tierSeq` | 档位序号 | 1/2/3/4/5 |
| `data.tiers[*].tierName` | 档位中文名 | 经济/舒适/轻奢/高端/豪华 |
| `data.tiers[*].tierDescription` | 档位描述 | 档位下方的说明文字 |
| `data.itinerary[*].hotels` | 某一天的酒店安排 | 按档位可能部分录入，**允许为空数组** |
| `data.itinerary[*].hotels[*].tierSeq` | 该酒店归属的档位 | 用于过滤到对应档位 tab 下展示 |

---

## 四、前端正确渲染逻辑

### 1. 档位 tab 列表

**严格使用 `data.tiers`**：

```js
// 正确
const tierTabs = data.tiers; // 5 档齐全，顺序按 tierSeq

// 错误 —— 会导致未录酒店的档位消失
const tierTabs = uniqBy(flatMap(data.itinerary, d => d.hotels), 'tierSeq');
```

### 2. 档位名称

**一律用 `tier.tierName` 字段**，禁止前端写死档位名数组：

```js
// 正确
<Tab label={tier.tierName} />

// 错误 —— 极易出现 "经奢" 这种幻觉
const TIER_NAMES = ['经济', '经奢', '轻奢', '高端', '豪华']; // 第 2 项就是错的
<Tab label={TIER_NAMES[tier.tierSeq - 1]} />
```

### 3. 每档下的酒店

按当前选中档位的 `tierSeq` 过滤：

```js
const hotelsOfCurrentTier = day.hotels.filter(h => h.tierSeq === currentTierSeq);
```

### 4. 空档位保留 tab，内容区显示空状态

即使某档位下 `hotels` 全部为空，**tab 仍然要显示**，内容区渲染空状态提示（如"该档位暂未配置酒店，点击添加"），**禁止隐藏 tab**。

---

## 五、排查步骤建议

1. **全前端代码 grep `经奢`** → 应能定位到写死的字符串来源，删除或修正为从 `tiers` 取值
2. **定位 Step2 住宿 tab 的数据源**：检查是否从 `itinerary[*].hotels` 聚合 `tierSeq` 推导档位 —— 如是，改为从 `data.tiers` 取
3. **检查档位名枚举数组**：搜 `['经济'` / `['经济','` 等关键词，排查是否有写死的档位名数组写反
4. **检查字典调用**：Step2 档位名是否误用了其他字典（如把酒店星级字典当档位用）

---

## 六、验收标准（前端修完后自测）

- [ ] 产品 `2045018534152478721` Step2「住宿安排」tab **严格显示 5 个档位**，名称依次等于 Step1：`经济 / 舒适 / 轻奢 / 高端 / 豪华`
- [ ] 档位 3（轻奢）、4（高端）、5（豪华）下未录酒店时，**tab 仍然显示**，内容区呈现空状态而非 tab 消失
- [ ] 档位 1（经济）、2（舒适）下能正确看到已录入的酒店
- [ ] **全前端代码 grep 不到「经奢」这个字符串**
- [ ] 其他已发布产品随机抽查 2 个，Step2 住宿 tab 档位数与 Step1 配置完全一致

---

## 七、影响范围

- **后端**：无变更，不需要重启服务
- **前端**：仅 Step2「住宿安排」tab 的档位渲染逻辑修复
- **数据**：无需订正，线上数据是正确的，仅前端展示错误
