# 退款明细 Tab 懒加载——出参契约重构

- **接口**：`GET /v3/admin/order/{id}/refund`
- **变更类型**：修改接口（出参字段调整）
- **日期**：2026-06-26
- **Issue**：[#4417](https://git.1814.love:8443/wx/HL/issues/4417)
- **PR**：[#4418](https://git.1814.love:8443/wx/HL/pulls/4418)
- **后端负责人**：yst

---

## ① 接口背景

订单详情页「退款明细」Tab 采用懒加载模式，前端单独调用此接口获取退款数据。
本次重构对出参 VO 结构进行语义修正：补充「退款原因」和「申请时间」两个展示字段，删除不展示的 `estimatedArriveDate`（预计到账日期）和 `items`（逐项明细）块，同时修正 `refundChannel` 字段的语义（此前误返回退款类型枚举值，现在统一返回真实渠道文案）。

---

## ② 变更清单

| 类型 | 字段 | 说明 |
|------|------|------|
| ✨ 新增 | `applications[].reason` | 退款原因文字 |
| ✨ 新增 | `applications[].appliedAt` | 退款申请时间 |
| ⚠️ 删除 | `applications[].estimatedArriveDate` | 预计到账日期，不再返回 |
| ⚠️ 删除 | `applications[].items[]` | 退款逐项明细块，不再返回 |
| 🔧 语义修正 | `applications[].refundChannel` | 固定返回 `"原路退回(微信)"` 字符串，不再返回退款类型枚举值 |

---

## ③ 接口详情

| 项目 | 值 |
|------|-----|
| **方法** | GET |
| **路径** | `/v3/admin/order/{id}/refund` |
| **描述** | 获取订单退款明细（Tab 懒加载） |
| **认证** | 需要管理后台 JWT（Bearer Token） |
| **幂等性** | 只读接口，天然幂等 |
| **限流** | 无特殊限流 |

---

## ④ 入参

### 4.1 路径参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long（字符串传输） | 是 | 订单 ID |

### 4.2 请求体

无请求体。

---

## ⑤ 出参字段

**顶层结构（RefundDetailVO）**

| 字段 | 类型 | 说明 |
|------|------|------|
| `totalRefundAmount` | String（BigDecimal 序列化） | 合计退款金额，单位：元 |
| `applications` | Array\<RefundApplicationVO\> | 退款申请列表（全部历史申请） |

> 当订单无任何退款申请时，整个 `refund` 节点返回 `null`，前端据此隐藏 Tab。

**applications[] 元素（RefundApplicationVO）——完整字段**

| 字段 | 类型 | 说明 |
|------|------|------|
| `applicationId` | Long（字符串序列化） | 退款申请 ID |
| `status` | String | 退款进度状态码（见 §⑥ 枚举） |
| `statusText` | String | 状态文案，如 `"待审批"` |
| `refundAmount` | String（BigDecimal 序列化） | 退款金额（实退额为空时回退计算额），单位：元 |
| `refundChannel` | String | 退款渠道，固定返回 `"原路退回(微信)"` |
| `reason` | String | **【新增】** 退款原因，如 `"同行儿童突发感冒,不参与本次出行"` |
| `appliedAt` | String（ISO 8601 LocalDateTime） | **【新增】** 申请时间，如 `"2026-04-25T11:30:00"` |
| `approverName` | String | 审批人姓名，未审批时为 `null` |
| `approvedAt` | String（ISO 8601 LocalDateTime） | 审批时间，未审批时为 `null` |
| `actualArriveDate` | String（ISO 8601 LocalDate） | 实际到账日期，未到账时为 `null` |
| `progress` | Array\<ProgressStepVO\> | 4 步时间线（APPLY / APPROVE / PAYOUT / ARRIVED） |

**progress[] 元素（ProgressStepVO）**

| 字段 | 类型 | 说明 |
|------|------|------|
| `step` | String | 步骤码：`APPLY` / `APPROVE` / `PAYOUT` / `ARRIVED` |
| `label` | String | 步骤中文标签，如 `"已申请"` |
| `status` | String | 步骤状态：`DONE`（已完成）/ `ACTIVE`（进行中）/ `PENDING`（待进行） |
| `occurredAt` | String（ISO 8601） | 该步骤发生时间，未到达时为 `null` |

---

## ⑥ 枚举 / 数据字典

### refundChannel（退款渠道）

| 值 | 含义 |
|----|------|
| `"原路退回(微信)"` | 系统当前唯一退款渠道，所有退款均原路退至微信支付 |

> 注意：此前误将退款类型枚举值（如 `PARTIAL` / `FULL`）回填到该字段，本次已修正为真实渠道文案。

### applications[].status（退款申请状态）

| 值 | 含义 |
|----|------|
| `PENDING_APPROVE` | 待审批 |
| `PENDING_PAYOUT` | 审批通过，待退款打出 |
| `PENDING_ARRIVAL` | 退款已发出，待到账 |
| `COMPLETED` | 已完成到账 |
| `REJECTED` | 已驳回 |

### progress[].status（时间线步骤状态）

| 值 | 含义 |
|----|------|
| `DONE` | 该步骤已完成 |
| `ACTIVE` | 当前正在该步骤 |
| `PENDING` | 尚未到达该步骤 |

---

## ⑦ 错误码

| 错误码 | 说明 | 触发条件 |
|--------|------|---------|
| `404` / 业务错误 | 订单不存在 | `id` 对应订单不存在 |
| `403` | 无权访问 | JWT 鉴权失败或无该订单访问权限 |

---

## ⑧ 示例

### 8.1 典型成功——订单含一笔退款申请（已完成）

**请求**

```http
GET /v3/admin/order/1902345678901234567/refund
Authorization: Bearer <admin-token>
```

**响应**

```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "totalRefundAmount": "1280.00",
    "applications": [
      {
        "applicationId": "1902345678901234568",
        "status": "COMPLETED",
        "statusText": "已完成",
        "refundAmount": "1280.00",
        "refundChannel": "原路退回(微信)",
        "reason": "同行儿童突发感冒,不参与本次出行",
        "appliedAt": "2026-04-25T11:30:00",
        "approverName": "张三",
        "approvedAt": "2026-04-25T14:00:00",
        "actualArriveDate": "2026-04-27",
        "progress": [
          {
            "step": "APPLY",
            "label": "已申请",
            "status": "DONE",
            "occurredAt": "2026-04-25T11:30:00"
          },
          {
            "step": "APPROVE",
            "label": "已审批",
            "status": "DONE",
            "occurredAt": "2026-04-25T14:00:00"
          },
          {
            "step": "PAYOUT",
            "label": "退款已发出",
            "status": "DONE",
            "occurredAt": "2026-04-25T14:30:00"
          },
          {
            "step": "ARRIVED",
            "label": "已到账",
            "status": "DONE",
            "occurredAt": "2026-04-27T09:15:00"
          }
        ]
      }
    ]
  }
}
```

---

### 8.2 边界情况——无退款申请（refund 节点为 null）

**请求**

```http
GET /v3/admin/order/1902345678901234999/refund
Authorization: Bearer <admin-token>
```

**响应**

```json
{
  "code": 200,
  "msg": "success",
  "data": null
}
```

> 前端收到 `data` 为 `null` 时应隐藏「退款明细」Tab，不做渲染。

---

### 8.3 业务失败——订单不存在

**请求**

```http
GET /v3/admin/order/9999999999999999999/refund
Authorization: Bearer <admin-token>
```

**响应**

```json
{
  "code": 404,
  "msg": "订单不存在",
  "data": null
}
```

---

## ⑨ 业务边界

**适用场景**

- 仅用于「退款明细」Tab 懒加载，不作为退款管理工作台的数据源
- 展示该订单全部退款申请历史（含已完成、已驳回的历史申请）

**不适用**

- 不返回退款政策/原因配置列表（有单独接口）
- 不返回退款工作台列表数据

**特殊边界**

- 一笔订单采用**串行退款**模式：前一笔退款完成后才能发起下一笔，正常情况下不会同时存在多个进行中的申请
- `refundAmount` 字段：实退额（`actualRefundAmount`）有值时返回实退额，否则返回前端计算额，前端无需特殊处理，直接展示即可
- `approverName` / `approvedAt`：待审批状态下为 `null`，前端渲染时需判空

---

## ⑩ 修改前后对比

### 字段级对比

| 字段 | 变更前 | 变更后 |
|------|--------|--------|
| `applications[].reason` | **不存在** | ✨ 新增，退款原因文字 |
| `applications[].appliedAt` | **不存在** | ✨ 新增，申请时间 ISO 8601 |
| `applications[].estimatedArriveDate` | ⚠️ 存在，预计到账日期 | **已删除**，不再返回 |
| `applications[].items[]` | ⚠️ 存在，逐项退款明细块 | **已删除**，不再返回 |
| `applications[].refundChannel` | 误返回退款类型枚举值（如 `PARTIAL`） | 🔧 修正为渠道文案 `"原路退回(微信)"` |

### 行为级对比

| 场景 | 变更前 | 变更后 |
|------|--------|--------|
| 读取退款渠道 | 返回类型枚举 `PARTIAL`/`FULL`，前端当渠道用会显示错误 | 返回渠道文案，直接渲染即可 |
| 退款原因展示 | 无此字段，需前端单独请求其他接口 | 内联在申请对象中，无需额外请求 |
| 申请时间展示 | 无此字段 | 内联在申请对象中 |

---

## ⑪ 影响评估 / 回滚

**破坏兼容性变更（前端必须同步处理）**

- `estimatedArriveDate` 已从出参中删除，前端如有读取此字段的代码，收到 `undefined` 不会报错，但需确认是否有展示逻辑需要移除
- `items[]` 已从出参中删除，前端如有遍历 `items` 的渲染逻辑需要移除
- `refundChannel` 语义变化：若前端曾将该字段作为退款类型判断（如 `PARTIAL` → 部分退款），需改为直接展示文案，不再做枚举判断

**前端同步上线建议**

1. 移除 `estimatedArriveDate` 渲染逻辑
2. 移除 `items[]` 相关渲染块（如退款逐项明细表格）
3. `refundChannel` 改为直接渲染字符串，不做枚举 switch
4. 新增 `reason`（退款原因）和 `appliedAt`（申请时间）展示

**回滚方案**

- 若需回滚，回退 PR #4418 对应 commit 即可恢复旧出参结构
- 回滚后 `reason` / `appliedAt` 消失，`estimatedArriveDate` / `items[]` 恢复，`refundChannel` 恢复为枚举值

---

## ⑫ 注意事项

1. **`refundChannel` 不再是枚举值**：固定返回 `"原路退回(微信)"` 文案字符串，前端直接渲染，不要做枚举 switch 判断
2. **`appliedAt` 格式为 ISO 8601 LocalDateTime**（如 `"2026-04-25T11:30:00"`，无时区后缀），前端渲染时自行格式化
3. **`data` 为 null 时代表无退款**：不是接口异常，是正常业务状态，Tab 应隐藏
4. **`actualArriveDate` 是 LocalDate**（仅日期 `"2026-04-27"`），与 `appliedAt` / `approvedAt`（LocalDateTime）格式不同，注意区分
5. **金额字段均为字符串**（BigDecimal 序列化防 JS 精度丢失），不要用 `parseFloat` 直接计算

---

## ⑬ 关联 / 联系人

| 项目 | 链接 |
|------|------|
| Issue | [#4417 退款明细Tab懒加载出参重构](https://git.1814.love:8443/wx/HL/issues/4417) |
| PR | [#4418 feat(order-v3/refund): 退款明细Tab懒加载出参重构](https://git.1814.love:8443/wx/HL/pulls/4418) |
| 后端负责人 | yst |
