支付宝支付集成全解析:从原理到实践,避坑指南

摘要在前后端分离的项目中集成支付功能是常见需求,本文将详细介绍如何在若依 (RuoYi) 框架中集成支付宝沙箱支付,包括环境搭建、代码实现和测试验证全流程。

支付环境准备

开发环境

  • 后端:Spring Boot (若依框架)
  • 前端:Vue.js
  • 支付 SDK:alipay-sdk-java
  • 开发工具:IntelliJ IDEA, VS Code
  • JDK:1.8+

配置沙箱应用环境

打开支付宝开放平台,官网:支付宝开放平台,登录个人账户,然后点击控制台找到里面的沙箱。

 沙箱环境是支付宝提供的测试环境,无需真实资金即可测试支付流程。

 在沙箱应用页面,我们可以获取到:

接入的时候选择自定义密钥,自定义密钥需要下载密钥工具。

  • 下载支付宝密钥生成工具:官方下载地址
  • 生成应用公钥和私钥
  • 将应用公钥配置到沙箱应用中,然后获取支付宝公钥

 

选择你的设备类型下载。

下载完成后,生成密钥

生成应用公钥和应用私钥

将应用公钥配置到沙箱应用中,然后获取支付宝公钥

然后我们就接入了支付宝开放平台。

设置内网穿透环境

由于本地开发环境无法被支付宝服务器直接访问,需要使用内网穿透工具将本地服务暴露到公网。

  1. 注册并登录 NATAPP
    访问 NATAPP 官网 注册账号

  2. 创建免费隧道

    • 选择免费隧道
    • 配置本地端口为后端服务端口 (如 8080) 
  3. 下载并运行客户端

    • 下载对应系统的客户端
    • 使用命令启动:natapp.exe -authtoken=你的隧道authtoken
    • 启动成功后会得到一个公网访问地址 

下载完成之后为下图的 natapp.exe

注意:不要直接双击启动。而是在目录上 输入 cmd。

在命令行窗口中输入如下命令

natapp.exe -authtoken=你的authtoken

启动成功如下图

这里的地址会因为网络环境的不同,每次启动该地址都会变化
其实这里就是将项目运行的地址代理到了内网穿透的地址,但是因为每次启动都会变化,所以还是建议使用localhost:8080

后端实现

一、支付宝支付集成流程概述

1. 整体架构

支付宝支付系统采用 “前端请求→后端处理→支付宝交互→异步通知” 的架构模式:

  • 前端:发起支付请求(网页跳转或扫码)
  • 后端:生成支付参数,调用支付宝 API
  • 支付宝:处理支付请求,返回支付页面或二维码
  • 异步通知:支付结果通过回调 URL 通知商户系统
2. 支付流程步骤
  1. 商户系统创建订单:用户下单后,系统生成唯一订单 ID
  2. 调用支付接口:后端根据订单信息调用支付宝支付 API
  3. 生成支付页面 / 二维码:支付宝返回 HTML 表单或二维码链接
  4. 用户支付:用户通过支付宝完成支付
  5. 异步通知处理:支付宝通过回调 URL 通知商户支付结果
  6. 订单状态更新:商户系统根据通知结果更新订单状态

二、核心实现代码解析

引入支付宝 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  # 沙箱网关

支付流程测试

  1. 启动服务

    • 启动 NATAPP 内网穿透
    • 启动若依后端服务 (8080 端口)
    • 启动前端服务 (81 端口)
  2. 创建订单
    在系统中创建一个待支付的订单

  3. 发起支付

    • 在订单列表点击 "支付" 按钮
    • 系统会打开支付宝支付页面
  4. 完成支付

    • 使用沙箱环境提供的买家账号登录
    • 输入沙箱支付密码完成支付
    • 支付完成后会跳转到配置的 returnUrl 页面
  5. 验证结果

    • 查看订单状态是否更新为 "已支付"
    • 检查异步通知是否正常处理

 

来到支付宝开放平台,找到买家信息,登录进行支付 

确认支付 

支付成功 

支付成功后,对比商家信息和买家信息中的账户余额

三、开发过程中遇到的主要问题及解决方案

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. 常见测试场景
  • 正常支付流程测试
  • 重复支付测试
  • 超时未支付测试
  • 支付取消测试
  • 部分退款测试

本文所借鉴文章支付宝沙箱支付(保姆级教程)-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值