diff --git a/changelogs/2026-04/2026-04-18_product-v2_publish-validation-order-detail-deps.md b/changelogs/2026-04/2026-04-18_product-v2_publish-validation-order-detail-deps.md new file mode 100644 index 0000000..f0129b4 --- /dev/null +++ b/changelogs/2026-04/2026-04-18_product-v2_publish-validation-order-detail-deps.md @@ -0,0 +1,227 @@ +# fix(product-v2): 上架校验补齐订单详情依赖字段(7 类新校验) + 订单详情字段补齐 + +> **服务**: hl-product-service-v2 (端口 8083) + hl-order-service-v2 (端口 8084) +> **PR**: #854 https://git.1814.love:8443/wx/HL/pulls/854 +> **Issue**: #846 +> **Merge commit**: `a77c4505` +> **日期**: 2026-04-18 +> **影响范围**: +> - 管理端【产品上架】预检 + 上架接口(校验规则收紧,可能 400) +> - 小程序【订单详情】弹窗(住宿/用车/餐饮/领队 4 处补齐字段) + +--- + +## 一、背景 + +排查发现产品 v2 上架前校验 `ProductValidationService` 原本只做 11 项基础字段校验,对**订单详情 6 个弹窗(门票/住宿/餐饮/用车/领队/摄影)**所需要的关键字段覆盖不全,导致: +- 产品能上架,但用户下单后订单详情字段空白、统计恒为 0、Feign 返回 404 降级 +- 订金配置不完整,下单时 `depositAmount` 落 null +- GROUP 产品可能没班期或班期无领队,订单详情领队弹窗永远"未分配" + +本 PR 同时修复: +1. **P0x3 + P1x4 = 7 类新增上架校验**(收紧规则,老产品可能上不了架) +2. **订单详情依赖的 Feign 快照字段补齐**(5 项 VO 字段补强 + 1 个 internal 接口新增) + +--- + +## 二、上架校验收紧(P0 + P1 共 7 条) + +### 变更接口 + +| # | 接口 | 方法 | 路径 | 变更类型 | +|---|------|------|------|----------| +| 1 | 上架预检 | GET | `/admin/product/{id}/validate-publish` | 返回 issues 列表新增 7 类错误提示 | +| 2 | 上架/下架切换 | POST | `/admin/product/{id}/toggle-publish` | 上架动作先跑 validatePublish,有 issues 则 400 拒绝 | + +### 新增 7 条校验规则 + +| # | 等级 | 触发条件 | 错误提示文案(示例) | +|---|------|---------|---------------------| +| 1 | P0 | `payment_type=DEPOSIT` 时 `depositAmount>0` 和 `depositRatio∈[1,99]` 都没填 | `订金支付方式(payment_type=DEPOSIT)必须配置订金金额(>0)或订金比例(1-99)` | +| 2 | P0 | `payment_type=DEPOSIT` 时 `balance_due_days` 为 null | `订金支付方式(payment_type=DEPOSIT)必须配置尾款到期天数 balance_due_days` | +| 3 | P0 | 行程节点缺少 `HOTEL` 类型 | `缺少酒店节点,订单详情住宿弹窗将为空` | +| 3 | P0 | 行程节点缺少 `RESTAURANT` 类型 | `缺少餐饮节点,订单详情餐饮弹窗将为空` | +| 3 | P0 | 行程节点 `SCENIC` + `ACTIVITY` 合计 = 0 | `缺少景点/活动节点,订单详情门票/活动弹窗将为空` | +| 4 | P1 | SCENIC 节点 `resourceId` 为 null | `SCENIC 景点节点(nodeId=X 名称=Y)缺少 resourceId,订单门票弹窗将无法显示封面/原价` | +| 5 | P1 | `tripNights ≥ 1` 且 非自理早餐(`breakfast ≠ SELF`)天数占比 < 70% | `餐饮配置不足:行程共 N 晚,至少 M 天早餐应该非自理(SELF),当前仅 K 天非自理,订单详情'已含早餐数'统计将不准确` | +| 6 | P1 | `product_route_info.vehicle_model_id` 为 null | `路线总览未配置实际用车车型(product_route_info.vehicle_model_id),订单用车弹窗将无法展示车型信息` | +| 7 | P1 | `product_type=GROUP` 且 `group_tour_batch` 为空 | `GROUP(小蒙马)产品至少需要 1 个班期(group_tour_batch),否则订单领队弹窗将永远为空` | +| 7 | P1 | `product_type=GROUP` 某班期 `group_batch_staff` 中没有 `staff_role=LEADER` 成员 | `班期(batchNo=X batchId=Y)缺少 LEADER 角色成员,订单详情领队弹窗将显示'未分配'` | + +### 校验接口响应示例 + +**成功(全部通过)**: +```json +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": [] +} +``` + +**失败(上架预检返回多条 issues)**: +```json +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": [ + "订金支付方式(payment_type=DEPOSIT)必须配置订金金额(>0)或订金比例(1-99)", + "缺少酒店节点,订单详情住宿弹窗将为空", + "SCENIC 景点节点(nodeId=1234 名称=额尔古纳湿地)缺少 resourceId,订单门票弹窗将无法显示封面/原价", + "GROUP(小蒙马)产品至少需要 1 个班期(group_tour_batch),否则订单领队弹窗将永远为空" + ] +} +``` + +**toggle-publish 触发上架失败时**(HTTP 仍 200,`code` 为业务错误码): +```json +{ + "code": 400, + "success": false, + "message": "上架前校验失败:缺少酒店节点...;路线总览未配置实际用车车型...", + "data": null +} +``` + +### 前端必须配合的改动 + +#### 1. 产品编辑页「上架前」先调 `/validate-publish` 给出整改清单 + +原型建议:点"上架"按钮前先 GET `/admin/product/{id}/validate-publish`,拿到 `data: []` 则允许上架,否则展示 issues 清单供运营整改后再试。 + +```js +// 推荐流程 +const res = await GET(`/admin/product/${id}/validate-publish`); +if (res.data.length === 0) { + await POST(`/admin/product/${id}/toggle-publish`, { remark: '...' }); +} else { + // 弹出整改清单,每行是一条中文错误提示 + showIssuesModal(res.data); +} +``` + +#### 2. 展示多条错误 + +`toggle-publish` 失败时后端的 `message` 会把所有 issues 用 `;` 拼接,前端可以按 `;` 切分展示,或者优先用 `validate-publish` 拿结构化列表。 + +--- + +## 三、兼容性风险(前端 + 运营重点关注) + +### 老产品可能上不了架 + +以下情况产品会被拒绝上架,需要运营**先补数据**: + +| 场景 | 解决 | +|------|------| +| 产品只有 NOTE/CUSTOM/TRANSPORT/FREE 节点 | 至少补 1 个 SCENIC/ACTIVITY + 1 个 HOTEL + 1 个 RESTAURANT 节点 | +| 老 DEPOSIT 产品只填了 `depositAmount`/`depositRatio` 没填 `balanceDueDays` | 编辑页补填尾款到期天数 | +| 老 GROUP 产品没建班期 | 至少创建 1 个班期 | +| 老 GROUP 产品班期没绑定领队 | 每个班期至少加 1 个 `staff_role=LEADER` 的成员 | +| 所有行程早餐都是 SELF(自理) | 至少 70% 的天早餐改成非 SELF(如 HOTEL/CAMP/SPECIAL) | +| SCENIC 景点节点只填了名称没关联景区资源 | 编辑节点绑定 `resourceId` | +| 路线总览没选实际车型 | 路线总览页选车型 | + +### 早餐 70% 校验严格度说明 + +- 5 天行程(`tripNights=4`):至少 `⌈4 × 0.7⌉ = 3` 天早餐非 SELF +- 6 天行程(`tripNights=5`):至少 `⌈5 × 0.7⌉ = 4` 天早餐非 SELF +- 7 天行程(`tripNights=6`):至少 `⌈6 × 0.7⌉ = 5` 天早餐非 SELF(只能有 1 天 SELF) + +前端表单要能保存 `SELF/HOTEL/CAMP/SPECIAL` 四种早餐类型,上架校验才能放行。 + +### 已上架产品不会受影响 + +**只有在「上架」动作时(`toggle-publish` 切到 PUBLISHED)才校验**,已经处于上架状态的老产品没动它,下架再上架才会触发。 + +--- + +## 四、订单详情字段补齐(小程序 MP 端) + +### 变更接口 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 订单详情 | GET | `/mp/order/{orderId}/detail` | 响应字段扩展 | 4 个弹窗 VO 字段补全,数据从产品快照取 | + +具体定位到订单详情 VO 层次: + +#### 1) `MpHotelItemVO`(订单住宿弹窗项)新增字段 + +| 字段 | 类型 | 可空 | 说明 | +|------|------|------|------| +| `hotelType` | String | 是 | 酒店类型(5星/特色民宿/蒙古包等),资源服务降级时可能为 null | + +#### 2) `MpVehicleDetailVO`(订单用车弹窗)新增字段 + +| 字段 | 类型 | 可空 | 说明 | +|------|------|------|------| +| `coverUrl` | String | 是 | 车型封面图 URL(资源服务暂未提供,预留 null) | +| `seatCount` | Integer | 是 | 座位数(来自产品快照 `vehicleModels.seatCount`) | +| `vehicleCategory` | String | 是 | 车辆分类,字典 `vehicle_type`:`CAR`=小轿车 / `SUV`=越野车 / `VAN`=商务车 / `BUS`=大巴 / `MINIBUS`=中巴 | +| `driverYearsRequired` | String | 是 | 驾龄要求(资源服务暂未提供,预留 null) | +| `features` | List\ | 是 | 配置/亮点列表(资源服务暂未提供,预留空数组) | + +#### 3) `MpMealStatsVO.breakfastCount`(订单餐饮统计)行为修复 + +**旧行为**:`breakfastCount` 永远返回 `0`,因为产品 v2 快照没下发 `mealType` 字段,订单 `OrderMealAssignmentService` 无法区分早午晚餐。 + +**新行为**:产品快照补齐 `nodes[].mealType`(`BREAKFAST`/`LUNCH`/`DINNER`) 后,订单端按餐别分桶计数,`breakfastCount` 真实反映"产品内置多少餐早餐"。前端展示文案"已含 N 早"不用改,数值正确即可。 + +#### 4) GROUP 产品订单详情【领队/摄影】弹窗不再永远"未分配" + +**旧行为**:订单服务调 `productFeignClient.getBatchStaff(batchId)` 永远 404,`fallback` 返空 list → 所有 GROUP 订单领队/摄影弹窗显示"未分配"。 + +**新行为**:产品服务新增了 `GET /internal/product/schedule/{batchId}/staff` internal endpoint(详情见后端 changelog),订单服务能拿到真实的班期成员,按 `staffRole` 过滤展示领队/摄影/司机。 + +### 前端展示建议 + +#### 订单住宿弹窗 +原型上要求在酒店名下显示【5 星】【特色民宿】【蒙古包】标签 → 直接用 `hotelType`,为 null 则不展示标签。 + +#### 订单用车弹窗 +原型字段对照: +- "车型名":`vehicleType` +- "座位数":`seatCount`(null → "—") +- "车辆类型":翻译 `vehicleCategory` 字典(null → "—") +- "驾龄要求":`driverYearsRequired`(null → "—"或隐藏) +- "车辆配置":`features`(空数组 → 隐藏) + +--- + +## 五、下单/报价逻辑 + +本 PR **不改下单/报价核心逻辑**,只改: +- 上架阶段收紧校验(卡在 `toggle-publish`) +- 订单详情 `VO` 补字段(从已有产品快照多读几个字段) + +C 端已下单的历史订单,订单详情读的是创建时序列化的 `product_snapshot`,老订单没有新字段,仍会是 null。**仅新下的订单会有完整字段**。 + +--- + +## 六、不受影响的接口 + +- 小程序首页/列表/产品详情:零影响 +- 产品创建/编辑/复制/下架:零影响 +- 产品报价 `/admin/product/item/{id}/quote`:零影响 +- 价格日历 `/admin/product/item/{id}/price-calendar/*`:零影响 + +--- + +## 七、受影响服务 + 重启提示 + +本次需要**同时重启** 2 个服务: +- `hl-product-service-v2`(8083):新增校验 + 新增 internal endpoint +- `hl-order-service-v2`(8084):VO 字段扩展 + Feign 快照反序列化新字段 + +测试环境重启顺序:先 product-service-v2,再 order-service-v2(order 依赖 product 的 internal 接口)。 + +--- + +## 八、相关链接 + +- Issue: https://git.1814.love:8443/wx/HL/issues/846 +- PR: https://git.1814.love:8443/wx/HL/pulls/854 +- Merge commit: `a77c4505`