# 统一价格日历接口 /admin/product/item/{id}/pricing-calendar

**日期**: 2026-04-23
**PR**: #1290 (Closes #1286)
**服务**: hl-product-service-v2
**类型**: feat（新增接口，零破坏性）

---

## 背景

之前 admin 查价格日历需要分场景调两个不同接口：
- CORE/CUSTOM → `GET /admin/product/item/{id}/price-calendar`
- GROUP（小蒙马） → `GET /admin/product/item/{id}/schedule/list`

**根因**：GROUP 定价真相源是 `group_tour_batch.adult_price`（班期表），不走 `product_price_calendar`。前端要按 `productType` 分流调用。

本次新增**统一入口**，后端按 productType 聚合两种数据源，前端一个接口搞定。

---

## 新接口

### `GET /admin/product/item/{id}/pricing-calendar`

**Query 参数**
- `month`（可选）：`yyyy-MM`，过滤月份。不传返全部
- `tierSeq`（可选）：档位序号，**仅 CORE/CUSTOM 生效**，GROUP 忽略

**响应**
```json
{
  "code": 200,
  "data": {
    "productType": "CORE | GROUP | CUSTOM",
    "items": [
      { ... 统一条目 VO ... }
    ]
  }
}
```

### 统一条目 VO 字段表

| 字段 | 类型 | CORE/CUSTOM | GROUP |
|------|------|-------------|-------|
| **通用** |
| `date` | LocalDate | 每日价格日 | 出发日（departureDate）|
| `adultPrice` | BigDecimal | adultSellPrice | adultPrice |
| `childPrice` | BigDecimal | childSellPrice | childPrice |
| `toddlerDiscount` | BigDecimal | ✅ | ✅ |
| `infantPrice` | BigDecimal | ✅ | ✅ |
| `stock` | Integer | dailyStock | productStockLimit（无则 maxParticipants）|
| `sold` | Integer | sold | enrolledCount |
| `sellable` | Boolean | dailyStock 空或 sold<stock | batchStatus∈{ENROLLING,NEARLY_FULL} |
| **CORE/CUSTOM 专属** |
| `id` | Long | 日历记录主键 | null |
| `tierSeq` | Integer | 档位序号 | null |
| `priceType` | String | NORMAL/PEAK/HOLIDAY/SPECIAL | null |
| `rangeId` | Long | 批量区间标识 | null |
| **GROUP 专属** |
| `batchId` | Long | null | 班期ID |
| `batchNo` | String | null | 班期编号 |
| `batchName` | String | null | 班期名称 |
| `batchStatus` | String | null | ENROLLING/NEARLY_FULL/FULL/FINISHED/CANCELLING/CANCELLED |
| `endDate` | LocalDate | null | 班期结束日 |
| `enrollmentDeadline` | LocalDate | null | 报名截止日 |
| `maxRooms` | Integer | null | 总房间数 |
| `bookedRooms` | Integer | null | 已预订房间数 |

---

## 前端渲染建议

```javascript
const resp = await api.getUnifiedPricingCalendar(productId, { month, tierSeq });
switch (resp.data.productType) {
  case 'CORE':
  case 'CUSTOM':
    // 矩阵日历: 按 tierSeq × date 渲染档位 × 日期表格
    renderMatrixCalendar(resp.data.items);
    break;
  case 'GROUP':
    // 列表日历: 按出发日渲染班期卡片，显示 batchName/batchStatus/endDate
    renderBatchCalendar(resp.data.items);
    break;
}
```

---

## 示例

### CORE 响应
```json
{
  "productType": "CORE",
  "items": [
    {
      "date": "2026-05-01",
      "adultPrice": 1999.00,
      "childPrice": 999.00,
      "id": 1780000000000000001,
      "tierSeq": 1,
      "priceType": "NORMAL",
      "stock": 50,
      "sold": 10,
      "sellable": true,
      "batchId": null
    }
  ]
}
```

### GROUP 响应
```json
{
  "productType": "GROUP",
  "items": [
    {
      "date": "2026-07-06",
      "adultPrice": 4580.00,
      "childPrice": 2290.00,
      "batchId": 2043697199761584129,
      "batchNo": "Q202607062043697199757389826",
      "batchName": "7月暑期团",
      "batchStatus": "ENROLLING",
      "endDate": "2026-07-10",
      "stock": 25,
      "sold": 0,
      "sellable": true,
      "tierSeq": null,
      "priceType": null
    }
  ]
}
```

---

## 向后兼容

✅ **老接口保持不变**：
- `GET /admin/product/item/{id}/price-calendar` — 不动
- `GET /admin/product/item/{id}/schedule/list` — 不动

前端可**逐步迁移**到新接口，无需一次性全量改造。

---

## 验证

测试服网关 7 场景全过：
- CORE 全量 → 92 条 tierSeq/priceType 齐 ✅
- GROUP 全量 → 4 条班期 batchId/batchStatus/endDate 齐 ✅
- CUSTOM → 62 条走 CORE 分支 ✅
- CORE/GROUP ?month=2026-05 → 正确过滤 ✅
- CORE ?tierSeq=1 → 从 92 条过滤到 79 条 ✅
- 老接口 /price-calendar → 92 条不变零回归 ✅

---

## 部署

测试服已部署: task `a1ca3b50` success。
正式环境上线前**无需任何 DDL**，纯接口聚合。
