退款逆向链路:正向只有一条路,逆向有多少条?—— 从退款状态机到四步级联回滚

导语:前面几篇拆了 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 天(物流 + 质检)。如果把两者绑在一起,客户退个款要等半个月,体验极差。解耦之后,钱先退、货慢慢收,两不耽误。

我见过不少系统把退款和退货绑成一个流程:客户申请退货 → 寄回商品 → 仓库签收 → 质检通过 → 触发退款。这条链路看起来逻辑自洽,但实操中问题很多:仓库签收后质检排了三天队,客户等到第五天还没收到退款,直接投诉到平台。平台的自动仲裁会判定商家"拖延退款",罚款 + 扣分。解耦之后,退款可以在审核通过当天就执行,退货入库按自己的节奏走。

八、落地建议

  1. 退款审核要不要自动化? 低金额(比如 ¥50 以下)可以自动审核通过,省人力。高金额走人工审批。阈值可以根据历史退款欺诈率动态调整。
  2. 部分退款的金额计算要写清楚规则。 是退商品原价、还是扣除优惠后的实付价?建议在前端退款页面直接展示计算明细,减少客诉。
  3. 库存差异要有兜底机制。 优雅降级虽然保证了退款不卡,但库存差异不能放任不管。建议每日跑一次对账脚本:退款成功但库存未释放的记录 → 自动生成补录单 → 运营确认后执行。
  4. B2B 退货质检要有时限。 收到退货后 48 小时内必须完成质检入库,否则自动提醒仓库主管。超时未入库的退货单应该触发客诉预警。

两个问题想听听你们的做法:

  1. 你们的部分退款金额怎么算的?有没有遇到过"客户退了 3 件但券是按 5 件发的"这种分摊争议?
  2. 退款审核是全量人工还是有自动通过的规则?自动通过的阈值设的多少?

不方便公开说的可以私信我。退款逆向链路的完整代码(MallRefundServiceImpl + StockLockServiceImpl + SalesReturnServiceImpl + 表结构 DDL)我正在整理,需要的也可以私信我发你。
系列回顾:++《物流对接全链路代码解读》++ · ++《OMS++ ++路由核心代码解读》++ · ++《库存预占:下单后该不该锁》++

相关推荐
我才是银古2 个月前
从零构建 GIS 数据引擎:方案驱动架构的设计与实践
gis·空间分析·gdal·质检·ai平台
thubier(段新建)4 个月前
三方物流平台-OMS系统架构设计方案
系统架构·oms
得助智能-垂类大模型7 个月前
得助智能证券质检系统:客服推诿、违规承诺回报率、案件关联质检场景应用!
金融·证券·质检·合规·智能质检·得助智能·券商
菊风 Juphoon1 年前
菊风智能质检:重塑金融业合规与风控的新标杆
实时音视频·质检·录音录像
YisquareTech1 年前
ESB 在零售,物流,制造,保险,医疗行业的应用方式
wms·集成·erp·esb·制造业·oms·零售业
nangonghen1 年前
oceanbase oms工具实时迁移oceanbase至mysql
mysql·oceanbase·oms
徐礼昭|商派软件市场负责人2 年前
支持各大平台账单处理,支持复杂业财数据的精细化对账|商派OMS
大数据·数据库·人工智能·oms·财务对账
lynn-fish2 年前
在质量检验中,如何才能提高生产效率
制造·数字化·质量管理·生产管理·智能化·质量检验·质检