add: C端价格日历支持月份缺省回退最近可售月 (PR #2172, Closes #2167)

接口入参 month/startDate/endDate 全改为可选, 响应新增 month 字段
测试服 10/10 case 端到端通过 (含 CUSTOM + GROUP 缺省入参真测)
这个提交包含在:
API Changelog Bot 2026-05-13 11:02:04 +08:00
父节点 998d90d425
当前提交 d7d93b0030

查看文件

@ -0,0 +1,165 @@
---
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`)不写缓存。