Skip to content

退款不是把钱退回去就完了,说说完整的资金链路

谁会卡在这

用户申请退款,你调了退款接口,微信返回成功。你以为这事完了。

月底对账,商户平台的流水和你系统里的数字对不上。差的那几笔,全是退款。

踩错的代价

退款比支付复杂,而大部分人是按支付的复杂度去设计它的。

支付是一个动作:用户付钱,你收到回调,改状态,完事。

退款是一条链路,而且中间可以被人打断:用户申请、商家审核、可能拒绝、可能要求退货、货要寄回来、商家要收货、然后才退钱。每一步都可能停住,也可能回退。

按支付的模型去做退款,会踩到三样:

状态不够用。 你以为退款只有「退了/没退」,实际至少七种。

钱和货不同步。 退款成功了但货没收到,或者收到货了钱还没退。这两件事在两个系统里,各走各的。

对账对不上。 你系统里记了一笔退款,平台侧记了另一个金额——因为运费退不退、部分退还是全退、优惠券怎么算,这些你没记清楚。

先看「退款」在库里占几张表

支付这件事,在库里基本是一张支付单加一张回调流水。

退款不是。数一下带 refund 的表,5 张

装什么
售后单一次售后申请的本体
售后单明细这次退的是哪几件、各退多少
售后单状态流水每次状态变更留一条
购物金退款单用购物金付的那部分,单独一张
分账退款单走分账的订单,回退也要单独记

后两张是这张表最值得看的地方。 它们的存在说明一件事:用户付钱的方式有几种,退款单就可能有几张表——因为退回去的路径跟付进来的路径必须一一对应。

引用到售后单的类,全库 73 个

再看常量文件,146 行里有三组值得单独说:

售后类型   2 种   仅退款 / 退货退款
退回的货   3 种   不处理 / 良品 / 次品
操作日志   8 种

中间那行是第一版设计里绝对想不到的:退回来的货,要标品相。

良品能回架继续卖,次品不能。这个字段决定的是这次退款要不要把库存加回去——判断错了,货架上就出现一件卖不出去的东西,或者少了一件本该能卖的。

8 种操作日志里,有两种也是后来才加的:一个叫「强制退款」(不走商家同意,平台直接退),一个叫「拒收货」(商家收到货但拒绝签收)。

这两个动作在「用户申请 → 商家同意 → 退钱」那张理想流程图上,一个都画不出来。它们是真实世界里退款流程被打断之后,不得不补的口子。

退款有七个状态,不是两个

真实项目里,售后单的状态是这样的:

状态谁触发
0待审核用户提交申请后
1商家拒绝商家
2退款中商家同意,钱在路上
3已退款支付平台回调确认
4用户退货用户填了物流单号
5商家待收货货在路上
6已撤销用户自己取消申请

看这张表能看出两件事:

第一,退款是一条独立的状态机,跟主订单状态并行跑。一个订单可以同时是「已收货」和「退款中」——这是完全合法的,用户收到货之后申请退款嘛。

所以退款状态不能塞进订单状态里。 它们是两个维度,不是一条线上的两段。

第二,有分叉。 「用户退货 → 商家待收货 → 已退款」是一条路,「商家同意 → 直接退款」是另一条路。前者要等物流,后者不用。

只退钱不退货(比如虚拟商品、部分退款)和退货退款,是两条不同的路径,代码里得分开。

一张售后单上有 8 个金额字段

上面是表的数量,这一节是单张表的内部。

售后单本体 47 个字段。里头跟钱直接相关的有 8 个,再加 2 个积分字段:

字段退给谁 / 从谁那儿扣
退款金额用户
退运费金额用户,单独一列
商家退款金额从商户账户出
平台退款金额从平台账户出
退还平台优惠券补贴退回平台
退一级返佣从推广人那儿扣回来
退二级返佣同上,第二层
退款积分抵扣金额积分抵的那部分折成钱
退还使用积分加回用户积分账户
扣除赠送积分从用户账户扣走

「把钱退回去」这句话,在这张表上是 10 个数。

三个方向值得注意:

一、退款金额被拆成了商家和平台两份。 一笔退款,钱从两个口袋出——因为当初这单的收入也是分给两边的。合成一个「退款金额」,退完就分不清该找谁要。

二、有两个字段是往回扣的,不是往外退的。 退返佣、扣赠送积分——用户退货,之前发出去的奖励要收回来。

而这两笔往往已经被花掉了:佣金可能已经提现,赠送积分可能已经抵扣了下一单。所以这两个字段真正对应的是一个负数余额问题,不是一次转账。

三、运费单独一列。 因为「退不退运费」是一个独立的业务决策,跟退多少货款无关。合并进退款金额,这个决策就没地方记了。

这 10 个数里少记一个,月底对账就差一笔。 而且差的那笔通常不是主金额——主金额谁都记得——是运费、是补贴、是那两笔往回扣的。

完整的资金链路

退款成功只是链路的中间,不是终点。完整的链路是这样:

用户申请

商家审核 ──拒绝──→ 结束
  ↓ 同意
需要退货?
  ├ 是 → 用户寄回 → 商家收货
  └ 否 ─────────────────┐
  ↓                     │
调用退款接口 ←───────────┘

等待退款回调(这里也要幂等)

├ 改售后状态
├ 写资金流水
└ 回退关联数据
   积分 / 优惠券 / 库存 / 分账

进当日对账单

月底汇总核对

最容易漏的是右边那一列。 退款不只是把钱退回去,还要把当初因为这笔订单产生的东西回退掉

  • 送出去的积分要扣回来(扣不回来怎么办?余额不够怎么办?)
  • 用掉的优惠券要不要退还
  • 扣掉的库存要不要加回去
  • 已经分出去的账要发起分账回退

这几件事没有一件是自动的。 而且它们各自都可能失败。

三个必须做的事

一、退款回调也要幂等

跟支付回调一模一样的道理:退款回调也会重推。

不幂等的后果是:退款状态被重复处理,关联的积分被扣两次、库存被加两次。 而且因为退款本身的金额是对的,你在对账里看不出来——出问题的是那些「回退动作」。

处理方式跟支付回调一致:已处理就返回成功,别抛异常。

二、资金流水要单独记,而且只增不改

别把资金变动的记录塞在订单表里。

订单表记的是「这个订单现在什么状态」,是一个当前值。资金流水记的是「什么时候发生了什么,金额多少」,是一串事件

两者的区别在对账的时候会要命:订单表告诉你「这单退款了」,但它不能告诉你「退了多少、什么时候退的、退到哪去了、运费退没退」。

真实项目里这块是分开的:订单流水记录、用户账单、商户日结单、商户月结单——四张表,各管一段

流水表的规矩只有一条:只追加,不修改。 记错了就再记一条冲正的,不要改原来那条。因为对账靠的是流水的连续性,一旦允许改,你就永远不知道昨天那份对账单是基于哪个版本算的。

三、日结和月结都要有

只做月结对账是不够的。

月底发现差三笔,你要在三十天的数据里找它们。做了日结,你只需要在一天里找。

日结单每天跑一次,跟平台的当日流水核对。差异当天暴露,当天能查。等到月底再对,你连当时发生了什么都想不起来了。

对账对不上,先查这五处

按出现频率排:

一、运费退了没有。 退款金额和退运费金额是两个字段。全额退款要不要退运费,是个业务决策,但代码里必须明确。这一项对不上最常见。

二、部分退款没记清楚。 一个订单退两次,每次退一部分。你系统里记了两条还是一条?金额是累加的还是覆盖的?

三、优惠券和积分抵扣的部分。 用户用 100 元券买了 200 的东西,实付 100。退款退多少?退 100 还是 200?券退不退?这个必须在设计时就定死,写在代码注释里。

四、退款手续费。 有些通道退款是要手续费的,平台侧扣了,你系统里没记。

五、时间边界。 23:59 发起、00:01 成功的退款,算哪天的?你按发起时间记,平台按完成时间记,这笔就跨天了。约定一个口径,两边一致。

常见问题对照表

现象真正的原因解法
退款成功了,但积分/库存没回退只处理了退款状态,漏了关联回退把回退动作纳入退款完成的处理单元
积分被扣了两次退款回调重推,回退动作不幂等退款回调也要幂等,跟支付回调一个道理
对账差额刚好等于运费退运费金额没算进去,或算重了明确全额退是否含运费,两边口径统一
部分退款后金额对不上多次退款的记录方式不一致每次退款单独一条流水,不覆盖
用券订单退款金额有争议抵扣部分怎么退没定义设计时定死并写进注释,别留给运行时判断
跨天的退款两边归属不同一边按发起时间、一边按完成时间统一口径,通常跟平台侧对齐
月底对账差几笔,查不出是哪天的只有月结,没有日结加日结,把排查范围从 30 天压到 1 天
改了历史流水记录后越对越乱流水表允许修改流水只追加不修改,错了记冲正

这条链路上的其他几篇

  • 《支付回调一定会重复,你的幂等挡不挡得住》
  • 《订单状态机是怎么烂掉的,以及怎么不烂》

签名-C

大粽子