# 修复:daily-mileage 节点过滤 & Step5 上架预检必填校验 > **服务**: hl-product-service-v2 (端口 8083) > **PR**: #754 > **Issue**: #751 > **日期**: 2026-04-17 > **影响范围**: Step2 行程编排里程试算、Step5 补充信息上架预检 --- ## 问题 1(P1 阻塞):Step2 行程编排「计算每日里程」接口 400 ### 现象 前端在 Step2 行程编排点"下一步",后端对节点列表里 `resourceId` 为空的自由活动节点返回: ```json { "code": 400, "message": "days[0].nodes[1].resourceId: resourceId不能为空" } ``` ### 根因 `DailyMileageCalcReqVO.NodeRef.resourceId` 挂了 `@NotNull`,但业务上 `FREE/NOTE/TRANSPORT/CUSTOM/PHOTOGRAPHY` 等节点本就不需要 resourceId(它们没有对应的景区/活动/酒店资源)。 ### 修复 - 移除 `@NotNull` 和 `required=true` - 接口层面接受任何节点类型传入;Service 层按节点类型白名单自动过滤,不抛错 --- ## 问题 2(业务规则):距离计算节点范围收窄 + 酒店去重 ### 业务规则 每日驾车距离/时间计算只取:**景区(SCENIC)+ 游玩项目(ACTIVITY)+ 当天第一个酒店(HOTEL)**。 ### 规则前后对比 | 节点类型 | 旧逻辑 | 新逻辑 | |----------|--------|--------| | SCENIC | ✅ 参与 | ✅ 参与 | | ACTIVITY | ✅ 参与 | ✅ 参与 | | HOTEL | ✅ 全部参与 | ⚠️ **每天只保留 sortOrder 最小的那 1 个** | | RESTAURANT | ✅ 参与 | ❌ **不参与** | | SERVICE | ✅ 参与 | ❌ **不参与** | | TRANSPORT / FREE / CUSTOM / NOTE / PHOTOGRAPHY | ❌ 不参与 | ❌ 不参与(无变化) | 举例:当天节点序列 `景区1 → 餐厅 → 景区2 → 酒店A → 酒店B → 活动1` - 旧:6 点参与 → `dist(景1→餐→景2→店A→店B→活1)` - 新:4 点参与 → `dist(景1→景2→店A→活1)`(餐厅/酒店B 过滤) ### 影响接口 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/admin/product/item/{id}/daily-mileage` | Step3 实时试算(前端传节点序列) | | GET | `/admin/product/item/{id}/mileage` | 总里程(基于保存后的节点) | | GET | `/admin/product/item/{id}/daily-mileage-saved` | 基于保存后节点的按天里程 | ### 请求/响应契约 **请求字段(无变化,仅校验收紧/放宽)**: ```json { "days": [ { "dayNumber": 1, "nodes": [ {"nodeType": "SCENIC", "resourceId": 3001}, {"nodeType": "FREE"}, // resourceId 可空 {"nodeType": "RESTAURANT", "resourceId": 4001}, {"nodeType": "HOTEL", "resourceId": 5001}, {"nodeType": "HOTEL", "resourceId": 5002} // 同日第二个酒店 → 过滤 ] } ] } ``` **响应结构(无变化)**: ```json { "code": 200, "success": true, "data": [ {"dayNumber": 1, "mileage": 42.35, "duration": 3600} ] } ``` ### 字段规范 | 字段 | 类型 | 说明 | |------|------|------| | `days[].dayNumber` | Integer | 天序号,从 1 开始 | | `days[].nodes[].nodeType` | String | 节点类型(字典 `product_node_type`):`SCENIC / ACTIVITY / HOTEL / RESTAURANT / SERVICE / TRANSPORT / FREE / CUSTOM / NOTE` | | `days[].nodes[].resourceId` | Long | **只在 SCENIC/ACTIVITY/HOTEL 时必填**;其他节点允许为空 | --- ## 问题 3:Step5 上架预检补必填校验 ### 现象 产品 Step5 补充信息里的「保险方案」「合同方案」「行程保障模板」即使没选,**`validatePublish` 也不报 issue**,导致产品能被提交上架但实际缺字段。 ### 修复 在 **`GET /admin/product/item/{id}/validate-publish`** 的 L4 条款校验阶段,新增 3 条 issue: | 条件 | 新增 issue 文案 | |------|-----------------| | `insuranceNotice` ∈ `{INCLUDED, OPTIONAL}` 且 `insuranceSchemeId == null` | `"保险方案未选择"` | | `contractSchemeId == null`(不论保险类型) | `"合同方案未选择"` | | `guaranteeTemplateId == null`(不论保险类型) | `"行程保障模板未选择"` | ### 响应示例(缺字段时) ```json { "code": 200, "success": true, "data": [ "保险方案未选择", "合同方案未选择", "行程保障模板未选择" ] } ``` ### 保存草稿接口保持宽松 - `PUT /admin/product/item/{id}/supplement` **不新增校验** —— 继续允许分步填写(空字段能保存) - 校验只发生在**上架预检**阶段 ### 字典 insurance_notice | Code | 说明 | |------|------| | `INCLUDED` | 含保险(必选保险方案) | | `OPTIONAL` | 可选保险(必选保险方案) | | `EXCLUDED` | 不含保险(可不选保险方案) | --- ## 前端适配 ### Step2 行程编排 - **无需改动**。前端传原样节点序列即可;后端自动按节点类型过滤不参与计算的节点。 - 旧行为:"自由活动"节点进入里程接口时 400 —— 已修复,不再会出现。 ### Step3 实时里程试算 - 如果前端之前有"过滤掉餐厅/服务"的代码,**可以去除**(后端已处理)。不去除也无影响(重复过滤 = 过滤)。 ### Step5 上架预检 - 前端调 `validate-publish` 时,错误列表里会**新出现**如下 3 个文案,请前端错误映射里补上: - `"保险方案未选择"` - `"合同方案未选择"` - `"行程保障模板未选择"` - 表现为"点上架 → 后端返回这些 issue → 前端提示用户去 Step5 补充"。 --- ## 单元测试 - `AmapDrivingServiceTest`: 21 条(含"同日多酒店取首"、"餐厅/服务过滤"、"跨天多酒店各自取首"三条新增) - `ProductValidationServiceTest`: 14 条(含 insuranceScheme / contractScheme / guaranteeTemplate 缺失各 1 条) ## 测试环境验证 `api.test.1814.love:9443` 实测通过: | 用例 | 结果 | |------|------| | `POST /daily-mileage` 混入 FREE 节点 | 200 ✅ | | `POST /daily-mileage` 同日 3 个 HOTEL | 200(按规则只取首个)✅ | | `POST /daily-mileage` 含 RESTAURANT+SERVICE | 200(已过滤)✅ | | `GET /validate-publish` 缺 3 字段 | 3 条 issue ✅ | ## 重启提示 需要重启 `hl-product-service-v2`(端口 8083 + 8183)—— 通过 Deploy Panel 重新部署最新 jar。