摘要在前后端分离的项目中集成支付功能是常见需求,本文将详细介绍如何在若依 (RuoYi) 框架中集成支付宝沙箱支付,包括环境搭建、代码实现和测试验证全流程。
支付环境准备
开发环境
- 后端:Spring Boot (若依框架)
- 前端:Vue.js
- 支付 SDK:alipay-sdk-java
- 开发工具:IntelliJ IDEA, VS Code
- JDK:1.8+
配置沙箱应用环境
打开支付宝开放平台,官网:支付宝开放平台,登录个人账户,然后点击控制台找到里面的沙箱。
沙箱环境是支付宝提供的测试环境,无需真实资金即可测试支付流程。
在沙箱应用页面,我们可以获取到:
- APPID:沙箱应用的唯一标识
- 支付宝网关:沙箱环境的网关地址 (支付宝 - 网上支付 安全快速!)
- 密钥
接入的时候选择自定义密钥,自定义密钥需要下载密钥工具。
- 下载支付宝密钥生成工具:官方下载地址
- 生成应用公钥和私钥
- 将应用公钥配置到沙箱应用中,然后获取支付宝公钥
选择你的设备类型下载。
下载完成后,生成密钥
生成应用公钥和应用私钥
将应用公钥配置到沙箱应用中,然后获取支付宝公钥
然后我们就接入了支付宝开放平台。
设置内网穿透环境
由于本地开发环境无法被支付宝服务器直接访问,需要使用内网穿透工具将本地服务暴露到公网。
-
注册并登录 NATAPP
访问 NATAPP 官网 注册账号 -
创建免费隧道
- 选择免费隧道
- 配置本地端口为后端服务端口 (如 8080)
-
下载并运行客户端
- 下载对应系统的客户端
- 使用命令启动:
natapp.exe -authtoken=你的隧道authtoken
- 启动成功后会得到一个公网访问地址
下载完成之后为下图的 natapp.exe
注意:不要直接双击启动。而是在目录上 输入 cmd。
在命令行窗口中输入如下命令
natapp.exe -authtoken=你的authtoken
启动成功如下图
这里的地址会因为网络环境的不同,每次启动该地址都会变化
其实这里就是将项目运行的地址代理到了内网穿透的地址,但是因为每次启动都会变化,所以还是建议使用localhost:8080
后端实现
一、支付宝支付集成流程概述
1. 整体架构
支付宝支付系统采用 “前端请求→后端处理→支付宝交互→异步通知” 的架构模式:
- 前端:发起支付请求(网页跳转或扫码)
- 后端:生成支付参数,调用支付宝 API
- 支付宝:处理支付请求,返回支付页面或二维码
- 异步通知:支付结果通过回调 URL 通知商户系统
2. 支付流程步骤
- 商户系统创建订单:用户下单后,系统生成唯一订单 ID
- 调用支付接口:后端根据订单信息调用支付宝支付 API
- 生成支付页面 / 二维码:支付宝返回 HTML 表单或二维码链接
- 用户支付:用户通过支付宝完成支付
- 异步通知处理:支付宝通过回调 URL 通知商户支付结果
- 订单状态更新:商户系统根据通知结果更新订单状态
二、核心实现代码解析
引入支付宝 SDK 依赖
在 pom.xml 中添加依赖:
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>
支付宝工具类(AlipayUtils)
核心功能包括:
- 初始化支付宝客户端
- 生成网页支付表单
- 生成二维码支付链接
- 验证回调签名
- 查询订单状态
关键代码示例:
@Component
public class AlipayUtils {
// 从配置文件读取参数
@Value("${alipay.appId}")
private String appId;
@Value("${alipay.merchantPrivateKey}")
private String merchantPrivateKey;
@Value("${alipay.alipayPublicKey}")
private String alipayPublicKey;
@Value("${alipay.gatewayUrl}")
private String gatewayUrl;
@Value("${alipay.charset}")
private String charset;
@Value("${alipay.signType}")
private String signType;
@Value("${alipay.notifyUrl}")
private String notifyUrl;
@Value("${alipay.returnUrl}")
private String returnUrl;
private AlipayClient alipayClient;
// 初始化AlipayClient
@PostConstruct
public void init() {
this.alipayClient = new DefaultAlipayClient(
gatewayUrl,
appId,
merchantPrivateKey,
"json",
charset,
alipayPublicKey,
signType
);
}
/**
* 生成网页支付表单
*/
public String createPagePayForm(Long orderId, String totalAmount, String subject, String body) throws AlipayApiException {
// 创建支付请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(notifyUrl);
request.setReturnUrl(returnUrl);
// 构建业务参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderId.toString());
bizContent.put("total_amount", formatAmount(totalAmount));
bizContent.put("subject", subject);
bizContent.put("body", body);
bizContent.put("timeout_express", "30m");
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toJSONString());
// 执行请求并返回支付表单
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if (!response.isSuccess()) {
throw new AlipayApiException("创建支付宝支付表单失败: " + response.getSubMsg());
}
return response.getBody();
}
/**
* 生成二维码支付链接
*/
public String createQrCode(Long orderId, String totalAmount, String subject, String body) throws AlipayApiException {
// 实现逻辑类似网页支付,使用AlipayTradePrecreateRequest
// ...
}
/**
* 查询订单状态
*/
public String queryOrderStatus(Long orderId) throws AlipayApiException {
// 使用AlipayTradeQueryRequest查询订单状态
// ...
}
/**
* 验证支付宝回调签名
*/
public boolean verifySignature(Map<String, String> params) {
// 实现签名验证
// ...
}
// 其他辅助方法:金额格式化、参数验证等
// ...
}
支付控制器(AlipayController)
核心接口包括:
- 网页支付接口(生成支付页面)
- 二维码支付接口(生成支付二维码)
- 异步通知接口(处理支付宝回调)
- 订单状态查询接口
关键代码示例:
@RestController
@RequestMapping("/alipay")
public class AlipayController {
@Autowired
private AlipayUtils alipayUtils;
@Autowired
private ISysOrderService orderService;
/**
* 网页支付接口
*/
@GetMapping(value = "/pagePay", produces = "text/html;charset=utf-8")
public String pagePay(@RequestParam Long orderId) {
try {
// 查询订单信息
SysOrder order = orderService.selectSysOrderById(orderId);
if (order == null) {
return buildErrorPage("订单不存在");
}
if (!"待支付".equals(order.getStatus())) {
return buildErrorPage("订单状态异常,当前状态: " + order.getStatus());
}
// 创建支付表单
return alipayUtils.createPagePayForm(
order.getId(),
order.getTotalPrice().toString(),
"订单支付: " + order.getProductId(),
"订单支付详情"
);
} catch (Exception e) {
e.printStackTrace();
return buildErrorPage("创建支付页面失败: " + e.getMessage());
}
}
/**
* 支付宝异步通知接口
* 用于接收支付宝支付结果通知,必须为POST请求
*/
@PostMapping("/notify")
public String notify(HttpServletRequest request) {
try {
// 处理通知参数
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 = alipayUtils.verifySignature(params);
if (!signVerified) {
return "fail";
}
// 处理支付结果
String tradeStatus = params.get("trade_status");
String outTradeNo = params.get("out_trade_no");
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
// 更新订单状态为已支付
Long orderId = Long.parseLong(outTradeNo);
SysOrder order = orderService.selectSysOrderById(orderId);
if (order != null && !"已支付".equals(order.getStatus())) {
order.setStatus("已支付");
order.setPayTime(new Date());
orderService.updateSysOrder(order);
}
}
return "success";
} catch (Exception e) {
e.printStackTrace();
return "fail";
}
}
/**
* 查询订单支付状态接口
*/
@GetMapping("/queryStatus")
public AjaxResult queryStatus(@RequestParam Long orderId) {
// 实现逻辑
// ...
}
// 其他辅助方法
// ...
}
订单实体类
package com.ruoyi.system.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 订单对象 sys_order
*
* @author ruoyi
* @date 2025-06-25
*/
public class SysOrder extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 订单ID */
private Long id;
/** 下单用户ID(外键) */
@Excel(name = "下单用户ID", readConverterExp = "外=键")
private Long userId;
/** 产品ID(根据类型关联对应表) */
@Excel(name = "产品ID", readConverterExp = "根=据类型关联对应表")
private Long productId;
/** 产品类型 */
@Excel(name = "产品类型")
private String productType;
/** 产品所属部门ID(商店) */
@Excel(name = "产品所属部门ID", readConverterExp = "商=店")
private Long deptId;
/** 购买数量 */
@Excel(name = "购买数量")
private Long quantity;
/** 单价 */
@Excel(name = "单价")
private BigDecimal unitPrice;
/** 总价(quantity * unit_price) */
@Excel(name = "总价", readConverterExp = "q=uantity,*=,u=nit_price")
private BigDecimal totalPrice;
/** 订单状态 */
@Excel(name = "订单状态")
private String status;
/** 支付时间 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date payTime;
public void setId(Long id)
{
this.id = id;
}
public Long getId()
{
return id;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public Long getUserId()
{
return userId;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public Long getProductId()
{
return productId;
}
public void setProductType(String productType)
{
this.productType = productType;
}
public String getProductType()
{
return productType;
}
public void setDeptId(Long deptId)
{
this.deptId = deptId;
}
public Long getDeptId()
{
return deptId;
}
public void setQuantity(Long quantity)
{
this.quantity = quantity;
}
public Long getQuantity()
{
return quantity;
}
public void setUnitPrice(BigDecimal unitPrice)
{
this.unitPrice = unitPrice;
}
public BigDecimal getUnitPrice()
{
return unitPrice;
}
public void setTotalPrice(BigDecimal totalPrice)
{
this.totalPrice = totalPrice;
}
public BigDecimal getTotalPrice()
{
return totalPrice;
}
public void setStatus(String status)
{
this.status = status;
}
public String getStatus()
{
return status;
}
public void setPayTime(Date payTime)
{
this.payTime = payTime;
}
public Date getPayTime()
{
return payTime;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("userId", getUserId())
.append("productId", getProductId())
.append("productType", getProductType())
.append("deptId", getDeptId())
.append("quantity", getQuantity())
.append("unitPrice", getUnitPrice())
.append("totalPrice", getTotalPrice())
.append("status", getStatus())
.append("payTime", getPayTime())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}
前端实现
在 Vue 组件中添加支付功能,以订单列表页面为例:
<template>
<div class="app-container cart-style">
<!-- 订单列表 -->
<el-table
v-loading="loading"
:data="orderList"
border
class="cart-table"
>
<!-- 表格列定义 -->
<!-- ... -->
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-credit-card"
@click="handlePay(scope.row)"
v-if="scope.row.status === '待支付'"
>支付</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: "PersonalOrderCart",
data() {
return {
// 数据定义
// ...
};
},
methods: {
// 其他方法
// ...
/**
* 处理支付
*/
handlePay(row) {
// 打开支付页面
const payUrl = `http://localhost:8080/alipay/pagePay?orderId=${row.id}`;
window.open(payUrl, '_blank');
}
}
};
</script>
配置文件
在application.yml
中配置支付宝相关参数:
alipay:
appId: 2021000148634241 # 你的沙箱APPID
merchantPrivateKey: 你的应用私钥 # 从密钥工具生成
alipayPublicKey: 你的支付宝公钥 # 从开放平台获取
notifyUrl: http://XXXXXX/alipay/notify # 内网穿透地址+通知接口
returnUrl: http://localhost:81/order # 支付完成后跳转的前端页面
signType: RSA2
charset: utf-8
gatewayUrl: https://XXXXXXXXXX/gateway.do # 沙箱网关
支付流程测试
-
启动服务
- 启动 NATAPP 内网穿透
- 启动若依后端服务 (8080 端口)
- 启动前端服务 (81 端口)
-
创建订单
在系统中创建一个待支付的订单 -
发起支付
- 在订单列表点击 "支付" 按钮
- 系统会打开支付宝支付页面
-
完成支付
- 使用沙箱环境提供的买家账号登录
- 输入沙箱支付密码完成支付
- 支付完成后会跳转到配置的 returnUrl 页面
-
验证结果
- 查看订单状态是否更新为 "已支付"
- 检查异步通知是否正常处理
来到支付宝开放平台,找到买家信息,登录进行支付
确认支付
支付成功
支付成功后,对比商家信息和买家信息中的账户余额
三、开发过程中遇到的主要问题及解决方案
1. JSON 解析错误(关键问题)
问题描述:
调用支付宝 API 时抛出 JSON解析错误
,堆栈跟踪显示错误发生在 AlipayUtils
类的 pagePay
方法中。
原因分析:
- 商品标题包含特殊字符(如双引号、换行符)
- 金额格式不符合要求(未保留两位小数)
- 重复设置
bizContent
参数导致 JSON 格式混乱
解决方案:
- 对商品标题进行清理,移除非法字符:
String cleanSubject = subject.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5,.-]", "");
- 严格格式化金额为两位小数:
BigDecimal bd = new BigDecimal(totalAmount); String formattedAmount = bd.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString();
- 确保
bizContent
只设置一次,避免重复覆盖
2. 签名验证失败
问题描述:
支付宝异步通知时签名验证始终失败,导致无法正确处理支付结果。
原因分析:
- 密钥配置错误(私钥与公钥不匹配)
- 签名参数包含非法字符(换行、空格、全角字符)
- 未使用正确的签名验证方法(如使用 RSA2 而非 RSA)
解决方案:
- 重新生成 RSA2 密钥对,确保配置文件中的密钥无换行、无空格
- 对签名参数进行特殊处理:
String sign = params.get("sign"); if (sign != null) { sign = sign.replaceAll("\\s+", "").replaceAll("\u3000", ""); params.put("sign", sign); }
- 使用
AlipaySignature.rsaCheckV1
方法进行签名验证
3. 网络请求失败(ERR_NAME_NOT_RESOLVED)
问题描述:
前端无法访问支付宝沙箱域名(如 excashier-sandbox.dl.alipaydev.com
),报错 ERR_NAME_NOT_RESOLVED
。
原因分析:
- 本地 DNS 服务器无法解析支付宝沙箱域名
- 网络防火墙屏蔽了支付宝相关域名
- 内网穿透工具配置不正确
解决方案:
- 手动配置 DNS 服务器为
223.5.5.5
(阿里云 DNS)或8.8.8.8
(Google DNS) - 检查网络环境,尝试切换网络(如使用手机热点)
- 确保内网穿透工具正常运行,回调 URL 可被公网访问
4. 跨域请求问题
问题描述:
前端页面尝试直接访问支付宝 API,报错 No 'Access-Control-Allow-Origin' header
。
原因分析:
- 前端错误地尝试直接调用支付宝 API
- 混淆了同步返回 URL 和异步通知 URL 的用途
解决方案:
- 前端仅负责发起支付请求,不直接与支付宝 API 交互
- 所有支付宝 API 调用均通过后端完成
- 正确配置同步返回 URL(用户支付后跳转的页面)和异步通知 URL(支付宝回调的接口)
四、测试与调试技巧
1. 沙箱环境配置
- 注册支付宝开放平台开发者账号
- 创建沙箱应用,获取 APPID
- 生成 RSA2 密钥对,配置应用公钥
- 使用沙箱账号进行测试(买家账号和卖家账号)
2. 调试工具
- 打印详细日志:在关键步骤添加日志输出,记录请求参数和响应结果
- 使用 Postman 测试接口:模拟支付宝异步通知,验证签名和业务逻辑
- 查看支付宝开放平台日志:在开发者后台查看 API 调用日志和错误详情
3. 常见测试场景
- 正常支付流程测试
- 重复支付测试
- 超时未支付测试
- 支付取消测试
- 部分退款测试