在JeecgBoot里折腾支付模块,是很多做低代码平台二次开发的兄弟迟早会遇到的事。业务方提需求的时候总是一句话:“就加个微信支付和支付宝支付嘛,很简单。”真上手之后才发现,第三方支付不是加个SDK就行那么简单,下单、验签、回调、对账、退款、超时关单,一环扣一环,稍有不慎就是线上事故。
这篇内容我会用一套完整可落地的思路,讲清楚在JeecgBoot低代码平台上集成微信支付和支付宝支付时,到底怎么设计支付服务、建订单表、写回调接口、挂到JeecgBoot的Online表单和代码生成器上,以及我把整个流程走下来踩过的坑。如果你正打算在自己维护的JeecgBoot项目里接入微信或支付宝支付,这篇东西可以直接当参考手册来用。
1. 为什么要在JeecgBoot里集成支付模块
1.1 低代码平台做交易业务并不矛盾
很多人对低代码平台的印象停留在“做做内部管理系统”“表单加列表,增删改查完事”。实际上,JeecgBoot这类基于Spring Boot生态的低代码平台,底子就是一个标准Java工程,所谓的低代码只是把常规的后台CRUD和权限、字典、报表这些能力模板化了。它并不限制你写原生Service、Controller,也不限制你引入第三方依赖。
所以当你需要在业务系统里加入支付能力时,完全可以把支付作为一个独立模块嵌进去,用JeecgBoot的表单能力管业务订单,用代码生成器把支付流水表快速生成出来,再手写支付服务层的核心逻辑。这样既享受了低代码开发的效率,又不牺牲复杂业务场景的灵活性。
我当时接手的一个订单中心项目,就是用JeecgBoot做底层框架,前端用自带的Vue3版本,数据库MySQL。需求方要求用户在电脑端扫码支付,同时业务系统要有完整的订单流水、退款记录和财务对账功能。如果从零搭一套Spring Boot + Vue项目,光权限体系和字典管理就够折腾两周,而在JeecgBoot基础上做,这些基础能力已经全部齐了。
1.2 两种集成路径怎么选
在JeecgBoot里接入支付,业内基本有两条路:
一种是完全依赖Online表单开发,把订单表和支付流水表都用在线表单配置出来,通过代码生成器生成单表CRUD,然后在生成的Controller上想办法加入支付逻辑。这种方式适合订单状态简单、支付渠道单一的轻场景,开发量小,但应付不了复杂的支付回调、退款、对账流程。
另一种是把支付能力作为一个标准Spring Boot模块来开发,在JeecgBoot工程的业务包下面新建支付相关的Service、Controller、配置类和工具类,订单表和支付流水表依然用JeecgBoot的代码生成器来生成,但业务逻辑独立编写。这样支付模块可以复用,后续接其他渠道、改退款策略、加对账任务都方便。
我个人强烈建议走第二种路线。原因很简单:支付不是简单“调个接口把钱收进来”就结束的功能,它还涉及回调通知修复、订单超时关闭、异常对账、退款同步等一系列异常处理。这些逻辑如果全堆在Online表单生成的Controller里,代码会迅速失控。拿一张表格对比看得更清楚:
| 对比维度 | 纯Online表单方案 | 独立支付模块方案 |
|---|---|---|
| 开发速度 | 很快 | 中等 |
| 扩展性 | 差,改逻辑要动生成代码 | 好,支付逻辑独立演进 |
| 回调处理 | 难,要硬塞进Controller | 天然适合独立接口 |
| 对账/定时任务 | 基本放不进表单体系 | 可以配合JeecgBoot定时任务模块 |
| 维护成本 | 后期高 | 前期稍高,后期低 |
2. 支付模块架构设计与技术选型
2.1 支付服务抽象:一套接口适配微信和支付宝
支付模块设计的第一步,不是急着写微信支付代码,而是先定义一个支付抽象层。因为微信支付和支付宝虽然业务上等价,但接口风格、参数名称、签名机制、回调报文格式完全不一样。如果业务层直接绑定微信SDK,后续要加支付宝,就得改一堆业务代码,甚至把订单表都污染了。
我常用的做法是先定义三个核心对象:支付请求参数、支付返回结果、统一支付服务接口。
支付请求参数里只放业务关心的字段:业务订单号、支付金额、商品描述、支付渠道、用户标识。支付返回结果统一返回:是否成功、支付跳转地址、二维码内容、第三方流水号、错误信息。而支付服务接口就定义几个方法:创建支付、主动查询订单、申请退款、查询退款、处理回调通知。
public interface PayService { PayResult createPayment(PayRequest request); PayQueryResult queryPayment(String outTradeNo); RefundResult refund(RefundRequest request); PayNotifyResult handleNotify(String body, Map<String, String> headers); }微信支付写一个WxPayServiceImpl,支付宝写一个AlipayPayServiceImpl,两个实现类内部各调各的SDK,外部暴露给业务层的是完全一样的方法签名。业务侧只需要根据用户选择的支付渠道,路由到对应的实现类。
这样做的最大好处是把支付细节彻底隔离了。订单模块的代码永远只需要关心“支付成功没有、退款成功没有”,不需要知道微信签名算法还是支付宝公钥解密那堆事情。后续就算支付渠道升级了SDK版本,改动也都收敛在各自实现类内部,不会波及业务层。
2.2 渠道接入选型:官方SDK还是聚合SDK
说实话,微信支付和支付宝的官方SDK质量都不算差,但风格差异很大。支付宝的官方Java SDK封装得比较完善,页面跳转支付、手机网站支付、退款查询都有现成API,而且签名和验签都已经帮你处理好,用起来省心。微信支付的官方Java SDK做成了多个独立包,需要自己手动组合,参数构造和签名验签也要自己多花心思。
市面上有WxJava、IJPay这类聚合封装框架,同时兼容微信和支付宝,一套API通吃,确实能省不少事。但我的建议是,如果你只是接这两个支付渠道,优先用官方SDK,或者跟官方SDK靠得很近的封装。原因有两个:一是官方文档齐全,遇到问题能搜到最多解决方案;二是聚合框架虽然API统一,但内部包装了一层自己的逻辑,真出了问题排查链路反而更长。
我自己接入时,支付宝用的是官方SDK,微信支付用的是官方新版Java SDK。虽然写了两个实现类,工作量稍大,但胜在逻辑透明,出问题能直接定位到某一行代码。
2.3 支付配置项与安全设计
支付模块的配置建议全部走Spring Boot的配置文件,而不是放进数据库。因为数据库配置项有缓存的可能,改完要清缓存才生效,这对支付参数来说是很大的隐患。比如支付宝的网关地址切换、微信证书序列号更新,应当做到改配置后重启即生效,不要引入额外的中间层。
需要关注的配置项大致有这些:
| 配置项 | 微信支付V3 | 支付宝 | 说明 |
|---|---|---|---|
| 应用标识 | appId | appId | 公众号/应用AppID |
| 商户号 | mchId | 无 | 微信支付专属 |
| 私钥 | merchantPrivateKey | merchantPrivateKey | 用于签名 |
| 平台证书 | wechatpayCert | alipayPublicKey | 用于验签 |
| 回调地址 | notifyUrl | notifyUrl | 必须公网HTTPS可达 |
| 支付网关 | apiV3Url | gatewayUrl | 沙箱和正式环境不同 |
安全方面必须牢记三个底线:私钥不能进数据库、不能打到日志、不能发给前端。微信支付V3的API密钥和商户私钥,要配置在环境变量或者专门配置中心里,版控配置文件只能放占位符。回调接口要严格验签,支付宝要对notify报文做RSA验签,微信支付要校验请求头里的签名信息和平台证书。
还有一个实操经验:微信支付的平台证书序列号会变,证书过期前一定要安排时间更新,否则之前正常的支付会突然全部验签失败,而且这个故障还不是一眼能看出来的,日志里通常只会报证书相关异常。
3. 订单表和支付流水表怎么设计
3.1 支付订单表字段与状态机
支付模块的表结构是整个方案的地基。我见过很多人在订单表里直接塞支付状态字段、第三方流水号、退款状态,一个表承担所有职责,最后数据乱七八糟。更推荐的做法是:业务订单表归业务系统管,支付模块单独维护一张支付流水表,用业务订单号关联。
支付流水表的核心字段至少要有:支付流水号(模块内自生成)、业务订单号、支付渠道、支付状态、支付金额(单位:分或元,需全局统一)、第三方交易号、预支付ID、付款人标识、支付时间、退款金额、退款时间、回调报文、创建时间和更新时间。
状态字段建议用数字枚举,0表示待支付,1表示已支付,2表示已退款,3表示已关闭,-1表示支付失败。有人喜欢用英文字符串,比如PENDING、SUCCESS,但实际使用中数字枚举在数据库查询和状态流转判断上更简洁,配合JeecgBoot的字典表,前端直接映射成中文标签就行。
建表时注意几个细节:第三方交易号要建立唯一索引,因为回调通知时要用它查流水;业务订单号也要建普通索引,因为主动查询支付状态频率很高;状态字段必须加索引,定时关单任务每天都要按状态扫数据。
CREATE TABLE `pay_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `pay_order_no` varchar(64) NOT NULL COMMENT '支付流水号', `biz_order_no` varchar(64) NOT NULL COMMENT '业务订单号', `channel` varchar(16) NOT NULL COMMENT '支付渠道: WX/ALIPAY', `pay_amount` bigint NOT NULL COMMENT '支付金额,单位分', `pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已退款 3已关闭', `transaction_id` varchar(64) DEFAULT NULL COMMENT '第三方交易号', `prepay_id` varchar(128) DEFAULT NULL COMMENT '预支付ID', `payer_uid` varchar(64) DEFAULT NULL COMMENT '付款人标识', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `refund_amount` bigint DEFAULT '0' COMMENT '退款金额,单位分', `refund_time` datetime DEFAULT NULL COMMENT '退款时间', `notify_data` text COMMENT '原始回调报文', `expire_time` datetime DEFAULT NULL COMMENT '订单过期时间', `create_by` varchar(32) DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_pay_order_no` (`pay_order_no`), UNIQUE KEY `uk_transaction_id` (`transaction_id`), KEY `idx_biz_order_no` (`biz_order_no`), KEY `idx_pay_status` (`pay_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付流水表';这里把pay_amount设计成bigint单位分,是因为所有主流支付渠道的金额单位都不一样,微信是分,支付宝是元,但你没处理好就会出大问题。内部统一用分,转成微信直接传数值,转支付宝元的时候除以100,精度问题就完全规避了。
3.2 在JeecgBoot中把表和CRUD页面跑起来
有了SQL脚本,接下来就是JeecgBoot的拿手好戏了。打开JeecgBoot的Online表单管理,选择“在线表单开发”菜单,可以直接通过SQL导入方式创建表结构对应的表单配置,JeecgBoot会自动分析字段,生成表单样式和列表查询条件。
这一步有几个技巧:
- 字段注释一定要写清楚,因为JeecgBoot会直接把注释作为前端列表的列名和表单的label,注释写得太随意,后面页面交互文案就要手工改半天。
- 字典值要提前配置好。pay_status、channel这两个字段建议在系统管理里的字典管理中预置好字典项,比如channel的字典项是WX/微信支付、ALIPAY/支付宝支付,pay_status的字典项是0/待支付、1/已支付、2/已退款、3/已关闭。这样生成页面后,列表展示和表单下拉直接就有值了,不需要二次开发。
- 需要参与列表查询的字段,比如pay_status、channel、biz_order_no,在表单配置里把“是否作为查询条件”勾上。
Online表单配置好之后,用JeecgBoot的代码生成器一键生成前后端代码。生成的Controller、Service、Mapper都是标准的Spring Boot结构,可以直接丢进你的项目工程里。
这里要提醒一点:代码生成器生成的Service层是单表CRUD逻辑,不要直接在生成的Service里写支付下单逻辑,更不要在生成的Controller里加回调接口。正确的做法是让生成的代码负责支付流水表的基本管理页面,支付核心逻辑写在独立的PayService实现类中,回调接口也单独建Controller,避免后续重新生成代码时把你手写的东西覆盖掉。
4. 微信支付和支付宝支付的实操实现
4.1 微信支付V3统一下单流程
微信支付V3的下单接口是POST到/v3/pay/transactions/native,适合电脑网页扫码支付场景。整个流程分三步:构建下单请求、调用微信接口拿到二维码链接、把二维码返回给前端展示。
先引入微信支付Java SDK依赖,在JeecgBoot的pom.xml里加上对应版本包。如果你是Maven工程,引入后请留意版本号是否与项目其他依赖冲突,尤其是Spring Boot的版本,微信SDK对Java版本有一定要求,项目过于老旧时建议先升级基础环境。
<dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-java</artifactId> <version>0.2.12</version> </dependency>构建配置类时,需要注入商户号、商户私钥、商户证书序列号、微信支付平台证书和APIv3密钥。这里有个细节:微信支付V3的私钥格式是PKCS8,如果你的私钥是PKCS1格式,需要先做格式转换,否则签名会一直失败。
接下来写统一下单核心方法。先组装请求参数,再调用SDK发送请求,拿到响应之后把code_url返回给调用方。code_url是一个形如weixin://wxpay/bizpayurl?pr=xxxx的链接,前端用二维码组件把它转成二维码图片,用户拿起微信扫一扫就能调起支付。
public String createWxNativePay(PayRequest request) { String outTradeNo = request.getPayOrderNo(); // 注意金额单位:微信API收的是“分” Integer totalFee = request.getPayAmount().intValue(); String description = request.getSubject(); // 构建request NativePayRequest payRequest = new NativePayRequest.Builder() .appid(wxPayProperties.getAppId()) .mchid(wxPayProperties.getMchId()) .description(description) .outTradeNo(outTradeNo) .notifyUrl(wxPayProperties.getNotifyUrl()) .amount(new Amount.Builder().total(totalFee).build()) .build(); // 调用服务 NativePayService service = new NativePayService.Builder() .config(config) .build(); NativePayTransaction response = service.create(payRequest); return response.getCodeUrl(); }从业务侧看,逻辑非常简单。但实际走一遍就会发现,90%的坑都在配置上,而不是在代码上。密钥格式、证书序列号是否匹配、APIv3密钥是否和商户平台设置的保持一致,任何一个对不上,微信接口都会返回401或者验签失败。
微信支付V3接口返回的code_url有效期一般是2小时,但业务上通常要把支付订单的过期时间定得更短,比如30分钟。超时后要主动调用关单接口,避免用户一直拿着一张旧二维码扫码,钱付了但订单业务已经过期,造成后续流程错乱。
4.2 支付宝电脑网站支付流程
支付宝电脑网站支付走的是form表单自动提交的路子,和微信扫码支付体验上有点不同。调用支付宝下单接口,拿到一段HTML表单内容,前端把这段表单写入页面,浏览器会自动跳转到支付宝收银台。
支付宝SDK的接入相对简单,参数构造也更人性化一些。创建AlipayTradePagePayRequest对象,设置业务订单号、金额、商品标题和回调地址,然后调用client.pageExecute拿到结果。注意支付宝的金额单位是元,需要把内部存储的分转回元,这里最容易出现金额差一百倍的低级错误。
public String createAlipayPagePay(PayRequest request) { AlipayTradePagePayRequest payRequest = new AlipayTradePagePayRequest(); payRequest.setReturnUrl(alipayProperties.getReturnUrl()); payRequest.setNotifyUrl(alipayProperties.getNotifyUrl()); // 注意金额单位:支付宝API收的是“元” String amount = String.format("%.2f", request.getPayAmount() / 100.0); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", request.getPayOrderNo()); bizContent.put("total_amount", amount); bizContent.put("subject", request.getSubject()); bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); payRequest.setBizContent(bizContent.toString()); AlipayTradePagePayResponse response = alipayClient.pageExecute(payRequest); return response.getBody(); }支付宝回调通知和同步返回地址要区分清楚。
returnUrl是用户付完款跳转回商户页面的地址,只做页面展示,不能作为订单处理依据。notifyUrl才是服务端异步通知地址,所有订单状态的更新必须依靠notifyUrl收到的内容。
如果现场支付了但程序没有拿到回调,要用定时任务或者主动查询接口补偿,而不是傻等支付宝通知。
4.3 统一支付调度入口
支付渠道的逻辑都有了,业务侧调用时不应该分别去new微信服务或支付宝服务。在JeecgBoot项目里,推荐用一个统一的PaymentDispatcher来路由渠道,支付流水表里存了channel字段,直接用channel找到对应的PayService实现。
渠道路由可以简单写成一个Map,在Spring启动时把两个实现类注册进去:
@Component public class PaymentDispatcher { private final Map<String, PayService> payServiceMap; public PaymentDispatcher(List<PayService> payServices) { payServiceMap = payServices.stream() .collect(Collectors.toMap(s -> s.supportChannel(), s -> s)); } public PayService route(String channel) { PayService service = payServiceMap.get(channel); if (service == null) { throw new UnsupportedOperationException("不支持的支付渠道: " + channel); } return service; } }加上这段代码后,业务层只需要知道面向用户的“业务订单号”和“渠道”,剩下的交给支付服务。这笔支付属于微信还是支付宝,业务层根本不用关心。
5. 回调通知的验签与订单状态核销
5.1 回调接口设计:安全先行
支付流程中最核心的环节不是下单,而是回调通知。用户付完钱,支付渠道把结果异步通知到商户的notifyUrl地址。如果这里处理不及时或者验签不严,极容易出现用户钱扣了、订单还是待支付状态的情况。
微信支付V3的回调报文是密文,需要先解密,再验签。支付回调接口拿到请求头里的Wechatpay-Signature、Wechatpay-Serial、Wechatpay-Timestamp这些信息,先校验平台证书的序列号,再对报文做签名验证,验证通过后才能继续处理。SDK里提供了NotificationParser工具类,把回调报文和请求头传进去,直接解析出回调对象。
@PostMapping("/notify/wx") public String wxNotify(HttpServletRequest request) throws IOException { String body = request.getReader().lines().collect(Collectors.joining()); Map<String, String> headers = buildHeaderMap(request); NotificationParser parser = new NotificationParser(RSAVerifierFactory.builder() .config(config) .build()); Transaction transaction = parser.parse(headers, body, Transaction.class); // 判断订单支付状态 if ("SUCCESS".equals(transaction.getTradeState())) { handlePaidOrder(transaction.getOutTradeNo(), transaction.getTransactionId(), transaction.getAmount().getPayerTotal()); } // 微信要求必须返回200应答体 return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}"; }支付宝的回调验签方式不太一样,它把sign参数放在表单里,应用接收到回调后在本地用支付宝公钥对参数排序拼接后的字符串做RSA2验签。验签通过,还要比对业务订单号是否存在、订单金额是否和回调金额一致、订单状态是否已经是终态。只有全部通过,才能更新订单状态,然后返回success字符串通知支付宝不要再重发。
5.2 幂等处理:回调重复通知不会错
支付回调天然是可能重复的。微信和支付宝都有重试机制,如果商户接口返回异常,过一会支付平台会重新推送。所以回调处理的逻辑必须保证幂等,重复通知不会导致订单状态被错误重置或者重复记账。
幂等的实现方案我用的是数据库状态锁定加事务控制。处理回调时,先按支付流水号查出记录,如果状态已经是已支付,直接返回成功应答,不重复处理。只有待支付状态,才执行状态更新操作。为了防止并发情况下两个线程同时读到待支付状态,更新SQL需要加上状态条件:
UPDATE pay_order SET pay_status = 1, transaction_id = #{transactionId}, pay_time = #{payTime}, notify_data = #{notifyData} WHERE pay_order_no = #{payOrderNo} AND pay_status = 0如果更新的影响行数是0,说明已经被其他线程更新过了,直接忽略这次回调即可。这个写法比“先查再改”具备更强的并发安全保障。
5.3 主动查询机制与超时关单
正常流程下,用户付款后回调通知马上就会到达,订单状态自动变成已支付。但总有意外场景,比如回调服务器挂了、网络闪断、回调延迟。所以支付服务里必须要有一个主动查询通道,用户在前端关闭支付弹窗再重新打开时,前端可以调一次“查询支付结果”接口,服务端拿着支付流水号向微信或支付宝发起主动查询,从第三方拿到最权威的支付状态。
另外,待支付订单不能一直躺在数据库里。JeecgBoot自带定时任务模块,可以配置一个每天每分钟执行的关单任务。任务逻辑很简单:扫描支付状态为待支付且expire_time小于当前时间的记录,对每笔记录调用微信关单接口或者支付宝关闭交易接口,然后更新流水状态为已关闭。
@Component("closeExpirePayOrderTask") public class CloseExpirePayOrderTask { @Scheduled(cron = "0 0/1 * * * ?") public void run() { List<PayOrder> expireOrders = payOrderMapper.selectExpiredOrders(); for (PayOrder order : expireOrders) { try { PayService payService = paymentDispatcher.route(order.getChannel()); payService.closeOrder(order.getPayOrderNo()); order.setPayStatus(3); // 已关闭 payOrderMapper.updateById(order); } catch (Exception e) { log.error("关闭超时订单失败, payOrderNo={}", order.getPayOrderNo(), e); } } } }定时关单任务在最初集成时经常被忽略,直到出现用户扫码付款后订单却一直在“待支付”状态挂着的投诉,才追悔莫及。定期关闭过期订单,既是支付平台的要求,也是业务状态一致性的需要。
6. JeecgBoot与支付模块联调实操记录
6.1 配置好JeecgBoot的支付参数
在JeecgBoot项目中集成支付,必须先把支付参数放到application.yml里。建议单独建一个支付配置段,把微信和支付宝参数分开管理:
pay: wx: app-id: wx1234567890 mch-id: 1600000000 mch-serial-no: 32D5FD23 private-key-path: /config/wx/merchant_private_key.pem api-v3-key: xxxxx notify-url: https://api.yourdomain.com/pay/notify/wx alipay: app-id: 2021000123456789 private-key-path: /config/alipay/app_private_key.pem alipay-public-key-path: /config/alipay/alipay_public_key.pem notify-url: https://api.yourdomain.com/pay/notify/alipay return-url: https://www.yourdomain.com/pay/return这里要特别注意,private-key和api-v3-key这类敏感信息不建议直接写进git仓库。实际项目里我是配合环境变量或者配置中心的占位符来做,确保代码仓库泄露也不会直接泄露密钥。
6.2 通过JeecgBoot代码生成器把支付流水管起来
前面已经提到用Online表单建表,实操时我更推荐一种方式:先在Navicat或者其他数据库工具里手动执行建表SQL,然后再回到JeecgBoot的Online表单开发中“导入数据库表”。这样可以保证SQL的字段设计完全受控,不会被Online表单的字段解析规则干扰。
导入表后,配置列表显示字段、查询字段、表单类型。代码生成器会产出Controller、Service、Dao、Mapper XML、Vue页面等一整套代码,配置好包名、模块名,直接生成下载,放到项目工程中。
生成的Controller自动带上了JeecgBoot的分页查询、导出Excel、添加、编辑、删除接口。支付流水表日常管理页面就有了,运营同事可以直接在系统里筛选某一天的支付流水,导出Excel给财务做对账。
6.3 把支付服务织入业务代码
业务系统里用户发起支付的调用链,我举一个前端后端的实际交互例子。用户在订单详情页点击“去支付”,选择微信或者支付宝,前端拿到支付订单号后调用后端接口。后端要做的事如下:
第一步,根据业务订单信息生成支付流水记录,状态为待支付;第二步,调用PaymentDispatcher路由到对应渠道的PayService;第三步,拿到渠道返回的支付参数,拼接成前端需要的响应格式。对于微信Native支付,返回code_url;对于支付宝电脑网站支付,返回form表单HTML。
@PostMapping("/startPay") public Result<String> startPay(@RequestBody StartPayRequest request) { // 1. 创建支付流水记录 PayOrder payOrder = createPayOrder(request); // 2. 路由到对应支付渠道 PayService payService = paymentDispatcher.route(request.getChannel()); PayResult payResult = payService.createPayment(buildPayRequest(payOrder)); // 3. 返回前端需要的支付参数 return Result.OK(payResult); }前端拿到响应后,如果是微信扫码,直接在弹窗里用二维码组件渲染;如果是支付宝,就把返回的form HTML插入当前页面,自动提交。用户完成支付后,前端跳转或轮询查询支付结果接口,后端基于主动查询机制返回已支付状态,页面提示支付成功。
6.4 沙箱环境和生产环境切换
开发阶段建议先对接支付宝沙箱和微信测试商户号。支付宝沙箱环境相对成熟,申请开通沙箱应用后,有专用的沙箱网关地址和测试买家账号。微信支付的测试环境需要通过测试商户号申请,流程比支付宝复杂一些,毕竟涉及真实资金约束和商户资质验证。
环境切换的配置可以直接利用Spring Boot的多Profile特性,application-dev.yml配置沙箱参数,application-prod.yml配置正式参数,启动时指定profile切换。不要让代码里出现硬编码的网关地址,更不要不判断环境直接走生产支付,测试环境误扣款是很严重的事故。
7. 常见问题排查与避坑实录
7.1 签名验证失败的常见原因
签名验证失败是微信支付接入中最让人头大的问题,我排查过的场景里五种原因占比最高:
第一,私钥格式错误。微信支付要求PKCS8格式的私钥,不要直接复制商户平台下载的原始私钥串,先转格式再用。
第二,商户证书序列号和私钥不匹配。证书序列号是商户平台里下载证书时对应的那个值,换成新证书后,序列号和私钥文件必须同步更新,很多人只换私钥忘了换序列号。
第三,APIv3密钥设置不一致。APIv3密钥是你在商户平台单独设置的32位密钥,不是APIv2的密钥,也不是证书密钥,经常有人搞混。
第四,系统时间偏差过大。微信支付验签时对请求时间有校验,服务器时间如果偏差超过几分钟,签名会直接校验失败,这一点在云服务器上偶尔会遇到,等待系统时间同步后再试。
第五,回调验签时平台证书缓存了旧值。微信支付平台证书有轮换机制,平台证书更新后,本地缓存的旧证书验签就会失败,SDK通常会自动更新,但如果你的代码里手工缓存了证书对象,就要注意刷新。
7.2 回调收不到或者延迟过久
回调收不到,第一个要查的不是代码,而是网络通路。微信支付和支付宝的回调服务器都在公网,你的回调接口必须公网可达且是HTTPS协议。本地开发阶段可以借助内网穿透工具临时调试,但生产环境必须部署到正规的、有备案的服务器上。
第二个要查的是回调地址是否正确配置。微信的notifyUrl在每次下单请求时都会带上去,下单时传错,服务端收到的回调就会打到错误地址上。
第三个要排查的是回调接口响应是否合规。微信支付要求回调接口返回特定的JSON结构且HTTP状态码为200,如果返回200但是报文格式不对,微信会判定失败并持续重试。支付宝则要求返回字符串success,其他任何内容都会被视为失败,触发重试。
值得注意的是,整个回调链路中任何一个环节有HTTP代理、网关或安全过滤,都可能导致响应体被改写。要留意网关有没有把success字符串或者{"code":"SUCCESS"}结构给吞掉。
7.3 订单状态不一致和金额对不上
订单状态不一致,大概率是业务侧更新了订单状态但支付流水表没有联动更新,或者两者更新不在同一个事务里。支付流水表才应该是支付状态的唯一事实来源,业务订单上的支付状态字段只能作为冗余展示,每次都要以流水表为准回写。
金额对不上最经典的坑就是分和元搞混。微信支付接口金额单位是分,支付宝是元,同一笔100元的订单,微信传10000,支付宝传100.00。内部存数据库建议统一用分(bigint),避免小数点精度问题。回调验签时,微信回调里金额单位是分,支付宝回调里是元,都要先归一化再跟流水表比对。
有一次线上对账差点出错,就是因为支付宝回调金额是字符串"100.00",我直接和数据库里存的10000比较,当然是失败的。把回调金额按渠道规则转成“分”之后再做比较,问题就解决了。
7.4 退款和部分退款的设计建议
退款逻辑和下单逻辑一样,也要通过PayService接口抽象。微信退款接口需要传商户私钥签名,支付宝退款接口相对简单。退款前要先检查流水状态必须是已支付,且累计退款金额加本次退款金额不能超过原支付金额。退款成功后,更新流水的退款金额和退款状态。
部分退款场景下,一个流水可以有多笔退款记录,不要只用一个refund_amount字段存最后结果,建议额外建一张refund_record表记录每一笔退款明细。如果只有全额退款场景,一个字段倒也够用,但财务对账时需要明细就会很痛苦。
一些额外的经验体会
整个支付模块在JeecgBoot里跑通之后,我个人最深刻的体会是:低代码平台的边界其实是开发者自己画出来的。JeecgBoot擅长生成标准的CRUD页面,但它的代码生成器产出的东西完全允许你手动改、手动扩展,它不会限制你接入微信支付宝还是其他第三方能力。关键是要在生成代码和手写逻辑之间保持清晰的层次,能交给生成器的交给生成器,不能交给生成器的(像支付回调验签这种涉及到钱的逻辑)一定要独立出来。
还有一个小经验:支付流水表从第一天开始就要做每日对账任务,不能等着出问题再补。我是在JeecgBoot里配置了一个凌晨跑批的对账任务,拉取前一天微信支付和支付宝的账单文件,和本地支付流水逐笔核对,金额不一致或者本地没记录的自动标记异常。这个对账任务看起来不起眼,但每次他帮我拦住问题的时候,我都觉得当初多花那半天写得真值。
最后再分享一个细节。支付回调接口一定要保留完整的原始日志,包括请求头、报文体、验签结果、处理结果,打成独立日志文件保留至少30天。线上排查支付问题的时候,这份日志就是你还原现场的唯一依据。只会在业务日志里打一条“支付成功”是远远不够的。