# 订单详情「终止行程」接口终态汇总（出行中订单）—— 管理后台

- 端类型：管理后台
- 变更类型：修改接口（**终态汇总篇**，无新契约变化；合并 #3556 系列 3 篇 + #3965 金额 String 化）
- 关联 Issue：#3556　PR：#3557（主改造）/ #3559（预览状态校验加固）/ #3965（金额 String 化）
- 日期：2026-06-26

---

## ① 接口背景

「终止行程」用于**出行中（TRAVELLING）订单**提前中止：定制师按"停在第几天 + 逐项资源是否已用"提交，后端按实际成交价权威核算退款额、写入核单返还，订单转为「已完成→待核单」。

此前该能力的 changelog 分散在 3 篇（接口改造 / 退款预览新增 / 旧路径下线），且金额格式在 #3965 后由数字改为字符串。**本篇为一处对接的终态汇总**，前端按此一份即可对接，旧 3 篇仅作历史留档。

> ⚠️ 旧路径 `POST /v3/admin/order/{id}/cancel/on-trip` **已删除**，调用返回 404，必须改调 `/terminate`。
> ⚠️ 所有 Long ID 与金额均**字符串化**返回（防 JS 精度丢失），请求传金额相关 ID 也用字符串。

---

## ② 变更清单（两个接口配套使用）

| # | 方法 | 路径 | 用途 |
|---|---|---|---|
| 1 | POST | `/v3/admin/order/{id}/terminate/refund-preview` | 终止退款预览（拿全资源清单，前端本地算退款） |
| 2 | POST | `/v3/admin/order/{id}/terminate` | 终止行程（提交执行，落核单返还） |

统一响应 `Result<T>`：`{ code, message, data, success }`，`code=200` 成功。认证：管理端 JWT（`Authorization` 头）。

**对接流程**：点「终止行程」→ 调①拿整趟资源清单 → 前端按所选结束天本地标已用/可退并算退款 → 调②只回传 `used` 判定 + `adjustAmount`（不传金额单价，后端权威重算防篡改）。

---

## ③ 接口 1：终止退款预览

```
POST /v3/admin/order/{id}/terminate/refund-preview
```
- 入参：仅 path `id`（订单 ID），**无请求体**。一次性拿整趟全部资源，结束天选择/已用标记/退款计算全在前端本地完成，不需每选一天再调。
- 幂等：是（只读，不落库）。

### 出参 `Result<OrderTerminateRefundPreviewRespVO>`

| 字段 | 类型 | 说明 |
|---|---|---|
| paidAmount | String(金额) | 客户已付金额（前端算退款基线用） |
| tripDays | Integer | 行程总天数（画时间轴） |
| departDate | String(date) | 出发日期 `yyyy-MM-dd`（第 N 天日期 = 出发日 + N−1） |
| tripCurrentDay | Integer | 当前到第几天（时间轴默认选中点；出发前/未知=1） |
| rooms | Array&lt;Item&gt; | 住宿清单（每晚×每组房一行） |
| tickets | Array&lt;Item&gt; | 门票清单（每景点每天一行） |
| vehicles | Array&lt;Item&gt; | 用车清单（**整车按天展开，每天一行**，同段车 refId 相同） |
| insurance | Array&lt;Item&gt; | 保险清单（整单 1 行，locked=true 锁定不退） |

**Item（TerminateRefundItemVO，四类资源行通用）**

| 字段 | 类型 | 说明 |
|---|---|---|
| refId | String | 资源配单记录 ID（提交终止时按此回传已用判定；用车多天共用同一 refId 靠 dayNumber 区分；保险可能为 null） |
| name | String | 资源名称快照（酒店名/景点名/车型/保险产品名） |
| dayNumber | Integer | 对应第几天（保险为 null） |
| dealPrice | String(金额) | 成交单价（住宿=每间每晚 / 门票=每张 / 用车=每天每辆日租 / 保险=保费） |
| quantity | Integer | 数量（住宿=间数 / 门票=张数 / 用车=车辆数 / 保险=1） |
| totalAmount | String(金额) | 本行小计 = dealPrice × quantity |
| locked | Boolean | 是否锁定不退（保险恒 true，其余 false） |
| lockedReason | String | 锁定原因（如"保险已生效不退"；未锁定为 null） |

> 响应**不含**"是否已用"标记与"退款合计"——由前端按所选结束天（`dayNumber ≤ 结束天` 默认已用，之后默认可退）实时计算。

---

## ④ 接口 2：终止行程（执行）

```
POST /v3/admin/order/{id}/terminate
Content-Type: application/json
```
- 幂等：是（订单一旦终止变 COMPLETED，二次调用因状态非 TRAVELLING 返 581018）。

### 入参 Body（OrderTerminateTripReqVO）

| 字段 | 类型 | 必填 | 说明 | 校验 |
|---|---|:--:|---|---|
| cancelReason | String | ✅ | 终止原因（调整额说明并入此处，不单列字段） | 非空 |
| endDayNumber | Integer | ✅ | 停在第几天（截断点，1-based） | ≥ 1 ≤ 行程总天数 |
| rooms | Array&lt;{refId, used}&gt; | ✅ | 住宿各行已用判定（列表非 null，可空数组） | — |
| tickets | Array&lt;{refId, used}&gt; | ✅ | 门票各行已用判定 | — |
| vehicles | Array&lt;{refId, dayNumber, used}&gt; | ✅ | 用车各行已用判定（同车跨天靠 dayNumber 区分） | — |
| adjustAmount | String(金额) | ❌ | 人工调整额（±，正=多退 负=少退），默认 0 | — |

**rooms / tickets 行（ResourceUsedItem）**：`refId`(String,必填) + `used`(Boolean,必填 true=已用不退/false=未用可退)
**vehicles 行（VehicleUsedItem）**：`refId`(String,必填) + `dayNumber`(Integer,必填≥1) + `used`(Boolean,必填)

> `refId` 全部取自接口①返回值。入参**只传 `used` 判定 + `adjustAmount`，不传任何金额单价**——退款额由后端按预览同源成交价权威重算。保险不在入参，后端自动按锁定不退处理。

### 出参 `Result<OrderTerminateTripRespVO>`

| 字段 | 类型 | 说明 |
|---|---|---|
| terminateRefundId | String | 终止退款主表 ID |
| baselineRefund | String(金额) | 系统建议基线 = max(0, paidAmount − usedAmount) |
| adjustAmount | String(金额) | 人工调整额（回显入参，默认 0） |
| finalRefund | String(金额) | 最终返还额 = max(0, baselineRefund + adjustAmount)，最低 0 |
| settlementRefundId | String | 核单返还记录 ID（核单模块写入后回填） |
| newStatus | String | 终止后订单粗状态（恒 COMPLETED） |
| newFlowStatus | String | 终止后流程细状态（恒 PENDING_REVIEW） |

---

## ⑤ 枚举 / 数据字典

- newStatus：`COMPLETED` 已完成（终止后进完成态转核单，**不再是旧的 CANCELLED**）
- newFlowStatus：`PENDING_REVIEW` 待核单（等核单录入与结算复核）
- 资源分类：通过 rooms/tickets/vehicles/insurance 四数组区分，无独立 resourceType 字段
- locked：true=锁定不退（保险恒 true）/ false=可退

---

## ⑥ 错误码

| code | 含义 | 触发场景 |
|---|---|---|
| 581007 | 订单不存在 | id 无对应订单 |
| 581018 | 出行中取消仅适用于出行中订单 | 订单当前状态非 TRAVELLING（未出发/已终止/已完成，含二次终止） |

---

## ⑦ 示例

### 接口①响应（预览，节选）
```json
{ "code": 200, "success": true, "data": {
  "paidAmount": "6000.00", "tripDays": 7, "departDate": "2026-05-12", "tripCurrentDay": 3,
  "rooms": [ {"refId":"96011","name":"拉萨瑞吉·大床房","dayNumber":1,"dealPrice":"1280.00","quantity":1,"totalAmount":"1280.00","locked":false,"lockedReason":null} ],
  "vehicles": [ {"refId":"98001","name":"丰田赛那 商务车","dayNumber":1,"dealPrice":"600.00","quantity":1,"totalAmount":"600.00","locked":false,"lockedReason":null} ],
  "insurance": [ {"refId":null,"name":"安联境内旅行险·尊享版","dayNumber":null,"dealPrice":"256.00","quantity":1,"totalAmount":"256.00","locked":true,"lockedReason":"保险已生效不退"} ]
} }
```

### 接口②请求（终止）
```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, "success": true, "data": {
  "terminateRefundId": "9610000001", "baselineRefund": "2264.00", "adjustAmount": "-300.00",
  "finalRefund": "1964.00", "settlementRefundId": "9620000001",
  "newStatus": "COMPLETED", "newFlowStatus": "PENDING_REVIEW" } }
```
### 边界（不填调整额，默认 0）
```json
请求: { "cancelReason":"客户主动终止","endDayNumber":5,"rooms":[],"tickets":[],"vehicles":[] }
响应: { "code":200,"data":{ "baselineRefund":"0.00","adjustAmount":"0.00","finalRefund":"0.00","newStatus":"COMPLETED","newFlowStatus":"PENDING_REVIEW" } }
```
### 异常 —— 调旧路径（前端联调易踩）
```
POST /v3/admin/order/{id}/cancel/on-trip  →  HTTP 404 Not Found
```
旧路径后端无任何映射，不做 301/308 跳转，必须改调 `/terminate`。
### 异常 —— 订单非出行中
```json
{ "code": 581018, "message": "出行中取消仅适用于出行中订单", "success": false }
```

---

## ⑧ 业务边界

- ✅ 适用：订单当前粗状态 = TRAVELLING（出行中）
- ❌ 不适用：未出发订单走「取消订单」流程（`cancel/pre-trip`）；已终止/已完成二次调用返 581018
- ⚠️ finalRefund 最低 0，不会负数；退款额进核单返还由财务复核放款；保险恒不退
- ⚠️ 终止后订单是「已完成（待核单）」而非「已取消」，列表/详情状态展示需按新枚举对齐

---

## ⑨ 修改前后对比（相对最早 cancel/on-trip 旧实现）

| 维度 | 改前 | 改后（终态） |
|---|---|---|
| 路径 | `POST /{id}/cancel/on-trip` | `POST /{id}/terminate`（旧路径 404） |
| 退款预览 | 无独立预览 | `POST /{id}/terminate/refund-preview`（新增） |
| 退款额来源 | 定制师手填一个数 | 后端按已用资源 + 成交价权威核算 |
| 退款是否进核单 | 否（仅记录） | 是（写核单返还，财务复核放款） |
| 入参 | returnAmount/returnRemark 手填 | endDayNumber + rooms/tickets/vehicles 已用判定 + adjustAmount |
| 出参 | returnAmount 回显 | terminateRefundId/baselineRefund/adjustAmount/finalRefund/settlementRefundId/newFlowStatus |
| 金额/ID 格式 | 数字 | **字符串（#3965 后）** |
| 终止后状态 | CANCELLED 已取消 | COMPLETED 已完成 → PENDING_REVIEW 待核单 |

---

## ⑩ 影响评估 / 回滚

- 接口已稳定上线（#3557/#3559/#3965 均已合并部署），本篇为终态汇总，不含新后端改动，无回滚项。
- 前端「终止行程」页面对接：① 进入调①拿资源清单 → ② 选结束天 + 勾选已用 + 可选调整额 → ③ 调②提交 → ④ 终态按「已完成/待核单」展示并刷新详情。
- 旧 3 篇（`06_3556_终止行程接口改造`、`06_3556_终止退款预览`、`11_3556_出行中取消路径下线`）保留历史留档，对接以本篇为准。

---

## ⑪ 注意事项

- Long ID + 金额（terminateRefundId/baselineRefund/finalRefund/paidAmount/refId 等）均字符串化，请求/解析均按字符串。
- 写操作需前端按钮防重复点击 + loading。
- 必须先调预览拿 refId，再提交终止；提交不传金额单价（后端权威重算）。

---

## ⑫ 关联 / 联系人

- Issue：https://git.1814.love:8443/wx/HL/issues/3556
- PR：https://git.1814.love:8443/wx/HL/pulls/3557 ・ https://git.1814.love:8443/wx/HL/pulls/3559 ・ https://git.1814.love:8443/wx/HL/pulls/3965
- 历史留档：`changelogs-v2/2026-06/06_3556_终止行程接口改造-修改接口-管理后台.md` 等 3 篇
- 后端负责人：腰苏图
- 接口已部署测试服，Knife4j 可见（`/v3/admin/order/{id}/terminate`、`/terminate/refund-preview`）。
