支付系统开发(四):退款与对账流程

支付做到现在,收款链路基本跑通了。但真正麻烦的事才刚开始——退款和对账。线上跑了一段时间后发现,退款场景远比想象中复杂,对账则是每天悬在头上的一把刀。这篇记录下我在退款与对账模块的设计思路和踩过的坑。

退款流程设计

退款类型

系统需要支持三种退款方式:

  • 全额退款:整笔订单全部退回,最简单
  • 部分退款:退订单中的某几个商品,或者退部分金额
  • 退款到原支付方式:微信支付的退回微信,支付宝的退回支付宝

部分退款是最复杂的。一笔订单可能多次部分退款,需要校验累计退款金额不能超过原支付金额。我在退款表里加了 total_refunded 字段来做累加校验。

退款状态机

退款单独维护了一套状态机,和支付订单的状态是分开的:

退款申请 → 退款审核中 → 退款处理中 → 退款成功
                ↓                    ↓
            审核拒绝             退款失败

为什么要单独建退款表而不是在支付订单表上加状态?因为一笔支付订单可能对应多笔退款,而且退款本身有独立的生命周期。退款单需要记录:退款原因、退款金额、退款渠道流水号、退款发起时间和完成时间。

退款与渠道交互

调用微信退款接口和支付宝退款接口的逻辑差异很大。微信退款需要传原交易单号和退款单号,且必须使用证书;支付宝退款相对简单,直接用 trade_no 就行。

我抽了一个 RefundExecutor 接口来屏蔽渠道差异:

type RefundExecutor interface {
    ExecuteRefund(ctx context.Context, req *RefundRequest) (*RefundResponse, error)
    QueryRefund(ctx context.Context, refundNo string) (*RefundStatus, error)
}

微信和支付宝分别实现这个接口。上层退款服务只操作 RefundExecutor,不关心具体渠道。

退款异步处理

退款不是实时的。微信退款到账时间是1-3个工作日,支付宝快一些但也不是秒到。所以退款请求提交后,需要定时轮询退款状态。

我用了一个定时任务,每5分钟扫描一次状态为"退款处理中"的退款单,调用渠道的退款查询接口更新状态。如果超过7天还没到账,标记为异常并告警。

对账流程

对账是支付系统里最枯燥但最重要的环节。说白了就是核对我们系统里的交易记录和支付渠道的交易记录是否一致。

T+1 对账

我们采用 T+1 对账,即每天凌晨对前一天的交易做核对。流程如下:

  1. 下载渠道账单:凌晨2点从微信/支付宝下载前一天的交易账单(CSV格式)
  2. 解析账单:将渠道账单解析成统一格式,存入对账临时表
  3. 逐笔核对:用交易流水号将本地交易和渠道交易进行匹配
  4. 生成差异报告:记录所有不匹配的交易

长款与短款

对账最常见的差异有两种:

长款(我们有,渠道没有)

  • 用户支付成功,但渠道回调还没到(一般是跨天的边界交易)
  • 本地记录了支付成功,但渠道那边实际失败了(罕见但严重)

短款(渠道有,我们没有)

  • 用户支付成功,渠道回调了但我们没收到或处理失败
  • 这种情况比较危险,意味着钱收了但我们没记录

长款一般等一天再看,很多跨天交易第二天就能对上了。短款需要立即处理——先补单,把渠道的交易记录补录到系统中。

对账差异处理

我设计了一个差异处理工作流:

  1. 对账脚本发现差异后,自动创建一条"对账差异记录"
  2. 长款差异:自动挂起,T+2 再次核对,如果还对不上才告警
  3. 短款差异:立即触发补单流程,同时发送告警通知
  4. 金额不一致:直接告警,人工处理

自动对账脚本设计

对账脚本是一个独立的定时任务,核心逻辑大致如下:

1. 根据日期下载渠道账单文件
2. 解析账单 → []ChannelTransaction
3. 查询本地当天所有交易 → []LocalTransaction
4. 以 channel_trade_no 为 key 做双向匹配:
   - 本地有、渠道无 → 记录长款
   - 渠道有、本地无 → 记录短款
   - 都有但金额不一致 → 记录差异
5. 写入 recon_diff 表
6. 更新 recon_batch 表(对账批次状态、差异数量)
7. 发送对账报告邮件

对账涉及的数据量可能很大,我用了分页查询+批量比对的方式,避免一次性把所有交易加载到内存。

金额精度问题

对账时最容易出bug的地方是金额精度。微信返回的金额单位是"分",支付宝返回的是"元"(字符串),我们系统内部统一用"分"(int64)。解析渠道账单时一定要做好单位转换,不然差一分钱都会产生差异记录。

异常处理

退款和对账模块需要考虑很多异常场景:

退款异常

  • 渠道退款接口超时:标记为待查询状态,由定时任务后续查询
  • 渠道返回余额不足:这意味着商户账户余额不够退款,需要告警运营充值
  • 重复退款请求:用退款单号做幂等,渠道也支持幂等

对账异常

  • 账单下载失败:重试3次,如果还失败就告警并记录今日未对账
  • 解析失败:一般是渠道账单格式变了,需要人工介入
  • 对账超时:数据量大时可能跑很久,设置了最大执行时间,超时后告警

报警机制

对账模块接入了告警系统,以下场景会触发告警:

  • 短款差异超过 N 笔
  • 单笔差异金额超过阈值
  • 对账任务执行失败
  • 退款长时间未到账

告警通过企业微信和邮件双通道发送。说实话,对账告警是最让人紧张的——特别是早上开会前手机叮叮响,一看是对账差异告警。

踩过的坑

  1. 退款回调和支付回调可能冲突:微信退款成功后也会发回调通知,但回调的 URL 和支付回调是同一个。需要在回调处理器里区分是支付回调还是退款回调。
  2. 跨天交易对账:23:59:59 发起的交易,可能在次日 00:00:01 才到渠道。这类交易在T+1对账时对不上,必须有容错机制。
  3. 退款金额校验遗漏:最开始没有校验累计退款金额,导致出现过退款金额超过支付金额的bug,幸好在测试阶段发现了。

退款和对账这块,代码量不算特别大,但需要考虑的边界情况特别多。任何一个疏忽都可能造成资金损失,所以写代码时格外小心,测试用例也写得比其他模块多很多。下一篇准备聊聊幂等性和防重设计,这也是支付系统里绕不开的话题。