# 每日行程里程接口重构:GET 改 POST,前端传节点顺序实时试算 > **服务**: hl-product-service-v2 (端口 8083,经网关 8080) > **PR**: #744 > **Issue**: #737 > **提交**: `7143671a` > **日期**: 2026-04-17 > **类型**: refactor (破坏性变更) > **影响范围**: 管理端产品编辑 Step3「路线设计」 --- ## ⚠️ 破坏性变更(Breaking Change)⚠️ **旧的 `GET /admin/product/item/{id}/daily-mileage` 接口已删除**,前端必须切换到新的 `POST` 接口,否则页面上的里程/时长数据将无法获取。 --- ## 改动概述 每日行程距离/时长的计算接口,由"后端从 DB 读取已保存的节点"改为"前端把当前编辑态的节点顺序传过来",使得 **Step3 路线设计过程中即可实时试算,不再依赖必须先保存 Step2**。 --- ## 使用场景变化 | 场景 | 旧方案 | 新方案 | |------|--------|--------| | 触发时机 | 必须先保存 Step2 行程,再点按钮查询 | Step3 路线设计过程中随时触发 | | 数据来源 | 后端从 DB 按产品 ID 读取已保存节点 | 前端将"编辑态"节点顺序传给后端 | | 前端体验 | 改了顺序必须保存才能看新距离 | 拖动节点后立即可试算 | --- ## 旧接口(已删除) | 方法 | 路径 | |------|------| | ~~GET~~ | ~~`/admin/product/item/{id}/daily-mileage`~~ | 旧接口无请求体,后端从 DB 读取。**调用会返回 404/405,请立即切换**。 --- ## 新接口 | 方法 | 路径 | |------|------| | POST | `/admin/product/item/{id}/daily-mileage` | **路径参数**: - `id`:产品 ID(`Long`,Path Variable),保持与其他接口一致;当前后端不依赖它从 DB 读节点,仅用于日志/鉴权上下文 **请求体**:`DailyMileageCalcReqVO` --- ## 请求体字段表 ### 顶层:`DailyMileageCalcReqVO` | 字段 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------| | `days` | `List` | 是 | `@NotEmpty`,**最多 30 天**(`@Size(max=30)`) | 按天分组的节点列表 | ### `days[]`:`Day`(每日行程) | 字段 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------| | `dayNumber` | `Integer` | 是 | ≥1 | 天序号(1,2,3...),与响应中的 `dayNumber` 对应 | | `nodes` | `List` | 是 | `@NotEmpty`,**单日最多 50 个**(`@Size(max=50)`) | 该天的节点列表,**按驾车顺序** | ### `days[].nodes[]`:`Node`(节点) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `nodeType` | `String` | 是 | 节点类型,见下方「节点类型处理规则」 | | `resourceId` | `Long` | 是 | 资源 ID,对应资源服务主键 | --- ## 节点类型处理规则 **只有以下 5 种"有坐标"的节点会被计算距离**: | nodeType | 说明 | |----------|------| | `SCENIC` | 景点 | | `ACTIVITY` | 活动 | | `HOTEL` | 酒店 | | `RESTAURANT` | 餐厅 | | `SERVICE` | 服务/服务区 | **以下类型会被静默忽略**(传了也不报错,但不参与计算): | nodeType | 说明 | |----------|------| | `TRANSPORT` | 交通段 | | `PHOTOGRAPHY` | 拍摄/打卡 | | `FREE` | 自由活动 | | `CUSTOM` | 自定义 | | `NOTE` | 备注 | 前端可以放心把整天的节点全部传过来,后端会自行过滤。 --- ## 响应字段表 **响应结构不变**:`Result>` | 字段 | 类型 | 说明 | |------|------|------| | `dayNumber` | `Integer` | 天序号,与请求中的 `dayNumber` 一一对应 | | `mileage` | `BigDecimal` | 里程,单位:**公里(km)**,保留 1 位小数 | | `duration` | `Integer` | 驾车时长,单位:**分钟(min)** | --- ## 请求示例 ```http POST /admin/product/item/123/daily-mileage Content-Type: application/json ``` ```json { "days": [ { "dayNumber": 1, "nodes": [ { "nodeType": "SCENIC", "resourceId": 3001 }, { "nodeType": "ACTIVITY", "resourceId": 5001 }, { "nodeType": "RESTAURANT", "resourceId": 6001 }, { "nodeType": "HOTEL", "resourceId": 7001 } ] }, { "dayNumber": 2, "nodes": [ { "nodeType": "HOTEL", "resourceId": 7001 }, { "nodeType": "SCENIC", "resourceId": 3002 }, { "nodeType": "SERVICE", "resourceId": 8001 }, { "nodeType": "SCENIC", "resourceId": 3003 } ] } ] } ``` --- ## 响应示例 ```json { "code": 0, "data": [ { "dayNumber": 1, "mileage": 52.3, "duration": 78 }, { "dayNumber": 2, "mileage": 143.7, "duration": 195 } ], "msg": "" } ``` --- ## 前端使用建议(Step3 路线设计实时试算) 1. **触发时机**:用户在 Step3 拖动/增删节点后(推荐 debounce 300~500ms 再请求),或点击"重新计算里程"按钮 2. **数据来源**:直接用前端当前"编辑态"的节点顺序组装请求体,**不需要先保存草稿** 3. **渲染**:按 `dayNumber` 匹配回请求中的每一天,展示 `mileage` 公里 + `duration` 分钟 4. **类型过滤**:无需前端过滤节点类型,整天节点直接传即可 5. **空值处理**:如果某一天节点都没有坐标(例如全是 FREE/CUSTOM),后端会返回 `mileage=0, duration=0` --- ## 校验约束汇总 | 规则 | 违反时错误码 | |------|-------------| | `days` 不能为空 | 400 参数校验失败 | | `days` 最多 30 天 | 400 参数校验失败 | | `days[].nodes` 不能为空 | 400 参数校验失败 | | `days[].nodes` 单日最多 50 个 | 400 参数校验失败 | --- ## 需要重启的服务 **hl-product-service-v2**(端口 8083,经网关 8080)—— 通过 Deploy Panel 重新部署最新 jar。 前端测试前请确认该服务已重启。