支付模块 (pay)
概述
RuoYi-Plus 支付模块 (ruoyi-common-pay
) 是一个统一的支付服务解决方案,提供了完整的支付、退款、回调处理功能。模块采用策略模式设计,支持多种支付方式的无缝集成。
核心特性
- 🚀 统一接口: 提供统一的支付、退款、查询接口
- 💳 多支付方式: 支持微信支付、支付宝、余额支付等
- 🏢 多租户支持: 完整的多租户配置管理
- 🔒 安全可靠: 完整的签名验证和回调处理
- 📊 配置管理: 智能化的配置初始化和管理
- 🎯 事件驱动: 支付成功事件发布机制
支持的支付方式
支付方式 | 支持类型 | Handler类 |
---|---|---|
微信支付 | JSAPI、NATIVE、APP、H5 | WxPayHandler |
支付宝 | WAP、PAGE、APP | AliPayHandler |
余额支付 | 账户余额 | BalancePayHandler |
快速开始
1. 添加依赖
xml
<dependency>
<groupId>plus.ruoyi</groupId>
<artifactId>ruoyi-common-pay</artifactId>
</dependency>
2. 基础使用
java
@Autowired
private PayService payService;
// 发起支付
PayRequest request = PayRequest.createWxJsapiRequest(
appId, mchId, "商品描述", outTradeNo,
new BigDecimal("0.01"), openId, clientIp
);
PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request);
// 申请退款
RefundRequest refundRequest = RefundRequest.createWxRefundRequest(
appId, mchId, outTradeNo, outRefundNo,
totalFee, refundFee, "退款原因"
);
RefundResponse refundResponse = payService.refund(DictPaymentMethod.WECHAT, refundRequest);
架构设计
整体架构
mermaid
graph TB
A[Controller层] --> B[PayService统一服务]
B --> C{支付方式路由}
C --> D[WxPayHandler]
C --> E[AliPayHandler]
C --> F[BalancePayHandler]
D --> G[微信支付API]
E --> H[支付宝API]
F --> I[余额业务逻辑]
J[PayConfigManager] --> K[配置初始化]
K --> L[WxPayInitializer]
K --> M[AliPayInitializer]
N[回调处理] --> B
B --> O[PaySuccessEvent]
核心组件
PayService - 统一服务入口
负责路由不同支付方式到对应的处理器,提供统一的调用接口。
java
@Service
public class PayService {
// 根据支付方式自动路由到对应处理器
public PayResponse pay(DictPaymentMethod paymentMethod, PayRequest request);
public RefundResponse refund(DictPaymentMethod paymentMethod, RefundRequest request);
public NotifyResponse handleNotify(DictPaymentMethod paymentMethod, NotifyRequest request);
}
PayHandler - 支付处理器接口
定义了支付处理器的标准接口,所有支付方式都需要实现此接口。
java
public interface PayHandler {
DictPaymentMethod getPaymentMethod();
PayResponse pay(PayRequest request);
RefundResponse refund(RefundRequest request);
PayResponse queryPayment(String outTradeNo, String appid);
RefundResponse queryRefund(String outRefundNo, String appid);
NotifyResponse handleNotify(NotifyRequest request);
}
PayConfigManager - 配置管理器
管理所有支付配置,支持多租户场景下的配置隔离。
java
@Service
public class PayConfigManager {
// 配置格式: {tenantId}:{paymentMethod}:{appid}
public PayConfig getConfig(String tenantId, String paymentMethod, String appid);
public void registerConfig(PayConfig config);
public List<PayConfig> getConfigsByTenant(String tenantId);
}
支付方式详解
微信支付
支持类型
- JSAPI: 微信小程序/公众号支付
- NATIVE: 扫码支付
- APP: APP支付
- H5: 手机网页支付
配置要求
java
// 必需配置
String appId = "wx1234567890123456";
String mchId = "1234567890";
String mchKey = "your_wx_api_key";
// 可选配置
String certPath = "cert/apiclient_cert.p12"; // 退款需要
String apiV3Key = "your_api_v3_key"; // API v3需要
使用示例
JSAPI支付 (小程序/公众号)
java
PayRequest request = PayRequest.createWxJsapiRequest(
appId, mchId, "商品描述", outTradeNo,
new BigDecimal("0.01"), openId, clientIp
);
PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request);
// 返回的payInfo可直接用于前端调起支付
Map<String, String> payInfo = response.getPayInfo();
NATIVE支付 (扫码)
java
PayRequest request = PayRequest.createWxNativeRequest(
appId, mchId, "商品描述", outTradeNo,
new BigDecimal("0.01"), clientIp
);
PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request);
// 获取二维码
String qrCodeBase64 = response.getQrCodeBase64();
String codeUrl = response.getCodeUrl();
H5支付 (手机网页)
java
PayRequest request = PayRequest.createWxH5Request(
appId, mchId, "商品描述", outTradeNo,
new BigDecimal("0.01"), clientIp, sceneInfo
);
PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request);
// 跳转到支付页面
String payUrl = response.getPayUrl();
支付宝
支持类型
- WAP: 手机网站支付
- PAGE: 电脑网站支付
- APP: APP支付
配置要求
java
// 必需配置
String appId = "2021000000000000";
String privateKey = "your_private_key";
String alipayPublicKey = "alipay_public_key";
// 证书模式 (推荐)
String certPath = "cert/appCertPublicKey.crt";
String keyPath = "cert/alipayCertPublicKey_RSA2.crt";
使用示例
WAP支付 (手机网站)
java
PayRequest request = PayRequest.createAlipayWapRequest(
appId, "商品描述", outTradeNo,
new BigDecimal("0.01"), returnUrl
);
PayResponse response = payService.pay(DictPaymentMethod.ALIPAY, request);
// 返回支付表单,直接输出到页面即可
String payForm = response.getPayForm();
PAGE支付 (电脑网站)
java
PayRequest request = PayRequest.createAlipayPageRequest(
appId, "商品描述", outTradeNo,
new BigDecimal("0.01"), returnUrl
);
PayResponse response = payService.pay(DictPaymentMethod.ALIPAY, request);
String payForm = response.getPayForm();
APP支付
java
PayRequest request = PayRequest.createAlipayAppRequest(
appId, "商品描述", outTradeNo, new BigDecimal("0.01")
);
PayResponse response = payService.pay(DictPaymentMethod.ALIPAY, request);
// 返回APP调起参数
String payInfo = response.getPayUrl();
余额支付
余额支付是系统内置的支付方式,无需第三方接口,适合积分、储值卡等场景。
java
PayRequest request = PayRequest.createBalanceRequest(
outTradeNo, new BigDecimal("10.00"), "余额支付", clientIp
);
PayResponse response = payService.pay(DictPaymentMethod.BALANCE, request);
退款处理
微信退款
java
RefundRequest request = RefundRequest.createWxRefundRequest(
appId, mchId, outTradeNo, outRefundNo,
totalFee, refundFee, "用户申请退款"
);
RefundResponse response = payService.refund(DictPaymentMethod.WECHAT, request);
支付宝退款
java
RefundRequest request = RefundRequest.createAlipayRefundRequest(
appId, outTradeNo, outRefundNo, refundFee, "退款原因"
);
RefundResponse response = payService.refund(DictPaymentMethod.ALIPAY, request);
余额退款
java
RefundRequest request = RefundRequest.createBalanceRefundRequest(
outTradeNo, outRefundNo, refundFee, "余额退款"
);
RefundResponse response = payService.refund(DictPaymentMethod.BALANCE, request);
回调处理
回调地址规则
系统自动生成回调地址,格式为:
{baseApi}/payment/notify/{paymentMethod}/{merchantId}
例如:
- 微信支付:
https://api.example.com/payment/notify/wechat/1234567890
- 支付宝:
https://api.example.com/payment/notify/alipay/2021000000000000
回调验证流程
- 数据完整性检查: 验证必需参数
- 签名验证: 使用对应平台的公钥/密钥验证签名
- 业务状态检查: 确认支付状态为成功
- 事件发布: 发布
PaySuccessEvent
事件 - 响应返回: 返回平台要求的格式
监听支付成功事件
java
@Component
@Slf4j
public class PaymentEventListener {
@EventListener
public void handlePaymentSuccess(PaySuccessEvent event) {
String outTradeNo = event.getOutTradeNo();
String totalFee = event.getTotalFee();
String paymentMethod = event.getPaymentMethod();
// 处理业务逻辑
// 1. 更新订单状态
// 2. 发货处理
// 3. 积分发放
// 4. 发送通知
log.info("支付成功: 订单={}, 金额={}, 方式={}",
outTradeNo, event.getTotalAmountYuan(), paymentMethod);
}
}
配置管理
配置初始化
系统启动时会自动初始化所有租户的支付配置:
- 扫描租户: 从平台配置和支付配置中获取所有租户ID
- 构建配置: 为每个租户构建
PayConfig
对象 - 分组初始化: 按支付方式分组,调用对应的初始化器
- 注册配置: 将配置注册到
PayConfigManager
配置结构
java
@Data
public class PayConfig {
private String configId; // 格式: {tenantId}:{paymentMethod}:{appid}
private String tenantId; // 租户ID
private DictPaymentMethod paymentMethod; // 支付方式
private String appid; // 应用ID
private String mchId; // 商户号
private String mchKey; // 商户密钥
private String certPath; // 证书路径
// ... 其他配置项
}
多租户配置隔离
每个租户的配置完全隔离,通过配置ID进行区分:
java
// 获取指定租户的支付配置
PayConfig config = configManager.getConfig(tenantId, "wechat", appId);
// 获取租户的所有配置
List<PayConfig> configs = configManager.getConfigsByTenant(tenantId);
状态查询
支付状态查询
java
// 查询微信支付状态
PayResponse response = payService.queryPayment(
DictPaymentMethod.WECHAT, outTradeNo, appId
);
if (response.isSuccess()) {
String tradeState = response.getTradeState();
Date payTime = response.getPayTime();
// 处理查询结果
}
退款状态查询
java
// 查询退款状态
RefundResponse response = payService.queryRefund(
DictPaymentMethod.WECHAT, outRefundNo, appId
);
if (response.isSuccess()) {
String refundStatus = response.getRefundStatus();
// 处理查询结果
}
工具类
PayUtils - 支付工具类
java
// 生成订单号
String outTradeNo = PayUtils.generateOutTradeNo();
String outTradeNo = PayUtils.generateOutTradeNo("ORDER");
// 生成退款单号
String outRefundNo = PayUtils.generateOutRefundNo();
// 金额转换
String fenStr = PayUtils.yuanToFen("10.50"); // 1050
String yuanStr = PayUtils.fenToYuan("1050"); // 10.50
// 敏感信息掩码
String maskedMchId = PayUtils.maskMchId("1234567890"); // 123****890
String maskedAppId = PayUtils.maskAppId("wx1234567890123456"); // wx12****3456
PayNotifyUrlBuilder - 回调地址构建
java
// 构建回调地址
String notifyUrl = PayNotifyUrlBuilder.buildNotifyUrl(
DictPaymentMethod.WECHAT, mchId
);
// 解析回调地址
String[] parsed = PayNotifyUrlBuilder.parseNotifyUrl(notifyUrl);
String paymentType = parsed[0];
String merchantId = parsed[1];
最佳实践
1. 订单号生成
java
// 推荐使用系统提供的订单号生成方法
String outTradeNo = PayUtils.generateOutTradeNo("PAY"); // PAY20241212143025123456
String outRefundNo = PayUtils.generateOutRefundNo("REF"); // REF20241212143025123456
2. 异常处理
java
try {
PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request);
if (!response.isSuccess()) {
log.error("支付失败: {}", response.getMessage());
// 处理支付失败
}
} catch (ServiceException e) {
log.error("支付异常: {}", e.getMessage(), e);
// 处理异常情况
}
3. 回调幂等性
支付回调可能会重复推送,业务处理时需要保证幂等性:
java
@EventListener
@Transactional
public void handlePaymentSuccess(PaySuccessEvent event) {
String outTradeNo = event.getOutTradeNo();
// 检查订单是否已处理(幂等性保证)
if (orderService.isPaid(outTradeNo)) {
log.info("订单已处理,跳过: {}", outTradeNo);
return;
}
// 更新订单状态
orderService.updateOrderStatus(outTradeNo, "PAID");
// 其他业务处理...
}
4. 金额处理
java
// 统一使用 BigDecimal 处理金额,避免精度丢失
BigDecimal amount = new BigDecimal("10.50");
// 避免使用 double 类型
// ❌ double amount = 10.50;
// ✅ BigDecimal amount = new BigDecimal("10.50");
5. 配置安全
生产环境中,敏感配置信息应该加密存储:
java
// 敏感信息脱敏日志输出
log.info("初始化微信支付: appId={}, mchId={}",
PayUtils.maskAppId(appId),
PayUtils.maskMchId(mchId)
);
常见问题
Q: 支付回调收不到?
A: 检查以下几点:
- 确保回调地址可以从外网访问
- 检查
app.base-api
配置是否正确 - 确认防火墙没有拦截回调请求
- 查看回调日志,确认签名验证是否通过
Q: 微信支付提示签名错误?
A: 检查以下配置:
mchKey
(API密钥) 是否正确- 参数是否按照微信要求进行签名
- 字符编码是否为 UTF-8
Q: 支付宝支付失败?
A: 检查以下配置:
appId
格式是否正确(16位数字)- 应用私钥是否正确
- 支付宝公钥是否配置
- 是否开通了对应的支付产品
Q: 多租户环境下配置混乱?
A: 确保:
- 每个租户的配置ID唯一
- 配置初始化时按租户隔离
- 使用时传入正确的
appId
参数
Q: 如何扩展新的支付方式?
A: 按照以下步骤:
- 实现
PayHandler
接口 - 创建对应的
PayInitializer
- 在
DictPaymentMethod
中添加新的支付方式枚举 - 添加相关配置支持