docs(changelog-v2): 终止行程接口终态汇总(管理后台 #3556)

合并 #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>
这个提交包含在:
yaosutu 2026-06-26 10:04:25 +08:00
父节点 ede5dd5c30
当前提交 de1292823a

查看文件

@ -0,0 +1,221 @@
# 订单详情「终止行程」接口终态汇总(出行中订单)—— 管理后台
- 端类型:管理后台
- 变更类型:修改接口(**终态汇总篇**,无新契约变化;合并 #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 天日期 = 出发日 + N1 |
| tripCurrentDay | Integer | 当前到第几天(时间轴默认选中点;出发前/未知=1 |
| rooms | Array&lt;Item&gt; | 住宿清单(每晚×每组房一行) |
| tickets | Array&lt;Item&gt; | 门票清单(每景点每天一行) |
| vehicles | Array&lt;Item&gt; | 用车清单(**整车按天展开,每天一行**,同段车 refId 相同) |
| insurance | Array&lt;Item&gt; | 保险清单(整单 1 行,locked=true 锁定不退) |
**ItemTerminateRefundItemVO,四类资源行通用**
| 字段 | 类型 | 说明 |
|---|---|---|
| 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
### 入参 BodyOrderTerminateTripReqVO
| 字段 | 类型 | 必填 | 说明 | 校验 |
|---|---|:--:|---|---|
| cancelReason | String | ✅ | 终止原因(调整额说明并入此处,不单列字段) | 非空 |
| endDayNumber | Integer | ✅ | 停在第几天截断点,1-based | ≥ 1 ≤ 行程总天数 |
| rooms | Array&lt;{refId, used}&gt; | ✅ | 住宿各行已用判定(列表非 null,可空数组 | — |
| tickets | Array&lt;{refId, used}&gt; | ✅ | 门票各行已用判定 | — |
| vehicles | Array&lt;{refId, dayNumber, used}&gt; | ✅ | 用车各行已用判定(同车跨天靠 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 字段
- lockedtrue=锁定不退(保险恒 true/ false=可退
---
## ⑥ 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| 581007 | 订单不存在 | id 无对应订单 |
| 581018 | 出行中取消仅适用于出行中订单 | 订单当前状态非 TRAVELLING未出发/已终止/已完成,含二次终止) |
---
## ⑦ 示例
### 接口①响应(预览,节选)
```json
{ "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":"保险已生效不退"} ]
} }
```
### 接口②请求(终止)
```json
{
"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"
}
```
### 接口②响应(终止)
```json
{ "code": 200, "success": true, "data": {
"terminateRefundId": "9610000001", "baselineRefund": "2264.00", "adjustAmount": "-300.00",
"finalRefund": "1964.00", "settlementRefundId": "9620000001",
"newStatus": "COMPLETED", "newFlowStatus": "PENDING_REVIEW" } }
```
### 边界(不填调整额,默认 0
```json
请求: { "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`
### 异常 —— 订单非出行中
```json
{ "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,再提交终止;提交不传金额单价后端权威重算
---
## ⑫ 关联 / 联系人
- Issuehttps://git.1814.love:8443/wx/HL/issues/3556
- PRhttps://git.1814.love:8443/wx/HL/pulls/3557 ・ https://git.1814.love:8443/wx/HL/pulls/3559 ・ https://git.1814.love:8443/wx/HL/pulls/3965
- 历史留档:`changelogs-v2/2026-06/06_3556_终止行程接口改造-修改接口-管理后台.md` 等 3 篇
- 后端负责人:腰苏图
- 接口已部署测试服,Knife4j 可见(`/v3/admin/order/{id}/terminate``/terminate/refund-preview`)。