# 【⚠️修改接口·管理后台】终止行程接口改造（资源级退款计算） (#3556)

> **PR**: #3557 | **服务**: hl-order-service-v3 | **更新时间**: 2026-06-06
> ⚠️ **破坏性变更**：入参 / 出参结构均调整，前端必须同步改造。

## 1. 接口背景

终止行程原先只让定制师手填一个返还金额，既不准确也没真正进入核单结算。现改为**资源级退款计算**：定制师在弹框里选择「停在第几天」并逐项标记资源是否已用，提交时后端按实际成交价重新核算退款额并写入核单返还。

## 2. 变更清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 终止行程 | POST | `/v3/admin/order/{id}/terminate` | 修改接口 | 入参从「手填返还额」改为「资源已用判定 + 调整额」；出参补退款明细字段 |

## 3. 接口详情

### 3.1 终止行程

- **使用场景**：出行中订单提前终止时调用，配合「终止退款预览」接口（见同日新增接口 changelog）使用
- **认证**：需要管理端 JWT（`Authorization` 头）
- **幂等性**：是（同一订单二次调用因状态已变为「已完成」返回 581018）
- **限流**：无

## 4. 接口入参

### 4.1 路径参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | String | ✅ | 订单 ID |

### 4.2 请求体字段

| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| cancelReason | String | ✅ | 终止原因（调整金额的说明也写在此处，不再单列字段） | 非空 |
| endDayNumber | Integer | ✅ | 停在第几天 | 1 ≤ 值 ≤ 行程总天数 |
| rooms | Array<{refId, used}> | ✅ | 住宿各行已用判定 | — |
| tickets | Array<{refId, used}> | ✅ | 门票各行已用判定 | — |
| vehicles | Array<{refId, dayNumber, used}> | ✅ | 用车各行已用判定（同车跨天靠 dayNumber 区分） | — |
| adjustAmount | BigDecimal | ❌ | 人工调整额（正=多退，负=少退），默认 0 | — |

> `rooms[].refId` / `tickets[].refId` / `vehicles[].refId` 取自「终止退款预览」接口返回的 `refId`。
> 请求**只需传**资源是否已用（`used`）+ 调整额，**不传任何金额单价**——退款额由后端按预览同源的成交价重新核算（防篡改）。保险不在入参，由后端自动按锁定不退处理。

### 4.2.1 rooms / tickets 行（ResourceUsedItem）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| refId | String | ✅ | 资源记录 ID |
| used | Boolean | ✅ | 该项是否已使用（true=已用不退 / false=未用可退） |

### 4.2.2 vehicles 行（VehicleUsedItem）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| refId | String | ✅ | 用车记录 ID |
| dayNumber | Integer | ✅ | 第几天 |
| used | Boolean | ✅ | 当天该车是否已使用 |

## 5. 出参（响应）

### 5.1 响应字段（`data`）

| 字段 | 类型 | 说明 |
|------|------|------|
| terminateRefundId | String | 终止退款记录 ID |
| baselineRefund | BigDecimal | 系统按已用资源算出的建议退款额 |
| adjustAmount | BigDecimal | 人工调整额（回显入参） |
| finalRefund | BigDecimal | 最终退款额（= baselineRefund + adjustAmount，最低 0） |
| settlementRefundId | String | 核单返还记录 ID（已写入核单 Step5 返还，供财务复核展示） |
| newStatus | String | 终止后订单状态（`COMPLETED`=已完成） |
| newFlowStatus | String | 终止后流程状态（`PENDING_REVIEW`=待核单） |

## 6. 枚举 / 数据字典

### 6.1 newStatus（订单状态）

**所属字段**：`newStatus` | **类型**：`String`

| 值 | 中文 | 说明 |
|----|------|------|
| `COMPLETED` | 已完成 | 终止后订单进入完成态，转入核单流程 |

### 6.2 newFlowStatus（流程状态）

**所属字段**：`newFlowStatus` | **类型**：`String`

| 值 | 中文 | 说明 |
|----|------|------|
| `PENDING_REVIEW` | 待核单 | 终止后等待核单录入与结算复核 |

## 7. 错误码

| code | 含义 | 触发场景 |
|------|------|----------|
| 581018 | 出行中取消仅适用于出行中订单 | 订单当前状态非「出行中」时调用（含二次终止） |

## 8. 示例

### 8.1 典型成功

**请求**：
```
POST /v3/admin/order/60001234567890/terminate
Authorization: Bearer {token}
```
```json
{
  "cancelReason": "客户中途高反送医终止；林芝段酒店已付全款不可退，调整 -300",
  "endDayNumber": 3,
  "rooms": [
    {"refId": "96011", "used": true},
    {"refId": "96014", "used": false}
  ],
  "tickets": [
    {"refId": "97001", "used": true}
  ],
  "vehicles": [
    {"refId": "98001", "dayNumber": 1, "used": true},
    {"refId": "98001", "dayNumber": 2, "used": true},
    {"refId": "98001", "dayNumber": 3, "used": true}
  ],
  "adjustAmount": -300.00
}
```

**响应**：
```json
{
  "code": 200,
  "message": "ok",
  "success": true,
  "data": {
    "terminateRefundId": "9610000001",
    "baselineRefund": 2264.00,
    "adjustAmount": -300.00,
    "finalRefund": 1964.00,
    "settlementRefundId": "9620000001",
    "newStatus": "COMPLETED",
    "newFlowStatus": "PENDING_REVIEW"
  }
}
```

### 8.2 边界情况

**场景说明**：不填调整额（默认 0），退款额 = 基线额。

**请求（片段）**：
```json
{ "cancelReason": "客户主动终止", "endDayNumber": 5, "rooms": [], "tickets": [], "vehicles": [] }
```

**响应（片段）**：
```json
{ "code": 200, "data": { "baselineRefund": 0.00, "adjustAmount": 0.00, "finalRefund": 0.00, "newStatus": "COMPLETED", "newFlowStatus": "PENDING_REVIEW" } }
```

### 8.3 业务失败（异常）

**场景说明**：订单非出行中（如已确认待出发或已终止过）。

**响应**：
```json
{ "code": 581018, "message": "出行中取消仅适用于出行中订单", "success": false }
```

## 9. 业务边界

- ✅ **适用场景**：订单当前状态 = 出行中（TRAVELLING）
- ❌ **不适用场景**：未出发订单请走「取消订单」流程；已终止/已完成订单二次调用返回 581018
- ⚠️ **特殊边界**：
  - `finalRefund` 最低为 0，不会出现负数
  - 退款额最终进入核单返还，由财务复核环节确认放款
  - 保险恒不退，无需在入参体现

## 10. 修改前后对比

### 10.1 字段级对比

**入参**：

| 字段 | 改前 | 改后 |
|------|------|------|
| cancelReason | ✅ 终止原因 | ✅ 终止原因（+ 调整说明并入此处） |
| returnAmount | ✅ 手填返还金额（必填） | ❌ **删除**（改由后端按资源核算） |
| returnRemark | ✅ 返还备注（必填） | ❌ **删除**（说明并入 cancelReason） |
| endDayNumber | — | ✅ **新增**，停在第几天 |
| rooms / tickets / vehicles | — | ✅ **新增**，各资源行已用判定 |
| adjustAmount | — | ✅ **新增**（可选），人工调整额 |

**出参**：

| 字段 | 改前 | 改后 |
|------|------|------|
| settlementRefundId | ✅（此前恒为 null） | ✅ 真实核单返还记录 ID |
| returnAmount | ✅ 返还额（回显入参） | ❌ **删除** |
| newStatus | ✅ | ✅（不变） |
| terminateRefundId | — | ✅ **新增** |
| baselineRefund / adjustAmount / finalRefund | — | ✅ **新增**，退款明细金额 |
| newFlowStatus | — | ✅ **新增** |

### 10.2 行为级对比

| 行为 | 改前 | 改后 |
|------|------|------|
| 退款额来源 | 定制师手填一个数 | 按已用资源 + 实际成交价由后端核算 |
| 退款是否进核单 | 否（仅记录，未落账） | 是（写入核单返还，财务复核放款） |
| 配合接口 | 无 | 需先调「终止退款预览」拿资源清单 |

## 11. 影响评估 / 回滚

### 11.1 影响评估

- **是否破坏向后兼容**：是。入参删除 `returnAmount` / `returnRemark`，新增多个必填字段；出参删除 `returnAmount`。前端按旧契约调用会校验失败。
- **前端是否必须同步上线**：是。终止行程页面需改造为「预览拉清单 → 选结束天 + 勾选已用 → 提交」流程。
- **前端 workaround 清理点**：若此前有「手填返还额」输入框，应替换为资源勾选界面；旧的 returnAmount/returnRemark 入参与回显逻辑可删除。

### 11.2 回滚方案

- **回滚方式**：revert PR #3557
- **回滚后清理**：无需数据迁移（新功能，无历史数据依赖）

## 12. 注意事项

- ⚠️ 本接口为**破坏性改动**，前端务必在后端上线同期改造，否则终止行程功能不可用。
- 必须配合同日「终止退款预览」新增接口使用：预览拿全部资源清单 → 前端本地按结束天标已用/可退 → 提交时只回传 `used` 判定与 `adjustAmount`。

## 13. 关联 / 联系人

### 13.1 链接

- **Issue**: [#3556](https://git.1814.love:8443/wx/HL/issues/3556)
- **PR**: [#3557](https://git.1814.love:8443/wx/HL/pulls/3557)
- **Merge commit**: [773dcdacf](https://git.1814.love:8443/wx/HL/commit/773dcdacf)
- **配套新增接口**: 终止退款预览（同日 changelog）

### 13.2 联系人

- **后端负责人**: @yst
- **前端对接（管理后台）**: 终止行程弹框页面
