# 每日行程里程接口重构：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。

前端测试前请确认该服务已重启。
