# 退款与对账

> docId：`XD-OP-08` ｜ version：`1.0.0` ｜ status：`PUBLISHED`

## 1. 退款接口

```http
POST <BASE_URL>/api/open/v1/refunds
{
  "outTradeNo": "SHOP20260926A0001",
  "outRefundNo": "RF20260926A0001",
  "refundAmountFen": 100,
  "reason": "买家申请"
}
```

| 项 | 规则 |
| --- | --- |
| 可退前提 | 仅**支付成功**的订单可退 |
| 金额 | 累计退款不得超过实付金额；**失败的退款不占额度**；超限返回 `422 OPEN_API_LIMIT_EXCEEDED` |
| 幂等 | 同 `outRefundNo` 同内容 → `200` 幂等重放；同号异内容 → `409` |
| 受理后被拒 | `502`（支付机构拒绝，平台不向外暴露内部原因） |
| 结果不确定 | 状态 `UNKNOWN`，**不判死**，等事件收敛（见 §2） |

退款状态：`REQUESTED` → `PENDING` → `SUCCEEDED` / `FAILED`（另有 `UNKNOWN`）。

> **一个容易踩的 `409`**：退款按**下单时冻结的收款分配快照**发起回退（实际到账路径以支付服务方原交易与分账合同为准），因此前提是该单**有可用的快照**。缺少快照（历史单或异常单）时，创建退款返回 `409` `OPEN_API_STATE_INVALID`——这表示该单**不能自助退款**，请带 `outTradeNo` 与 `requestId` 走官网反馈渠道人工处置，**不要换号重试**。

**请只用查询退款接口和自己库里记录的 `outRefundNo` 判退款最终结果**——不要用「发起退款后等待固定时长」这种猜测式判断。

## 2. 结果不确定（`UNKNOWN`）时的正确姿势

`UNKNOWN` 表示「平台与支付机构之间的结果暂时不可知」。正确处理是**查证收敛**，不是重做动作：

```text
退款 UNKNOWN
 ├─ 等 REFUND_SUCCEEDED / REFUND_FAILED 事件（唯一自动收敛路径）
 ├─ 调 GET /api/open/v1/refunds/{outRefundNo} 看当前状态（本地权威读：不会触发外呼、也不会改变状态）
 └─ 长时间仍 UNKNOWN → 把该笔挂到「待核」队列，带 outRefundNo + requestId 走官网反馈人工介入
      ❌ 不要用新的 outRefundNo 再退一次（可能造成重复退款）
      ❌ 不要把 UNKNOWN 当失败直接回滚交付（可能钱已退）
```

> **别指望「反复查单」把状态查出来**：查询接口是**平台本地账本的只读投影**，它不会去问支付机构——`UNKNOWN` 的收敛只发生在支付机构回通知时。人工兜底是最终手段。

订单侧同理：订单 `UNKNOWN` 时不要重新下单、不要判失败。

## 3. 对账

最小对账闭环（建议每日跑一次）：

1. **拉你自己的流水**：以你的库为准（订单号、金额、状态）。
2. **拉平台侧**：逐笔 `GET /api/open/v1/payments/{outTradeNo}` 与 `GET /api/open/v1/refunds/{outRefundNo}`。
3. **对齐三类差异**：

| 差异 | 处置 |
| --- | --- |
| 平台成功、你这边未成功 | 你的事件消费漏了 → 按事件 `eventId` 补齐落账（这就是为什么要接事件 + 查单双路径） |
| 你这边成功、平台未成功 | 你的落账提前了 → 这是**误判终态**（多半把回跳/页面状态当成了终态），把交付动作回退到「以查单为准」 |
| 金额/费率不一致 | 用该笔的 `feeProjection`（含 `ruleVersion`）核对，不要用你库里的费率表 |

4. **`ruleVersion` 对齐**：若同一批订单的 `ruleVersion` 出现变化，说明期间发生过配置变更——按逐笔口径对，不要按批次统一算。**但它只是全局配置版本**：平台对**单个商家**调价不会改变它，所以逐笔核对要以该笔的 `merchantRateBps` / `companyServiceFeeRateBps` 为准，不能只看版本号有没有变。

## 4. 结算读回

- 逐笔结算状态看门户「资金 / 结算」页（**逐笔清单支持翻页**）；**到账周期与口径以门户生效配置为准**（见 [费率与限额](./07-fees-and-limits.md)）。
- 对账发现「平台已结算、你未入账」时：先看该笔订单的渠道与结算状态，再带着 `outTradeNo` + `requestId` 走官网反馈渠道。

## 5. 退款常见误区

1. **用新的 `outRefundNo` 重试 `UNKNOWN` 的退款** → 重复退款。
2. **把 `502` 直接当成「没退成功」** → 要先查退款状态再决定，`502` 只代表「本次请求被拒」。
3. **按「发起时间 + 固定时长」判断到账** → 结算周期与渠道相关，会变。
4. **对已关闭/已失败订单发退款** → `409`/`422`，先查单确认订单状态。
