# 【✨新增接口·管理后台】终止行程·退款预览 (#3556)

> **PR**: #3557 | **服务**: hl-order-service-v3 | **更新时间**: 2026-06-06

## 1. 接口背景

出行中订单因特殊原因需要提前终止行程时，需要给定制师一个「按已用资源算应退多少」的界面。本接口提供终止退款弹框所需的**全部资源清单 + 行程时间轴信息**，定制师选择「停在第几天」后，由前端本地标记每项资源已用/未用并实时算出退款额，最终在终止提交接口落账。

## 2. 变更清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 终止退款预览 | POST | `/v3/admin/order/{id}/terminate/refund-preview` | 新增接口 | 返回终止退款弹框所需资源清单 + 行程信息 |

## 3. 接口详情

### 3.1 终止退款预览

- **使用场景**：出行中订单点击「终止行程」打开退款弹框时调用一次，拿到整趟全部资源清单
- **认证**：需要管理端 JWT（`Authorization` 头）
- **幂等性**：是（只读查询，不落库）
- **限流**：无

## 4. 接口入参

### 4.1 路径参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | String | ✅ | 订单 ID |

### 4.2 请求体字段

无请求体。前端**一次性**拿到整趟全部资源，「停在第几天」的选择与已用/未用标记、退款金额计算全部在前端本地完成，**不需要每选一天再调本接口**。

## 5. 出参（响应）

### 5.1 响应字段（`data`）

| 字段 | 类型 | 说明 |
|------|------|------|
| paidAmount | BigDecimal | 已付金额（公司账上已收，前端算退款基线用） |
| tripDays | Integer | 行程总天数（画时间轴用，如 7 天 6 晚 = 7） |
| departDate | String(date) | 出发日期（`yyyy-MM-dd`，第 N 天日期 = 出发日 + N−1） |
| tripCurrentDay | Integer | 当前已出行到第几天（时间轴默认选中点） |
| rooms | Array<ResourceItem> | 住宿资源清单（每晚每组房一行） |
| tickets | Array<ResourceItem> | 门票资源清单（每景点每天一行） |
| vehicles | Array<ResourceItem> | 用车资源清单（**整车已按天展开，每天一行**） |
| insurance | Array<ResourceItem> | 保险清单（整单一行，锁定不退） |

### 5.2 ResourceItem 字段（rooms / tickets / vehicles / insurance 各行通用）

| 字段 | 类型 | 说明 |
|------|------|------|
| refId | String | 资源记录 ID（提交终止时按此回传已用判定；用车多天共用同一 refId，靠 dayNumber 区分；保险可能为 null） |
| name | String | 资源名称（酒店名 / 景点名 / 车型 / 保险产品名） |
| dayNumber | Integer | 对应行程第几天（保险为 null） |
| dealPrice | BigDecimal | 成交单价（住宿=每间每晚价 / 门票=每张价 / 用车=每天每辆日租 / 保险=保费） |
| quantity | Integer | 数量（住宿=房间数 / 门票=张数 / 用车=车辆数 / 保险=1） |
| totalAmount | BigDecimal | 本行小计（= dealPrice × quantity） |
| locked | Boolean | 是否锁定不退（保险恒 true，其余 false） |
| lockedReason | String | 锁定原因（如「保险已生效不退」；未锁定为 null） |

> 响应**不含**「是否已用」标记与「退款合计」——这些由前端按所选结束天（`dayNumber ≤ 结束天` 默认已用，之后默认可退）实时计算。

## 6. 枚举 / 数据字典

本接口无枚举字段。资源分类通过 `rooms` / `tickets` / `vehicles` / `insurance` 四个数组区分，无独立 `resourceType` 字段。

## 7. 错误码

| code | 含义 | 触发场景 |
|------|------|----------|
| 581018 | 出行中取消仅适用于出行中订单 | 订单当前状态非「出行中」时调用本接口 |

## 8. 示例

### 8.1 典型成功

**请求**：
```
POST /v3/admin/order/60001234567890/terminate/refund-preview
Authorization: Bearer {token}
（无请求体）
```

**响应**：
```json
{
  "code": 200,
  "message": "ok",
  "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},
      {"refId": "96014", "name": "拉萨瑞吉度假酒店·大床房", "dayNumber": 4, "dealPrice": 1280.00, "quantity": 1, "totalAmount": 1280.00, "locked": false, "lockedReason": null}
    ],
    "tickets": [
      {"refId": "97001", "name": "布达拉宫门票", "dayNumber": 2, "dealPrice": 200.00, "quantity": 2, "totalAmount": 400.00, "locked": false, "lockedReason": null}
    ],
    "vehicles": [
      {"refId": "98001", "name": "丰田赛那 商务车", "dayNumber": 1, "dealPrice": 600.00, "quantity": 1, "totalAmount": 600.00, "locked": false, "lockedReason": null},
      {"refId": "98001", "name": "丰田赛那 商务车", "dayNumber": 2, "dealPrice": 600.00, "quantity": 1, "totalAmount": 600.00, "locked": false, "lockedReason": null},
      {"refId": "98001", "name": "丰田赛那 商务车", "dayNumber": 3, "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": "保险已生效不退"}
    ]
  }
}
```

### 8.2 边界情况

**场景说明**：某天住两个酒店（同一 dayNumber 出现多行）。

**响应（rooms 片段）**：
```json
"rooms": [
  {"refId": "96021", "name": "林芝希尔顿·双床房", "dayNumber": 2, "dealPrice": 980.00, "quantity": 1, "totalAmount": 980.00, "locked": false, "lockedReason": null},
  {"refId": "96022", "name": "林芝工布庄园·大床房", "dayNumber": 2, "dealPrice": 880.00, "quantity": 1, "totalAmount": 880.00, "locked": false, "lockedReason": null}
]
```

### 8.3 业务失败（异常）

**场景说明**：订单非出行中（如已确认待出发）时调用。

**响应**：
```json
{ "code": 581018, "message": "出行中取消仅适用于出行中订单", "success": false }
```

## 9. 业务边界

- ✅ **适用场景**：订单当前状态 = 出行中（TRAVELLING）
- ❌ **不适用场景**：未出发（待支付 / 待完善 / 已确认待出发）→ 返回 581018；这些状态请走「取消订单」流程，不走终止退款
- ⚠️ **特殊边界**：
  - 保险行恒 `locked=true`，不参与退款
  - 同一天可有多行（多酒店 / 多门票 / 多车），按 refId 区分
  - 用车一段车按行程天数展开为多行，多行共用同一 refId

## 13. 关联 / 联系人

### 13.1 链接

- **Issue**: [#3556](https://git.1814.love:8443/wx/HL/issues/3556)
- **PR**: [#3557](https://git.1814.love:8443/wx/HL/pulls/3557)
- **Merge commit**: [773dcdacf](https://git.1814.love:8443/wx/HL/commit/773dcdacf)
- **关联 PR（预览状态校验加固）**: [#3559](https://git.1814.love:8443/wx/HL/pulls/3559)

### 13.2 联系人

- **后端负责人**: @yst
- **前端对接（管理后台）**: 终止行程弹框页面
