ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java支付系统源码实战:SpringBoot对接支付宝微信银联全攻略

Java支付系统源码实战:SpringBoot对接支付宝微信银联全攻略 简介这款Java互联网支付系统源码基于SpringBoot构建完整集成了支付宝、微信支付与银联支付三大主流渠道面向需要学习支付接口开发或搭建支付模块的Java开发者也适用于电商、服务预订、在线教育等业务场景。包内共250个文件以Java源码为核心辅以js、html、css等前端资源、gif演示图以及证书和配置文件压缩包整体仅1.53MB目录结构清晰便于按需查阅和二次开发。资源内容覆盖支付请求构造、回调验签、异步通知处理、退款与交易查询等关键代码并包含了签名算法、HTTPS通信等安全实现细节能够帮助读者理解三种支付方式的完整交互流程。目前已有774人学习下载适合希望快速掌握SpringBoot整合多支付渠道、并参照真实项目进行实践的中级开发者。1. Java互联网支付系统从订单到账单的闭环这套源码帮你跳过三个月试错做支付接入这件事说难不算难说简单也绝不容易。这套基于SpringBoot的Java互联网支付系统源码把支付宝、微信、银联三条主流接入路径全部打通统一下单、回调验签、状态更新、退款查询都有完整代码案例。别看文档写得厚实际上光支付宝一家就有当面付、网站支付、App支付好几个产品线签名规则各不相同微信支付又有一套证书体系银联更是证书文件和报文格式的堆叠。很多团队第一个支付需求排期三个月一半时间耗在环境配置和联调上。这套源码适合两类人一是要接入支付的Java后端工程师拿来改改配置就能跑通闭环二是想系统看懂支付系统设计的人读代码比看文档快得多。下面按「架构设计→支付宝→微信与银联→避坑→跑通验证」的顺序拆。2. 支付系统的地基订单状态机、回调闭环与三渠道统一抽象2.1 支付系统到底要管哪几件事拿到这套源码第一件事不是去看支付宝的代码而是先把它整个包结构过一遍。一个生产级支付系统本质上就是四件事下单、支付、回调、对账和退款。如果哪个项目把这四件事混在一个Service里后面改渠道的时候一定会吃苦头。这套源码的包结构拆得很清楚controller层只做参数接收不写业务逻辑service层按渠道拆了alipay、wechat、unionpay三个包core里面是订单、流水、回调通知的公共逻辑。我一般建议团队照这个结构来拆因为支付渠道是最容易变动的地方。支付宝升级SDK、微信调整API版本、银联改报文格式都是你不可控的变化渠道代码必须能独立替换否则每次渠道调整都要把整个支付模块翻一遍回归测试工作量成倍增长。这也是这套源码第一个值得抄作业的地方渠道隔离不是设计洁癖是实实在在的维护成本问题。下单这个动作核心产物是订单号和支付参数。订单号由业务系统生成传给支付网关回调的时候再原样返回。这套代码里对订单号的处理值得注意它不是UUID直接拿来用而是拼上了渠道标识和日期比如20250101_8001_000001。这样日志里扫一眼就知道是哪天哪个渠道的单子排查问题时非常顺手。UUID当订单号也不是不行但出问题的时候你对着日志里的乱码字符串想按时间维度筛选都做不到。回调是支付系统的命门。用户扫码付完钱支付平台会异步通知你的服务器你收到通知后要验签、查单、改状态、通知业务方。这套源码里回调处理是全部代码里最厚的部分因为它要处理验签失败、重复通知、金额不一致这些情况。后面我会单独开一章讲坑这里先记住一个结论回调处理得越保守后续对账越轻松。2.2 订单状态机与数据库设计支付订单的状态设计决定了整个系统的复杂程度。状态设少了退款、关单、超时这些场景没法表达设多了每个状态流转都要写一堆判断容易把人绕晕。这套源码用的是经典五状态待支付、已支付、已退款、已关闭、部分退款。数据库设计上核心就是订单表和流水表。订单表的核心字段我列出来CREATE TABLE pay_order ( id bigint(20) NOT NULL AUTO_INCREMENT, out_trade_no varchar(32) NOT NULL COMMENT 商户订单号, channel varchar(16) NOT NULL COMMENT 渠道alipay/wechat/unionpay, trade_no varchar(64) DEFAULT NULL COMMENT 支付平台流水号, amount_fen int(11) NOT NULL COMMENT 订单金额单位分, status tinyint(4) NOT NULL COMMENT 0待支付 1已支付 2已退款 3已关闭 4部分退款, notify_time datetime DEFAULT NULL COMMENT 最后回调时间, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_out_trade_no (out_trade_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里要特别说明两个字段的用意。第一个是amount_fen金额以「分」为单位用整数存。这是支付系统最基础也最容易被新手忽略的纪律用浮点数存金额必出精度问题数据库层面用整数分比DECIMAL更省心也方便和微信、银联的接口直接对齐。第二个是uk_out_trade_no唯一索引这是幂等处理的第一道防线同样的订单号在物理上不可能插入两次。流水表pay_flow则是每次状态变更都记一条类似于银行流水。下单记一条、回调成功记一条、退款记一条每一条都带上渠道返回的交易号。这套源码里所有对账、出账的操作都从流水表读数据而不是直接读订单表。这个习惯很实用查问题的时候能看到每一步发生了什么而不是只看到一个孤零零的最终状态。2.3 三渠道的差异与统一支付接口设计支付宝、微信、银联三家接口风格完全不一样有传JSON的有传表单的有传XML的但这套源码把它们收口到了一个统一的PaymentService接口上。接口定义大概这样public interface PaymentService { /** 下单返回支付参数二维码串/跳转链接 */ PayResult placeOrder(PayRequest request); /** 处理异步回调验签后返回是否标记成功 */ CallbackResult handleNotify(MapString, String notifyParams); /** 主动查单用于补偿回调丢失 */ QueryResult queryOrder(String outTradeNo); /** 退款返回退款流水号 */ RefundResult refund(String outTradeNo, int refundAmountFen); }每个实现类里才是各渠道的SDK调用。这样做的好处很明显业务系统只需要依赖PaymentService接口不需要知道当前跑的是哪个渠道。切换渠道时改配置的default指向即可业务代码一行不动。源码里三个实现类还各自维护了自己的配置类用ConfigurationProperties绑定配置项的命名和渠道SDK的术语保持一致看代码的时候不会产生「这个字段到底是支付宝的还是微信的」这种困惑。三家渠道的风格差异这里先给一个总览后面两章分别展开渠道签名方式金额单位回调节点主要SDK支付宝RSA2公私钥字符串元异步通知可配公钥验签alipay-sdk-java微信APIv3 商户证书整数分回调加密需解密验签wechatpay-java银联证书文件签名整数分同步异步双通知官方acp-sdk这个抽象还有一个隐藏价值对账系统只需要面向PaymentService接口做轮询查单不需要知道各渠道查单协议差异。后面第6章我会演示怎么用这个接口写一个每30秒自动查单的补偿任务。3. 支付宝接入实战沙箱配置、下单签名与异步回调验签3.1 开放平台配置与SDK依赖引入支付宝接入第一步不是写代码是去开放平台拿到三样东西应用AppID、应用私钥、支付宝公钥。这套源码用的是「自管证书」模式也就是用支付宝官方工具生成RSA2密钥对公钥上传给平台私钥自己保存。RSA2是现在唯一推荐的签名算法源码里signType字段直接写RSA2。如果哪天看到旧代码写RSA那是SHA1签名新商户已经申请不到了遇到就得升级。源码里支付宝配置集中在application-alipay.ymlalipay: app-id: 2021000123456789 private-key: MIIEvQIBADANBg... # 应用私钥PKCS8格式 alipay-public-key: MIIBIjANBg... # 支付宝公钥 gateway: https://openapi.alipay.com/gateway.do notify-url: https://api.example.com/pay/alipay/notify return-url: https://example.com/pay/result注意两个容易出错的地方。第一private-key必须是PKCS8格式。很多人在开放平台下载的是PKCS1格式的私钥直接贴进去会报「密钥格式错误」需要在本地用openssl转成PKCS8一行命令openssl pkcs8 -topk8 -inform PEM -in pkcs1.pem -out pkcs8.pem -nocrypt。第二gateway生产环境是openapi.alipay.com沙箱环境要换成openapi.alipaydev.com。这两个地址写反了报错信息是「网关鉴权失败」但真正的根因是域名配错排查起来很迷惑。SDK依赖方面这套源码用的是支付宝官方alipay-sdk-javadependency groupIdcom.alipay.sdk/groupId artifactIdalipay-sdk-java/artifactId version4.39.57.ALL/version !-- 具体版本以Maven仓库官方最新为准 -- /dependency这个依赖会把所有支付产品接口类打包进来体积不小但胜在一个包搞定。版本升级时重点看两点接口签名有没有变、底层HTTP客户端有没有换。我自己遇到过SDK从4.x升到5.x后execute方法签名变化的兼容性问题所以源码里这块特意把SDK调用封装在AlipayClientHolder里升级SDK时只需要改一个类。3.2 统一下单与二维码生成扫码支付场景源码里用的是支付宝「预下单 返回二维码」方案也就是当面付的alipay.trade.precreate。为什么不直接用alipay.trade.page.pay因为业务是PC端生成二维码让用户用手机扫不需要页面跳转。precreate返回一个qrCode字符串转成二维码图片挂在页面上即可。核心代码长这样AlipayClient alipayClient new DefaultAlipayClient( gateway, appId, privateKey, json, UTF-8, alipayPublicKey, RSA2); AlipayTradePrecreateRequest request new AlipayTradePrecreateRequest(); request.setNotifyUrl(notifyUrl); request.setBizContent( {\out_trade_no\:\ outTradeNo \, \total_amount\:\ amountYuan \, \subject\:\ subject \, \timeout_express\:\30m\} ); AlipayTradePrecreateResponse response alipayClient.execute(request); if (response.isSuccess()) { return response.getQrCode(); // 拿到二维码串前端转成二维码图片 }这里有个极易踩的坑total_amount是字符串格式的「元」不是分。源码里在构造参数前已经把分转成了元用String.format(%.2f, amountFen / 100.0)但如果你直接把整数分传进去比如1.5元写成150支付宝会直接拒绝。反过来如果把1.5写成1.5而不是1.50虽然支付宝能解析但部分字段回显时会出现格式不一致所以统一的%.2f格式化不能省。timeout_express这个参数也值得留意它控制二维码的有效期源码里设了30分钟。这个值要和你订单表里「待支付」状态的超时关单任务保持一致否则会出现二维码还能扫、本地订单已经被关掉的情况。生产环境我一般用15到30分钟太短用户来不及支付太长占着库存不放。另外要注意这个字段的单位是分钟拼字符串的时候别带上其他单位后缀。3.3 异步回调验签与订单状态流转支付成功之后支付宝会异步通知你的notifyUrl最多通知8次间隔逐渐拉长。源码里回调处理的思路很清晰验签 → 取参 → 幂等更新 → 返回success或failure。RequestMapping(/pay/alipay/notify) public String alipayNotify(HttpServletRequest request) throws AlipayApiException { MapString, String params ServletUtils.getParamMap(request); boolean signVerified AlipaySignature.rsaCheckV1( params, alipayPublicKey, UTF-8, RSA2); if (!signVerified) { return failure; // 验签失败告知支付宝不要继续重试 } String outTradeNo params.get(out_trade_no); String tradeNo params.get(trade_no); String tradeStatus params.get(trade_status); // 只处理 TRADE_SUCCESS / TRADE_FINISHED if (TRADE_SUCCESS.equals(tradeStatus) || TRADE_FINISHED.equals(tradeStatus)) { payService.markPaid(outTradeNo, tradeNo); } return success; }两个细节必须说清楚。第一回调通知里的参数验签用的是rsaCheckV1它会把所有参数按字典序拼接后再验签这个方法名里的V1容易让人误以为是旧版签名其实它对应的是RSA2签名算法参数拼接顺序和另一套rsaCheck有一处不同用错方法会导致验签诡异失败——签名的参数顺序是这里最典型的玄学问题。第二处理完业务逻辑后必须返回纯文本success一个字都不能多不能返回JSON否则支付宝会判定处理失败继续重试。源码里这里用了ResponseBody返回字符串写法上很克制。回调里的金额校验源码在markPaid里面做了params.get(total_amount)转成整数分和订单表里的amount_fen比对不一致就直接返回failure并记录异常日志。这条校验不能省虽然概率极低但万一出现金额不一致而系统标记成功财务对账会非常难受到时候要翻所有渠道的流水才能定位问题。另外要注意trade_status的取值。支付宝的回调里会有WAIT_BUYER_PAY、TRADE_SUCCESS、TRADE_FINISHED三种状态。WAIT_BUYER_PAY是用户还没付不需要处理TRADE_SUCCESS是一般意义上的支付成功TRADE_FINISHED表示交易完成且不可退款。源码里把两个成功态都纳入markPaid但退款场景下需要额外判断TRADE_FINISHED之后支付宝是不允许发起退款的所以如果你的业务允许用户支付后一段时间内退款必须单独记录收到的是哪个状态。4. 微信支付V3与银联接入证书体系差异与统一收银台4.1 微信支付V3的APIv3签名与回调微信支付的接入体验和支付宝差别很大。支付宝是一对公私钥微信支付在V3版本里是「商户API证书 APIv3密钥 商户号」三个要素任何一个缺失都跑不通。源码里微信支付的配置长这样wechat: mch-id: 1900001234 app-id: wxd2f5c... api-v3-key: 32位随机字符串 merchant-private-key: -----BEGIN PRIVATE KEY-----... merchant-serial-number: 证书序列号微信支付V3的所有请求请求头里要带上Authorization: WECHATPAY2-SHA256-RSA2048签名。签名过程是构造签名串请求方法 换行 请求路径 换行 时间戳 换行 随机串 换行 请求体用商户私钥做SHA256-RSA签名再拼上商户证书序列号。这段逻辑如果自己写非常容易在换行符或请求体编码上翻车。源码里直接用了微信官方wechatpay-javaSDK签名头由SDK自动构造省掉了一大批低级错误。下单接口调用代码如下WechatPayClient client new WechatPayClient.Builder() .merchantId(mchId).privateKey(merchantPrivateKey) .merchantSerialNumber(merchantSerialNumber) .apiV3Key(apiV3Key).build(); NativePayService nativePayService new NativePayService.Builder(client).build(); NativePayTransactionRequest txRequest new NativePayTransactionRequest() .setAppid(appId).setMchid(mchId) .setOutTradeNo(outTradeNo) .setAmount(new Amount().setTotal(amountFen)) // 注意微信单位就是分 .setDescription(subject) .setNotifyUrl(wechatNotifyUrl); Transaction result nativePayService.createOrder(txRequest); String codeUrl result.getCodeUrl(); // 拿这个串生成二维码一个关键差异微信支付的amount.total单位就是分和支付宝按「元」传字符串完全不同。如果按支付宝的习惯把1.5元传成1.50微信支付会直接报参数错误反过来把微信的150分传给支付宝也会被拒。这套源码在三渠道各自的Service实现里做了单位转换统一入口都用分这是统一抽象带来的实在好处。微信V3回调的处理比支付宝多一步回调内容经过了AES-256-GCM加密需要先用api-v3-key解密报文再验签。解密后的resource字段里才是out_trade_no、transaction_id、trade_state这些真正的业务字段。源码里用SDK的NotificationParser解析回调解析出来的参数结构和支付宝完全不同但最终都收口到payService.markPaid这个方法。有一个常见坑解密失败时很多人会怀疑是api-v3-key配错其实更常见的是回调头里的Wechatpay-Signature和Wechatpay-Timestamp没有被正确传递代理层把请求头过滤掉了。源码里回调接口的日志把整个请求头打出来了这个习惯值得保留。4.2 银联网关支付的证书与跳转银联支付是三家里最「传统」的它的签名体系建立在证书文件上商户需要申请「签名证书」「验签证书」两套加上银联的「验签证书」三个证书文件缺一不可。这套源码里银联的配置集中在acp开头的一组参数里启动时要求证书文件放在指定目录路径配错会直接抛FileNotFoundException而且报错信息容易让人误以为是网络问题。我建议把这几个证书文件的路径做成配置项而不是写死在代码里万一证书到期更换只需要改配置重启不用重新编译。银联的主流方案是网关跳转用户提交订单后后端拼接一份表单POST到银联网关银联在用户完成支付后先跳回frontUrl浏览器回调再通过backUrl后台通知把结果告诉服务器。源码里两个URL都做了处理但代码注释写得很清楚frontUrl只是给用户看的「支付完成」页面不能当作业务成功的依据业务结果必须以后台backUrl通知为准。银联回调验签的核心代码AcpService acpService new AcpService(); MapString, String data acpService.decodeFormResponse(params); boolean verify acpService.validate(data, UTF-8); if (!verify) { log.error(银联回调验签失败: {}, outTradeNo); return error; } String respCode data.get(respCode); if (00.equals(respCode)) { // respCode为00才是交易成功 payService.markPaid(data.get(orderId), data.get(queryId)); }银联和支付宝、微信有个明显差异响应码respCode不只是「00 / 非00」两种还有03不确定、06部分成功等中间态。源码里对非00响应做了单独处理不标记成功也不直接标记失败而是把状态留在「待支付」等待后续查单任务去确认。这块逻辑新手容易写成「非00就是失败」一旦用户其实支付成功但本地订单显示失败后续退款和客服处理会非常痛苦。银联还有一个细节queryId是银联返回的交易查询流水号退款的origQryId就是用它。源码里把queryId存进了流水表的channel_flow_no字段退款时直接取出来用不用重新查。这个字段在三家渠道里含义不同——支付宝存的是trade_no微信存的是transaction_id——所以统一用trade_no字段名语义上保持对齐。4.3 收银台聚合三种渠道如何共存收银台是前端到支付后端的第一个入口这套源码把它设计成了一个普通的下单接口前端传channel字段alipay/wechat/unionpay后端根据channel从Spring容器里拿到对应的PaymentService实现类。代码在PaymentServiceFactory里用了最简单的策略模式Component public class PaymentServiceFactory { private final MapString, PaymentService serviceMap; public PaymentServiceFactory(ListPaymentService services) { serviceMap services.stream() .collect(Collectors.toMap(s - s.channel(), s - s)); } public PaymentService get(String channel) { PaymentService service serviceMap.get(channel); if (service null) { throw new UnsupportedOperationException(不支持的支付渠道: channel); } return service; } }ListPaymentService依赖注入会把三个实现类全部收进来channel()方法返回各自渠道名工厂负责按名字取实现。后续要接第四家渠道比如云闪付只需要新写一个实现类标注好channel工厂代码一行都不用改。这个模式比在统一Service里写if-else要干净得多排查问题时沿着channel字段就能定位到具体实现类。这里分享一个源码之外的实践经验收银台的下单接口一定要做防重复提交。同一订单号重复下单支付宝和银联会返回「订单已存在」微信会报ORDER_EXISTS错误。源码里通过Transactional加订单号唯一索引双重保证但HTTP层的防重也可以配合加上用Redis的SETNX out_trade_no做一分钟内的幂等控制体验会更好。另外收银台接口建议把三个渠道的响应结构统一成{ channel, payParams, expireAt }前端拿到payParams后按渠道分别渲染二维码或跳转链接这样前端逻辑也只需要写一次。5. 支付系统避坑指南回调丢失、重复通知与金额精度支付这一行文档看得再多不如真实踩一次坑记得牢。这一章我把这套源码里标注了大量注释的踩点整理成五条每条都是「现象 → 原因 → 解决」的结构照着排查能省一半联调时间。5.1 回调丢失用户付了钱服务器没收到通知现象用户手机显示支付成功但平台订单一直停在「待支付」查支付宝或微信后台又确实有这笔交易。原因回调通知是网络请求丢包、服务器重启、部署期间接口短暂不可达都会导致通知丢失。支付宝和微信会重试但重试有次数上限而且都在支付成功后的短暂窗口内过了窗口就再也不通知了。只依赖回调做状态更新等于把业务成功寄托在网络请求的运气上。解决以「主动查单」为主「被动回调」为辅。这套源码里PaymentService接口的queryOrder就是干这个的配一个定时任务每30秒扫描一次超过2分钟仍未支付且未关闭的订单主动向渠道发起查单。如果查询结果是已支付走一遍和回调相同的markPaid逻辑。我一般把这个任务的调度间隔按订单量调整线上量少就30秒一次量大可以考虑只对高金额订单做高频查单。Scheduled(fixedDelay 30000) public void compensatePendingOrders() { ListPayOrder pendingList orderMapper.findPendingTimeout(2, 30); for (PayOrder order : pendingList) { PaymentService service serviceFactory.get(order.getChannel()); QueryResult result service.queryOrder(order.getOutTradeNo()); if (result.isPaid()) { payService.markPaid(order.getOutTradeNo(), result.getTradeNo()); } else if (result.isClosed()) { orderMapper.updateStatusClosed(order.getOutTradeNo()); } } }这段代码里findPendingTimeout的两个参数含义是超过2分钟、且创建时间在30分钟内的待支付订单。这里要注意不要扫全部历史待支付单否则几十万条死单每次都被拉出来查一轮渠道查单接口有频率限制很容易触发风控。合理的做法是只扫最近30天或最近N小时的订单。5.2 重复通知同一笔订单被回调了多次现象日志里看到同一笔订单的markPaid被调用了好几遍有时是回调重复有时是「回调 查单补偿」同时在跑。原因支付宝和微信都承诺通知可能重复多次而且回调请求可能在中间链路被重放补偿查单任务也可能和回调在时间上撞车。如果没有幂等保护第一次标记成功后第二次执行会把流水表写重甚至触发订单状态回退。解决幂等处理三道闸。第一道是数据库唯一索引pay_flow表针对out_trade_no type建唯一约束重复插入直接异常退出。第二道是状态判断markPaid方法进入事务后先查订单状态已经是「已支付」就直接return不重复记账。第三道是路由层面回调接口再加一层RedisSETNX同一笔订单号在10秒内只允许一个处理请求进入。Transactional public void markPaid(String outTradeNo, String tradeNo) { PayOrder order orderMapper.selectByOutTradeNoForUpdate(outTradeNo); if (order null) { log.error(订单不存在回调异常: {}, outTradeNo); return; } if (order.getStatus() PAY_STATUS_PAID) { log.info(订单已支付忽略重复回调: {}, outTradeNo); return; } // 校验回调金额 if (order.getAmountFen() ! currentNotifyAmountFen) { throw new NotifyAmountMismatchException(outTradeNo); } order.setStatus(PAY_STATUS_PAID); order.setTradeNo(tradeNo); flowMapper.insert(PayFlow.successFlow(outTradeNo, tradeNo)); }这里selectByOutTradeNoForUpdate用了行锁保证同一时刻只有一个线程在处理这笔订单的支付状态更新这是并发回调场景下最稳妥的写法。注意ForUpdate必须配合事务使用锁在事务提交时才释放所以markPaid上的Transactional不能去掉否则行锁立即失效幂等就破功了。5.3 金额精度一分钱引发的对账事故现象订单金额在对账时出现几分钱差异比如用户付了1.50元平台账单上却是1.49元或1.51元。原因用double或float存金额。Java里0.1 0.2 0.30000000000000004一旦在JSON序列化、数据库类型转换过程中出现精度丢失就会在某个角落露出这个差值。尤其是在金额做加减法时比如订单金额加运费再减优惠券浮点数每一步都可能累积误差。解决两个禁用一个统一。禁用浮点类型存金额数据库全部用整数分或DECIMAL(10,2)禁用double做金额运算。渠道交互时支付宝要求字符串形式的「元」微信和银联要求整数「分」在Service内部统一使用「分」int/long只有和渠道SDK交互的那一层做格式转换。这套源码里所有金额字段都叫amountFen命名即约束看到这个后缀就不会有人敢往里传小数。退款接口的入参同样用refundAmountFen退款金额不能大于订单剩余可退金额这个校验在退款Service里也写了。还有一个隐藏坑JSON序列化时整数分如果被Jackson转成科学计数法或者带小数点的数字微信支付会报「金额无效」。所以和渠道交互的DTO里金额字段建议用Integer或Long类型而不是int基本类型避免反序列化的边界问题。5.4 证书与密钥管理的翻车点现象配置了私钥和公钥订单请求却报「签名验证失败」或「证书格式错误」测试环境好好的上生产就不行或者过了一段时间突然开始报错。原因三个常见问题。第一私钥格式不对支付宝要求PKCS8微信要求PKCS1格式混用必然报错。第二证书序列号和私钥不匹配微信支付V3请求头里的merchant-serial-number必须是当前有效证书的序列号证书续期或轮换后序列号会变代码里旧的序列号没同步就一直验签失败。第三密钥硬编码在配置文件里代码仓库泄露后别人就能拿私钥发起退款请求这个风险比签名失败严重得多。解决格式问题看报错关键词Bad base64是密钥内容错误invalid signature多数是格式或序列号不匹配。证书轮换时先把新证书序列号写进配置并发布再等15分钟确认签名正常后再废弃旧证书避免切换窗口导致验签失败。密钥管理上生产环境至少要把私钥从配置文件挪到环境变量或配置中心源码里的demo虽然写在yml里但注释里明确标注了「生产环境必须外部化」这个提示。5.5 沙箱和生产是两个世界现象沙箱环境用模拟账号支付一切正常切到生产环境后同样的代码报「应用未授权」或「商户号不存在」。原因沙箱环境的AppID、商户号、证书和生产的完全独立而且沙箱里没有微信支付的真实商户号和证书。很多人的误区是「代码在沙箱通了生产改个key就能跑」实际上生产环境还需要在开放平台配置应用权限——支付宝要签约「当面付」产品微信要开通「Native支付」银联要申请商户证书后做终端绑定。这些权限审批走完通常要1到3个工作日不提前申请就会卡在联调最后一步。解决项目排期时上线前两周就把支付功能的权限申请提交上去。切生产环境时使用一套独立的配置文件application-prod.yml逐项核对网关地址、AppID、商户号、证书序列号、回调域名。我一般会在切完生产后真扫一块钱验证整条链路再进对账流程。另外沙箱和生产两套配置建议用Spring的profile机制隔离防止测试环境误连生产网关。6. 把这份源码跑起来配置清单、沙箱联调与一条验证命令6.1 启动前的配置清单实际把这套源码拉到本地跑通最快的路径是「先沙箱、后生产」。沙箱阶段只需要申请一个支付宝沙箱应用微信和银联可以先写死测试数据。启动前把三样东西配好MySQL库表源码自带的schema.sql建库建表、application.yml里的数据源地址、application-alipay.yml里的沙箱网关和应用密钥。按这个清单核完项目基本能直接起。6.2 沙箱联调的关键步骤支付宝沙箱的流程是开放平台进入沙箱环境创建应用后生成沙箱AppID、沙箱私钥和沙箱公钥用配套的沙箱账号在支付宝App里扫码支付。回调地址这一环要注意沙箱同样要求回调地址是公网可访问的URL本地开发时可以指向一台有公网IP的测试服务器把后端服务部署上去或者利用云厂商的容器映射能力做转发让沙箱平台能访问到本地接口。6.3 验证整条链路的检查点代码跑通后我建议你按这个顺序验证下单接口返回二维码 → 扫码支付 → 看日志里回调到达且验签通过 → 查订单状态变成已支付 → 等30秒看是否被重复通知应被幂等拦截。一条命令验证支付状态curl http://localhost:8080/pay/query?outTradeNo20250101001返回{status:PAID,tradeNo:202501012200...}就说明整条链路闭环了。把这个检查点走完这套源码才算真正变成你自己的东西。做支付系统这些年我最大的教训就是永远不要相信回调一定会到永远不要相信金额一定精确永远不要跳过幂等直接上线。从那以后我每次接新渠道都强制走一遍「下单 → 沙箱扫码 → 回调验签 → 查单补偿 → 幂等重放 → 生产小额真付」的流程一次都不敢少。希望帮到你。本文还有配套的精品资源点击获取
返回列表