资讯动态

支付宝扫码支付对接实战:Spring Boot当面付预下单到回调避坑指南

发布时间:2026/10/14 8:35:46 来源:尧图企业网站定制
我刚接手一个线下门店收银系统的重构对方开口第一句就是“先给我一份支付宝扫码支付的示例源码能跑就行。”这种需求我前后接过不下十次。网上关于支付宝扫码支付的资料确实多但最大的问题不是找不到代码而是找了一堆代码却拼不出一条完整能跑的链路有的是旧版SDK写法有的是只贴下单不贴回调有的直接忽略了验签和查单。等你真去对接才会发现从下单到二维码展示从异步通知到主动查单再到退款和超时关单每个环节都有藏在角落里的细节。所以这篇就是把一份可以直接搬进项目的支付宝扫码支付示例源码拆开讲从技术选型、工程结构、核心代码、沙箱联调到避坑指南全部过一遍。不需要你是支付领域的老手只要你有Spring Boot基础、会看Java代码就能照着把整个扫码支付流程跑通。1. 扫码支付到底在做什么需求拆解与技术选型1.1 从用户扫码到资金入账完整链路是怎样的很多人第一次接触扫码支付以为就是“用户扫一下码钱到账了”。真要在代码层面跑通链路其实是一张清晰的流程图用户用支付宝App扫描动态二维码 → 手机端拉起支付宝收银台 → 用户确认支付并输入密码/指纹 → 支付宝扣款成功 → 后端收到支付宝异步通知或通过查单接口确认订单状态 → 更新本地订单状态 → 完成业务发货或核销。这里最关键的一点是动态二维码。很多线下门店墙上贴的收款码是固定码它只能收款不能携带订单信息。你没法知道这15块钱是哪一笔订单付的也没法针对某个订单发起退款、核销或对账。而扫码支付里的动态码是通过下单接口每次动态生成二维码里面绑定了唯一的商户订单号。用户扫码后完成支付后端就能准确知道是哪一单收到了钱。这套链路对应到支付宝开放平台的“当面付”产品核心接口就是预下单接口alipay.trade.precreate。它在业务上解决了几个核心问题订单与支付状态关联、支付结果可查询、支持退款、支持对账、支持超时关单。整个支付代码的核心其实就是下文会展开的四个接口调用预下单、查单、异步通知、退款。1.2 为什么选了当面付的扫码支付接口支付宝开放平台的支付产品线其实有好几个很多人一上来就犯迷糊。拿当面付来说下面还分两种场景。一种是“用户出示付款码商户用扫码枪/摄像头扫用户”对应接口是alipay.trade.pay。另一种是“商户展示二维码用户打开App扫一下”对应接口才是alipay.trade.precreate。我需要的是后者。原因很简单门店收银系统里用户面前的往往是收银屏幕上的动态二维码或者打印小票上的二维码用户用手机扫这种方式不需要额外买扫码枪也不需要收银员学习新操作顾客体验也自然。为什么不选电脑网站支付alipay.trade.page.pay因为电脑网站支付需要用户在浏览器里跳转到支付宝收银台适合PC网页端不适合用户拿手机直接扫二维码完成付款的线下场景。为什么不选手机网站支付alipay.trade.wap.pay手机网站支付适合H5环境下拉起收银台比如用户在手机浏览器里打开商城页面但在扫码场景里用户已经处于支付宝App的扫码界面再跳一次收银台反而画蛇添足。为什么不选付款码支付因为扫码枪和摄像头有硬件成本而且和“用户主动扫商户码”的体验正好相反。所以在需求是“收银屏幕显示二维码、用户支付宝扫码付款、后端能收到状态”这三个条件下alipay.trade.precreate就是唯一最合适的方案。1.3 技术栈选型为什么直接选官方SDK扫码支付本质上是构造请求参数、加签、发送HTTP请求、接收响应、验签的一组流程。这些工作官方SDK都已经封装好了。技术栈上我用的Java Spring BootMaven依赖直接引入官方SDK即可。当然你自己也能用HttpClient手写请求把参数按规则排序、拼接、加签、发送、反序列化再把支付宝的响应报文验签。但这里面容易错的地方不少拼接顺序错了签名失败、中文编码错了签名失败、时间格式错了接口报错。所以我的建议很直接能用官方SDK就不要自己造轮子省下的时间够你排查好几个业务Bug。支付功能的核心难点不在发送请求而在对账和异常处理。2. 工程结构设计与核心代码流程2.1 示例源码的工程模块怎么搭我习惯把支付相关代码单独放在一个包下面不要散落在业务代码各处方便后续维护和审计。以Spring Boot工程为例代码结构大致这样config支付宝配置类读取appId、私钥、公钥、网关等参数。controller支付相关接口的入口包括下单接口、异步通知接收接口。service支付核心业务逻辑下单、查单、退款都放这里。model/entity本地订单实体字段至少包含商户订单号、支付金额、订单状态。util二维码生成工具类。配置项建议放到application.yml里并通过Spring的ConfigurationProperties绑定alipay: app-id: xxxx app-private-key: xxxx alipay-public-key: xxxx gateway: https://openapi.alipay.com/gateway.do notify-url: https://你的域名/api/pay/notify私钥这种敏感配置不要写死在Git仓库里。真实的项目里我会用环境变量或配置中心动态注入本地开发环境才用配置文件兜底。网关地址在沙箱环境用沙箱网关正式环境用正式网关这个后面专门说。2.2 生成支付二维码预下单接口的完整说明下单接口是整条支付链路的第一个环节。先看代码public String createQrCode(String outTradeNo, BigDecimal amount, String subject) throws AlipayApiException { AlipayTradePrecreateRequest request new AlipayTradePrecreateRequest(); request.setNotifyUrl(alipayProperties.getNotifyUrl()); AlipayTradePrecreateModel model new AlipayTradePrecreateModel(); model.setOutTradeNo(outTradeNo); model.setTotalAmount(amount.toString()); model.setSubject(subject); model.setQrCodeTimeoutExpress(30m); request.setBizModel(model); AlipayTradePrecreateResponse response alipayClient.execute(request); if (response.isSuccess()) { return response.getQrCode(); } // 如果失败仔细看 subCode 和 subMsg throw new RuntimeException(下单失败: response.getSubCode() - response.getSubMsg()); }这里我要重点解释几个参数它们比编码本身更容易踩坑outTradeNo是商户订单号。这个订单号在你自己的系统里必须全局唯一。支付宝对重复的商户订单号有校验同一个订单号再次下单会报OUT_TRADE_NO_REPEATED。我的习惯是业务前缀 日期 雪花ID/自增ID比如PAY20250607153000123既好追溯又保证不重复。totalAmount必须用字符串格式的金额单位是元可以带两位小数。这里最大的坑来自Java的浮点数绝对不要用double直接参与金额计算0.1 0.2会出现精度误差。项目里金额全程用BigDecimal入库时以“分”为单位存储整型展示给用户或者传给支付宝时再转成“元”。qrCodeTimeoutExpress是二维码的有效期格式是数字加单位m表示分钟h表示小时d表示天。我一般把动态码有效期设置成30分钟。为什么不要设太长时间越短订单状态越明确。用户长时间不支付订单到期自动关闭不会出现一笔订单挂着一个月后突然支付成功的情况。下单成功之后response.getQrCode()拿到的是“二维码内容”实际上是一个https://qr.alipay.com/xxx的链接。这一步和很多人想的不一样支付宝并不会直接给你一张图片需要你自己把这个链接生成二维码图片才能展示给用户或者打印出来。用ZXing生成二维码图片的常见写法QRCodeWriter qrCodeWriter new QRCodeWriter(); MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); BitMatrix bitMatrix qrCodeWriter.encode(qrCodeUrl, BarcodeFormat.QR_CODE, 300, 300, hints); BufferedImage image MatrixToImageWriter.toBufferedImage(bitMatrix); // 再把 BufferedImage 转成 base64 输出给前端展示二维码图片的尺寸可以根据场景调整一般收银屏幕展示用300x300足够打印小票建议至少300x300太小的图在热敏纸上扫不出来。2.3 支付结果怎么拿主动查单与异步通知二维码展示出来了用户也扫码付了款接下来就是怎么确认“这笔订单确实支付成功”。支付宝给了两条路异步通知服务器主动推给你和主动查单你自己去问。两条路都要实现这是项目上线的底线要求。先说主动查单。它的典型场景是前端拿二维码去轮询后端“查到订单支付成功了吗”后端再去支付宝查单。查单接口是alipay.trade.query代码示例如下public boolean queryPayStatus(String outTradeNo) throws AlipayApiException { AlipayTradeQueryRequest request new AlipayTradeQueryRequest(); AlipayTradeQueryModel model new AlipayTradeQueryModel(); model.setOutTradeNo(outTradeNo); request.setBizModel(model); AlipayTradeQueryResponse response alipayClient.execute(request); if (response.isSuccess()) { String tradeStatus response.getTradeStatus(); return TRADE_SUCCESS.equals(tradeStatus); } return false; }查单返回的状态字段是tradeStatus重要的几个值包括WAIT_BUYER_PAY等待付款TRADE_CLOSED未付款交易超时关闭或者在付款前撤销TRADE_SUCCESS交易支付成功TRADE_FINISHED交易结束且不可退款。扫码支付场景里我们只需要把TRADE_SUCCESS作为“已支付”的判定条件。查单频率怎么定我的经验是前端的轮询接口每2到3秒查一次连续查10次左右如果还没支付就不要无限轮询下去给前端一个“订单已超时请重新下单”的提示。支付毕竟是异步行为用户可能中途切出去接了个电话轮询太久既浪费服务器资源也可能被支付宝限流。再说异步通知。支付宝在处理完一笔交易之后会向下单时传入的notifyUrl发送一个POST请求把支付结果推到你的服务器。这是支付成功最可靠、最及时的通知方式也是必须实现好的环节。接收异步通知的Controller示例PostMapping(/notify) public String alipayNotify(HttpServletRequest request) throws Exception { MapString, String params new HashMap(); MapString, String[] requestParams request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); } // 第一步验签 boolean signVerified AlipaySignature.rsaCheckV1(params, alipayProperties.getAlipayPublicKey(), 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); BigDecimal totalAmount new BigDecimal(params.get(total_amount)); // 从本地数据库查订单检查订单号存在、金额一致、状态未更新 PayOrder order payOrderMapper.selectByOutTradeNo(outTradeNo); if (order null || order.getAmount().compareTo(totalAmount) ! 0) { return failure; } // 第三步幂等更新订单状态 if (TRADE_SUCCESS.equals(tradeStatus)) { payOrderMapper.markPaid(outTradeNo, tradeNo); } return success; }验签用的alipayPublicKey是支付宝公钥不是你在开放平台上传的应用公钥很多人第一次就死在这里。用错公钥的典型现象就是验签一直失败、回调一直处理不了。trade_status的取值和查单接口一样但作为通知方支付宝官方文档明确说明TRADE_SUCCESS才是支付成功的最终状态。注意这里的TRADE_FINISHED什么时候出现通常会出现在部分退款或者交易完成后的场景扫码支付业务中一般不需要把它当成功状态去触发发货否则可能重复处理。最后这行return success;是支付宝异步通知机制的特殊约定必须原样返回字符串“success”不带引号、不加JSON格式、不要返回200之外的其他内容。如果你返回其他内容支付宝会认为通知失败然后按一定频率重试通知直到你返回“success”为止。重试次数多的时候这个流量还挺烦人所以收到通知后要尽快落库并响应。2.4 售后能力不能省退款与关闭订单很多偷懒的示例源码只写了下单和回调没写退款。但真实项目里退款功能早晚要用到。用户买错了、操作员扫错了、订单取消要原路退回这都是高频场景。退款接口调用public void refund(String outTradeNo, BigDecimal refundAmount, String refundReason) throws AlipayApiException { AlipayTradeRefundRequest request new AlipayTradeRefundRequest(); AlipayTradeRefundModel model new AlipayTradeRefundModel(); model.setOutTradeNo(outTradeNo); model.setRefundAmount(refundAmount.toString()); model.setRefundReason(refundReason); request.setBizModel(model); AlipayTradeRefundResponse response alipayClient.execute(request); if (!response.isSuccess()) { throw new RuntimeException(退款失败: response.getSubCode()); } }退款支持全额退款也支持部分退款。同一个订单可以多次部分退款累加金额不能超过订单总金额。退款金额如果传错了哪怕多退一分钱后期的对账都会多一笔差异单。所以我的习惯是退款之前先查一次原订单的已付金额和已退金额计算出可退金额再发起退款请求。关闭订单接口alipay.trade.close是针对未支付订单的。在某些场景里用户下单生成二维码之后一直没付款管理员手动取消这笔订单此时先用关单接口把支付宝侧的订单关掉再更新本地状态避免它后面被支付成功。不过如果订单设置了qrCodeTimeoutExpress超时时间超时后支付宝会自动关闭不需要我们额外调一次。3. 沙箱环境调试与密钥配置要点3.1 沙箱环境怎么申请和正式环境差在哪没有正式商户号之前想联调支付功能怎么办支付宝开放平台提供了沙箱Sandbox环境功能上模拟了真实支付通道你可以在里面完成从下单到支付成功的整套流程特别适合开发阶段。在开放平台控制台进入沙箱相关页面后你会拿到一套沙箱专用的AppID、沙箱网关地址、沙箱商户账号、买家账号等信息。沙箱和正式环境最大的区别一是在于网关地址不同二是在于沙箱支付不需要真实扣款用沙箱买家账号登录支付宝App沙箱版就可以模拟扫码付款。沙箱网关地址是有固定格式的具体可以在开放平台的沙箱控制台里找到。正式环境网关则是https://openapi.alipay.com/gateway.do。项目里我习惯把网关地址单独抽成配置项切换环境时只需要改一个配置值。必须提醒一点沙箱环境的AppID、密钥和正式环境完全隔离。经常有人拿着沙箱的AppID去调正式网关结果一直报权限错误排查半天才发现环境配串了。3.2 应用私钥、应用公钥、支付宝公钥到底各管什么密钥体系是支付宝开放平台的老大难很多人第一关就被绕晕。我用大白话说明白你需要做的是用密钥生成工具生成一对RSA密钥一个叫“应用私钥”一个叫“应用公钥”。应用私钥你自己保存好用来给请求参数签名应用公钥上传到支付宝开放平台。支付宝会用这把应用公钥去验证你的签名是否合法。与此同时支付宝自己也有密钥对。支付宝公钥是支付宝官方提供给你的你需要配置到本地用来验证支付宝推送给你的通知和响应是否真的来自支付宝。注意上传到平台的是应用公钥本地存的是应用私钥和支付宝公钥。如果把应用公钥和支付宝公钥搞混了最常见的表现就是请求能发出去但验签总是不对。Java里用的私钥需要是PKCS8格式的密钥生成时选择对应的格式。如果直接把PKCS1格式的私钥塞进去SDK会直接报格式错误。加签方式默认用RSA2对应的签名算法是SHA256WithRSA密钥长度建议2048位以上。当面付接口不需要AES密钥很多人以为要配三个密钥其实AES密钥只在部分加密报文接口里用得到。流程一句话总结应用私钥留在本地签名 → 支付宝用应用公钥验签支付宝用支付宝私钥签名 → 本地用支付宝公钥验签。两个方向四把钥匙各司其职。3.3 上线前必须检查的配置项清单正式环境联调和上线之前我会把下面这些项从头到尾过一遍任何一项有问题都会在线上环境带来实际资损或者体验事故。你可以把它当一份Checklist直接抄走产品签约状态当面付产品必须已签约很多“无权限”的报错都来自没签约。网关地址确认指向正式环境而不是沙箱网关。AppID确认是正式的AppID而不是沙箱的AppID。应用私钥格式确认是PKCS8且和上传的应用公钥是一对。支付宝公钥确认是开放平台上最新展示的支付宝公钥。回调地址必须是公网可达地址且在控制台已配置或代码已传。HTTPS线上强烈建议用HTTPS。回调地址如果走HTTP中间被篡改的风险高。服务器时间如果服务器时间和标准时间差太多签名时间戳校验会失败。金额单位确认全链路统一为“元”传给支付宝的金额是字符串形式。日志输出打印请求与响应完整报文尤其是错误码和错误消息方便线上排查。4. 我在实测里踩过的坑和排查方法4.1 下单报错“无权限”或“ISV权限不足”这是扫码支付对接中出现频率最高的问题。明明代码照着文档写一调用就报无权限。无权限的原因通常就三类第一当前Alipay账号没有签约“当面付”产品。去开放平台控制台的产品签约页面确认当面付为“已生效”状态。签约是需要审核的有些账号提交申请后审核还没通过也会报无权限。第二AppID配错了。代码里用的是别的应用的AppID或者一不小心把沙箱AppID用到了正式网关。一个简单有效的方法把AppID和网关地址单独打日志联调时第一时间看这两个值。第三应用未上线或未审核通过。如果应用只创建但没提交审核或者审核未通过接口权限也受限。遇到这类报错不要先怀疑代码先用控制台确认“当前这个AppID对应的应用是否已签约、是否已上线”往往一眼就能定位。4.2 异步通知收不到或者收到了却处理不了回调收不到是最让人头疼的问题因为不是实时报错而是程序一直等不到状态更新。排查的时候按顺序来第一步确认notifyUrl是否公网可达。最简单的方法是在你自己的手机上用浏览器访问一下这个地址能通就是能通。如果是在公司内网环境联调回调地址填了局域网IP支付宝当然访问不到这时可以用内网穿透工具临时暴露一个公网地址来调试。第二步确认路由没写错。Controller里接收POST请求的映射路径必须和notifyUrl一致且接口必须返回客户端一个纯文本的“success”。如果接口内部抛异常了支付宝拿不到“success”就会按照它的重试策略反复推送。第三步看日志。很多人回调接不到其实是收到了但验签失败导致代码直接返回了“failure”。日志里打出来signVerifiedfalse的时候去核对一下支付宝公钥配置是否真的正确。重复回调也是个经典问题。支付宝的通知机制是“不确认就重试”所以同一笔订单可能会收到多次通知。幂等处理办法很简单更新订单状态时加一个条件例如UPDATE pay_order SET status PAID WHERE out_trade_no ? AND status UNPAID受影响行数为0就说明之前已经处理过直接返回success即可。4.3 金额精度和重复订单号问题金额精度问题我用一句话给你定位看看你的金额字段是不是double或者float。如果是换成BigDecimal并且用字符串构造BigDecimal.valueOf(...)或者new BigDecimal(19.90)。数据库存储金额建议以“分”为单位的整型这样既不会丢精度也方便对账。重复订单号的问题常见于并发场景下两个请求同时生成了同一个订单号。解决方案是数据库给out_trade_no加唯一索引同时在代码里用分布式ID生成订单号。这个约束要数据库兜底不能只靠代码判断因为总有并发会漏掉。4.4 查单轮询频率怎么定才合理查单接口本身有频率限制。我的项目里是这样设计的前端拿到二维码后后端开一个定时任务每3秒查一次支付宝单个订单最多查10次即最多持续约30秒。如果30秒还没支付成功就直接告诉前端“二维码已失效请刷新重试”。同时把异步通知作为真正的成功判定标准查单只是辅助前端刷新状态不负责最终入账。这样既不会频繁打支付宝接口也不会出现用户明明付了款但前端一直显示待支付的情况。有一种极端情况异步通知因为网络问题一直没收到用户又确实付了款。这时候唯一的补救手段就是用查单接口主动核对状态。因此查单逻辑不能只在前端轮询里用还应该有一个后台定时扫描任务专门处理那些“本地状态未支付但超过15分钟”的订单去支付宝查一下真实状态。这个兜底逻辑很值钱它能把支付链路里的“假未支付”问题捞回来。4.5 高频问题速查表现象常见原因解决办法下单报无权限未签约当面付、AppID不对控制台确认签约状态和AppID请求报签名错误私钥格式不对、支付宝公钥配错确认PKCS8私钥上传应用公钥后再取支付宝公钥沙箱能通正式通不了网关或AppID用了沙箱的值改成正式网关和正式AppID异步通知收不到回调地址不可达、路由不一致内网穿透调试核对notifyUrl回调收到但处理失败验签失败或业务校验不过打日志检查支付宝公钥、订单金额同一订单重复回调未做幂等处理更新SQL加上状态判断条件支付成功但本地一直待支付异步通知丢失启动定时查单兜底任务金额算不对使用了浮点数运算金额统一用BigDecimal入库按分关于金额和入账核对的建议我再多说一句无论你即时链路做得多完善都必须有一个T1的对账任务。每天凌晨从支付宝下载前一日的账单和本地支付订单逐笔比对找出差异订单手动处理。这是我做支付几年下来最深刻的体会——支付功能稳定运行靠的从来不是某一个环节写得多完美而是有一套完整的、能自愈的兜底机制。最后把我自己的一个习惯分享给你生产环境联调阶段我会专门用一个线上测试商品下单后先自己扫一次码完整走通“下单→短信提示→入库→对账”全链路再放量测试。整个支付宝扫码支付对接过程里最耗费时间的从来不是写代码而是排环境问题。所有密钥、网关、回调、签名这些基础配置每一样都单独写进日志输出不依赖眼睛去对配置文件。等这套配置跑顺了剩下的业务开发就很快了。希望这篇内容能帮你少走几步弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑