如果你搜索过“JAVA对接支付宝支付”这个关键词,大概率会翻到一堆旧SDK时代的复制粘贴文档,要么只给一个下单Demo就草草收场,要么讲的是已经被支付宝废弃的老接口。真正把异步回调、对账、退款、幂等等生产环境必须处理的细节讲清楚的,真的不多。去年我在一个电商项目里第一次完整对接支付宝,从沙箱联调到上线后追查一笔支付回调丢失,前前后后折腾了一周多,踩了不少文档里根本没写的坑。
这篇就把完整链路拆开给你看。从申请沙箱、生成密钥、引入SDK,到写下单接口、处理异步通知、做主动查询、写退款逻辑,再到上线前必须排查的高频坑,全部按实际可跑通的流程来讲。适合刚接触支付集成的后端开发者,如果你其实已经接过了微信支付或其它支付渠道,也可以把这篇当成一份核对清单,重点看回调验签和幂等控制这两块。
1. 交易链路拆解:先别写代码,搞懂支付宝支付的“游戏规则”
很多新手拿到对接任务,第一反应就是找个现成代码复制,然后对着编译错误无从下手。这里我想先拦一下,支付对接和普通CRUD接口最大的区别在于:你写的请求会跳到别人服务器上,别人也随时会回调你的服务器,整个过程涉及两个系统的状态同步。如果你不理解这条链路里每个节点谁先谁后,就算把代码跑通,一上线也会出各种匪夷所思的问题。
1.1 用户扫码之后,系统之间到底发生了什么
拿最常见的电脑网站支付来举例。用户在浏览器点了“支付宝支付”,你的后端拿着订单号去调用支付宝的预下单接口,支付宝返回一段自动提交的HTML表单,这个表单被返回给浏览器后,页面就自动跳转到支付宝的收银台。用户在收银台输入账号密码完成付款,支付宝的服务器确认扣款成功,这时它要做两件事:第一,在浏览器层面同步跳转回你配置的return_url;第二,在服务器层面异步给你配置的notify_url发送一条POST通知。
这两件事要分开理解,很多人初次接触容易混淆。同步跳转只是告诉用户“支付成功”,它并不可靠,用户可能付完款直接关了浏览器,也可能在跳转过程中断网,所以业务上绝不能拿return_url当支付成功的依据。真正决定订单要不要发货、要不要放行的是异步通知,也就是notify_url。支付宝会以服务器的身份发起POST请求,里面带着订单号、交易号、支付金额、交易状态、签名等一堆参数,你的后端验签通过后,才应该把订单状态改成“已支付”。
这里还有一层容易被忽略的逻辑:支付宝的异步通知不是只发一次。如果它发通知后没收到你返回的“success”,或者你的服务处理超时、返回了非“success”内容,它会按间隔继续重发。重试时间大致是4分钟、10分钟、15分钟、30分钟……直到成功或通知失败达到上限。所以你的回调接口必须设计成“扛得住重复调用”,也就是接口要幂等。
1.2 后端真正要接的其实只有四类接口
支付宝开放平台对外交易相关的API看起来很吓人,什么当面付、电脑网站支付、手机网站支付、App支付,各有各的接口,但你剥开外表看,业务上后端真正要处理的只有四类操作:
- 预下单:把订单信息发给支付宝,拿到一个可执行跳转的页面或者支付串。
- 异步通知:被动接收支付宝推送的支付结果,验签、改写订单状态。
- 主动查询:在不确定支付结果的时候,主动问支付宝“这笔订单到底付了没有”。
- 退款:把已支付的金额原路退回给用户,有可能部分退款,也可能整单退。
把这四类操作的边界和状态含义理清楚,你再去翻官方文档,会发现代码怎么写都绕不开这四个方向。下面每个环节我都会给代码片段,但代码只是骨架,真正难的是边界处理和异常状态判断,这也是我花大量篇幅讲流程的原因。
2. 沙箱、密钥和SDK:准备工作里的“隐形门槛”
说句实在话,支付宝支付的代码量并不大,很多第一次对接的人卡在准备工作上。要么找不到沙箱应用入口,要么分不清应用私钥和支付宝公钥,要么SDK引入以后版本冲突。这部分我把每一步操作的位置和容易误解的概念讲清楚。
2.1 沙箱账号与关键参数对照
进入支付宝开放平台的控制台,在“开发服务”里找到“沙箱”应用。沙箱环境提供了一套完全独立的测试参数,和正式环境最大的区别是网关地址不同,所有请求都会打到支付宝的沙箱服务器上,不会产生真实扣款。
沙箱里你需要重点记下这几个参数:
| 参数 | 沙箱配置值示例 | 说明 |
|---|---|---|
| APPID | 2021000123456789 | 应用唯一标识 |
| 支付宝网关 | https://openapi-sandbox.dl.alipaydev.com/gateway.do | 生产环境是 openapi.alipay.com,千万别搞混 |
| 商户UID | 2088开头的数字 | 沙箱商户的基本信息 |
| 沙箱买家账号 | 一串带@的测试账号 | 用这个账号在收银台登录付款 |
| 沙箱买家登录密码 | 控制台默认生成 | 支持修改 |
注意沙箱环境下的买家账号和密码是支付宝沙箱平台提供的测试账号,不是你自己注册的那套账号。联调时你用自己真实的支付宝账号去扫沙箱的付款码,会一直报“用户不存在”或登录失败,这属于正常情况。解决方式就是去沙箱控制台复制它给的买家和商家测试账号。
沙箱还内置了一个“沙箱版支付宝”App的下载入口,如果你要测的是手机端支付,直接在测试手机上安装那个App,然后用沙箱账号登录。
2.2 密钥生成的两种方式与选择建议
支付宝官方推荐我们用支付宝开放平台密钥工具来生成密钥。打开密钥工具,选择“生成密钥”,工具会自动生成应用公钥和应用私钥,文件默认保存在你的电脑本地。
这里有两组到底哪个东西放到哪里的问题,我梳理一下:
- 应用私钥:保存在你自己的后端服务里,用于对你请求的参数做签名,绝对不能对外暴露,也不能提交到Git仓库。
- 应用公钥:上传到支付宝开放平台控制台,支付宝用它来验签,确认请求确实来自你的应用。
- 支付宝公钥:上传应用公钥后在控制台生成的另一串公钥,它属于支付宝,你的后端要保存它,用于验证支付宝回调给你的消息确实是支付宝发的。
这个过程很像寄一封带印章的信。应用私钥是你的印章,发请求时盖上自己的章;支付宝公钥是你手里握着的“支付宝官方印章样本”,支付宝回调时你要比对印章真伪。两头各拿一个公钥,形成一个互验的闭环。
现在支付宝已经不支持RSA1代的SHA1签发了,全部要求RSA2,也就是SHA256withRSA。你在生成密钥时选择RSA2,配置参数里签名类型直接写“RSA2”。
2.3 引入SDK与配置类封装
Maven项目里引入支付宝官方SDK,坐标如下:
<dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.39.117.ALL</version> </dependency>这个版本是目前比较新的4.x版本。如果你的项目里已经存在旧版,最好统一升级,避免后面因为SDK接口签名不一致报一堆编译错误。引入之后,在配置文件里维护好下面几个属性:
alipay: app-id: 2021000123456789 gateway-url: https://openapi-sandbox.dl.alipaydev.com/gateway.do app-private-key: 你的应用私钥 alipay-public-key: 你的支付宝公钥 sign-type: RSA2 charset: UTF-8 notify-url: https://你的域名/api/alipay/notify return-url: https://你的域名/pay/success然后封装一个统一的AlipayClient对象。我用的是构造后放入Spring容器的做法,AlipayClient本身是线程安全的,一个应用启动时创建一次就够了,不用每个请求都new。
@Configuration public class AlipayConfig { @Value("${alipay.app-id}") private String appId; @Value("${alipay.gateway-url}") private String gatewayUrl; @Value("${alipay.app-private-key}") private String appPrivateKey; @Value("${alipay.alipay-public-key}") private String alipayPublicKey; @Bean public AlipayClient alipayClient() { return new DefaultAlipayClient( gatewayUrl, appId, appPrivateKey, "json", "UTF-8", alipayPublicKey, "RSA2" ); } }这里有个小地方特别容易踩坑,就是DefaultAlipayClient的构造参数顺序不能乱。第七个参数是签名类型,传“RSA2”;如果使用证书模式,构造方法会不一样,这个在后面章节展开。
3. 四段核心代码实战:下单、回调、查询、退款一篇讲透
准备工作做完,下面就是实打实的代码。我会按业务调用顺序来写,每段代码都会配上解释,你看完可以直接往自己项目里套。
3.1 预下单:从参数组装到返回支付页面
电脑网站支付最常用的是alipay.trade.page.pay这个接口。它的响应不是JSON字符串,而是一段HTML表单文本,里面是一段form表单和一段自动提交的JavaScript。后端拿到这段HTML后,最简单的方式是直接以text/html的形式返回给前端浏览器,页面就会自动跳转到支付宝收银台。
核心代码如下:
public String createOrder(String orderNo, BigDecimal amount, String subject) { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setNotifyUrl(notifyUrl); request.setReturnUrl(returnUrl); AlipayTradePagePayModel model = new AlipayTradePagePayModel(); model.setOutTradeNo(orderNo); model.setTotalAmount(amount.setScale(2, BigDecimal.ROUND_HALF_UP).toString()); model.setSubject(subject); model.setProductCode("FAST_INSTANT_TRADE_PAY"); request.setBizModel(model); try { AlipayTradePagePayResponse response = alipayClient.pageExecute(request); if (response.isSuccess()) { // response.getBody() 返回的就是自动提交的HTML表单 return response.getBody(); } else { log.error("创建支付宝订单失败:{}", response.getMsg()); throw new RuntimeException("支付创建失败"); } } catch (AlipayApiException e) { throw new RuntimeException("支付宝调用异常", e); } }关键参数逐个看。
outTradeNo是商户订单号,必须在你的系统里全局唯一。这个字段对应你数据库订单表里的业务订单号,不是支付宝的交易号。支付宝会用这个单号来控制幂等,同一个forward订单号重复请求,返回结果是同一笔订单的支付信息。
totalAmount是订单总金额,单位是元。这里有个非常要命的问题:很多系统在数据库里存的是分,你必须在传给支付宝前转成元,并且用BigDecimal来做转换,千万不要直接拿double计算,否则会出现类似0.01元变成0.0099999元的问题。转换方式我放在后面专门讲。
productCode用在电脑网站支付场景下固定为FAST_INSTANT_TRADE_PAY。如果你是手机网站支付或App支付,这个字段会对应不一样的值,这也是新手最容易照抄报错的地方。
setNotifyUrl和setReturnUrl也是两个需要注意的点,它们表示支付宝在支付完成后,分别通过服务器链路和浏览器链路跳转回来的地址。这两个地址必须在支付宝开放平台配置过的域名下,否则支付宝会拒绝回调。沙箱环境下也要按这个逻辑配置。
还有一种情况,如果你想做的是App支付,用的就不是pageExecute而是sdkExecute,拿到的返回结果是orderStr字符串,这个字符串需要交给客户端SDK,由客户端拉起支付宝收银台。逻辑整体类似,但接口对象和响应处理方式不同,别混着用。
3.2 异步通知:验签、状态判断与“success”返回
异步通知是整个支付对接里最核心、最容易被忽略的环节。支付宝会把支付结果通过POST请求发送到你的notifyUrl,参数是表单格式。
第一件事永远是验签。代码很简单,但背后意义重大。如果不验签就处理订单,任何能访问你回调地址的人都可以伪造通知,把订单改成已支付,然后你的系统就会白发货。这是直接的经济损失漏洞。
@RequestMapping(value = "/api/alipay/notify", method = RequestMethod.POST) @ResponseBody public String notify(HttpServletRequest request) { Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); } try { boolean signVerified = AlipaySignature.rsaCheckV1( params, alipayPublicKey, "UTF-8", "RSA2" ); if (!signVerified) { log.error("支付宝异步通知验签失败"); return "failure"; } } catch (AlipayApiException e) { log.error("验签异常", e); return "failure"; } String appId = request.getParameter("app_id"); if (!appId.equals(alipayAppId)) { log.error("app_id 不匹配,疑似伪造通知"); return "failure"; } String tradeStatus = request.getParameter("trade_status"); String outTradeNo = request.getParameter("out_trade_no"); String tradeNo = request.getParameter("trade_no"); // 只有 TRADE_SUCCESS 或 TRADE_FINISHED 才代表用户已实际支付成功 if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { // 幂等判断:查本机订单状态 Order order = orderMapper.selectByOrderNo(outTradeNo); if (order == null) { log.error("订单不存在:{}", outTradeNo); return "failure"; } if ("PAID".equals(order.getStatus())) { // 已经处理过,直接返回 success,避免重复操作 return "success"; } // 加锁或者用乐观锁更新状态 int count = orderMapper.updateStatusIfUnpaid(outTradeNo); if (count == 0) { // 有并发竞争,说明其他线程或本次已经改过状态 return "success"; } // 落支付流水、扣库存、发消息等等 paymentService.handlePaidOrder(order, tradeNo); } return "success"; }这里有几个点值得展开。
验签方法AlipaySignature.rsaCheckV1传入的是参数Map,它内部会把参数里sign和sign_type排除,然后对剩余参数按字典序排序,拼接后做RSA验签。所以你在包装params时一定要注意,不要在里面掺杂额外的参数。如果你在回调地址加了别的HTTP查询参数,比如/notify?source=test,验签大概率会失败,因为支付宝在签名时并不会携带你加的那个参数。
校验完签名还不够,最好再校验一下app_id是否和你的应用匹配。虽然验签已经能说明消息来源是支付宝,但如果沙箱配置持续混乱,这一步能尽早拦截。
trade_status这个字段值有几种:WAIT_BUYER_PAY表示等待买家付款,TRADE_CLOSED表示订单关闭或退款成功,TRADE_SUCCESS表示已付款成功,TRADE_FINISHED表示交易完成且不可退款。后两个都是“最终成功”状态,只要用户付了款,支付宝后续会补充发送TRADE_FINISHED。所以业务上不管你收到的是TRADE_SUCCESS还是TRADE_FINISHED,只要订单状态还没变“已支付”,都可以放心地把订单改成已支付。真正不能碰的是WAIT_BUYER_PAY,它还停留在付款前阶段。
幂等判断那几步,很多人会省掉,觉得支付宝通知一次就够了。实际上通知重试机制至少会持续24小时。如果你不做状态判断,每次重建订单都把支付流水插一遍、把库存扣一遍,后果不堪设想。“订单不存在返回failure”这个分支也要写,虽然正常情况不会出现,但一旦出现,说明你漏了创建订单,此时不应该稳住支付宝让它停止重试,而是应该返回failure,让支付宝继续重试,同时你去日志里排查原因。
3.3 主动查询兜底:解决“我明明付了钱,订单却是待支付”的问题
异步通知是支付宝主动推给你的,但网络不可靠。有时候支付宝通知发过来,你服务器正在重启,或者请求网关超时,通知被丢弃了。这个时候用户肯定已经付了钱,订单却还停留在“未支付”,用户来催促,客服一查全懵。
地道做法是主动查询。支付宝提供了alipay.trade.query接口,入参只要out_trade_no或trade_no,就能实时查回这笔订单在支付宝侧的最新状态。
public boolean checkPayStatus(String outTradeNo) { AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); AlipayTradeQueryModel model = new AlipayTradeQueryModel(); model.setOutTradeNo(outTradeNo); request.setBizModel(model); try { AlipayTradeQueryResponse response = alipayClient.execute(request); if (response.isSuccess()) { String tradeStatus = response.getTradeStatus(); String tradeNo = response.getTradeNo(); if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { // 同样走一遍幂等更新 orderService.markAsPaid(outTradeNo, tradeNo); return true; } } } catch (AlipayApiException e) { log.error("查询支付宝订单失败", e); } return false; }主动查询通常用在三个位置:
一是支付结果页轮询。用户支付完成后跳转回你的站点,前端每2秒调一次后端“查订单”接口,后端如果发现本机订单状态还没变成已支付,就调一次支付宝查询,查到成功就直接返回“支付成功”。这种体验能解决大概80%的回调丢失情况。
二是定时任务对账。每天跑一个任务,把数据库里状态还是“未支付”但创建时间超过30分钟的订单捞出来,批量调用支付宝查询,如果发现对方已付款,就补单。这个能做到99.9%的补偿。
三是用户手动点击“刷新支付结果”按钮。虽然看起来low,但在应急场景下非常实用。
查询接口还有一个妙用,就是关闭订单。如果你有一个订单超过一定时间未支付,你想把它关掉,可以调alipay.trade.close。关闭后,支付宝会把该订单置为TRADE_CLOSED。如果关闭的时候用户刚好在付款,会有一条明确的提示。这个接口配合定期清理过期未支付订单很合适。
3.4 退款:部分退款、原路退回与幂等控制
退款逻辑看起来和查询差不多,但有一个细节很容易忽略:支付宝退款接口支持部分退款,而且允许多次退,只要累计退款金额不超过原订单金额就行。这个特性直接用好了就是“订单多次部分退款”的业务实现。
退款的核心代码如下:
public void refund(String outTradeNo, BigDecimal refundAmount, String outRequestNo) { AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); AlipayTradeRefundModel model = new AlipayTradeRefundModel(); model.setOutTradeNo(outTradeNo); model.setRefundAmount(refundAmount.setScale(2, BigDecimal.ROUND_HALF_UP).toString()); // 退款单号,用于幂等防重 model.setOutRequestNo(outRequestNo); request.setBizModel(model); try { AlipayTradeRefundResponse response = alipayClient.execute(request); if (response.isSuccess() && response.getCode().equals("10000")) { log.info("退款成功,退款金额:{}", refundAmount); } else { log.error("退款失败:{} {}", response.getCode(), response.getSubMsg()); throw new RuntimeException("退款失败"); } } catch (AlipayApiException e) { log.error("退款异常", e); throw new RuntimeException("退款异常"); } }outRequestNo是退款请求号,用来标识这笔退款操作。如果你之前用某个outRequestNo发起过一次退款,再次用同样的outRequestNo去请求,支付宝不会重复退款,而是把上一次的结果返回给你。这一点非常重要,你的退款接口如果没做自己的幂等控制,至少要保证每次退款后台生成的退款单号唯一,并且相同退款单号不重复调用。
退款也存在异步通知的情况,但不是所有退款都会发送。官方约定是退到余额会通知,退到银行卡也有通知,但通知枚举和支付通知不一致。稳妥做法是:退款结果不要完全依赖异步通知,退款接口返回成功后,定时向支付宝发起查询退款接口alipay.trade.fastpay.refund.query,用来双向核对最终结果。
另外提醒一句,退款金额不能超过订单的剩余可退金额,否则支付宝会报“退款金额超过订单可退余额”。如果业务上允许部分退款,你的数据库就要维护已退金额总和,并在发起退款前做一次校验。
4. 从联调到上线:几个“看文档也能漏掉”的高频坑
代码能跑通只是第一步。真正上线前,有几个问题是沙箱里很容易“顺利绕过”、到生产环境就爆发的。我按踩坑频率从高到低说。
4.1 金额单位:元与分的精度陷阱
支付宝所有金额字段用的单位都是元,而绝大多数Java后端在数据库里存的是分,为了绕过double精度问题直接存整数。当你把分转成元给支付宝时,最安全的写法是:
BigDecimal yuan = new BigDecimal(fen).divide(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP);注意new BigDecimal(fen)传的是int/long构造,不是BigDecimal.valueOf(fen),valueOf在fen超过一定大小时会转成double再转BigDecimal,有精度隐患。同时setScale(2)必须保留两位小数,支付宝偶尔对“10”这种没小数位的字符串也能接受,但为了统一,建议全部转成两位。
在收到异步通知的时候,支付宝返回的total_amount是字符串形式的元,比如“0.01”。如果业务数据库存的是分,你需要反向转成整数分,别拿字符串转Float再乘100,那会出0.009999999这种脏数据。
4.2 异步通知重复与接口幂等:不是“可能重复”,而是“一定会重复”
支付通知的重试机制我刚才讲过,但这里再单独强调一次:它在4小时以内会反复重试至少8次,最长重试周期可能是48小时。这意味着同一个通知在同一分钟内到达两次也完全正常。
业务处理幂等,有一个最丑但最稳的方式,就是在数据库里建唯一约束。比如支付流水表里,对“订单号+支付渠道交易号”加唯一索引,重复插入直接报DuplicateKeyException,你捕获到就当已成功处理。如果你的订单表本身就是状态机驱动,也可以直接用“CAS式”的更新语句:
UPDATE t_order SET status = 'PAID', pay_time = now(), alipay_trade_no = ? WHERE order_no = ? AND status = 'UNPAID';受影响行数为1才继续做扣库存、发货等动作,为0就说明要么订单状态已经更新过,要么订单不存在,直接返回success。这种方式不需要加分布式锁,也不需要引入额外的Redis锁,非常适合中小项目。
千万不要在回调里先查订单状态,再在代码里if判断,最后update。因为并发时两个请求可能同时都查到“未支付”,然后同时进入业务处理,造成重复扣库存。要么那句SQL把状态作为更新条件,要么给订单更新加行锁,两条路选一条。
4.3 同步跳转和异步通知双链路:到底信谁
前端在支付宝付款成功后,地址栏会重定向到return_url,你可以在页面上展示“支付成功”,让用户安心。但后端处理支付成功的真正触发点,只能依赖异步通知。这是支付宝官方推荐的模型,也是很多新手犯错的根源。
主动查询在这里又能救一次场。前端轮询接口时,发现订单状态还是未支付,后端立即主动调一次alipay.trade.query,即使异步通知因为网络原因没到,主动查询也能拿到结果。
测试的时候可以故意制造这类场景:后端先把notify_url故意配错,前端走完支付,然后看看轮询和主动查询能不能把订单状态修正。这样能验证你的兜底链路是否真的可靠。
4.4 沙箱与生产模式切换:公钥模式、证书模式与验签失败排查
支付宝开放平台支持“公钥模式”和“证书模式”两种方式。沙箱环境默认使用公钥模式,生产环境官方更推荐证书模式。证书模式需要在控制台下载三个文件:应用公钥证书、支付宝公钥证书、支付宝根证书。使用证书模式时,构造AlipayClient的方式和公钥模式不一样,要注意SDK里专门有CertAlipayRequest对象。
如果你的项目里既有公钥模式代码又切了证书模式,最常见的报错是“验签失败”。排查方向基本固定:
一是确认你用的公钥是不是支付宝公钥,不是应用公钥。很多人上传应用公钥后,把应用公钥自己也存了一份,最后验签当然通不过。
二是确认签名字符串编码。SDK默认UTF-8,如果你的环境不是UTF-8,验签也可能失败。这个要在构造AlipayClient时固定传UTF-8。
三是确认生产环境网关地址是否改到了openapi.alipay.com。忘了这步的代价很直接:线上请求全部打到沙箱,用户付款全部失败。
四是确认异步通知接收端的签名类型必须和后端AlipayClient里的签名类型一致,一个用RSA2一个用RSA,必定验签失败。
还有一个小技巧,验签失败时,把参数Map里所有key-value原样打印到日志,我遇到过好几次是参数在上级网关中被重新编码,导致空格变成加号,中文变成乱码,最终验签失败。这时候你要检查部署环境里的过滤器或SpringBoot的编码配置,保证CharacterEncodingFilter先于业务过滤器执行。
5. 上线前照着核对一遍:我的个人自查清单与验收建议
代码写完,联调通过,不代表可以立刻上生产。支付这个环节一旦出事就是真金白银的损失。我每次上线支付模块前都会过一遍下面的清单,这直接复制自踩坑后的总结。
5.1 配置与字段核对表
| 检查项 | 正确姿势 | 常见问题 |
|---|---|---|
| 支付宝网关 | 生产环境必须是 https://openapi.alipay.com/gateway.do | 沿用沙箱地址导致线上不可用 |
| 签名类型 | RSA2 | 配置成RSA会报验签错 |
| 应用私钥 | 只放在后端配置文件或密钥管理服务中 | 泄露到前端或GitHub |
| 支付宝公钥 | 与上传应用公钥后生成的一一对应 | 拿应用公钥当支付宝公钥用 |
| 金额单位 | 元,两位小数,BigDecimal转换 | double精度溢出产生0.0099 |
| out_trade_no | 全局唯一,建议含业务含义信息 | 一批高并发产生重复,被支付宝拦截 |
| 回调业务幂等 | 唯一索引或状态CAS更新 | 重复回调导致库存重复扣减 |
| notify_url | 公网可达的POST接口 | 本机联调时直接配置局域网址,生产不可达 |
5.2 上线前先做三轮演练
第一轮,正常链路。用沙箱账号真实支付一笔0.01元订单,确认异步通知在1秒内到达,订单状态正确更新,前端支付结果页正常展示。
第二轮,异常链路。在支付宝收银台付款完成但还没回到你的站点时,直接关闭浏览器。等几分钟后,看后端有没有收到异步通知,如果没有,手动触发主动查询接口,确认能补单。这模拟的是最频繁发生的“用户付完款就关了页面”场景。
第三轮,重复推送。后端回调里打印参数后,用Postman或脚本向你的notify_url重复发送同样的POST参数。如果订单状态没有被二次扣库存,说明你的幂等逻辑过关了。
做完这三轮,再考虑切生产配置灰度。我个人在实际项目里还会在回调入口加一个监控埋点,当支付宝通知连续失败5次时告警到钉钉或企业微信。这样即使我的轮询补单兜底做得好,也能及时知道回调链路本身出了问题,避免用户吐槽“明明付了钱待支付”才发现。
支付对接本身不复杂,复杂的是边界情况的处理。只要把通知验签、状态机、幂等这三件事做扎实,基本可以应对99%的线上场景。如果后续遇到诡异的问题,我建议先抓日志,看支付宝返回的code和subMsg,大多数答案都在那两个字段里,比看文档自己猜高效得多。