diff --git a/changelogs/2026-05/13_improve_product_v2_mp_price_calendar_default_month.md b/changelogs/2026-05/13_improve_product_v2_mp_price_calendar_default_month.md new file mode 100644 index 0000000..1fb6abc --- /dev/null +++ b/changelogs/2026-05/13_improve_product_v2_mp_price_calendar_default_month.md @@ -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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `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`)不写缓存。