标签:#Java #SpringBoot #支付宝沙箱 #支付开发 #后端实战
前言
在做电商、订单类业务系统时,支付功能是必不可少的模块。正式支付宝支付需要企业资质,个人开发者可以使用支付宝沙箱环境完成支付功能开发与调试,无需真实资金。本文基于 SpringBoot + Alipay Easy SDK 实现网页支付,包含环境准备、密钥配置、代码实现、异步回调、常见踩坑总结,完整可运行,我自己的项目中也使用这套方案完成对账支付模块开发。
重要概念区分
return_url:同步跳转地址,支付完成后浏览器页面跳转,不可信,仅做页面展示,不能作为修改订单状态的依据。notify_url:异步通知地址,支付宝服务端 POST 请求推送支付结果,这是修改订单状态的唯一可靠来源,必须公网可访问,localhost本地无法接收,需要内网穿透工具测试。
一、沙箱环境准备
1.1 进入支付宝沙箱控制台
支付宝开放平台地址:支付宝开放平台
- 登录支付宝账号,进入控制台,找到左下角【沙箱】进入沙箱应用页面。
- 获取核心参数:
- APPID:沙箱应用 ID
- 沙箱网关:
https://openapi.alipaydev.com/gateway.do(⚠️正式环境要去掉 dev) - 接口加签方式:选择系统默认密钥(快速测试),复制
应用私钥、支付宝公钥。
- 沙箱测试账号:页面可以获取买家账号密码;下载【支付宝沙箱版 APP】,只能用沙箱账号登录做支付测试,普通支付宝 APP 无法测试沙箱支付。
⚠️注意:不要泄露应用私钥,私钥用于我们程序签名;支付宝公钥用于程序验签。
二、项目依赖引入
Maven pom.xml 引入支付宝 Easy SDK,简化签名、验签逻辑,不需要手写 RSA 加密。
<!--支付宝Easy SDK--> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-easysdk</artifactId> <version>2.2.0</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>三、配置文件编写 application.yml
server: port: 8080 alipay: app-id: 你的沙箱APPID app-private-key: 你的应用私钥 alipay-public-key: 你的支付宝公钥 同步跳转地址,支付完成浏览器跳转页面,可以是前端页面 return-url: http://127.0.0.1:8080/pay/success 异步通知地址!!必须公网可访问,本地测试使用natapp内网穿透地址 notify-url: http://xxx.natappfree.cc/pay/notify四、Java 代码实现
4.1 读取配置实体类 AliPayProperties
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "alipay") public class AliPayProperties { private String appId; private String appPrivateKey; private String alipayPublicKey; private String returnUrl; private String notifyUrl; }4.2 支付宝 SDK 初始化配置类
项目启动时初始化一次 SDK 全局配置,沙箱网关 host 固定
openapi.alipaydev.com
import com.alipay.easysdk.factory.Factory; import com.alipay.easysdk.kernel.Config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; @Component public class AliPayInitConfig { @Autowired private AliPayProperties aliPayProperties; @PostConstruct public void initAlipay() { Config config = new Config(); //沙箱环境域名 config.gatewayHost = "openapi.alipaydev.com"; config.signType = "RSA2"; config.appId = aliPayProperties.getAppId(); config.merchantPrivateKey = aliPayProperties.getAppPrivateKey(); config.alipayPublicKey = aliPayProperties.getAlipayPublicKey(); config.notifyUrl = aliPayProperties.getNotifyUrl(); // 设置全局配置,只需要初始化一次 Factory.setOptions(config); } }4.3 Controller 支付接口
import com.alipay.easysdk.factory.Factory; import com.alipay.easysdk.payment.page.models.AlipayTradePagePayResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/pay") public class AliPayController { @Autowired private AliPayProperties aliPayProperties; /** 网页支付接口 @param subject 订单标题 @param outTradeNo 商户订单号(自己业务系统订单号,唯一) @param totalAmount 订单金额 @return 返回form表单html,前端直接渲染自动跳转支付宝收银台 */ @GetMapping("/goPay") public String goPay(String subject, String outTradeNo, String totalAmount, HttpServletResponse response) throws Exception { AlipayTradePagePayResponse resp = Factory.Payment.Page() .pay(subject, outTradeNo, totalAmount, aliPayProperties.getReturnUrl()); return resp.getBody(); } /** 同步跳转 return_url,仅页面展示,不能修改订单状态! */ @GetMapping("/success") public String payReturn(HttpServletRequest request){ log.info("支付同步跳转,参数:{}",request.getParameterMap()); // 这里只做页面展示,不要在这里更新数据库订单! return "<h1>支付页面跳转成功,请等待异步通知确认订单结果</h1>"; } /** 支付宝异步通知接口 POST!! 支付宝服务器主动调用,修改订单状态的核心接口 */ @PostMapping("/notify") public String payNotify(HttpServletRequest request) throws Exception{ log.info("收到支付宝异步通知"); //1. 获取所有回调参数 Map<String,String> paramMap = new HashMap<>(); Map<String,String[]> requestMap = request.getParameterMap(); for(String key : requestMap.keySet()){ paramMap.put(key,requestMap.get(key)[0]); } //2. SDK做签名校验,校验请求是否来自支付宝,防止伪造请求 boolean signVerified = Factory.Payment.Common().verifyNotify(paramMap); if(!signVerified){ log.error("异步通知验签失败,请求非法"); return "fail"; } //3. 判断交易状态,TRADE_SUCCESS代表支付成功 String tradeStatus = paramMap.get("trade_status"); if("TRADE_SUCCESS".equals(tradeStatus)){ //商户订单号,我们自己系统的订单号 String outTradeNo = paramMap.get("out_trade_no"); //支付宝交易号 String tradeNo = paramMap.get("trade_no"); String totalAmount = paramMap.get("total_amount"); log.info("订单:{},支付宝交易号:{},支付金额:{},支付成功",outTradeNo,tradeNo,totalAmount); // =========业务逻辑:修改数据库订单状态为已支付,执行对账逻辑========= // orderService.updateOrderSuccess(outTradeNo,tradeNo,totalAmount); // ⭐必须返回字符串 success,支付宝收到success才停止重试回调! return "success"; } return "fail"; } }4.4 支付交互时序图
下图展示了用户浏览器、SpringBoot 后端与支付宝沙箱服务器之间的完整支付交互流程,涵盖发起支付、同步跳转和异步通知三个关键环节:
sequenceDiagram participant U as 用户浏览器 participant B as SpringBoot 后端 participant A as 支付宝沙箱服务器 U->>B: 1. 访问 /pay/goPay(携带订单参数) B->>A: 2. 调用支付宝 SDK 发起支付请求 A-->>B: 3. 返回支付表单 HTML B-->>U: 4. 返回 form 表单页面 U->>A: 5. 自动跳转沙箱收银台并完成付款 A-->>U: 6. 同步跳转 return_url(仅页面展示) A->>B: 7. 异步通知 notify_url(POST,携带交易结果) B->>B: 8. 验签并更新订单状态 B-->>A: 9. 返回 success(停止重试通知)五、测试流程
- 启动 SpringBoot 项目,本地
notify_url需要使用内网穿透工具(natapp),把本地 8080 端口映射为公网地址,替换 yml 中 notify-url 配置,支付宝服务器需要公网访问这个接口才能推送回调通知。 - 浏览器访问接口示例:
http://127.0.0.1:8080/pay/goPay?subject=外协工单支付&outTradeNo=ORD202610020001&totalAmount=88.50 - 页面会自动渲染支付宝的 form 表单,跳转到沙箱收银台;
- 使用支付宝沙箱版 APP,登录沙箱买家账号扫码完成付款;
- 支付完成浏览器跳转到
/pay/success同步页面; - 支付宝服务器 POST 请求访问内网穿透的
/pay/notify,打印日志,更新订单业务。
注意:普通支付宝 APP 扫码沙箱二维码会报错,必须下载沙箱版本客户端!
六、高频踩坑总结(避坑)
- 异步通知收不到
notify_url 必须公网可访问,localhost、127.0.0.1 支付宝外网无法访问,必须内网穿透或者部署服务器;接口请求方式是 POST,不要写 GetMapping。处理完成必须返回
success,否则支付宝会持续重试通知(最多 8 次)。
- 验签失败
- 确认私钥、支付宝公钥复制完整,不要带多余换行空格;
- 沙箱网关不要写成正式网关,沙箱网关带
dev; - 复制密钥的时候不要复制
-----BEGIN PRIVATE KEY-----标记。
- 同步 return_url 收到参数,但是订单状态没更新
return_url 只是浏览器跳转,用户可以篡改参数,绝对不能用同步跳转去修改订单状态,必须以异步 notify_url 为准。
- 金额格式报错金额字符串,保留两位小数,例如
"0.01",不要传数字类型,不要传整数。 - 沙箱切换正式环境上线
- 修改网关地址为正式网关:
https://openapi.alipay.com/gateway.do(删除 dev) - 替换正式环境 appId、密钥;notify_url 改为线上公网域名地址。
七、拓展:项目业务结合
在我的外协加工跟催管理系统中,就是使用这套沙箱支付方案:工单对账完成之后调用支付接口,异步通知收到支付成功后,更新工单对账状态、保存支付宝交易流水,完成业务闭环。实际开发中还要考虑:订单幂等(防止异步通知多次调用重复更新订单)、订单超时关闭、退款接口等业务逻辑。
八、参考官方文档
支付宝开放平台文档:小程序文档 - 支付宝文档中心