--- 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`(**本次无变化**)。 ### 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(腰苏图) - **前端消费方**:管理后台订单详情 - 终止行程操作