---
date: 2026-05-13
type: backend-improvement
service: hl-product-service-v2
priority: high
notify: ["@mmg"]
status: verified
---

# C端价格日历支持月份缺省回退最近可售月, 响应新增 `month` 字段 (已测试服验证)

> **PR**: [#2172](https://git.1814.love:8443/wx/HL/pulls/2172) — 已合并 dev + 部署测试服
> **Issue**: [#2167](https://git.1814.love:8443/wx/HL/issues/2167)
> **状态**: ✅ **测试服已验证** (api.test.1814.love:9443) — 10/10 case 端到端通过

---

## ⚠️ 关键变化

### 入参契约放宽

`/mp/product/{id}/price-calendar` 接口的 `month` / `startDate` / `endDate` 三个参数**全部改为可选**：

- 三参数全空 → 后端**自动定位**"从今天起最近一个有可售日期所在月份"并返回该月日历。
- 旧调用方（detail.js / confirm.js / supplement.js 等传 month 或 startDate+endDate）**行为完全不变**。

### 响应新增字段

`MpPriceCalendarRespVO` 新增 `month: yyyy-MM` 字段，前端缺省调用时**必须**用它来渲染日历头/切换上下月。

```json
{
  "month": "2026-07",     // 新增,服务端实际返回的月份;无可售时为 null
  "days": [ ... ]         // 既有结构不变
}
```

无可售日时 `month = null`, `days = []`,前端可展示"暂无可售日期"。

---

## 一、背景

前端两个场景下不知道该传哪个月份：
1. 用户首次进入产品详情页，希望默认展示"最近可售月份"。
2. 已上架产品的最近可售日不一定是本月（可能近月全过期/全无库存）。

原来前端只能先调 `/earliest-available-date` 再拿月份回调 `/price-calendar`，多一次 RT。本次改造后**一次接口即可拿到最近可售月日历 + 月份字段**。

---

## 二、变更接口清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | C端价格日历 | GET | `/mp/product/{id}/price-calendar` | 入参可选 + 响应新增字段 | 三日期参数全可选；响应新增 `month` |

---

## 三、接口详情

### 1. C端价格日历 `GET /mp/product/{id}/price-calendar`

**入参 VO**: `MpPriceCalendarReqVO`
**响应 VO**: `MpPriceCalendarRespVO`

#### 入参（Query）

| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `month` | Query | String | ❌ 改为可选 | yyyy-MM | 与 startDate/endDate 三选一可全空 |
| `startDate` | Query | String | ❌ 改为可选 | yyyy-MM-dd | 若传必须与 endDate 同月配对 |
| `endDate` | Query | String | ❌ 改为可选 | yyyy-MM-dd | 同上 |

**入参组合规则**：

| 组合 | 行为 |
|------|------|
| `startDate` + `endDate` 都非空 | 解析为它们所在月份范围（必须同月，否则抛 410109） |
| 仅 `month` 非空 | 解析为该月范围 |
| **三者全空** | **新行为**：自动定位"最近可售月" |
| 单传 `startDate` 或 `endDate` | 仍抛 410108 |
| `month` 格式非法（如 `2026/07`、`abc`、`2026-13`） | 仍抛 410108 |

#### 出参 `Result<MpPriceCalendarRespVO>`

| 字段 | 类型 | 说明 |
|------|------|------|
| `month` | String | **新增**。服务端实际返回的月份 `yyyy-MM`；无可售日时为 `null` |
| `days` | List | 既有结构不变（每日 date / priceType / adultPrice / childPrice / isSelectable / remainStock） |

#### 产品类型分流（全部 3 类覆盖）

| 类型 | 最近可售日来源 |
|------|--------------|
| CORE / CUSTOM | `product_price_calendar` 全档位齐全的最早未来日 |
| GROUP（小蒙马） | `group_tour_batch` 状态 `ENROLLING/NEARLY_FULL` 且报名未截止的最早出发日 |

#### 请求示例

**A. 缺省入参（新行为）**：
```
GET /mp/product/2045018534152478721/price-calendar
→ { "code":200, "data":{ "month":"2026-05", "days":[ {...30 条...} ] } }
```

**B. 显式 month（旧行为，兼容）**：
```
GET /mp/product/2045018534152478721/price-calendar?month=2026-07
→ { "code":200, "data":{ "month":"2026-07", "days":[ ... ] } }
```

**C. 缺省入参 + 产品完全无可售**：
```
GET /mp/product/{noSellable}/price-calendar
→ { "code":200, "data":{ "month":null, "days":[] } }
```

#### 错误码

| code | 触发场景 | 文案 |
|------|---------|------|
| 410108 | 单边 startDate / 单边 endDate / month 格式非法 | 参数格式错误：month 需为 yyyy-MM，或 startDate/endDate 必须同月成对且为 yyyy-MM-dd |
| 410109 | startDate / endDate 跨月 | startDate(X) 与 endDate(Y) 必须在同一个月份内 |
| 404 | 产品不存在 / 已下架 | 产品不存在或已下架 |

> **顺带的安全收益**：本次把产品可见性校验 `getVisibleProductOrThrow` 从"缓存命中后"提前到"参数解析前"。**堵住"产品下架但缓存内日历仍可读 5 分钟"的口子**。对前端无可见影响（响应内容不变），但下架产品立即返回 404。

---

## 四、测试服验证（硬性凭证，10/10 端到端通过）

测试环境 `api.test.1814.love:9443`，今天 2026-05-13。

| # | 用例 | 入参 | 期望 | 实测 |
|---|------|------|------|------|
| 1 | CORE 缺省入参 | pid=2045018534152478721 | month 非 null + days > 0 | ✓ month=2026-05, days=30 |
| 2 | CORE 显式 month=2026-07 | month=2026-07 | month=2026-07 | ✓ |
| 3 | CORE startDate+endDate 同月 | 2026-07-10 ~ 2026-07-20 | month=2026-07 | ✓ |
| 4 | **CUSTOM 缺省入参** | pid=2043696016590327809 | month 非 null | ✓ month=2026-07, days=31 |
| 5 | **GROUP 缺省入参** | pid=2044306857534636034 | month 非 null | ✓ month=2026-07, days=5 |
| 6 | 跨月异常 | startDate=2026-07-25&endDate=2026-08-05 | 410109 | ✓ |
| 7 | 单边 startDate | startDate=2026-07-15 | 410108 | ✓ |
| 8 | 单边 endDate | endDate=2026-07-15 | 410108 | ✓ |
| 9 | month 格式错 | month=2026/07 | 410108 | ✓ |
| 10 | 不存在产品 + 缺省入参 | pid=999999 | 404 visible 提前 | ✓ |

本地单测（hl-product-service-v2 全模块）1192/1192 全绿。

---

## 五、前端调用建议

1. **首次进入产品详情页**：直接调 `/mp/product/{id}/price-calendar` **不传任何参数**，拿响应 `month` 字段渲染日历头。
2. **用户切换上/下月**：传 `month=yyyy-MM` 显式查询（与新行为不互斥）。
3. **响应 `month === null`**：展示"该产品暂无可售日期"。
4. **旧代码不必立即改**：传 month / startDate+endDate 的旧逻辑完全兼容，按各自节奏迁移即可。

---

## 六、缓存

- 内容仍按 `mp:product:price-calendar:{productId}:{yyyy-MM}` 月份 key 缓存，5 分钟 TTL。
- 缺省入参时多一次 earliest 查询（不缓存，廉价、随时间变化）。
- 无可售月（`month=null`）不写缓存。
