导语:前面几篇拆了 OMS 路由、库存预占、物流对接------全是正向链路,订单从下单到签收一路往前。但做电商系统绕不开一个更难的问题:退款。客户要退部分商品怎么办?货在途中怎么办?用过的积分和优惠券要不要退?B2B 退货还要做质检?这篇从代码层面拆开退款逆向链路------六态状态机、部分退款、四步级联回滚、B2B 质检退货三段式、优雅降级。

封面图:退款逆向链路 ------ 正向下单 vs 逆向回滚的多路径设计
一、一个退款的复杂度远超你想象
陈总做日用品电商,上个月遇到一个客诉:客户买了 5 件商品(共 ¥1,268),收到后觉得其中 2 件不满意,要退这 2 件。
听起来简单,但技术侧要处理的事情有:
- 退款金额怎么算?这 2 件 ¥336,但订单用了 ¥50 优惠券和 500 积分,退款要不要按比例扣减?
- 这 2 件商品的库存要不要回滚?下单时锁了 5 件的库存、发了 5 件货,现在退 2 件,库存表怎么更新?
- 优惠券和积分怎么处理?整单退可以原样退回,部分退呢------券已经用了,积分已经扣了。
- 财务流水怎么冲正?原收款 ¥1,268,现在退 ¥336,对账时这笔怎么记?
一件退款,牵扯库存、财务、营销(积分/券)、售后四个子系统。正向下单只有一条路径,逆向退款的分支数是正向的好几倍。
这不是个案。根据陈总的数据,月均退款率 8%,其中 60% 是部分退款、30% 是整单退、10% 是取消订单。每种情况触发的回滚操作不同:取消订单要回滚积分+券+库存+信用额度四个子系统,部分退款只退商品价不退券,退货退款还要走质检入库。如果退款系统设计得不够灵活,每新增一种退款场景就要加一堆 if-else------三个月后代码就变成一坨谁也看不懂的"退款屎山"。
二、退款状态机:六态流转
系统里退款单有六个状态,流转规则非常严格:
| 状态 | 含义 | 可流转到 |
|---|---|---|
| 0 | 待审核 | → 1(通过)、2(拒绝)、6(取消) |
| 1 | 审核通过 | → 4(成功)、5(失败) |
| 2 | 审核拒绝 | 终态 |
| 4 | 退款成功 | 终态 |
| 5 | 退款失败 | 可重试 → 4 |
| 6 | 已取消 | 终态 |
注意两个设计要点:
第一,审核通过 ≠ 退款完成。 auditRefund() 只改状态为 1,真正执行退款是另一个方法 processRefund()。这样设计的好处是:审核人只管判断"该不该退",执行退款可以异步、可以批量、可以对接支付平台回调。
第二,退款失败不是终态。 状态 5(失败)可以重试------支付平台偶尔会超时或者限流,一次失败不代表永远失败。系统允许对状态 5 的记录重新调用 processRefund()。

退款状态机:六态流转 + 级联操作触发点
三、部分退款:按行项目粒度
退款申请时,前端传入的是 orderItemIds(选择退哪些行项目),而不是"退整个订单"。这让系统天然支持部分退款。
比如一个订单有 5 行商品,客户选了第 2 行和第 4 行退款:
```java
List<Long> orderItemIds = itemId_2, itemId_4;
BigDecimal refundAmount = item2.price × item2.qty + item4.price × item4.qty;
```
每条 MallRefundItem 绑定一个 orderItemId,退款金额 = 该行项目的 productPrice × quantity。
这里有一个细节:优惠券和积分不按比例分摊。 系统的设计是"部分退款只退商品价,不退券和积分"。券和积分只在整单退或取消订单时才退回。这个取舍是有意的------如果部分退款也要算券的分摊比例,逻辑会复杂很多(5 件商品用了一张 ¥50 券,退 2 件该退多少券?¥20?按比例 ¥22.3?),而且对用户来说也不直观。
不过这个设计有一个副作用:如果一个订单 ¥1,000 用了 ¥100 券,实付 ¥900。客户退了 ¥500 的商品,拿到 ¥500 退款,相当于实际只付了 ¥400 就买了 ¥500 的东西(因为 ¥100 券没按比例扣回)。对商家来说这是一笔隐性损失。解决方案有两种:一是退款金额按实付比例折算(退 ¥450 而不是 ¥500),二是部分退款时不退券但记录一个"券分摊差额",后续在财务对账时冲销。哪种更合理取决于业务方的容忍度。
四、取消订单:四步级联回滚
取消订单比退款更复杂,因为它要回滚整个下单过程。cancelOrder() 方法里有四步逆向操作,按顺序执行:
第一步:退回积分。 调用 memberPointsService.addPoints(userId, usePoints),把下单时扣的积分加回来。
第二步:退回优惠券。 调用 userCouponService.refundCoupon(),把优惠券的使用状态重置为未使用,清空关联的订单号。这张券重新出现在用户的"可用优惠券"列表里。
第三步:释放库存锁定。 调用 stockLockService.unlockStock(),遍历这笔订单关联的所有 stock_lock 记录,把每个批次的 lockedQty 减回来、availableQty 加回去。库存重新可卖。
第四步:释放信用额度。 B2B 客户可能有信用账期,下单时占用了信用额度。取消时调用 creditAccountService.releaseCredit() 把额度还回去。
这四步必须全部成功,取消才算完成。但实际落地时有一个关键问题:如果第三步(库存释放)失败了怎么办?
五、优雅降级:库存释放失败不阻断退款
processRefund() 里有一段很有意思的代码:
```java
try {
stockLockService.unlockStock(unlockReq);
} catch (Exception e) {
log.warn("退款释放库存失败, refundNo={}, orderNo={}, error={}",
refund.getRefundNo(), refund.getOrderNo(), e.getMessage());
// 不抛出异常,继续退款主流程
}
```
库存释放失败,只打 warn 日志,不阻断退款。为什么?因为退款是客户的钱,库存是公司的货。钱的问题不能因为货的问题卡住------客户等退款等 3 天已经要投诉了,不能因为库存系统的一个锁超时就让客户继续等。
库存差异可以后续通过盘点修复,但退款延迟会直接导致客诉和平台处罚。这是逆向链路里一个很重要的设计原则:对客户的承诺(退款到账)优先级高于内部数据的一致性(库存准确)。
当然,warn 日志不能只打不管。系统里应该配一个监控:每天扫描"退款成功但库存未释放"的记录(退款表 status=4 但对应的 stock_lock.status 还是 1),生成差异报告,运营手动确认后再做库存调整。
这个设计原则在逆向链路里普遍适用:对外承诺 > 内部一致性。 退款是对客户的承诺,必须按时到账;库存差异是内部问题,可以通过对账修复。类似的例子还有:物流拦截失败时应该先退款再追回包裹(而不是让退款等拦截结果),优惠券退回失败时应该先完成退款主体流程(而不是让退款等券系统恢复)。
六、B2B 退货:质检 → 入库 → 退款三段式
B2C 退款相对简单------客户说退就退,钱退了货不用管(或者后续再收)。但 B2B 退货严格得多,因为金额大、涉及对公结算。
SalesReturnServiceImpl 里设计了一个三段式流程:
第一段:审核。 退货申请进来(status=0),主管审批通过后进入 status=1(已审核待入库)。
第二段:质检入库。 仓库收到退货后做质检,根据结果分流:
- 合格品(qualityStatus=1):生成新批次,调用 inventoryService.adjustStock() 把库存加回来。这批货可以直接再卖。
- 不合格品(qualityStatus=2):报废处理,不增库存。
- 待返工(qualityStatus=3):生成特殊批次(编号后缀 -RW),进入返工队列。
第三段:退款。 入库完成后才能触发退款。B2B 还支持多次部分退款 ------每次退一部分,系统累加 refundAmount,和 totalAmount 对比,判断是"部分退款"还是"全部退款"。
```java
BigDecimal totalRefundAmount = currentRefundAmount.add(refundAmount);
if (totalRefundAmount.compareTo(totalAmount) >= 0) {
refundStatus = 2; // 全部退款
} else if (totalRefundAmount.compareTo(BigDecimal.ZERO) > 0) {
refundStatus = 1; // 部分退款
}
```

退款管理看板:待审核、退款成功、库存回滚统计、取消订单级联回滚流程
七、退款和退货的解耦
最后一个值得注意的设计:退款流程和退货流程是解耦的。
在 B2C 场景下,processRefund() 执行退款后,不管货有没有回来。如果客户选的是"仅退款"(type=1),系统直接释放库存锁定、完成退款,货物的实际回收由后续的退货入库单单独处理。如果客户选的是"退货退款"(type=2),系统也是先退款,退货入库走 SalesReturn 的独立流程。
为什么解耦?因为退款和退货的时效差异太大------退款通常 1-3 天要到账,但退货入库可能要等 7-15 天(物流 + 质检)。如果把两者绑在一起,客户退个款要等半个月,体验极差。解耦之后,钱先退、货慢慢收,两不耽误。
我见过不少系统把退款和退货绑成一个流程:客户申请退货 → 寄回商品 → 仓库签收 → 质检通过 → 触发退款。这条链路看起来逻辑自洽,但实操中问题很多:仓库签收后质检排了三天队,客户等到第五天还没收到退款,直接投诉到平台。平台的自动仲裁会判定商家"拖延退款",罚款 + 扣分。解耦之后,退款可以在审核通过当天就执行,退货入库按自己的节奏走。
八、落地建议
- 退款审核要不要自动化? 低金额(比如 ¥50 以下)可以自动审核通过,省人力。高金额走人工审批。阈值可以根据历史退款欺诈率动态调整。
- 部分退款的金额计算要写清楚规则。 是退商品原价、还是扣除优惠后的实付价?建议在前端退款页面直接展示计算明细,减少客诉。
- 库存差异要有兜底机制。 优雅降级虽然保证了退款不卡,但库存差异不能放任不管。建议每日跑一次对账脚本:退款成功但库存未释放的记录 → 自动生成补录单 → 运营确认后执行。
- B2B 退货质检要有时限。 收到退货后 48 小时内必须完成质检入库,否则自动提醒仓库主管。超时未入库的退货单应该触发客诉预警。
两个问题想听听你们的做法:
- 你们的部分退款金额怎么算的?有没有遇到过"客户退了 3 件但券是按 5 件发的"这种分摊争议?
- 退款审核是全量人工还是有自动通过的规则?自动通过的阈值设的多少?
不方便公开说的可以私信我。退款逆向链路的完整代码(MallRefundServiceImpl + StockLockServiceImpl + SalesReturnServiceImpl + 表结构 DDL)我正在整理,需要的也可以私信我发你。
系列回顾:++《物流对接全链路代码解读》++ · ++《OMS++ ++路由核心代码解读》++ · ++《库存预占:下单后该不该锁》++