# 退款申诉（新增）+ 统一售后工单接口下线（小程序端）

- 端类型：小程序端
- 变更类型：新增接口（4）+ 删除接口（统一售后工单 mp 端）
- 关联 Issue：#4250　PR：#4251 #4254
- 日期：2026-06-23

---

## ① 接口背景

售后体系重构：原"投诉 + 申诉统一售后工单"方向作废，拆为**投诉**与**申诉**两套独立能力。
- **申诉**：用户对"退款被拒"或"退款金额"不服时发起，**小程序入口仍是「退款申诉」**。申诉记录单独存（不再混入退款申请表）。
- **关键变化**：申诉**复用既有退款审批**——发起申诉后系统自动建一笔 PENDING 退款申请，由管理员在退款审批里复核（通过则退款 / 驳回则结束）；申诉本身不再有独立审批。
- 原统一售后工单接口（`/v3/mp/aftersale/ticket*`）**全部下线**。

---

## ② 变更清单

| # | 方法 | 路径 | 类型 |
|---|---|---|---|
| 1 | POST | `/v3/mp/refund/appeal` | 🆕 新增（发起申诉）|
| 2 | GET | `/v3/mp/refund/appeal/my` | 🆕 新增（我的申诉，分页）|
| 3 | GET | `/v3/mp/refund/appeal/{id}` | 🆕 新增（申诉详情）|
| 4 | POST | `/v3/mp/refund/appeal/{id}/withdraw` | 🆕 新增（撤回申诉）|
| 5 | ALL | `/v3/mp/aftersale/ticket*` | ❌ 删除（统一售后工单下线，调用返回 404）|

统一响应包装 `Result<T>`：`{ code, message, data, success }`，`code=200` 为成功。

---

## ③ 接口详情

### 1. 发起申诉　`POST /v3/mp/refund/appeal`
用户对一笔退款申请发起申诉。两种类型：`REFUND_REJECTED`（退款被拒，原退款 status=REJECTED 才可申诉）/ `REFUND_AMOUNT_DISPUTE`（退款金额异议，原退款 status=REFUNDED 求补差）。
> 发起成功后：① 落一条申诉记录（PENDING）② **自动建一笔 PENDING 退款申请**走退款审批 ③ 订单进入「售后中」。

### 2. 我的申诉　`GET /v3/mp/refund/appeal/my`
当前登录用户的申诉分页列表。

### 3. 申诉详情　`GET /v3/mp/refund/appeal/{id}`
单条申诉详情，含关联的两笔退款（来源退款 + 申诉触发的退款）。

### 4. 撤回申诉　`POST /v3/mp/refund/appeal/{id}/withdraw`
仅 `PENDING`（审批中）状态可撤回；撤回会一并取消申诉触发的那笔 PENDING 退款申请。

---

## ④ 入参

### 接口1 create（`@RequestBody`，登录态 userId 由网关注入）
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| refundApplicationId | long | 是 | 来源退款申请 ID（被申诉的那笔退款）|
| appealType | string | 是 | 申诉类型：`REFUND_REJECTED` / `REFUND_AMOUNT_DISPUTE` |
| appealReason | string | 是 | 申诉理由（≤2000 字）|
| appealAmount | string(decimal) | 否 | 期望退款金额（金额异议时填）|
| mediaTraceIds | string[] | 否 | 凭证图片机审 traceId 列表（前端上传后透传，≤12）|

### 接口2 my
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pageNo | int | 否 | 页码，默认 1 |
| pageSize | int | 否 | 每页条数，默认 20 |

### 接口3 detail / 接口4 withdraw
| 参数 | 位置 | 类型 | 说明 |
|---|---|---|---|
| id | path | long | 申诉 ID |

---

## ⑤ 出参

### 接口1 create　`Result<Long>`
`data` = 新建申诉 ID（字符串化 Long）。

### 接口2 my　`Result<PageResult<AppealMpRespVO>>`；接口3 detail　`Result<AppealMpRespVO>`
`PageResult`：`{ records[], total, page, pageSize }`。`AppealMpRespVO`：

| 字段 | 类型 | 说明 |
|---|---|---|
| id | string(Long) | 申诉 ID |
| orderId | string(Long) | 订单 ID |
| refundApplicationId | string(Long) | 来源退款申请 ID |
| appealType | string | 申诉类型（枚举值，见⑥）|
| appealTypeLabel | string | 申诉类型中文名 |
| appealReason | string | 申诉理由 |
| appealAmount | string(decimal) | 申诉退款金额 |
| appealStatus | string | 申诉状态（枚举值，见⑥）|
| appealStatusLabel | string | 申诉状态中文名 |
| applicantName | string | 申请人姓名 |
| reviewedAt | datetime | 审批完成时间（可空）|
| createTime | datetime | 创建时间 |
| sourceRefund | object | 来源退款信息（被申诉的原退款），见下 |
| triggeredRefund | object | 申诉触发的退款信息（申诉建单后自动生成的退款），见下 |

`sourceRefund` / `triggeredRefund` 结构（关联退款信息）：
| 字段 | 类型 | 说明 |
|---|---|---|
| refundApplicationId | string(Long) | 退款申请 ID |
| status | string | 退款状态（PENDING/APPROVED/REJECTED/REFUNDING/REFUNDED/CANCELLED/ABNORMAL）|
| actualAmount | string(decimal) | 实际退款金额（可空）|

### 接口4 withdraw　`Result<Void>`
`data` = null。

---

## ⑥ 枚举 / 数据字典

**申诉类型 appealType**：
| 值 | 含义 |
|---|---|
| REFUND_REJECTED | 退款被拒（原退款被驳回后申诉）|
| REFUND_AMOUNT_DISPUTE | 退款金额异议（已退款但金额有异议，求补差）|

**申诉状态 appealStatus**：
| 值 | 含义 |
|---|---|
| PENDING | 审批中（申诉触发的退款申请待退款审批）|
| REJECTED | 已驳回（退款审批驳回）|
| REFUNDED | 退款到账（申诉成功，退款已退）|
| WITHDRAWN | 已撤回 |

---

## ⑦ 错误码（段位 530600-530699）

| code | message |
|---|---|
| 530601 | 申诉必须关联一笔退款申请 |
| 530602 | 关联的退款申请不存在 |
| 530603 | 退款申请当前状态不满足申诉条件 |
| 530604 | 该退款申请已有处理中的申诉，请勿重复提交 |
| 530605 | 申诉退款金额超出可退余额 |
| 530606 | 申诉记录不存在 |
| 530607 | 无权操作此申诉 |
| 530608 | 当前申诉状态不允许撤回，仅待审核状态可撤回 |
| 530609 | 申诉类型无效 |

---

## ⑧ 示例

### 典型：发起申诉（退款被拒）
请求 `POST /v3/mp/refund/appeal`：
```json
{ "refundApplicationId": 20001, "appealType": "REFUND_REJECTED",
  "appealReason": "退款审核不合理，申请复核", "appealAmount": "300.00",
  "mediaTraceIds": [] }
```
响应：`{ "code":200, "message":"成功", "data":"40001", "success":true }`

### 典型：我的申诉
请求 `GET /v3/mp/refund/appeal/my?pageNo=1&pageSize=10`，响应（节选一项）：
```json
{ "code":200, "success":true, "data": { "total":1, "records":[
  { "id":"40001", "orderId":"10001", "appealType":"REFUND_REJECTED", "appealTypeLabel":"退款被拒",
    "appealStatus":"PENDING", "appealStatusLabel":"审批中", "appealAmount":"300.00",
    "sourceRefund": { "refundApplicationId":"20001", "status":"REJECTED", "actualAmount":null },
    "triggeredRefund": { "refundApplicationId":"20009", "status":"PENDING", "actualAmount":null } }
] } }
```

### 异常：重复申诉
对同一退款申请已有处理中申诉时再发起：
```json
{ "code":530604, "message":"该退款申请已有处理中的申诉，请勿重复提交", "success":false }
```

### 异常：调用已下线的工单接口
请求 `POST /v3/mp/aftersale/ticket`（旧统一售后工单接口）：
```json
{ "code":404, "message":"Not Found" }
```

---

## ⑨ 业务边界

- 申诉**复用退款审批**：发起后自动建 PENDING 退款申请，管理员在退款审批里复核——**前端不要再调用任何"申诉审批"接口**（已无独立申诉审批）。
- 申诉状态跟随其触发的退款申请：退款到账→REFUNDED，退款审批驳回→REJECTED。
- 撤回仅 PENDING 可撤，且会取消触发的 PENDING 退款。
- 申诉发起即把订单标记「售后中」，终结（REFUNDED/REJECTED/WITHDRAWN 且无其他活跃售后）后回切。

---

## ⑩ 修改前后对比

| | 修改前 | 修改后 |
|---|---|---|
| 申诉入口 | 统一售后工单 `POST /v3/mp/aftersale/ticket`（category=APPEAL）| `POST /v3/mp/refund/appeal` |
| 我的申诉 | `/v3/mp/aftersale/ticket/my` | `/v3/mp/refund/appeal/my` |
| 详情/撤回 | `/v3/mp/aftersale/ticket/{id}` `/{id}/withdraw` | `/v3/mp/refund/appeal/{id}` `/{id}/withdraw` |
| 审批 | 申诉独立 OA 审批 | 无（复用退款审批）|

---

## ⑪ 影响评估 / 回滚

- **破坏性**：`/v3/mp/aftersale/ticket*` 已删，调用返回 404，前端涉及"退款申诉"的页面**必须切到新申诉接口**。
- 投诉接口不在此列（投诉走 C 端经 BFF 的现有通道，无变化）。
- 回滚：后端回滚 PR #4251 #4254。

---

## ⑫ 注意事项

- 金额字段（appealAmount / actualAmount）均为**字符串**，前端按字符串处理防精度丢失。
- Long 型 ID 均字符串化返回（防 JS 精度）。
- 上传凭证图片仍走既有 wx 内容安全机审通道（mediaTraceIds 透传）。

---

## ⑬ 关联 / 联系人

- Issue：https://git.1814.love:8443/wx/HL/issues/4250
- PR：https://git.1814.love:8443/wx/HL/pulls/4251 ・ https://git.1814.love:8443/wx/HL/pulls/4254
- 后端负责人：腰苏图
- 已部署测试服并网关实调验证通过（申诉接口 200 / 工单接口 404）。
