---
schema: "hl-changelog/v2"
ticket: "5598"
title: "终止行程重复提交改一律 581049：COMPLETED 订单再次提交不再重放首次结果"
consumer: "admin"
change_type: "修改接口"
author: "yst(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui"
frontend_ref: "hl-admin@f1874ef91e12e49cf9014f4ad17b008fe08996aa"
target_release: ""
verified_at: ""
status_note: "PR #5604 已合并 dev-v3 并部署 TEST，网关实测：COMPLETED 订单再次提交 terminate 一律返回 581049「订单已终止，禁止重复提交」（同正文也不再 200 重放）。前端如依赖 #5460 的同正文重放取回结果逻辑需适配。"
updated_at: "2026-08-07"
base: "dev-v3"
---

# 🔧【修改接口·管理后台】终止行程重复提交改一律 581049 (#5598)

> **PR**：[#5604](https://git.1814.love:8443/wx/HL/pulls/5604) ｜ **服务**：`hl-order-service-v3` ｜ **更新时间**：2026-08-06 ｜ **消费端**：管理后台

## 1. 接口背景

终止行程（出行中提前结束订单）是**有副作用的写操作**：首次提交会把订单从 TRAVELLING 推进到 COMPLETED、生成终止退款单、写核单退款关联。此前（#5460）对"首次成功但响应丢失"的场景提供了幂等重放：同正文重放返回 200 和既有退款结果。实测中发现该重放语义让"重复提交"与"首次提交"边界模糊，且重放判定依赖请求指纹快照，对升级前的历史订单还要做语义等价兜底，复杂度高、收益低。

本次收紧为**状态机幂等**：订单一旦 COMPLETED（已终止/已完成），再次提交终止**一律拒绝**，返回 `581049「订单已终止，禁止重复提交」`，零副作用（不重写退款、不重发事件、不做指纹比对）。

**首次正常终止（TRAVELLING → COMPLETED）行为完全不变**，仍返回 200 和退款结果。

## 变更接口清单

| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 终止行程（出行中） | `POST` | `/v3/admin/order/:id/terminate` | 修改 | COMPLETED 订单重复提交：同正文重放由 `200` 返回既有结果改为一律 `581049`；581049 文案由「终止行程重试与首次终止边界或请求不一致」改为「订单已终止，禁止重复提交」 |

入参字段结构、出参字段结构**均无变化**。

## 3. 接口详情

### 3.1 终止行程（出行中）

- **接口说明**：出行中因特殊原因提前终止订单，订单进入 COMPLETED 状态，由核单/结算流程继续处理；后端按权威数据重算退款金额。
- **使用场景**：管理后台订单详情页，出行中订单执行"终止行程"操作。
- **认证**：需要管理后台登录态和订单操作权限；房务角色不可操作。
- **幂等性**：**不再提供重放幂等**。首次提交成功即生效；订单 COMPLETED 后任何再次提交（无论正文是否与首次一致）一律返回 `581049`，零副作用。
- **限流**：未声明接口级独立限流规则。

## 4. 接口入参

### 4.1 路径参数 / Query 参数

| 字段 | 位置 | 类型 | 必填 | 说明与校验 |
|---|---|---|---|---|
| `id` | Path | Long | 是 | 订单 ID（路径写作 `:id`） |

无 Query 参数。

### 4.2 请求体字段

请求体类型：`OrderTerminateTripReqVO`（**本次无变化**）。

| 字段 | 类型 | 必填 | 说明与校验 |
|---|---|---|---|
| `cancelReason` | String | 是 | 终止原因 + 调整说明（合并，不单列 adjustReason） |
| `endDayNumber` | Integer | 是 | 停在第几天（截断点，1-based），最小 1 |
| `lineUsages[]` | Array | 否 | 统一资源行已用判定列表；优先级高于旧分组数组，`lineKey` 来自 refund-preview 的 `refundLines[].lineKey` |
| `lineUsages[].lineKey` | String | 是（数组内） | 退款资源行 key，示例 `ITINERARY_NODE:97010:2` |
| `lineUsages[].used` | Boolean | 是（数组内） | 是否已使用 |
| `rooms[]` | Array | 否 | 住宿已用判定列表；兼容旧入参，新逻辑优先使用 `lineUsages` |
| `tickets[]` | Array | 否 | 门票/活动已用判定列表；兼容旧入参，`refId` = 行程节点 nodeId |
| `services[]` | Array | 否 | 服务已用判定列表；兼容旧入参，`refId` = 行程节点 nodeId |
| `supplies[]` | Array | 否 | 备品已用判定列表；兼容旧入参，`refId` 与预览返回一致 |
| `rooms[]/tickets[]/services[]/supplies[]` 元素 | Object | — | 结构为 refId Long 必填 + used Boolean 必填 |
| `vehicles[]` | Array | 否 | 用车每天已用判定列表；仅兼容接收，`used` 不参与计算，后端按车辆 serviceDate 与终止日权威重算 |
| `vehicles[].refId` | Long | 是（数组内） | 配车记录 ID（同段车跨天 refId 相同，靠 dayNumber 区分） |
| `vehicles[].dayNumber` | Integer | 是（数组内） | 第几天（1-based） |
| `vehicles[].used` | Boolean | 是（数组内） | 兼容字段：必填，但车辆退款金额不采用该值 |
| `adjustAmount` | Decimal | 否 | 人工调整额（允许负，默认 0；说明并入 cancelReason） |

## 5. 出参字段

响应类型：`Result<OrderTerminateTripRespVO>`（**本次无变化**）。

### 5.1 统一响应外层

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `code` | Integer | 否 | 成功为 `200`；失败见第 7 节 |
| `message` | String | 否 | 结果说明 |
| `data` | Object/null | 失败时为空 | 成功时为终止结果 |
| `traceId` | String | 是 | 链路追踪 ID |
| `success` | Boolean | 否 | `code=200` 时为 `true` |

### 5.2 成功响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `terminateRefundId` | String(Long) | 否 | 终止退款主表 ID，按字符串返回 |
| `baselineRefund` | String(Decimal) | 否 | 系统建议基线 = max(0, 已付金额 - 已用金额)，按字符串返回 |
| `adjustAmount` | String(Decimal) | 否 | 人工调整额（入参回显，默认 0），按字符串返回 |
| `finalRefund` | String(Decimal) | 否 | 最终返还额 = max(0, baselineRefund + adjustAmount)，按字符串返回 |
| `settlementRefundId` | String(Long) | 是 | 关联的核单退款结算 ID（核单模块写入后回填），按字符串返回 |
| `newStatus` | String | 否 | 终止后订单粗状态，固定 `COMPLETED` |
| `newFlowStatus` | String | 否 | 终止后流程细状态，固定 `PENDING_REVIEW` |

## 6. 枚举 / 数据字典

### 6.1 `data.newStatus`（订单粗状态，本接口相关取值）

| 值 | 中文 | 说明 |
|---|---|---|
| `TRAVELLING` | 出行中 | 终止前状态；只有该状态可首次提交终止 |
| `COMPLETED` | 已完成 | 终止后状态；该状态下再次提交一律 581049 |

### 6.2 `data.newFlowStatus`（流程细状态）

| 值 | 中文 | 说明 |
|---|---|---|
| `PENDING_REVIEW` | 待核单 | 终止成功后进入核单流程 |

本次无数值新增/删除/改义。

## 7. 错误码

| code | 含义 | 触发场景 |
|---|---|---|
| `200` | 终止成功 | 订单为 TRAVELLING 且校验通过；订单推进 COMPLETED 并生成终止退款 |
| `400` | 请求参数错误 | 缺 `cancelReason`/`endDayNumber`、数组内必填字段缺失等 |
| `581007` | 订单不存在 | `id` 对应订单不存在 |
| `581018` | 出行中取消仅适用于出行中订单 | 订单非 TRAVELLING 且非 COMPLETED（待出行/已取消等）时提交终止 |
| `581044` | 终止行程：结束日之前的资源必须标记为已使用 | 结束日前资源 used=false |
| `581047` | 终止行程：结束日不在订单行程范围内 | `endDayNumber` 越界 |
| `581048` | 终止行程：本地车辆费用服务日与订单行程不一致 | 车辆费用服务日与订单行程对不上 |
| **`581049`** | **订单已终止，禁止重复提交**（原「终止行程重试与首次终止边界或请求不一致」） | **订单已 COMPLETED 时再次提交终止，无论正文是否与首次一致** |
| `584100` | 车务车辆总车费暂时不可用，请稍后重试 | Fleet 车辆费用事实不可用，fail closed |

## 8. 示例

### 8.1 典型成功：出行中订单首次终止（行为不变）

**请求**：

```http
POST /v3/admin/order/2084280231786430466/terminate
Authorization: Bearer <管理后台访问令牌>
Content-Type: application/json
```

```json
{
  "cancelReason": "客户高反送医终止",
  "endDayNumber": 2,
  "adjustAmount": 0,
  "lineUsages": [
    {
      "lineKey": "HOTEL_ASSIGNMENT:96011:1",
      "used": true
    },
    {
      "lineKey": "ITINERARY_NODE:97010:1",
      "used": true
    }
  ],
  "vehicles": [
    {
      "refId": 9000004500000001,
      "dayNumber": 1,
      "used": true
    }
  ]
}
```

**响应**：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "terminateRefundId": "2084497785524011009",
    "baselineRefund": "6500.00",
    "adjustAmount": "0.00",
    "finalRefund": "6500.00",
    "settlementRefundId": "2084497785561759746",
    "newStatus": "COMPLETED",
    "newFlowStatus": "PENDING_REVIEW"
  },
  "traceId": null,
  "success": true
}
```

### 8.2 边界情况：COMPLETED 订单重复提交（本次变化点）

**场景说明**：首次终止已成功（无论响应是否丢失），对同一订单再次提交终止——与首次正文完全相同或不同，结果一样。改前同正文重放会返回 200 和首次结果；改后一律 581049，零副作用。

**请求**：同 8.1（正文可与首次一致，也可不一致）。

**响应**（TEST 网关实测）：

```json
{
  "code": 581049,
  "message": "订单已终止，禁止重复提交",
  "data": null,
  "traceId": null,
  "success": false
}
```

### 8.3 业务失败：结束日越界

**请求**：

```http
POST /v3/admin/order/2084280231786430466/terminate
Authorization: Bearer <管理后台访问令牌>
Content-Type: application/json
```

```json
{
  "cancelReason": "客户临时改行程",
  "endDayNumber": 99
}
```

**响应**：

```json
{
  "code": 581047,
  "message": "终止行程：结束日不在订单行程范围内",
  "data": null,
  "traceId": null,
  "success": false
}
```

## 9. 业务边界

- **适用场景**：订单为 TRAVELLING（出行中）时首次提交终止，走正常终止流程，返回 200 和退款结果。
- **不适用场景**：
  - 订单已 COMPLETED（已终止/已完成）：任何再次提交一律 `581049`，不产生任何副作用。
  - 订单为其他状态（待出行/已取消等）：`581018`。
- **特殊边界**：终止是有副作用操作，`581049` 不等于"重试成功"；前端拿到 581049 后如需终止结果（terminateRefundId / finalRefund），应改走订单详情 / 退款相关查询接口取数，不要再重放 terminate。
- **金额口径不变**：车辆已用仍以车辆 serviceDate 与终止日权威判定，`vehicles[].used` 仅为兼容字段。

## 10. 修改前后对比

### 10.1 字段级对比

| 项 | 原来 | 现在 |
|---|---|---|
| 入参字段结构 | `OrderTerminateTripReqVO` 全字段 | 不变 |
| 出参字段结构 | `OrderTerminateTripRespVO` 全字段 | 不变 |
| 581049 错误文案 | 终止行程重试与首次终止边界或请求不一致 | **订单已终止，禁止重复提交** |

### 10.2 行为级对比

| 场景 | 原来（#5460） | 现在 |
|---|---|---|
| TRAVELLING 订单首次终止 | `200` 返回退款结果，订单推进 COMPLETED | **不变** |
| COMPLETED 订单**同正文**重放 | `200`，返回既有 terminateRefundId / baselineRefund / adjustAmount / finalRefund / settlementRefundId | **`581049` 订单已终止，禁止重复提交**，零副作用 |
| COMPLETED 订单**不同正文**重试 | `581049`（旧文案） | `581049`（新文案），行为不变 |
| 升级前已终止的历史订单重放 | 按快照语义等价校验，等价返 200 / 不等价 581049 | 一律 `581049` |

## 11. 影响评估 / 回滚

### 11.1 影响评估

- **是否破坏向后兼容**：**是，行为级破坏**。依赖"同正文重放返回 200 取回首次结果"的前端重试/补偿逻辑会失效——现在重放拿到的是 581049。
- **前端是否必须同步上线**：**建议同步**。适配方式：
  1. 终止提交后若因超时/网络原因未收到响应，重试时收到 `581049` 应视为"订单已终止"（首次实际已成功），按成功路径收尾（刷新订单详情），不要当普通错误提示"提交失败"。
  2. 如需展示首次终止的退款结果，收到 581049 后拉订单详情/退款查询接口取数，不再从重放响应获取。
  3. 清理 #5460 时期按"同正文重放取回结果"编写的 workaround。
- 首次终止路径零变化，不重试的正常操作流程无感。

### 11.2 回滚方案

- 后端回滚即恢复 #5460 重放语义（同正文 200 / 不同正文 581049 旧文案）。前端兼容策略"581049 = 已终止，拉详情取数"在回滚后仍然成立（只是同正文重放会重新拿到 200），无需反向适配。

## 12. 注意事项

- `581049` 新文案是"订单已终止，禁止重复提交"，**不要再按旧文案"边界或请求不一致"做文案匹配**；按 code 判断。
- 收到 `581049` 时订单已是 COMPLETED，不要引导用户"修改正文后重试"——任何正文都会被拒。
- 终止结果字段（terminateRefundId / finalRefund 等）只有首次终止的 200 响应返回；之后只能从查询类接口获取。
- `581018` 与 `581049` 分工：非 TRAVELLING 且未终止过 → 581018；已 COMPLETED → 581049。


## 验证证据

- `mvn -pl hl-order-service-v3 -am verify` 全绿（含重复终止一律 581049 的回归测试）。
- TEST 网关实测（2026-08-06）：COMPLETED 订单再次提交 terminate 返回 `code=581049`、`message=订单已终止，禁止重复提交`、`success=false`，零副作用；TRAVELLING 订单首次终止仍 200 返回退款结果。
- 已部署 TEST（PR #5604 合并 dev-v3 后滚动发布）。

## 13. 关联 / 联系人

### 13.1 链接

- **Issue**：[#5598](https://git.1814.love:8443/wx/HL/issues/5598)
- **PR**：[#5604](https://git.1814.love:8443/wx/HL/pulls/5604)
- **Merge commit**：[047bb72951](https://git.1814.love:8443/wx/HL/commit/047bb72951)
- **被取代的契约**：[04_5460_终止行程幂等重放契约恢复-修改接口-管理后台.md](./04_5460_终止行程幂等重放契约恢复-修改接口-管理后台.md)（同正文重放语义本次起作废）

### 13.2 联系人

- **后端负责人**：@yst / yaosutu（腰苏图）
- **前端消费方**：管理后台订单详情 - 终止行程操作
