feat(product-v2): 每日行程里程接口改为 POST 实时试算 (PR #744)

- GET /admin/product/item/{id}/daily-mileage 已删除
- 新增 POST /admin/product/item/{id}/daily-mileage
- 前端在 Step3 路线设计过程中可传当前编辑态节点顺序实时试算
- 不再依赖必须先保存 Step2 行程
- 破坏性变更,前端必须切换新接口
这个提交包含在:
API Changelog Bot 2026-04-17 15:05:24 +08:00
父节点 f0f287105a
当前提交 d14c42fbe6

查看文件

@ -0,0 +1,194 @@
# 每日行程里程接口重构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<Day>` | 是 | `@NotEmpty`,**最多 30 天**`@Size(max=30)` | 按天分组的节点列表 |
### `days[]``Day`(每日行程)
| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| `dayNumber` | `Integer` | 是 | ≥1 | 天序号1,2,3...),与响应中的 `dayNumber` 对应 |
| `nodes` | `List<Node>` | 是 | `@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<List<DailyMileageVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `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。
前端测试前请确认该服务已重启。