退款全流程

从商户发起退款、平台校验、银行退款、退款结算,到失败反转和通知补偿的完整流程。
提示:建议先看总览版抓主链,再切到明细流程版看 pending、failed、refund_failed 这些关键分支。
这张图最关键的不是“调用了哪个退款接口”,而是你要讲清楚退款会同时影响 `refund`、`refund_bank`、`payment.amountRefunded`、`settlement` 和通知链路。尤其是银行处理中时,系统会先占住可退额度,后面失败还要走 `refund_failed` 冲回。
01

入口与校验

  • 商户 API、后台或内部接口进入 `RefundService.save`。
  • 先加退款锁,防止同一笔 `payment` 并发重复退款。
  • 再校验 `payment.statusDetail`、可退金额和本地结算余额。
关键点:不是所有成功支付都能退,`uncaptured`、`pending`、`failed` 这些状态通常不允许直接退款。
02

创建退款主单

  • 先生成 `refund` 主表,`id = re_xxx`,初始一般是 `pending`。
  • 写支付时间线和退款时间线,留下完整操作痕迹。
  • 然后根据原交易银行金额和汇率生成 `refund_bank`。
关键对象:`refund` 是平台视角主单,`refund_bank` 是银行侧退款执行记录。
03

银行退款三态

  • 银行可能返回 `succeeded`、`processing / retry` 或 `failed`。
  • `pending` 并不是没做事,而是已经先占住 `payment.amountRefunded`。
  • 这样可以避免银行处理中时再被重复申请退款。
关键点:pending 先占额,后续失败再冲回,这是退款链路里最容易被追问的设计。
04

结算与余额

  • `refund = pending / succeeded` 时发 `refund` 结算事件。
  • `pending -> failed` 时发 `refund_failed`,把之前扣掉的余额回补。
  • `settlement` 最终生成账务流水,而不是直接手改余额。
关键点:退款影响的是 `settle_transaction` 和 `settle_balance`,不是只改 `refund.status`。
05

通知、查单与修正

  • 终态才会给商户发 `refund_succeeded` 或 `refund_failed`。
  • 银行处理中时,靠查单、回调、重试、对账和后台改状态推进终态。
  • 如果状态修正牵涉资金,还要同步补发 settlement 事件。
关键点:退款是“状态 + 账务 + 通知”三条线并行闭环,不是单一同步接口。
06

防重复退款

  • `trade` 侧按 `paymentId` 加 Redis 锁,拿不到锁直接抛 `REFUND_PROCESSING`。
  • 锁内重新读取 `payment`,按 `amount - amountRefunded - amountDisputed` 算可退额度。
  • `pending` 和 `succeeded` 都先增加 `amountRefunded`;失败再回退并发 `refund_failed`。
  • `bank-channel` 侧用 `PaymentRepeatPayInterceptor` 按 `uri + mid + request_id` 防重复打银行。
关键点:业务上防超退靠 payment 锁和额度占用;通道侧 request_id 主要防同一请求重复提交。
07

拒付叠加退款

  • 拒付是银行/卡组织发起,可能真实出现“先退款、后拒付”的双重扣款。
  • 普通退款会扣 `amountDisputed`,避免已拒付金额再被主动退款。
  • 普通拒付录入限制累计拒付不超过交易本金,但故意不加 `amountRefunded`。
  • 全额退款后又全额拒付会标记 `refundToChargeBack`,ECA 自动退款只退差额。
关键点:拒付侧不能简单按已退款拦截,因为银行双扣可能真实发生;系统要能记录、标记和对账。
08

多币种退款汇差

  • 退款给持卡人仍按原交易币种退,比如 EUR 订单退 EUR。
  • 扣商户余额时沿用原交易结算币种,比如扣 USD 钱包。
  • 退款入账用退款发生时的实时汇率,`markUp = 0`。
  • 如果 T+1 入账 USD 105,30 天后退款按新汇率扣 USD 110,差额就是汇兑风险。
关键点:主链路不会按旧汇率封顶;余额不足时普通退款会被 `REFUND_BALANCE_NOT_ALLOW` 拦住。