# feat: 管理端产品列表 `/admin/product/item/list` 响应新增 `tierPrices` 档位价格摘要

- **日期**: 2026-04-20
- **PR**: [#955](https://git.1814.love:8443/wx/HL/pulls/955) (Closes #954)
- **类型**: FEATURE（响应新增字段，无破坏性变更）
- **服务**: hl-product-service-v2
- **前端是否需要改动**: **建议改动**（管理端产品列表新增"档位摘要"列展示）

---

## 一、为什么加这个字段

管理端产品管理列表此前每行只能看到：
- `tierCount`（档位总数）
- `startPrice`（产品起步价 = 所有档位最低）

但运营在列表里需要快速判断**每个档位是否已定价**、**哪个档位起步价多少**，否则要点进详情页一个个看，效率低。

本次新增 `tierPrices` 字段：**列表页保留无价档位**（startPrice=null），让前端可以一眼看出"档位待定价"的状态。

---

## 二、变更接口清单

| # | 方法 | 路径 | 影响 |
|---|------|------|------|
| 1 | GET | `/admin/product/item/list` | 响应 `records[]` 每项新增 `tierPrices` 字段（数组） |

**其他接口不变**：
- `GET /admin/product/{id}`（产品详情）：**行为零变化**。详情接口的 `tierPrices` **仍剔除无价档位**，与本次列表接口语义不同（见第五节）。
- 既有的 `tierCount` / `startPrice` 字段保留，不受影响。

---

## 三、字段定义

### 新增 `tierPrices`（数组）

每个产品记录新增 `tierPrices: TierPriceItem[]`，结构：

| 字段 | 类型 | 说明 | 可能为 null |
|------|------|------|-------------|
| `tierSeq` | Integer | 档位序号（从 1 起） | 否 |
| `tierName` | String | 档位名称（如"经济档"/"舒适档"/"豪华档"） | 否 |
| `tierDescription` | String | 档位描述（产品配置时填写） | 是（未填即 null） |
| `startPrice` | BigDecimal | **该档位**未来日期的最低售价；**无价档位返回 null** | **是** ⚠️ |

### 既有字段（保留，不变）

| 字段 | 说明 |
|------|------|
| `tierCount` | 档位总数（包含无价档位） |
| `startPrice` | 产品起步价（所有档位中最低；CORE/CUSTOM 取自 `product_price_calendar`，GROUP 同详情口径） |

---

## 四、响应示例对比

### 前

```json
{
  "code": 200,
  "data": {
    "total": 25,
    "records": [
      {
        "id": 1001,
        "name": "小蒙马·呼伦贝尔5日",
        "productType": "GROUP",
        "tierCount": 3,
        "startPrice": 4580.00
      }
    ]
  }
}
```

### 后

```json
{
  "code": 200,
  "data": {
    "total": 25,
    "records": [
      {
        "id": 1001,
        "name": "小蒙马·呼伦贝尔5日",
        "productType": "GROUP",
        "tierCount": 3,
        "startPrice": 4580.00,
        "tierPrices": [
          {
            "tierSeq": 1,
            "tierName": "经济档",
            "tierDescription": "标准住宿+常规交通",
            "startPrice": 4580.00
          },
          {
            "tierSeq": 2,
            "tierName": "舒适档",
            "tierDescription": "四星住宿+商务车",
            "startPrice": 5160.00
          },
          {
            "tierSeq": 3,
            "tierName": "豪华档",
            "tierDescription": "五星住宿+豪华车",
            "startPrice": null
          }
        ]
      }
    ]
  }
}
```

> 第 3 个档位 "豪华档" 未配价格 → `startPrice: null`，前端可展示为"档位待定价"。

---

## 五、列表 vs 详情语义差异（重要）

| 接口 | 无价档位（startPrice=null）处理 |
|---|---|
| **列表 `/admin/product/item/list`**（本次） | **保留**，`startPrice=null`，便于运营在列表里识别 |
| **详情 `/admin/product/{id}`**（不变） | **剔除**，`tierPrices` 中只保留有价档位 |

> 这个差异是设计意图：详情页面向"已成型可售产品"，列表页面向"运营治理"。

---

## 六、GROUP 类型的特别说明

| 产品类型 | 列表 `tierPrices[].startPrice` 数据源 |
|---|---|
| `CORE` / `CUSTOM` | `product_price_calendar` 表（按档位 + 日期取未来最低） |
| `GROUP`（小蒙马） | 仍走 `product_price_calendar`，**与详情接口口径一致** |

⚠️ **已知偏差**：GROUP 真实成交价存在 `group_tour_batch.adult_price`（班期表），与日历价可能存在差异。这是已知问题，本次不修复，保持与详情接口同口径以避免列表/详情数字不一致。

---

## 七、前端使用建议

### 推荐：列表新增"档位摘要"列

```vue
<el-table-column label="档位摘要" min-width="240">
  <template #default="{ row }">
    <div v-for="t in row.tierPrices" :key="t.tierSeq" class="tier-line">
      <span class="tier-name">{{ t.tierName }}</span>
      <span v-if="t.startPrice != null" class="tier-price">
        起 ¥{{ t.startPrice }}
      </span>
      <el-tag v-else type="warning" size="small">待定价</el-tag>
    </div>
  </template>
</el-table-column>
```

### ⚠️ 容错要求

- `tierPrices[].startPrice` **可能为 null**（无价档位），必须做 null 判断后再格式化金额，避免 `undefined.toFixed()` 报错
- `tierDescription` 可能为 null，渲染时做空值兜底
- `tierPrices` 数组本身不会为 null（无档位时为空数组 `[]`）

---

## 八、不兼容变更

**无**。纯新增字段，前端不读不受影响；既有 `tierCount` / `startPrice` 字段保留，含义与之前完全一致。

---

## 九、回归验证

测试环境部署完成后，用管理端 token 调用：

```bash
# 1. 列表接口检查 tierPrices 字段存在
curl "https://api.test.1814.love/admin/product/item/list?pageNo=1&pageSize=10" \
  -H "Authorization: Bearer {admin-token}" \
  | jq '.data.records[0] | {id, name, tierCount, startPrice, tierPrices}'

# 预期：tierPrices 字段存在；数组长度 == tierCount；含无价档位时 startPrice=null

# 2. 详情接口对比（应仍剔除无价档位）
curl "https://api.test.1814.love/admin/product/{id}" \
  -H "Authorization: Bearer {admin-token}" \
  | jq '.data.tierPrices'

# 预期：详情接口的 tierPrices 长度 ≤ 列表接口（剔除了无价档位）

# 3. GROUP 产品（小蒙马）口径核对
curl "https://api.test.1814.love/admin/product/item/list?productType=GROUP&pageNo=1&pageSize=5" \
  -H "Authorization: Bearer {admin-token}" \
  | jq '.data.records[] | {id, productType, tierPrices}'

# 预期：GROUP 产品的 tierPrices[].startPrice 与详情接口一致（同走 product_price_calendar）
```

**预期**：
- 列表新字段 `tierPrices` 必出现
- 无价档位 `startPrice: null` 而非被剔除
- 与详情接口语义差异符合第五节说明
