大家好,我是你们的 RuoYi-Vue-Pro 源码拆解系列的腻害兔。今天咱们来啃一个「硬骨头」------支付模块 yudao-module-pay。为什么说它是硬骨头?因为支付系统是几乎所有 ToC 产品里最核心、最不能出错的基础设施。钱的事,能马虎吗?
废话不多说,直接上干货。今天这篇文章,我会从产品经理和技术双视角,把这个支付模块扒个底朝天------它是怎么设计的、为什么这样设计、跟竞品比有什么优劣、以及如果让你重新设计你会怎么改。
一、今日模块概览
一句话说清楚:yudao-module-pay 是一个「支付中台」,它把支付宝、微信支付、钱包支付等多种支付渠道统一抽象成一套标准接口,让上层业务(商城、钱包充值、会员等)只需要对接一个 API 就能完成收款、退款、转账的全部能力。
你可以把它理解成一个「翻译官」------业务系统说"我要收 100 块钱",支付中台负责把这句话翻译成支付宝能听懂的指令、或者微信支付能听懂的指令,然后帮你盯着钱到账了没有,到账了再通知业务系统"钱到了,发货吧"。
这个模块一共包含 14 张数据库表、19 个 Controller、60 个 API 端点、12 个 Service 实现,覆盖了从支付应用管理、渠道配置、订单生命周期、退款、转账、回调通知、到用户钱包的完整链路。
二、技术选型分析
2.1 支付 SDK 选型:为什么用官方 SDK + WxJava?
| 支付渠道 | 使用的 SDK | 版本 | 替代方案 |
|---|---|---|---|
| 支付宝 | alipay-sdk-java(官方) | 4.40.607.ALL | 无(官方 SDK 是唯一选择) |
| 微信支付 | weixin-java-pay(WxJava) | 4.8.4 | 微信支付官方 SDK(v3) |
| 钱包支付 | 自研(无外部依赖) | --- | --- |
| 模拟支付 | 自研 MockPayClient | --- | --- |
为什么支付宝用官方 SDK? 这个没什么好讨论的,支付宝 SDK 是唯一正解。它封装了签名、验签、证书模式等所有底层细节,自己造轮子纯属给自己找不痛快。
为什么微信用 WxJava 而不是官方 SDK? 这是一个有意思的选择。WxJava(binarywang 开源)是一个社区驱动的微信 Java SDK,支持从 API V2(XML 协议)到 V3(JSON 协议)的全部接口,而且它不只是支付,还覆盖了公众号、小程序、企业微信等场景。RuoYi 选择 WxJava 的原因我推测有三:
- 历史兼容性:WxJava 在微信支付 V3 出来之前就已经很成熟了,很多老项目一直在用
- API 封装更友好:WxJava 的 API 风格比微信官方 SDK 更 Spring 一些,用起来更顺手
- 多渠道统一管理:WxJava 把公众号支付、小程序支付、APP 支付、Native 支付、H5 支付、条码支付 6 种场景统一在一个 Service 下,方便 RuoYi 做 14 种渠道的抽象
划重点: 如果你的新项目要从零开始做微信支付,建议直接用微信支付官方 V3 SDK(wechatpay-java),它是微信官方维护的,对 V3 协议的支持更完整、更新更及时。WxJava 虽然好用,但社区维护的 SDK 在响应速度和稳定性上天然有劣势。
2.2 ORM 选型:MyBatis-Plus
这个在框架层已经分析过了,支付模块继续沿用 MyBatis-Plus。值得一提的是,支付模块在 Mapper 层大量使用了 SQL 级别的 CAS(Compare And Swap)操作,比如钱包扣款:
sql
// 钱包扣款------SQL 级别的原子操作,WHERE balance >= X 是核心安全阀
@Update("UPDATE pay_wallet SET balance = balance - #{price}, " +
"total_expense = total_expense + #{price} " +
"WHERE id = #{id} AND balance >= #{price} AND deleted = 0")
int updateWhenConsumption(@Param("id") Long id, @Param("price") Integer price);
这种设计非常聪明------它把并发安全的最后一道防线放在了数据库层面,即使 Redis 分布式锁出了问题,SQL 的 WHERE balance >= #{price} 也能保证不会扣成负数。
2.3 分布式锁:Redisson
钱包余额操作使用了 Redisson 分布式锁,锁的粒度是钱包 ID 级别(pay:wallet:lock:{walletId})。这是一个合理的粒度选择------既不会因为全局锁导致性能瓶颈,也不会因为太细粒度导致一致性出问题。
三、需求溯源推演
好,现在我们来玩一个「时光倒流」的游戏------假设我是芋道团队的产品经理,这个支付模块最初的需求文档会是什么样的?
3.1 最初的需求场景
我猜最早的需求大概是这样的:
场景: 我们做了一个商城模块,用户下单后需要付款。商城开发团队说:"我需要对接支付宝和微信支付,帮我搞一下。"
问题: 如果商城直接调用支付宝/微信的 SDK,那以后 CRM 模块要收会员费、ERP 模块要收服务费,每个模块都要自己写一套支付对接逻辑。10 个业务模块 = 10 套支付代码,维护成本爆炸。
解决方案: 做一个统一的支付中台,业务模块只需要调用 payOrderApi.createOrder() 就能发起支付,不用关心底层是支付宝还是微信。
3.2 需求的逐步演化
从代码结构来看,这个模块的能力是逐步「长出来」的,不是一开始就规划好的:
- V1.0:最基础的收款能力------创建订单、对接支付宝/微信、处理回调。核心表 pay_order + pay_order_extension
- V1.5:加了退款------pay_refund 表,支持部分退款
- V2.0:加了转账/提现------pay_transfer 表,商家给用户打钱
- V2.5:加了通知重试机制------pay_notify_task + pay_notify_log,解决回调丢失问题
- V3.0:加了钱包系统------pay_wallet + pay_wallet_transaction,用户可以在平台充值消费
- V3.5:加了充值套餐------pay_wallet_recharge_package,充 100 送 20 的营销能力
- V4.0:加了 Demo 示例------pay_demo_order + pay_demo_withdraw,教其他模块怎么接入
产品思考: 这种「逐步长出来」的演化路径其实很健康。支付系统最怕的就是一开始过度设计------你都不知道业务需要什么,就搞一个超级复杂的抽象,结果 80% 的能力用不上,20% 真正需要的没设计好。芋道团队选择了「先跑通核心链路,再逐步扩展」的策略,这是务实的做法。
四、竞品对标分析
接下来我们看看同类开源项目是怎么做支付模块的。
| 对比维度 | RuoYi-Vue-Pro | JeecgBoot | Pig | Guns | SpringBlade |
|---|---|---|---|---|---|
| 是否有支付模块 | ✅ 完整 | ❌ 无内置 | ❌ 无内置 | ✅ 基础 | ❌ 无内置 |
| 支付渠道数量 | 14 种(6 微信 + 5 支付宝 + 钱包 + Mock) | --- | --- | 支付宝 + 微信 | --- |
| 支付中台架构 | ✅ 策略模式 + 工厂模式 | --- | --- | 简单封装 | --- |
| 钱包系统 | ✅ 完整(余额/冻结/充值/明细) | --- | --- | ❌ | --- |
| 退款支持 | ✅ 部分退款 + 多次退款 | --- | --- | 基础 | --- |
| 转账/提现 | ✅ 完整 | --- | --- | ❌ | --- |
| 回调通知重试 | ✅ 9 次指数退避 | --- | --- | ❌ | --- |
| 多租户支持 | ✅ 渠道配置租户隔离 | --- | ✅ | ❌ | --- |
| Demo 示例 | ✅ 完整的示例订单 + 示例提现 | --- | --- | ❌ | --- |
结论:在开源后台管理系统中,RuoYi-Vue-Pro 的支付模块是独一档的存在。 JeecgBoot、Pig、Guns、SpringBlade 这些知名开源项目要么没有支付模块,要么只有非常基础的封装。RuoYi-Vue-Pro 是目前开源界里支付能力最完整的 Java 后台系统,没有之一。
优势
- 渠道抽象做得好:14 种渠道通过统一的 PayClient 接口抽象,新增渠道只需实现一个类
- 扩展表设计精妙:pay_order_extension 记录每次渠道调用,支持换渠道重试而不丢失审计轨迹
- 通知重试机制可靠:9 次指数退避 + Redis 分布式锁 + 定时任务轮询,三重保障
- 钱包系统完整:余额、冻结、充值、退款、明细、充值套餐,一套完整的虚拟账户体系
劣势
- 缺少对账系统:没有看到自动对账模块(与渠道账单逐笔核对),这在生产环境中是必须的
- 缺少分账能力:没有看到分账(Profit Sharing)相关的设计,对于平台型商户来说这是一个硬需求
- 没有聚合支付:没有「一个二维码同时支持支付宝和微信」的聚合支付能力
- SDK 版本偏旧:微信支付建议迁移到官方 V3 SDK
五、核心业务流程
5.1 支付订单全生命周期
这是整个支付模块最核心的流程,我用 Mermaid 画出来:

5.2 退款流程
退款流程有一个非常关键的设计细节------即使渠道 API 调用超时,也要尝试处理退款结果。为什么?因为微信/支付宝可能在超时前已经处理了退款请求,只是响应没来得及返回。如果简单地把超时当作失败,就会出现「钱退了但系统不知道」的情况。
java
// PayRefundServiceImpl.createRefund() 的关键代码
try {
respDTO = client.unifiedRefund(reqDTO);
} catch (Exception e) {
// 注意:这里 catch 了所有异常!
// 因为渠道可能已经处理了退款,只是响应超时了
// 后续通过 syncRefund() 定时任务来补偿
}
// 无论成功还是异常,都调用 notifyRefund 来同步状态
this.notifyRefund(channelId, respDTO);
踩坑提醒: 这个设计是支付系统的「保命技能」。很多初级开发者写退款逻辑时,超时了就返回失败,结果导致重复退款或者资金不一致。芋道这个处理方式值得学习------先记录、后补偿、绝不丢单。
5.3 回调通知重试机制

重试频率为 15s, 15s, 30s, 180s, 1800s, 1800s, 1800s, 3600s,即 1 次立即 + 8 次重试 = 共 9 次,总跨度约 2.5 小时。这个频率设计参考了微信支付官方的回调频率,属于行业通用实践。
六、数据模型解读
6.1 核心表关系
整个支付模块 14 张表,可以分为 4 个域:
支付核心域(6 张表):
sql
pay_app (支付应用)
│
├──< pay_channel (支付渠道) ── 1:N,一个应用可以有微信、支付宝、钱包等多个渠道
│
├──< pay_order (支付订单) ── 1:N
│ │
│ ├──< pay_order_extension (订单扩展) ── 1:N,每次渠道调用产生一条
│ │
│ └──< pay_refund (退款单) ── 1:N,支持部分退款,一笔订单可退多次
│
└──< pay_transfer (转账单) ── 1:N
通知域(2 张表):
pay_notify_task (通知任务) │ └──< pay_notify_log (通知日志) ── 1:N,每次重试产生一条日志
钱包域(4 张表):
java
pay_wallet (用户钱包)
│
├──< pay_wallet_transaction (钱包流水) ── 1:N
│
└──< pay_wallet_recharge (充值记录) ── 1:N
│
└── > pay_wallet_recharge_package (充值套餐) ── N:1
pay_wallet_recharge ──> pay_order (关联支付订单)
pay_wallet_recharge ──> pay_refund (关联退款单)
示例域(2 张表):
pay_demo_order (示例订单) ──> pay_order + pay_refund pay_demo_withdraw (示例提现) ──> pay_transfer
6.2 为什么要有 pay_order_extension 这张表?
这是整个数据模型里最精妙的设计。很多支付系统只有一张 pay_order 表,但 RuoYi 拆成了「主表 + 扩展表」的结构。为什么?
核心原因:一次订单可能尝试多次支付。 比如用户下单后选了微信支付但没付款(二维码扫了但没输入密码),然后切换到支付宝支付。这两次渠道调用需要分别记录,因为:
- 两笔渠道流水号不同,需要分别对账
- 第一笔可能后来才回调(用户其实付了),需要知道到底哪笔是真正成功的
- 审计需求------出了资金问题需要追溯每次渠道调用的详情
pay_order.extensionId 指向最终成功的那条扩展记录,一个订单最终只会有一个「赢家」。
6.3 钱包表的设计亮点
pay_wallet 表有 4 个金额字段:balance(可用余额)、freezePrice(冻结金额)、totalExpense(累计消费)、totalRecharge(累计充值)。
为什么要区分 balance 和 freezePrice? 因为有些场景需要「先冻结再扣款」。比如钱包充值退款:用户充了 100 元(送了 20 元),要退款时不能直接扣 100 元余额,因为用户可能已经花了 50 元。正确的流程是:先冻结 50 元(可用余额减少 50,冻结增加 50),然后发起退款,退款成功后从冻结中扣除。
所有金额字段都是 int 类型,单位是分。这是支付系统的行业标准------用整数避免浮点精度问题。
七、产品设计亮点与槽点
7.1 让我眼前一亮的设计
1. Demo 示例模块------教科书级的接入示范
这个设计太贴心了。PayDemoOrderController + PayDemoOrderService 完整演示了一个业务模块如何接入支付中台:创建订单 → 发起支付 → 处理支付回调 → 发起退款 → 处理退款回调。同样,PayDemoWithdrawController 演示了如何接入转账能力。
对于新加入团队的开发者来说,看 Demo 代码比看文档快 10 倍。这就像 MyBatis-Plus 自带的示例项目一样------好的框架不是文档写得好,而是示例写得好。
2. validateOrderActuallyPaid()------防重复支付的杀手锏
在 submitOrder() 提交订单时,系统会做一件「看似多余但极其重要」的事:遍历该订单的所有扩展记录,不仅检查数据库状态,还会主动调用渠道 API 查询每笔扩展的真实状态。这是为了防止「回调丢失但用户实际已付款」的极端情况。
java
// 关键逻辑:不仅看本地状态,还要去渠道侧确认
for (PayOrderExtensionDO extension : extensions) {
if (extension.getStatus().equals(WAITING)) {
// 去渠道查一下,万一其实已经付了呢?
PayOrderRespDTO channelOrder = client.getOrder(extension.getNo());
// 如果渠道说已支付,本地直接更新状态
}
}
这个设计解决了一个经典的分布式系统问题:消息丢失。支付回调本质上是一个「不可靠的消息」,它可能因为网络问题丢失。如果只依赖回调,就会出现「钱扣了但订单还是待支付」的灾难。
3. 钱包作为支付渠道------优雅的统一抽象
WalletPayClient 把钱包支付实现为一个标准的 PayClient,和其他 13 种渠道享受完全相同的接口契约。这意味着业务模块完全不需要知道「钱包支付」和「微信支付」有什么区别------它们只是不同的 channelCode 而已。
7.2 我觉得可以改进的地方
1. 缺少对账系统
生产环境的支付系统必须有对账能力------每天凌晨从支付宝/微信下载前一天的账单文件,逐笔与本地订单核对。主要对三种情况:长款(渠道有但本地没有)、短款(本地有但渠道没有)、金额不一致。没有对账系统,出了资金问题只能靠人工排查,这在日均订单量过万后是不可接受的。
2. 渠道配置没有灰度/沙箱机制
pay_channel 表的配置直接就是生产环境的密钥。如果要切换到沙箱环境测试,需要修改数据库记录。建议增加 env 字段(production/sandbox),或者支持渠道级别的沙箱开关。
3. 通知重试次数固定,不可配置
NOTIFY_FREQUENCY 数组是硬编码在 PayNotifyTaskDO 里的常量。不同业务场景对通知可靠性的要求不同------订单支付成功通知可能需要更多次重试,而一些不太重要的通知可能 3 次就够了。建议把这个配置做成可配的。
4. 钱包充值退款时的冻结逻辑有复杂度风险
钱包充值退款的流程是:冻结余额 → 发起退款 → 退款成功扣减冻结和累计充值 / 退款失败解冻。这个流程涉及 3 步操作和 2 个分支,每一步都有失败的可能。虽然代码处理得很严谨,但这种「先冻结再异步退款」的模式对后续维护者来说理解成本很高,建议加一个状态机图来辅助理解。
八、发散性思考
8.1 这个模块还能做什么?
- 跨境支付:接入 PayPal、Stripe 等海外支付渠道,支持多币种结算。RuoYi 的 PayClient 抽象已经为此打好了基础,新增一个 PayPalPayClient 就行
- 订阅支付:支持周期性自动扣款(如月度会员),这在 SaaS 场景非常常见
- 组合支付:支持「钱包余额 + 微信支付」组合支付,先用钱包抵扣一部分,差额走第三方支付
- 资金归集:支持多个渠道的资金自动归集到一个主账户,方便财务管理
- 电子发票:支付成功后自动触发电子发票开具流程
8.2 如果让我重新设计
如果从零设计这个支付中台,我会在以下方面做不同的选择:
- 事件驱动架构:用 Spring Event 或消息队列替代当前的直接方法调用来处理支付成功后的下游通知。好处是解耦------支付模块不需要知道谁在监听,业务模块通过订阅事件来响应
- 对账优先:从 Day 1 就设计对账系统,而不是等业务跑起来后再补。对账是支付系统的「安全带」,不能事后加装
- 渠道配置热更新:用 Nacos/Apollo 等配置中心管理渠道密钥,支持运行时切换而不需要改数据库
- 幂等性框架:抽象一个通用的幂等性框架,而不是在每个 Service 里手动检查重复。比如基于 merchantOrderId + channelCode 的幂等键
- 状态机引擎:引入 Spring StateMachine 或自研一个轻量级状态机来管理订单/退款/转账的状态流转,让状态转换规则可视化、可配置
8.3 可以迁移到其他场景的设计思路
- 策略 + 工厂模式的多渠道抽象:这个模式可以迁移到短信发送(阿里云/腾讯云/华为云短信)、邮件发送(SMTP/SendGrid/阿里邮件推送)、OSS 存储(阿里云 OSS/腾讯云 COS/MinIO)等任何需要「统一接口 + 多实现」的场景
- 扩展表模式:适用于任何「一次业务可能尝试多次外部调用」的场景,比如物流下单(尝试多个快递公司)、消息推送(尝试多个推送渠道)
- 通知重试机制:可以直接复用到任何需要「可靠异步通知」的场景------订单状态变更通知、审批结果通知、库存预警通知等
- 钱包 + 冻结模型:适用于任何需要「虚拟账户」的场景------积分系统、优惠券账户、预授权冻结等
九、关键代码导读
最后,列出 5 个最值得仔细阅读的代码文件,按推荐阅读顺序排列:
1. PayClientFactoryImpl.java --- 支付渠道工厂
路径: yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java
为什么值得读: 这是整个支付中台的「枢纽」。它维护了 14 种渠道枚举到客户端实现类的映射关系,通过反射动态创建客户端实例。读懂了这个文件,你就理解了「策略模式 + 工厂模式」在支付场景下的最佳实践。
2. PayOrderServiceImpl.java --- 支付订单服务
路径: yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
为什么值得读: 这是整个模块代码量最大、逻辑最复杂的 Service。它包含了订单创建、提交、回调处理、主动同步、过期关闭的完整生命周期。特别关注 validateOrderActuallyPaid() 方法------这是防止「掉单」的关键逻辑。
3. AbstractPayClient.java --- 模板方法基类
路径: yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/AbstractPayClient.java
为什么值得读: 经典的模板方法模式。所有公共方法都是 final 的,负责参数校验、异常包装、日志记录;具体的渠道调用委托给子类的 do*() 方法。这种设计确保了所有渠道实现的一致性------你不需要担心某个渠道忘了做异常处理或者日志记录。
4. PayNotifyServiceImpl.java --- 通知重试服务
路径: yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
为什么值得读: 这是一个完整的「可靠消息投递」系统的实现。它用数据库持久化 + 定时任务轮询 + Redis 分布式锁 + 指数退避重试,实现了一个轻量级但可靠的消息通知机制。这个设计思路可以迁移到任何需要「确保消息送达」的场景。
5. PayWalletMapper.java --- 钱包数据访问层
路径: yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java
为什么值得读: 这个 Mapper 展示了如何在 SQL 层面实现原子性的余额操作。每个 update* 方法都带有 WHERE balance >= #{price} 的 CAS 条件,这是防止超扣的最后一道防线。配合 Redis 分布式锁使用,构成了「双重保险」的并发安全策略。做钱包、积分、余额类系统时一定要仔细看这个文件。
总结
yudao-module-pay 是我在 RuoYi-Vue-Pro 系列分析中看到的设计最完善的业务模块之一。它的核心价值在于:
- 统一抽象:14 种支付渠道统一为 PayClient 接口,新增渠道成本极低
- 可靠一致:通过扩展表、回调重试、主动同步三重机制保证资金一致性
- 开箱即用:钱包系统 + Demo 示例,业务模块几乎可以零成本接入
- 工程典范:策略模式、模板方法、CAS 并发控制、分布式锁------每一个都是教科书级的实践
如果满分 10 分,我给这个模块打 8.5 分。扣掉的 1.5 分主要在于缺少对账系统和分账能力------这两个是生产环境中绕不过去的需求。但考虑到它是一个开源项目,这个完成度已经非常令人印象深刻了。
系列文章导航:
- 上一篇:工作流 BPM
- 第九篇:支付模块(本文)
下一篇预告: 第十篇------CRM 客户关系管理模块(yudao-module-crm),看看 RuoYi 是怎么做客户管理、商机跟进和销售漏斗的。
觉得有用的话,点个赞支持一下呗~ 👍