diff --git a/changelogs-v2/2026-08/06_5598_终止行程重复提交改一律581049-修改接口-管理后台.md b/changelogs-v2/2026-08/06_5598_终止行程重复提交改一律581049-修改接口-管理后台.md new file mode 100644 index 0000000..d482c6d --- /dev/null +++ b/changelogs-v2/2026-08/06_5598_终止行程重复提交改一律581049-修改接口-管理后台.md @@ -0,0 +1,312 @@ +--- +schema: "hl-changelog/v2" +ticket: "5598" +title: "终止行程重复提交改一律 581049:COMPLETED 订单再次提交不再重放首次结果" +consumer: "admin" +change_type: "修改接口" +author: "yst(GIT)" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "PR #5604 已合并 dev-v3 并部署 TEST,网关实测:COMPLETED 订单再次提交 terminate 一律返回 581049「订单已终止,禁止重复提交」(同正文也不再 200 重放)。前端如依赖 #5460 的同正文重放取回结果逻辑需适配。" +updated_at: "2026-08-06" +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`(**本次无变化**)。 + +### 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(腰苏图) +- **前端消费方**:管理后台订单详情 - 终止行程操作