合并 #3556 系列 3 篇 + #3965 金额String化 为一份终态,供前端一处对接: - POST /v3/admin/order/{id}/terminate/refund-preview 终止退款预览 - POST /v3/admin/order/{id}/terminate 终止行程(执行) 点明: 旧路径 cancel/on-trip 已删返404; 金额+LongID 全String化; 终态 COMPLETED+PENDING_REVIEW(非CANCELLED); 581007/581018 错误码。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
订单详情「终止行程」接口终态汇总(出行中订单)—— 管理后台
- 端类型:管理后台
- 变更类型:修改接口(终态汇总篇,无新契约变化;合并 #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<Item> | 住宿清单(每晚×每组房一行) |
| tickets | Array<Item> | 门票清单(每景点每天一行) |
| vehicles | Array<Item> | 用车清单(整车按天展开,每天一行,同段车 refId 相同) |
| insurance | Array<Item> | 保险清单(整单 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<{refId, used}> | ✅ | 住宿各行已用判定(列表非 null,可空数组) | — |
| tickets | Array<{refId, used}> | ✅ | 门票各行已用判定 | — |
| vehicles | Array<{refId, dayNumber, used}> | ✅ | 用车各行已用判定(同车跨天靠 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(未出发/已终止/已完成,含二次终止) |
⑦ 示例
接口①响应(预览,节选)
{ "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":"保险已生效不退"} ]
} }
接口②请求(终止)
{
"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"
}
接口②响应(终止)
{ "code": 200, "success": true, "data": {
"terminateRefundId": "9610000001", "baselineRefund": "2264.00", "adjustAmount": "-300.00",
"finalRefund": "1964.00", "settlementRefundId": "9620000001",
"newStatus": "COMPLETED", "newFlowStatus": "PENDING_REVIEW" } }
边界(不填调整额,默认 0)
请求: { "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。
异常 —— 订单非出行中
{ "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:wx/HL#3556
- PR:wx/HL#3557 ・ wx/HL#3559 ・ wx/HL#3965
- 历史留档:
changelogs-v2/2026-06/06_3556_终止行程接口改造-修改接口-管理后台.md等 3 篇 - 后端负责人:腰苏图
- 接口已部署测试服,Knife4j 可见(
/v3/admin/order/{id}/terminate、/terminate/refund-preview)。