简介:支付宝当面付完整代码面向需要集成扫码支付功能的移动端或服务端开发者,是一套可直接参考落地的Java示例项目。压缩包共92个文件,以75个xml配置、9个java源码、2个properties配置为主体,辅以mvnw构建脚本、jar依赖和README说明,整体仅88KB,目录结构清晰,便于按功能模块检索学习。内容围绕当面付业务流程展开,覆盖前端SDK调用、订单创建、异步通知验签、沙箱环境测试等关键环节,也涉及OAuth2.0用户授权、数据加密与异常处理设计,能够帮助开发者理解二维码支付背后的技术框架和安全机制。同时,示例代码展示了如何通过合理日志排查问题、优化支付流程中的用户体验,并兼顾不同设备与系统版本的兼容性。目前已有992人学习下载,适合刚接触支付宝开放平台、希望缩短集成周期并减少排错成本的初中级开发者。
1. 解压“支付宝当面付完整代码.rar”,先看清这个工程在解决什么
解压“支付宝当面付完整代码.rar”,第一眼看到的是 demo2 这个标准 Spring Boot 工程,而不是一堆零散源码。pom.xml、mvnw、HELP.md 都在,src/main 和 src/test 分层清晰,说明它是一份能直接打开、能构建、能跑通整个支付链路的完整后端工程,不是网上那种随手截取的代码片段。当面付这个产品线有一个容易踩的认知差:它分为主扫和被扫两条路线,用户扫商家的二维码走 alipay.trade.precreate,商家用扫码枪扫用户的付款码走 alipay.trade.pay。这套代码的服务端部分,正是围绕这两条链路组织起来的。适合谁读:手里已有业务系统、需要用 Java 对接支付宝支付接口的服务端开发者,尤其是做线下收银、扫码点餐、自助终端这类场景的人。下面按“配置 → 下单 → 回调 → 沙箱”的顺序,把决定成败的细节拆开。
2. 密钥、依赖与配置:demo2 工程里最容易被忽略的三件事
2.1 RSA2 密钥三元组:先弄清谁签谁验
当面付的通信安全建立在 RSA2 签名上,密钥体系一共有三个角色:应用私钥、应用公钥、支付宝公钥。应用私钥只保存在你自己的服务器上,用来给请求参数签名;应用公钥上传到支付宝开放平台;支付宝公钥从开放平台获取,用来验证支付宝回调的签名。很多第一次接入的人会在这里栽跟头:拿着“应用公钥”去验支付宝的回调,验一万次都是失败的。
生成密钥对直接用 OpenSSL 即可:
openssl genrsa -out app_private_key.pem 2048 openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem第一条命令生成 2048 位的 RSA 私钥文件,第二条从私钥推导出对应的公钥文件。把 app_public_key.pem 的内容粘贴到开放平台“应用公钥”输入框,保存后平台会返回一串新的支付宝公钥,这串公钥才是验签时要用的。注意生成密钥时不要选 1024 位,RSA2 签名算法对密钥长度的最低要求是 2048 位,位数不够会直接导致下单接口报签名错误。私钥文件权限建议设为 600,不要提交到 Git 仓库。
2.2 Maven 依赖与工程结构辨识
demo2 里能看到 mvnw 和 mvnw.cmd,这是 Maven Wrapper,作用是固定构建工具版本。不管 CI 机器上装的是 Maven 3.6 还是 3.9,用 ./mvnw 构建都会下载指定版本,避免本地环境和线上构建结果不一致。引入支付宝官方 SDK 只需要在 pom.xml 加一个依赖:
<dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <!-- 按 Maven 中央仓库当前最新 release 替换,4.x 均支持当面付 --> <version>4.38.0.ALL</version> </dependency>这个依赖包把签名、验签、HTTP 请求、响应解析全部封装好了,不需要自己再用 HttpClient 手动拼参数。工程里 src/main 下是业务代码,src/test 下是测试代码,和 Spring Initializr 生成的标准结构一致。需要说明的是,支付宝 SDK 的版本更新比较频繁,官方会不定期修复签名边界问题,升级时重点看 release notes 里和 notify、rsaCheck 相关的变更,不要无脑升级。
2.3 application.yml 里到底该放哪些项
当面付的配置项不多,但每一项的来源都要能对上号:
alipay: app-id: 2021003122000000000 # 开放平台应用 AppID private-key: | -----BEGIN PRIVATE KEY----- MIIEvQIBADANBg... -----END PRIVATE KEY----- alipay-public-key: | -----BEGIN PUBLIC KEY----- MIIBIjANBg... -----END PUBLIC KEY----- gateway: https://openapi.alipay.com/gateway.do notify-url: https://api.example.com/pay/notify| 配置项 | 来源 | 说明 |
|---|---|---|
| app-id | 开放平台控制台 | 创建应用后自动生成,沙箱环境用沙箱应用的 AppID |
| private-key | 本地生成 | 应用私钥,只在服务端使用,用于请求签名 |
| alipay-public-key | 开放平台获取 | 上传应用公钥后平台返回,用于验签 |
| gateway | 官方固定 | 正式环境是 openapi.alipay.com,沙箱是 openapi.alipaydev.com |
| notify-url | 自己配置 | 支付宝异步通知的后端接口地址,必须是外网可访问的 URL |
private-key 和 alipay-public-key 用 YAML 的|块标量语法保留换行,这样处理 PEM 格式最稳妥。如果写成单行字符串,密钥内容里没有换行倒也能用,但复制粘贴时容易串入多余空格导致解析失败。
3. 从预下单到收银台:核心支付链路的代码落地
3.1 初始化 AlipayClient:所有调用的统一入口
SDK 的 DefaultAlipayClient 是线程安全的,整个应用只初始化一次,不要在每个请求里 new。把它声明成 Spring Bean 是最常见的做法:
@Configuration public class AlipayConfig { @Value("${alipay.app-id}") private String appId; @Value("${alipay.private-key}") private String privateKey; @Value("${alipay.alipay-public-key}") private String alipayPublicKey; @Value("${alipay.gateway}") private String gateway; @Bean public AlipayClient alipayClient() { return new DefaultAlipayClient( gateway, appId, privateKey, "json", "UTF-8", alipayPublicKey, "RSA2" ); } }构造函数里的七个参数依次是:网关地址、AppID、应用私钥、响应格式、字符集、支付宝公钥、签名类型。响应格式固定传json,字符集固定UTF-8,签名类型用RSA2。这里要特别注意,签名类型不是自己想传什么传什么,必须和开放平台上应用配置的签名算法一致,否则下单接口会返回“签名类型不匹配”。从工程实践看,新应用全部使用 RSA2,RSA1 已经处于淘汰边缘,新做的项目没有必要再兼容。
3.2 扫码支付预下单:alipay.trade.precreate
主扫模式的核心接口是 alipay.trade.precreate,服务端调用后支付宝返回一个二维码字符串,商家把字符串渲染成二维码,用户扫码后完成支付。下面是完整的预下单方法:
@Service public class PaymentService { private final AlipayClient alipayClient; private final String notifyUrl; public PaymentService(AlipayClient alipayClient, @Value("${alipay.notify-url}") String notifyUrl) { this.alipayClient = alipayClient; this.notifyUrl = notifyUrl; } public String preCreate(String outTradeNo, BigDecimal amount, String subject) throws AlipayApiException { AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); request.setNotifyUrl(notifyUrl); String bizContent = "{" + "\"out_trade_no\":\"" + outTradeNo + "\"," + "\"total_amount\":\"" + amount.setScale(2, RoundingMode.HALF_UP).toPlainString() + "\"," + "\"subject\":\"" + subject + "\"," + "\"timeout_express\":\"2h\"" + "}"; request.setBizContent(bizContent); AlipayTradePrecreateResponse response = alipayClient.execute(request); if (!response.isSuccess()) { throw new IllegalStateException("预下单失败: " + response.getSubMsg()); } return response.getQrCode(); } }这段代码里有几个细节值得注意。out_trade_no 是商户订单号,必须全局唯一,支付宝会用它做幂等键,同一个订单号重复调用预下单会返回同一笔交易。total_amount 的单位是“元”,不是“分”,金额格式化成两位小数后直接转字符串,不要用 BigDecimal 的 toString() 输出,否则可能出现0.010这种三位小数的串。timeout_express 传2h表示二维码两小时内有效,超过时间用户扫码会提示交易关闭。isSuccess() 判断的是业务码 code 是否为 10000,网络层面的异常由 AlipayApiException 抛出来,调用方需要区分“下单失败”和“网络异常”两种场景做不同的提示。
3.3 收银台轮询:二维码渲染、订单查询与关闭
拿到 qrCode 字符串后,后端通常把它返回给前端渲染成二维码图片。支付完成前,收银台页面需要不断询问后端订单状态,后端再调用 alipay.trade.query 向支付宝确认:
public String queryTradeStatus(String outTradeNo) throws AlipayApiException { AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); request.setBizContent("{\"out_trade_no\":\"" + outTradeNo + "\"}"); AlipayTradeQueryResponse response = alipayClient.execute(request); return response.getTradeStatus(); }接口返回的 tradeStatus 有三个值值得关注:WAIT_BUYER_PAY 表示等待付款,TRADE_SUCCESS 表示支付成功,TRADE_CLOSED 表示超时关闭或已退款。轮询策略建议间隔 3 秒,最长轮询 2 到 3 分钟,超过时间后不再轮询,改为提示用户稍后通过订单列表确认结果。轮询期间发现订单已支付,立即停止并刷新收银台状态。这里有个容易被忽略的优化点:轮询不是越频繁越好,支付宝端有接口频率限制,3 秒一次已经足够覆盖绝大多数收银场景。如果用户在别处已经完成支付,查询接口会立刻返回 TRADE_SUCCESS,不会产生副作用。
3.4 条码支付:被扫模式的关键差异
条码支付对应 alipay.trade.pay,用户打开支付宝付款码,商家用扫码枪读取 auth_code,后端带着这笔支付凭证直接提交:
public void pay(String outTradeNo, String authCode, BigDecimal amount, String subject) throws AlipayApiException { AlipayTradePayRequest request = new AlipayTradePayRequest(); request.setBizContent("{" + "\"out_trade_no\":\"" + outTradeNo + "\"," + "\"auth_code\":\"" + authCode + "\"," + "\"total_amount\":\"" + amount.setScale(2, RoundingMode.HALF_UP).toPlainString() + "\"," + "\"subject\":\"" + subject + "\"" + "}"); AlipayTradePayResponse response = alipayClient.execute(request); // 10000 表示支付成功,其余状态需要结合 result 判断 if (!response.isSuccess()) { throw new IllegalStateException("支付失败: " + response.getSubMsg()); } }主扫和被扫两种模式在工程上的差异集中在结果获取方式上:
| 维度 | 扫码支付(主扫) | 条码支付(被扫) |
|---|---|---|
| 请求接口 | alipay.trade.precreate | alipay.trade.pay |
| 支付凭证 | 服务端生成二维码 | 用户付款码 auth_code |
| 结果拿取 | 异步通知 + 轮询 | 同步返回 + 异步兜底 |
| 典型场景 | 商家立牌/台牌 | 超市收银、餐饮一体机 |
条码支付虽然同步返回结果,但网络抖动时响应可能丢失,这笔交易实际已经成功。所以被扫模式也要配置 notify_url,同步响应超时后靠异步通知兜底确认最终状态。demo2 工程如果被扫和主扫都做了,落库时建议以 trade_no(支付宝交易号)为业务唯一键,而不是只存商户订单号。
4. 支付宝回调的验签与幂等:别让通知重试打爆你的订单表
4.1 回调接口:不验签就写库等于裸奔
支付宝的异步通知是一个 POST 请求,带着支付结果参数打到 notify_url 上。如果不验签,任何知道这个地址的人都能伪造一笔成功支付的通知,把订单置为已支付然后白嫖商品。验签是回调处理的第一道关口:
@PostMapping("/pay/notify") public String notify(HttpServletRequest request) throws AlipayApiException { Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); } boolean signVerified = AlipaySignature.rsaCheckV1( params, alipayPublicKey, "UTF-8", "RSA2" ); if (!signVerified) { return "failure"; } // 后续业务处理... }rsaCheckV1 会从 params 里自动取出 sign 和 sign_type,然后用支付宝公钥对剩余参数做签名校验。这里需要注意,传入的 params 必须包含所有请求参数,不要在验签前手动移除 sign 字段以外的参数。验签要放在方法第一道逻辑,任何前置校验都不应该放在它前面。
4.2 四重校验:签名只是一张入场券
签名通过只能证明数据来源于支付宝,接下来还要校验业务字段是否和下单时一致。完整校验链路有四步:验签、校验 app_id、校验 out_trade_no 是否存在、校验 total_amount 是否等于订单金额。下面是完整的处理逻辑:
@PostMapping("/pay/notify") public String notify(HttpServletRequest request) throws AlipayApiException { Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); } if (!AlipaySignature.rsaCheckV1(params, alipayPublicKey, "UTF-8", "RSA2")) { return "failure"; } String appId = params.get("app_id"); String outTradeNo = params.get("out_trade_no"); String tradeStatus = params.get("trade_status"); String totalAmount = params.get("total_amount"); if (!configuredAppId.equals(appId)) { return "failure"; } if (!"TRADE_SUCCESS".equals(tradeStatus) && !"TRADE_FINISHED".equals(tradeStatus)) { return "success"; } Order order = orderService.getByOutTradeNo(outTradeNo); if (order == null || order.isPaid()) { return "success"; } if (order.getAmount().compareTo(new BigDecimal(totalAmount)) != 0) { // 金额不一致,记录告警,返回 failure 让支付宝重新通知 return "failure"; } orderService.markPaid(outTradeNo, params.get("trade_no")); return "success"; }trade_status 的取值里,TRADE_SUCCESS 和 TRADE_FINISHED 都表示交易成功,但语义上有差别:TRADE_SUCCESS 之后交易还可以退款,TRADE_FINISHED 表示交易已经完成且不可退。对普通商户来说,两个状态都按“支付成功”处理业务即可。total_amount 用 BigDecimal 比较而不是字符串 equals,避免0.01和0.010这种格式差异导致误判。
4.3 幂等处理与支付宝的重试语义
回调逻辑必须幂等,原因在于支付宝的通知不是只发一次。如果返回的响应不是字符串success,或者处理过程中抛了异常,支付宝会按递增时间间隔重试通知,最长持续若干小时。这意味着同一笔订单的支付成功通知可能收到多次,如果不做幂等控制,就可能出现重复发货、重复加积分之类的业务事故。
幂等落库建议用数据库状态机实现,而不是先查再更:
UPDATE orders SET status = 'PAID', trade_no = ?, paid_at = NOW() WHERE out_trade_no = ? AND status = 'UNPAID'这条 SQL 用WHERE status = 'UNPAID'做条件更新,受影响行数为 1 表示本次是首次支付完成,为 0 表示订单已经处理过,直接跳过发奖逻辑。相比“先 SELECT 再 UPDATE”,这种方式在并发场景下不会出现两个线程都查到 UNPAID 然后重复处理的问题。回调返回success的响应体必须是不带任何 HTML 标签的字符串,返回 200 状态码但响应体不是 success,支付宝同样会判定处理失败并重试。
5. 沙箱联调与上线前的五个高频坑
5.1 切沙箱只改一行配置
支付宝开放平台提供沙箱环境,最省事的是在配置层面做隔离:把 gateway 换成https://openapi.alipaydev.com/gateway.do,app_id 换成沙箱应用的 AppID,支付宝公钥也换成沙箱应用对应的那串。沙箱环境下有专用的买家账号,登录支付宝沙箱版 App 或者配套的模拟器完成“付款”动作,整个流程和真实环境完全一致。开发期验证回调时,本机无法接收外部请求,常见做法是用内网穿透工具把本地 port 映射出一个公网地址,把这个地址配成 notify-url。沙箱收到的每笔通知都会展示在开放平台沙箱控制台的“通知记录”里,排查收不到回调的问题比正式环境方便得多。
5.2 用 JUnit 回放一笔带签名的通知
每次在 App 里手动点支付再等回调,效率太低了。推荐在 src/test 里写一个回放工具,用沙箱的应用私钥手动签名,拼出和支付宝完全一致的请求报文:
@Test void replayNotify() throws Exception { String content = "app_id=2021003122000000000" + "&out_trade_no=DEMO20250101001" + "&trade_status=TRADE_SUCCESS" + "&total_amount=0.01" + "&trade_no=2025010122001000000000000000"; String sign = AlipaySignature.rsaSign( content, appPrivateKey, "UTF-8", "RSA2"); String notifyUrl = content + "&sign=" + URLEncoder.encode(sign, "UTF-8"); // 用 RestTemplate 或 MockMvc 把 notifyUrl 作为 POST body 发到 /pay/notify // 断言返回体为 success,订单状态变为 PAID }rsaSign 方法对 content 做签名,返回的 sign 需要 URL 编码后拼到报文末尾。把拼好的完整报文用 Postman 或 curl 发给本机接口,就能反复测试验签、幂等、金额校验这些逻辑,不用每次都在沙箱 App 里重新下一单。这套回放机制建议固化在工程里,回归测试时一键执行。
5.3 上线前逐条检查的五个坑
| 检查项 | 错误表现 | 正确做法 |
|---|---|---|
| 公钥配置 | 误用“应用公钥”验签 | 验签用“支付宝公钥”,在开放平台应用详情页获取 |
| 金额精度 | 0.01 元被存成 1 分或 0.010 | total_amount 单位是元,入库前统一用 BigDecimal.setScale(2) |
| 回调异常处理 | 业务代码抛异常,网关兜底返回 500 | notify 方法内部捕获全部异常,业务处理失败返回 failure |
| 私钥泄露 | 密钥上传到了 Git 仓库或交给前端 | 私钥只存在于服务端,.gitignore 排除 pem 和 yml 密钥项 |
| 通知超时 | 处理超时,支付宝重试导致重复操作 | 回调里只做状态更新和发奖,耗时操作丢进 MQ 异步处理 |
沙箱环境与正式环境的差异要单独确认清楚:沙箱不保证通知的到达时序,有时支付成功后立即查询还是 WAIT_BUYER_PAY;沙箱的扫码页面和真实 App 不完全一致,用模拟器测出来通过的二维码渲染逻辑,上线前务必用真机扫码走一遍。
本文还有配套的精品资源,点击获取