# 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\<String\> | 是 | 配置/亮点列表(资源服务暂未提供,预留空数组) |

#### 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`
