---
schema: "hl-changelog/v2"
ticket: "5460"
title: "终止行程接口幂等重放契约恢复：同正文重放返回既有结果，不同正文返回 581049"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@49f38c7cfb4d5859a13c6ab1a7fea540cb524de5"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "行为修复：恢复 #5379 已承诺的终止行程幂等重放语义（#5379 最终合并版丢失了早期迭代的幂等实现，导致首次终止成功后同正文重放被 581018 提前拦截、无法取回既有结果）。PR #5468 已合并 dev-v3 并部署 TEST，网关已验证：新订单首次终止 200、同正文重放返回既有 terminateRefundId/finalRefund、不同边界/正文返回 581049（修复前同场景实测 581018）。"
updated_at: "2026-08-04"
base: "dev-v3"
---

# 终止行程接口幂等重放契约恢复：同正文重放返回既有结果，不同正文返回 581049

> **服务**: hl-order-service-v3
> **PR**: #5468
> **Issue**: #5460
> **日期**: 2026-08-04
> **影响范围**: 管理后台订单终止行程（出行中终止）提交接口的重试/重放行为

---

## ⚠️ 关键变化

**#5379 契约承诺的幂等重放语义此前在 TEST 实测失效**：首次终止提交成功但响应丢失后，对同一订单原样重放同一请求，接口返回 `581018「出行中取消仅适用于出行中订单」`，而不是返回既有终止结果；用不同正文重试也返回 581018 而不是 `581049`。**现已修复**，行为与 #5379 changelog 契约一致：

| 场景 | 修复前实测 | 修复后 |
|------|-----------|--------|
| 相同正文重放（首次已成功） | 581018（错误） | `200`，返回既有终止结果（含 terminateRefundId / finalRefund） |
| 不同正文 / 不同终止边界重试 | 581018（错误） | `581049` 终止行程重试与首次终止边界或请求不一致 |

前端无需改动，但**超时/网络重试逻辑现在可以依赖该接口的幂等语义**：重试时原样复用首次请求正文即可取回既有结果，不要修改正文内容（包括 `vehicles[].used` 与 `lineUsages[].used`）。

## 二、变更接口清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 终止行程 | POST | `/v3/admin/order/:id/terminate` | 行为修复 | 幂等重放语义恢复（契约不变，见 #5379） |

## 三、接口详情

### 1. 终止行程 `POST /v3/admin/order/:id/terminate`

**请求体**: `OrderTerminateTripReqVO`（字段、必填性、约束均不变，与 #5379 一致）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `endDayNumber` | Integer | ✅ | 停在第几天（1-based）；**重试必须与首次完全一致** |
| `cancelReason` | String | ✅ | 终止原因；重试必须与首次完全一致 |
| `adjustAmount` | BigDecimal | 否 | 人工调整额；重试必须与首次一致（0 与 null 视为等价） |
| `lineUsages[]` | Array | 否 | 统一资源行已用判定；重试必须与首次一致 |
| `rooms[]` / `tickets[]` / `services[]` / `supplies[]` | Array | 否 | 兼容分组入参；重试必须与首次一致 |
| `vehicles[]` | Array | 否 | 用车行（refId/dayNumber/used）；used 仍参与重放一致性校验，金额以后端 Fleet 事实为准 |

**业务边界（重放判定规则）**：

1. 订单为 TRAVELLING 时：正常执行首次终止 → `200`，订单变 COMPLETED。
2. 订单已 COMPLETED（已终止）时，**先判定重放身份**，不再直接返回 581018：
   - 同边界 + 同正文（请求指纹一致）→ `200`，返回既有终止结果：`terminateRefundId`、`baselineRefund`、`adjustAmount`、`finalRefund`、`settlementRefundId`、`newStatus=COMPLETED`、`newFlowStatus=PENDING_REVIEW`；不产生任何副作用（不重复写退款、不重复发事件）。
   - 同边界但正文不一致，或边界不一致（`endDayNumber` 等不同）→ `581049 终止行程重试与首次终止边界或请求不一致`。
   - 升级前已终止的订单（无指纹快照）：按主表与明细快照做语义等价校验，等价重放同样返回既有结果，不等价返回 581049。
3. `endDayNumber` 超出订单行程 → `581047`（不变）；车辆费用不可用 → `584100` fail closed（不变）。

**错误码**：

| 错误码 | 含义 | 触发条件 |
|--------|------|----------|
| 581018 | 出行中取消仅适用于出行中订单 | 订单非 TRAVELLING 且**从未终止**（自然结束/已取消等）时提交终止 |
| 581049 | 终止行程重试与首次终止边界或请求不一致 | 已终止订单以不同边界/不同正文重试 |
| 581047 | 终止行程：结束日不在订单行程范围内 | endDayNumber 越界 |
| 581044 | 终止行程：结束日之前的资源必须标记为已使用 | 结束日前资源 used=false |

**典型成功示例（同正文重放）**：

请求：

```json
POST /v3/admin/order/2084280231786430466/terminate
Authorization: Bearer <token>
Content-Type: application/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 }
  ]
}
```

响应（首次终止与同正文重放均为此响应，重放时 terminateRefundId/finalRefund 与首次一致）：

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

**异常示例（不同正文重试）**：

```json
{
  "code": 581049,
  "message": "终止行程重试与首次终止边界或请求不一致",
  "success": false
}
```

## 四、验证证据

- `mvn -pl hl-order-service-v3 -am verify` 全绿（含 spotless + 全部单元/集成测试；新增幂等重放回归：首次终止写指纹 → 同正文重放 → 不同正文冲突三态）。
- TEST 网关验证（wx 定制师，2026-08-04）：新订单首次终止 200 且写指纹；同正文重放返回同一 terminateRefundId/finalRefund；不同 endDayNumber 返回 581049；升级前已终止订单同语义重放返回既有 terminateRefundId/finalRefund、不同边界返回 581049（修复前同场景实测 581018）。
- 已部署 TEST（dev-v3 滚动部署，双实例健康）。

## 五、相关文档

- 关联 Issue: [wx/HL#5460](https://git.1814.love:8443/wx/HL/issues/5460)
- 关联 PR: [wx/HL#5468](https://git.1814.love:8443/wx/HL/pulls/5468)
- 契约基线: [03_5379_终止行程车辆退款权威口径-修改接口-管理后台.md](./03_5379_终止行程车辆退款权威口径-修改接口-管理后台.md)

## 关联 / 联系人

### 链接

- **Issue**: [#5460](https://git.1814.love:8443/wx/HL/issues/5460)
- **PR**: [#5468](https://git.1814.love:8443/wx/HL/pulls/5468)
- **Merge commit**: [316a7d2c29](https://git.1814.love:8443/wx/HL/commit/316a7d2c29)
### 联系人

- **后端负责人**: @wx
