支付宝支付实战全攻略

一、前言:为什么必须掌握支付宝支付?

在移动支付主导的商业生态中,支付宝作为国内头部支付解决方案,覆盖了超10亿用户与数千万商户。对于Java开发者而言,掌握支付宝支付的企业级实现方案,不仅是核心业务落地的必备技能,更是应对高并发、高可用支付场景的关键能力。

二、支付宝支付核心认知:先搞懂这些底层逻辑

2.1 支付宝支付的核心参与者与链路

支付宝支付本质是「多方协同的资金流转链路」,核心参与者包括:商户系统、支付宝开放平台、用户、银行/清算机构。其核心链路可概括为:商户发起支付请求→支付宝验证并引导用户付款→用户完成支付→支付宝同步/异步通知商户→商户更新订单状态

2.2 核心支付场景区分(易混淆点)

支付宝开放平台提供多种支付产品,核心场景需明确区分:

  • 电脑网站支付:适用于PC端网页,用户在电脑上完成支付(跳转支付宝网页);

  • 手机网站支付:适用于H5页面,用户在手机浏览器中完成支付;

  • APP支付:适用于商户自有APP,用户通过APP调用支付宝SDK完成支付;

  • 小程序支付:适用于支付宝小程序内的支付场景。

本文重点覆盖「电脑网站支付」「APP支付」「异步通知处理」「退款」四大核心场景,覆盖80%企业级支付需求。

2.3 支付宝支付的核心安全机制:签名与验签

支付宝支付的安全性核心依赖「RSA2非对称加密」,所有交互数据均需通过签名验证,防止数据被篡改。核心逻辑如下:

  1. 商户侧:使用商户私钥对请求参数进行签名,将签名与参数一同发送给支付宝;

  2. 支付宝侧:使用商户公钥(商户提前配置到开放平台)验证签名合法性,验证通过后处理请求;

  3. 支付宝响应/通知:使用支付宝私钥签名,商户侧使用支付宝公钥验证签名。

关键区别:RSA与RSA2的差异------RSA2(SHA256WithRSA)是支付宝推荐的签名算法,密钥长度2048位,安全性更高;RSA(SHA1WithRSA)为旧版算法,不推荐新系统使用。本文全程采用RSA2算法。

2.4 支付流程核心链路流程图

三、前置准备:支付宝开放平台配置与环境搭建

3.1 支付宝开放平台配置步骤(关键操作)

  1. 注册支付宝商户账号并完成实名认证(https://b.alipay.com/);

  2. 登录支付宝开放平台(https://open.alipay.com/),创建应用(选择「自研应用」);

  3. 应用审核通过后,开通「电脑网站支付」「APP支付」「退款」等所需功能;

  4. 配置密钥:生成RSA2密钥对(商户私钥、商户公钥),将商户公钥上传至开放平台,获取支付宝公钥;

    • 密钥生成工具:支付宝开放平台提供的「密钥生成工具」(支持Windows/Mac);

    • 注意:商户私钥需妥善保管,不可泄露;

  5. 配置异步通知地址(Notify Url)与同步回调地址(Return Url):

    • 异步通知地址:用于接收支付宝支付结果的异步通知(必须为公网可访问地址);

    • 同步回调地址:用户支付完成后跳转的商户页面(仅用于展示结果,不可作为订单状态更新依据);

  6. 记录核心配置参数:appId(应用ID)、商户私钥、支付宝公钥、 Notify Url、Return Url。

3.2 开发环境搭建(Maven+JDK17)

3.2.1 核心依赖(最新稳定版)
复制代码
<dependencies>
    <!-- Spring Boot 核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.2.5</version>
    </dependency>
    <!-- Lombok(@Slf4j) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
    <!-- 支付宝SDK(最新稳定版) -->
    <dependency>
        <groupId>com.alipay.sdk</groupId>
        <artifactId>alipay-sdk-java</artifactId>
        <version>4.38.0.ALL</version>
    </dependency>
    <!-- FastJSON2(JSON工具) -->
    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.49</version>
    </dependency>
    <!-- MyBatis-Plus(持久层框架) -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.3.0</version>
        <scope>runtime</scope>
    </dependency>
    <!-- Swagger3(接口文档) -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
    </dependency>
    <!-- Spring 工具类 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>6.1.6</version>
    </dependency>
    <!-- Google 集合工具 -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>33.2.1-jre</version>
    </dependency>
</dependencies>
3.2.2 配置文件(application.yml)
复制代码
spring:
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/alipay_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  # 服务器配置
  server:
    port: 8080

# MyBatis-Plus 配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.jam.demo.entity
  configuration:
    map-underscore-to-camel-case: true # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志

# 支付宝配置
alipay:
  app-id: 你的APPID
  merchant-private-key: 你的商户私钥(RSA2,PKCS8格式)
  alipay-public-key: 你的支付宝公钥
  notify-url: http://你的公网IP:8080/api/alipay/notify # 异步通知地址
  return-url: http://你的公网IP:8080/api/alipay/return # 同步回调地址
  gateway-url: https://openapi.alipay.com/gateway.do # 正式环境网关(沙箱环境:https://openapi.alipaydev.com/gateway.do)
  charset: UTF-8
  sign-type: RSA2 # 签名类型

# Swagger3 配置
springdoc:
  api-docs:
    path: /v3/api-docs
  swagger-ui:
    path: /swagger-ui.html
    operationsSorter: method
  packages-to-scan: com.jam.demo.controller
3.2.3 数据库设计(MySQL8.0)

创建支付订单表(alipay_order),用于存储支付相关信息:

复制代码
CREATE TABLE `alipay_order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_no` varchar(64) NOT NULL COMMENT '商户订单号(唯一)',
  `alipay_trade_no` varchar(64) DEFAULT NULL COMMENT '支付宝交易号',
  `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额(元)',
  `subject` varchar(255) NOT NULL COMMENT '订单标题',
  `body` varchar(500) DEFAULT NULL COMMENT '订单描述',
  `pay_status` tinyint NOT NULL DEFAULT 0 COMMENT '支付状态:0-未支付,1-已支付,2-已退款,3-支付失败',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `refund_amount` decimal(10,2) DEFAULT 0.00 COMMENT '退款金额(元)',
  `refund_time` datetime DEFAULT NULL COMMENT '退款时间',
  `notify_time` datetime DEFAULT NULL COMMENT '异步通知时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_alipay_trade_no` (`alipay_trade_no`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付宝支付订单表';

四、核心工具类实现:支付宝签名与配置封装

4.1 支付宝配置类(AlipayConfig)

封装支付宝核心配置参数,通过@ConfigurationProperties读取application.yml中的配置:

复制代码
package com.jam.demo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * 支付宝配置类
 * @author ken
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "alipay")
public class AlipayConfig {

    /**
     * 应用ID
     */
    private String appId;

    /**
     * 商户私钥(RSA2,PKCS8格式)
     */
    private String merchantPrivateKey;

    /**
     * 支付宝公钥
     */
    private String alipayPublicKey;

    /**
     * 异步通知地址
     */
    private String notifyUrl;

    /**
     * 同步回调地址
     */
    private String returnUrl;

    /**
     * 支付宝网关地址
     */
    private String gatewayUrl;

    /**
     * 编码格式
     */
    private String charset;

    /**
     * 签名类型
     */
    private String signType;
}

4.2 支付宝核心工具类(AlipayUtils)

封装支付宝SDK的核心操作,包括支付请求、退款请求、支付结果查询、签名验证等,严格遵循阿里巴巴Java开发手册规范:

复制代码
package com.jam.demo.util;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.jam.demo.config.AlipayConfig;
import com.jam.demo.entity.AlipayOrder;
import com.jam.demo.enums.PayStatusEnum;
import com.jam.demo.service.AlipayOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.Map;

/**
 * 支付宝核心工具类
 * 封装支付、退款、查询等核心操作
 * @author ken
 */
@Slf4j
@Component
public class AlipayUtils {

    @Autowired
    private AlipayConfig alipayConfig;

    @Autowired
    private AlipayOrderService alipayOrderService;

    /**
     * 支付宝客户端(单例)
     */
    private AlipayClient alipayClient;

    /**
     * 初始化支付宝客户端
     */
    @PostConstruct
    public void init() {
        // 校验配置参数
        StringUtils.hasText(alipayConfig.getAppId(), "支付宝APPID不能为空");
        StringUtils.hasText(alipayConfig.getMerchantPrivateKey(), "商户私钥不能为空");
        StringUtils.hasText(alipayConfig.getAlipayPublicKey(), "支付宝公钥不能为空");
        StringUtils.hasText(alipayConfig.getGatewayUrl(), "支付宝网关地址不能为空");
        StringUtils.hasText(alipayConfig.getCharset(), "编码格式不能为空");
        StringUtils.hasText(alipayConfig.getSignType(), "签名类型不能为空");

        // 创建支付宝客户端(DefaultAlipayClient为线程安全类,可单例复用)
        alipayClient = new DefaultAlipayClient(
                alipayConfig.getGatewayUrl(),
                alipayConfig.getAppId(),
                alipayConfig.getMerchantPrivateKey(),
                "json",
                alipayConfig.getCharset(),
                alipayConfig.getAlipayPublicKey(),
                alipayConfig.getSignType()
        );
        log.info("支付宝客户端初始化完成");
    }

    /**
     * 电脑网站支付(PC端)
     * @param orderNo 商户订单号
     * @param totalAmount 订单总金额(元)
     * @param subject 订单标题
     * @param body 订单描述
     * @return 支付跳转HTML(直接响应给前端,引导用户跳转支付宝)
     * @throws AlipayApiException 支付宝API调用异常
     */
    public String pagePay(String orderNo, BigDecimal totalAmount, String subject, String body) throws AlipayApiException {
        // 1. 校验参数
        StringUtils.hasText(orderNo, "商户订单号不能为空");
        if (ObjectUtils.isEmpty(totalAmount) || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("订单金额必须大于0");
        }
        StringUtils.hasText(subject, "订单标题不能为空");

        // 2. 构建支付请求
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        // 设置异步通知地址
        request.setNotifyUrl(alipayConfig.getNotifyUrl());
        // 设置同步回调地址
        request.setReturnUrl(alipayConfig.getReturnUrl());

        // 构建请求参数(JSON格式)
        StringBuilder bizContent = new StringBuilder();
        bizContent.append("{")
                .append("\"out_trade_no\":\"").append(orderNo).append("\",")
                .append("\"total_amount\":\"").append(totalAmount).append("\",")
                .append("\"subject\":\"").append(subject).append("\",")
                .append("\"body\":\"").append(StringUtils.isEmpty(body) ? "" : body).append("\",")
                .append("\"product_code\":\"FAST_INSTANT_TRADE_PAY\"") // 电脑网站支付产品码
                .append("}");
        request.setBizContent(bizContent.toString());

        // 3. 调用支付宝API,获取响应
        AlipayTradePagePayResponse response = alipayClient.pageExecute(request);

        // 4. 校验响应结果
        if (!response.isSuccess()) {
            log.error("电脑网站支付请求失败,订单号:{},错误码:{},错误信息:{}",
                    orderNo, response.getCode(), response.getMsg());
            throw new AlipayApiException("电脑网站支付请求失败:" + response.getMsg());
        }

        log.info("电脑网站支付请求成功,订单号:{},支付宝响应:{}", orderNo, response.getBody());
        // 返回支付跳转HTML(前端直接渲染该HTML,会自动跳转到支付宝支付页面)
        return response.getBody();
    }

    /**
     * APP支付(生成支付凭证,用于APP唤起支付宝)
     * @param orderNo 商户订单号
     * @param totalAmount 订单总金额(元)
     * @param subject 订单标题
     * @param body 订单描述
     * @return 支付凭证(JSON格式,APP端解析后唤起支付宝)
     * @throws AlipayApiException 支付宝API调用异常
     */
    public String appPay(String orderNo, BigDecimal totalAmount, String subject, String body) throws AlipayApiException {
        // 1. 校验参数
        StringUtils.hasText(orderNo, "商户订单号不能为空");
        if (ObjectUtils.isEmpty(totalAmount) || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("订单金额必须大于0");
        }
        StringUtils.hasText(subject, "订单标题不能为空");

        // 2. 构建支付请求
        com.alipay.api.request.AlipayTradeAppPayRequest request = new com.alipay.api.request.AlipayTradeAppPayRequest();
        // 设置异步通知地址(APP支付无同步回调地址)
        request.setNotifyUrl(alipayConfig.getNotifyUrl());

        // 构建请求参数(JSON格式)
        StringBuilder bizContent = new StringBuilder();
        bizContent.append("{")
                .append("\"out_trade_no\":\"").append(orderNo).append("\",")
                .append("\"total_amount\":\"").append(totalAmount).append("\",")
                .append("\"subject\":\"").append(subject).append("\",")
                .append("\"body\":\"").append(StringUtils.isEmpty(body) ? "" : body).append("\",")
                .append("\"product_code\":\"QUICK_MSECURITY_PAY\"") // APP支付产品码
                .append("}");
        request.setBizContent(bizContent.toString());

        // 3. 调用支付宝API,获取响应(APP支付使用sdkExecute方法)
        com.alipay.api.response.AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);

        // 4. 校验响应结果
        if (!response.isSuccess()) {
            log.error("APP支付请求失败,订单号:{},错误码:{},错误信息:{}",
                    orderNo, response.getCode(), response.getMsg());
            throw new AlipayApiException("APP支付请求失败:" + response.getMsg());
        }

        log.info("APP支付请求成功,订单号:{},支付凭证:{}", orderNo, response.getBody());
        // 返回支付凭证(APP端使用该凭证唤起支付宝)
        return response.getBody();
    }

    /**
     * 查询支付结果
     * @param orderNo 商户订单号(与alipayTradeNo二选一,优先使用orderNo)
     * @param alipayTradeNo 支付宝交易号
     * @return 支付宝交易查询响应
     * @throws AlipayApiException 支付宝API调用异常
     */
    public AlipayTradeQueryResponse queryPayResult(String orderNo, String alipayTradeNo) throws AlipayApiException {
        // 1. 校验参数(orderNo与alipayTradeNo至少存在一个)
        if (StringUtils.isEmpty(orderNo) && StringUtils.isEmpty(alipayTradeNo)) {
            throw new IllegalArgumentException("商户订单号与支付宝交易号至少需传入一个");
        }

        // 2. 构建查询请求
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        // 构建请求参数(JSON格式)
        StringBuilder bizContent = new StringBuilder();
        bizContent.append("{");
        if (StringUtils.hasText(orderNo)) {
            bizContent.append("\"out_trade_no\":\"").append(orderNo).append("\"");
        } else {
            bizContent.append("\"trade_no\":\"").append(alipayTradeNo).append("\"");
        }
        bizContent.append("}");
        request.setBizContent(bizContent.toString());

        // 3. 调用支付宝API,获取响应
        AlipayTradeQueryResponse response = alipayClient.execute(request);

        // 4. 校验响应结果
        if (!response.isSuccess()) {
            log.error("支付结果查询失败,订单号:{},支付宝交易号:{},错误码:{},错误信息:{}",
                    orderNo, alipayTradeNo, response.getCode(), response.getMsg());
            throw new AlipayApiException("支付结果查询失败:" + response.getMsg());
        }

        log.info("支付结果查询成功,订单号:{},支付宝交易号:{},交易状态:{}",
                orderNo, response.getTradeNo(), response.getTradeStatus());
        return response;
    }

    /**
     * 退款请求
     * @param orderNo 商户订单号
     * @param alipayTradeNo 支付宝交易号(与orderNo二选一,优先使用orderNo)
     * @param refundAmount 退款金额(元,必须小于等于支付金额)
     * @param refundReason 退款原因
     * @return 退款响应
     * @throws AlipayApiException 支付宝API调用异常
     */
    public AlipayTradeRefundResponse refund(String orderNo, String alipayTradeNo, BigDecimal refundAmount, String refundReason) throws AlipayApiException {
        // 1. 校验参数
        if (StringUtils.isEmpty(orderNo) && StringUtils.isEmpty(alipayTradeNo)) {
            throw new IllegalArgumentException("商户订单号与支付宝交易号至少需传入一个");
        }
        if (ObjectUtils.isEmpty(refundAmount) || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("退款金额必须大于0");
        }
        StringUtils.hasText(refundReason, "退款原因不能为空");

        // 2. 校验订单状态(先查询本地订单,确保订单已支付)
        AlipayOrder alipayOrder = null;
        if (StringUtils.hasText(orderNo)) {
            alipayOrder = alipayOrderService.getByOrderNo(orderNo);
        } else {
            alipayOrder = alipayOrderService.getByAlipayTradeNo(alipayTradeNo);
        }
        if (ObjectUtils.isEmpty(alipayOrder)) {
            throw new IllegalArgumentException("订单不存在");
        }
        if (!PayStatusEnum.PAY_SUCCESS.getCode().equals(alipayOrder.getPayStatus())) {
            throw new IllegalArgumentException("只有已支付的订单才能发起退款,当前订单状态:" + alipayOrder.getPayStatus());
        }
        // 校验退款金额是否超过支付金额
        if (refundAmount.compareTo(alipayOrder.getTotalAmount()) > 0) {
            throw new IllegalArgumentException("退款金额不能超过支付金额,支付金额:" + alipayOrder.getTotalAmount());
        }

        // 3. 构建退款请求
        AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
        // 构建请求参数(JSON格式)
        StringBuilder bizContent = new StringBuilder();
        bizContent.append("{")
                .append("\"refund_amount\":\"").append(refundAmount).append("\",")
                .append("\"refund_reason\":\"").append(refundReason).append("\",");
        if (StringUtils.hasText(orderNo)) {
            bizContent.append("\"out_trade_no\":\"").append(orderNo).append("\"");
        } else {
            bizContent.append("\"trade_no\":\"").append(alipayTradeNo).append("\"");
        }
        bizContent.append("}");
        request.setBizContent(bizContent.toString());

        // 4. 调用支付宝API,获取响应
        AlipayTradeRefundResponse response = alipayClient.execute(request);

        // 5. 校验响应结果
        if (!response.isSuccess()) {
            log.error("退款请求失败,订单号:{},支付宝交易号:{},错误码:{},错误信息:{}",
                    orderNo, alipayTradeNo, response.getCode(), response.getMsg());
            throw new AlipayApiException("退款请求失败:" + response.getMsg());
        }

        log.info("退款请求成功,订单号:{},支付宝交易号:{},退款金额:{}",
                orderNo, response.getTradeNo(), refundAmount);
        return response;
    }

    /**
     * 验证支付宝异步通知签名
     * @param params 支付宝异步通知参数(request.getParameterMap())
     * @return 签名是否合法
     */
    public boolean verifyNotifySign(Map<String, String[]> params) {
        try {
            // 将Map<String, String[]>转换为Map<String, String>(支付宝SDK要求)
            Map<String, String> paramMap = com.alipay.api.internal.util.AlipaySignature.getSignCheckContentV1(params);
            // 调用支付宝SDK验证签名
            return com.alipay.api.internal.util.AlipaySignature.rsaCheckV1(
                    paramMap,
                    alipayConfig.getAlipayPublicKey(),
                    alipayConfig.getCharset(),
                    alipayConfig.getSignType()
            );
        } catch (AlipayApiException e) {
            log.error("验证支付宝异步通知签名失败", e);
            return false;
        }
    }
}

4.3 枚举类(PayStatusEnum)

定义支付状态枚举,规范订单状态管理:

复制代码
package com.jam.demo.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 支付状态枚举
 * @author ken
 */
@Getter
@AllArgsConstructor
public enum PayStatusEnum {

    /**
     * 未支付
     */
    UN_PAY(0, "未支付"),

    /**
     * 已支付
     */
    PAY_SUCCESS(1, "已支付"),

    /**
     * 已退款
     */
    REFUNDED(2, "已退款"),

    /**
     * 支付失败
     */
    PAY_FAIL(3, "支付失败");

    /**
     * 状态码
     */
    private final Integer code;

    /**
     * 状态描述
     */
    private final String desc;

    /**
     * 根据状态码获取枚举
     * @param code 状态码
     * @return 枚举实例
     */
    public static PayStatusEnum getByCode(Integer code) {
        for (PayStatusEnum statusEnum : values()) {
            if (statusEnum.getCode().equals(code)) {
                return statusEnum;
            }
        }
        return null;
    }
}

五、持久层与服务层实现:基于MyBatis-Plus

5.1 实体类(AlipayOrder)

复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 支付宝支付订单实体
 * @author ken
 */
@Data
@TableName("alipay_order")
public class AlipayOrder {

    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 商户订单号(唯一)
     */
    private String orderNo;

    /**
     * 支付宝交易号
     */
    private String alipayTradeNo;

    /**
     * 订单总金额(元)
     */
    private BigDecimal totalAmount;

    /**
     * 订单标题
     */
    private String subject;

    /**
     * 订单描述
     */
    private String body;

    /**
     * 支付状态:0-未支付,1-已支付,2-已退款,3-支付失败
     */
    private Integer payStatus;

    /**
     * 支付时间
     */
    private LocalDateTime payTime;

    /**
     * 退款金额(元)
     */
    private BigDecimal refundAmount;

    /**
     * 退款时间
     */
    private LocalDateTime refundTime;

    /**
     * 异步通知时间
     */
    private LocalDateTime notifyTime;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

5.2 Mapper接口(AlipayOrderMapper)

基于MyBatis-Plus的BaseMapper,无需编写基础CRUD代码:

复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.AlipayOrder;
import org.springframework.stereotype.Repository;

/**
 * 支付宝订单Mapper
 * @author ken
 */
@Repository
public interface AlipayOrderMapper extends BaseMapper<AlipayOrder> {

    /**
     * 根据商户订单号查询订单
     * @param orderNo 商户订单号
     * @return 订单实体
     */
    AlipayOrder selectByOrderNo(String orderNo);

    /**
     * 根据支付宝交易号查询订单
     * @param alipayTradeNo 支付宝交易号
     * @return 订单实体
     */
    AlipayOrder selectByAlipayTradeNo(String alipayTradeNo);
}

5.3 Mapper XML文件(AlipayOrderMapper.xml)

放在resources/mapper目录下,编写自定义查询SQL:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.AlipayOrderMapper">

    <!-- 根据商户订单号查询订单 -->
    <select id="selectByOrderNo" resultType="com.jam.demo.entity.AlipayOrder">
        SELECT
            id,
            order_no,
            alipay_trade_no,
            total_amount,
            subject,
            body,
            pay_status,
            pay_time,
            refund_amount,
            refund_time,
            notify_time,
            create_time,
            update_time
        FROM
            alipay_order
        WHERE
            order_no = #{orderNo}
    </select>

    <!-- 根据支付宝交易号查询订单 -->
    <select id="selectByAlipayTradeNo" resultType="com.jam.demo.entity.AlipayOrder">
        SELECT
            id,
            order_no,
            alipay_trade_no,
            total_amount,
            subject,
            body,
            pay_status,
            pay_time,
            refund_amount,
            refund_time,
            notify_time,
            create_time,
            update_time
        FROM
            alipay_order
        WHERE
            alipay_trade_no = #{alipayTradeNo}
    </select>

</mapper>

5.4 服务层接口(AlipayOrderService)

复制代码
package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.AlipayOrder;
import com.jam.demo.enums.PayStatusEnum;

import java.math.BigDecimal;

/**
 * 支付宝订单服务接口
 * @author ken
 */
public interface AlipayOrderService extends IService<AlipayOrder> {

    /**
     * 创建支付订单
     * @param orderNo 商户订单号
     * @param totalAmount 订单总金额(元)
     * @param subject 订单标题
     * @param body 订单描述
     * @return 订单实体
     */
    AlipayOrder createOrder(String orderNo, BigDecimal totalAmount, String subject, String body);

    /**
     * 根据商户订单号查询订单
     * @param orderNo 商户订单号
     * @return 订单实体
     */
    AlipayOrder getByOrderNo(String orderNo);

    /**
     * 根据支付宝交易号查询订单
     * @param alipayTradeNo 支付宝交易号
     * @return 订单实体
     */
    AlipayOrder getByAlipayTradeNo(String alipayTradeNo);

    /**
     * 更新订单支付状态
     * @param orderNo 商户订单号
     * @param alipayTradeNo 支付宝交易号
     * @param payStatus 支付状态
     * @param payTime 支付时间
     * @return 是否更新成功
     */
    boolean updatePayStatus(String orderNo, String alipayTradeNo, PayStatusEnum payStatus, String payTime);

    /**
     * 更新订单退款状态
     * @param orderNo 商户订单号
     * @param refundAmount 退款金额(元)
     * @param refundTime 退款时间
     * @return 是否更新成功
     */
    boolean updateRefundStatus(String orderNo, BigDecimal refundAmount, String refundTime);
}

5.5 服务层实现类(AlipayOrderServiceImpl)

复制代码
package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.AlipayOrder;
import com.jam.demo.enums.PayStatusEnum;
import com.jam.demo.mapper.AlipayOrderMapper;
import com.jam.demo.service.AlipayOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 支付宝订单服务实现类
 * @author ken
 */
@Slf4j
@Service
public class AlipayOrderServiceImpl extends ServiceImpl<AlipayOrderMapper, AlipayOrder> implements AlipayOrderService {

    /**
     * 时间格式化器(支付宝返回的时间格式:yyyy-MM-dd HH:mm:ss)
     */
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    @Transactional(rollbackFor = Exception.class)
    public AlipayOrder createOrder(String orderNo, BigDecimal totalAmount, String subject, String body) {
        // 1. 校验参数
        StringUtils.hasText(orderNo, "商户订单号不能为空");
        if (ObjectUtils.isEmpty(totalAmount) || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("订单金额必须大于0");
        }
        StringUtils.hasText(subject, "订单标题不能为空");

        // 2. 校验订单号是否已存在
        AlipayOrder existOrder = getByOrderNo(orderNo);
        if (!ObjectUtils.isEmpty(existOrder)) {
            throw new IllegalArgumentException("商户订单号已存在:" + orderNo);
        }

        // 3. 构建订单实体
        AlipayOrder alipayOrder = new AlipayOrder();
        alipayOrder.setOrderNo(orderNo);
        alipayOrder.setTotalAmount(totalAmount);
        alipayOrder.setSubject(subject);
        alipayOrder.setBody(body);
        alipayOrder.setPayStatus(PayStatusEnum.UN_PAY.getCode()); // 初始状态:未支付
        alipayOrder.setCreateTime(LocalDateTime.now());
        alipayOrder.setUpdateTime(LocalDateTime.now());

        // 4. 保存订单
        boolean saveSuccess = save(alipayOrder);
        if (!saveSuccess) {
            log.error("创建支付订单失败,订单号:{}", orderNo);
            throw new RuntimeException("创建支付订单失败");
        }

        log.info("创建支付订单成功,订单号:{}", orderNo);
        return alipayOrder;
    }

    @Override
    public AlipayOrder getByOrderNo(String orderNo) {
        StringUtils.hasText(orderNo, "商户订单号不能为空");
        return baseMapper.selectByOrderNo(orderNo);
    }

    @Override
    public AlipayOrder getByAlipayTradeNo(String alipayTradeNo) {
        StringUtils.hasText(alipayTradeNo, "支付宝交易号不能为空");
        return baseMapper.selectByAlipayTradeNo(alipayTradeNo);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updatePayStatus(String orderNo, String alipayTradeNo, PayStatusEnum payStatus, String payTime) {
        // 1. 校验参数
        StringUtils.hasText(orderNo, "商户订单号不能为空");
        StringUtils.hasText(alipayTradeNo, "支付宝交易号不能为空");
        ObjectUtils.isEmpty(payStatus, "支付状态不能为空");
        StringUtils.hasText(payTime, "支付时间不能为空");

        // 2. 查询订单
        AlipayOrder alipayOrder = getByOrderNo(orderNo);
        if (ObjectUtils.isEmpty(alipayOrder)) {
            log.error("更新支付状态失败,订单不存在,订单号:{}", orderNo);
            return false;
        }

        // 3. 更新订单信息
        alipayOrder.setAlipayTradeNo(alipayTradeNo);
        alipayOrder.setPayStatus(payStatus.getCode());
        alipayOrder.setPayTime(LocalDateTime.parse(payTime, DATE_TIME_FORMATTER));
        alipayOrder.setNotifyTime(LocalDateTime.now());
        alipayOrder.setUpdateTime(LocalDateTime.now());

        // 4. 保存更新
        boolean updateSuccess = updateById(alipayOrder);
        if (updateSuccess) {
            log.info("更新订单支付状态成功,订单号:{},支付状态:{}", orderNo, payStatus.getDesc());
        } else {
            log.error("更新订单支付状态失败,订单号:{}", orderNo);
        }
        return updateSuccess;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateRefundStatus(String orderNo, BigDecimal refundAmount, String refundTime) {
        // 1. 校验参数
        StringUtils.hasText(orderNo, "商户订单号不能为空");
        if (ObjectUtils.isEmpty(refundAmount) || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("退款金额必须大于0");
        }
        StringUtils.hasText(refundTime, "退款时间不能为空");

        // 2. 查询订单
        AlipayOrder alipayOrder = getByOrderNo(orderNo);
        if (ObjectUtils.isEmpty(alipayOrder)) {
            log.error("更新退款状态失败,订单不存在,订单号:{}", orderNo);
            return false;
        }

        // 3. 更新订单信息
        alipayOrder.setRefundAmount(refundAmount);
        alipayOrder.setRefundTime(LocalDateTime.parse(refundTime, DATE_TIME_FORMATTER));
        alipayOrder.setPayStatus(PayStatusEnum.REFUNDED.getCode()); // 状态更新为:已退款
        alipayOrder.setUpdateTime(LocalDateTime.now());

        // 4. 保存更新
        boolean updateSuccess = updateById(alipayOrder);
        if (updateSuccess) {
            log.info("更新订单退款状态成功,订单号:{},退款金额:{}", orderNo, refundAmount);
        } else {
            log.error("更新订单退款状态失败,订单号:{}", orderNo);
        }
        return updateSuccess;
    }
}

六、控制器实现:RESTful接口+Swagger3

6.1 全局异常处理器(GlobalExceptionHandler)

统一处理接口异常,返回标准化响应:

复制代码
package com.jam.demo.controller;

import com.alipay.api.AlipayApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常处理器
 * @author ken
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理非法参数异常
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleIllegalArgumentException(IllegalArgumentException e) {
        log.error("非法参数异常:", e);
        Map<String, Object> response = new HashMap<>(2);
        response.put("code", HttpStatus.BAD_REQUEST.value());
        response.put("message", e.getMessage());
        return response;
    }

    /**
     * 处理支付宝API异常
     */
    @ExceptionHandler(AlipayApiException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> handleAlipayApiException(AlipayApiException e) {
        log.error("支付宝API调用异常:", e);
        Map<String, Object> response = new HashMap<>(3);
        response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.put("message", e.getMessage());
        response.put("alipayErrorCode", e.getErrCode());
        return response;
    }

    /**
     * 处理通用异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> handleException(Exception e) {
        log.error("系统异常:", e);
        Map<String, Object> response = new HashMap<>(2);
        response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.put("message", "系统异常,请联系管理员");
        return response;
    }
}

6.2 支付控制器(AlipayController)

提供RESTful接口,包含创建订单、电脑网站支付、APP支付、同步回调、异步通知、退款等功能,添加Swagger3注解:

复制代码
package com.jam.demo.controller;

import com.alipay.api.AlipayApiException;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.jam.demo.entity.AlipayOrder;
import com.jam.demo.enums.PayStatusEnum;
import com.jam.demo.service.AlipayOrderService;
import com.jam.demo.util.AlipayUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.Map;

/**
 * 支付宝支付控制器
 * @author ken
 */
@Slf4j
@RestController
@RequestMapping("/api/alipay")
@Tag(name = "支付宝支付接口", description = "包含创建订单、支付、回调、退款等功能")
public class AlipayController {

    @Autowired
    private AlipayOrderService alipayOrderService;

    @Autowired
    private AlipayUtils alipayUtils;

    /**
     * 创建支付订单
     */
    @PostMapping("/createOrder")
    @Operation(summary = "创建支付订单", description = "生成支付宝支付订单,返回订单信息")
    @ApiResponse(responseCode = "200", description = "创建成功", content = @Content(schema = @Schema(implementation = AlipayOrder.class)))
    public Map<String, Object> createOrder(
            @Parameter(description = "商户订单号(唯一)", required = true) @RequestParam String orderNo,
            @Parameter(description = "订单总金额(元)", required = true) @RequestParam BigDecimal totalAmount,
            @Parameter(description = "订单标题", required = true) @RequestParam String subject,
            @Parameter(description = "订单描述") @RequestParam(required = false) String body
    ) {
        AlipayOrder alipayOrder = alipayOrderService.createOrder(orderNo, totalAmount, subject, body);
        Map<String, Object> result = Map.of(
                "code", 200,
                "message", "创建订单成功",
                "data", alipayOrder
        );
        return result;
    }

    /**
     * 电脑网站支付(PC端)
     * 直接返回支付宝支付跳转HTML,前端渲染后自动跳转
     */
    @GetMapping("/pagePay")
    @Operation(summary = "电脑网站支付", description = "生成支付宝PC端支付跳转页面,前端直接渲染")
    @ApiResponse(responseCode = "200", description = "生成支付页面成功", content = @Content(schema = @Schema(type = "string", example = "<!DOCTYPE html>...")))
    public String pagePay(
            @Parameter(description = "商户订单号", required = true) @RequestParam String orderNo,
            HttpServletResponse response
    ) throws AlipayApiException {
        // 1. 查询订单
        AlipayOrder alipayOrder = alipayOrderService.getByOrderNo(orderNo);
        if (alipayOrder == null) {
            throw new IllegalArgumentException("订单不存在,订单号:" + orderNo);
        }
        // 2. 校验订单状态(必须为未支付)
        if (!PayStatusEnum.UN_PAY.getCode().equals(alipayOrder.getPayStatus())) {
            throw new IllegalArgumentException("订单状态异常,当前状态:" + PayStatusEnum.getByCode(alipayOrder.getPayStatus()).getDesc());
        }
        // 3. 调用支付宝API,生成支付跳转HTML
        return alipayUtils.pagePay(orderNo, alipayOrder.getTotalAmount(), alipayOrder.getSubject(), alipayOrder.getBody());
    }

    /**
     * APP支付
     * 返回支付凭证,APP端解析后唤起支付宝
     */
    @GetMapping("/appPay")
    @Operation(summary = "APP支付", description = "生成APP支付凭证,APP端使用该凭证唤起支付宝")
    @ApiResponse(responseCode = "200", description = "生成支付凭证成功", content = @Content(schema = @Schema(type = "string", example = "alipay_sdk=alipay-sdk-java&app_id=xxx&biz_content=xxx")))
    public Map<String, Object> appPay(
            @Parameter(description = "商户订单号", required = true) @RequestParam String orderNo
    ) throws AlipayApiException {
        // 1. 查询订单
        AlipayOrder alipayOrder = alipayOrderService.getByOrderNo(orderNo);
        if (alipayOrder == null) {
            throw new IllegalArgumentException("订单不存在,订单号:" + orderNo);
        }
        // 2. 校验订单状态(必须为未支付)
        if (!PayStatusEnum.UN_PAY.getCode().equals(alipayOrder.getPayStatus())) {
            throw new IllegalArgumentException("订单状态异常,当前状态:" + PayStatusEnum.getByCode(alipayOrder.getPayStatus()).getDesc());
        }
        // 3. 调用支付宝API,生成支付凭证
        String payParam = alipayUtils.appPay(orderNo, alipayOrder.getTotalAmount(), alipayOrder.getSubject(), alipayOrder.getBody());
        return Map.of(
                "code", 200,
                "message", "生成支付凭证成功",
                "data", payParam
        );
    }

    /**
     * 同步回调(Return Url)
     * 注意:同步回调仅用于展示支付结果,不可作为订单状态更新的依据(用户可能直接关闭页面,导致同步回调不触发)
     */
    @GetMapping("/return")
    @Operation(summary = "同步回调", description = "用户支付完成后跳转的页面,仅用于展示结果")
    public String returnUrl(HttpServletRequest request) {
        // 1. 获取支付宝返回的参数
        Map<String, String[]> params = request.getParameterMap();
        log.info("支付宝同步回调参数:{}", params);

        // 2. 验证签名(可选,建议验证)
        boolean signValid = alipayUtils.verifyNotifySign(params);
        if (!signValid) {
            log.error("同步回调签名验证失败,参数:{}", params);
            return "<h1>支付结果验证失败</h1>";
        }

        // 3. 获取核心参数
        String orderNo = request.getParameter("out_trade_no");
        String tradeStatus = request.getParameter("trade_status");

        // 4. 引导用户查询订单状态(最终以商户系统订单状态为准)
        return String.format(
                "<h1>支付请求已提交</h1>" +
                "<p>商户订单号:%s</p>" +
                "<p>支付状态:%s</p>" +
                "<p>请以商户系统订单状态为准,<a href='/api/alipay/query?orderNo=%s'>点击查询最新状态</a></p>",
                orderNo, tradeStatus, orderNo
        );
    }

    /**
     * 异步通知(Notify Url)
     * 核心:支付宝支付结果的可靠通知,必须验证签名+校验参数,更新订单状态
     * 注意:支付宝会多次重试通知,需保证接口幂等性
     */
    @PostMapping("/notify")
    @Operation(summary = "异步通知", description = "支付宝支付结果的可靠通知,用于更新订单状态")
    @ApiResponse(responseCode = "200", description = "通知处理成功,返回success", content = @Content(schema = @Schema(type = "string", example = "success")))
    public String notifyUrl(HttpServletRequest request) {
        try {
            // 1. 获取支付宝通知参数
            Map<String, String[]> params = request.getParameterMap();
            log.info("支付宝异步通知参数:{}", params);

            // 2. 验证签名(必做,防止数据篡改)
            boolean signValid = alipayUtils.verifyNotifySign(params);
            if (!signValid) {
                log.error("异步通知签名验证失败,参数:{}", params);
                return "fail"; // 返回fail,支付宝会重试
            }

            // 3. 提取核心参数并校验
            String outTradeNo = request.getParameter("out_trade_no"); // 商户订单号
            String tradeNo = request.getParameter("trade_no"); // 支付宝交易号
            String tradeStatus = request.getParameter("trade_status"); // 交易状态(TRADE_SUCCESS表示支付成功)
            String totalAmount = request.getParameter("total_amount"); // 支付金额
            String gmtPayment = request.getParameter("gmt_payment"); // 支付时间
            String sign = request.getParameter("sign"); // 签名(已验证)

            // 3.1 核心参数非空校验
            if (org.springframework.util.StringUtils.isEmpty(outTradeNo) 
                || org.springframework.util.StringUtils.isEmpty(tradeNo)
                || org.springframework.util.StringUtils.isEmpty(tradeStatus)
                || org.springframework.util.StringUtils.isEmpty(totalAmount)
                || org.springframework.util.StringUtils.isEmpty(gmtPayment)) {
                log.error("异步通知参数不完整,缺少核心参数,参数:{}", params);
                return "fail";
            }

            // 4. 校验订单存在性
            AlipayOrder alipayOrder = alipayOrderService.getByOrderNo(outTradeNo);
            if (alipayOrder == null) {
                log.error("异步通知处理失败,订单不存在,订单号:{}", outTradeNo);
                return "fail";
            }

            // 5. 校验金额一致性(防止支付金额被篡改)
            if (new BigDecimal(totalAmount).compareTo(alipayOrder.getTotalAmount()) != 0) {
                log.error("异步通知金额不一致,订单号:{},商户系统金额:{},支付宝通知金额:{}",
                        outTradeNo, alipayOrder.getTotalAmount(), totalAmount);
                return "fail";
            }

            // 6. 处理支付状态(仅处理TRADE_SUCCESS状态,保证幂等性)
            if ("TRADE_SUCCESS".equals(tradeStatus)) {
                // 6.1 校验当前订单状态(仅未支付订单需要更新)
                if (PayStatusEnum.UN_PAY.getCode().equals(alipayOrder.getPayStatus())) {
                    // 6.2 更新订单支付状态
                    boolean updateSuccess = alipayOrderService.updatePayStatus(
                            outTradeNo, tradeNo, PayStatusEnum.PAY_SUCCESS, gmtPayment
                    );
                    if (!updateSuccess) {
                        log.error("异步通知更新订单状态失败,订单号:{}", outTradeNo);
                        return "fail";
                    }
                    log.info("异步通知处理成功,订单号:{},支付宝交易号:{},支付金额:{}",
                            outTradeNo, tradeNo, totalAmount);
                } else {
                    // 订单已处理过,直接返回success(幂等性处理)
                    log.info("异步通知重复处理,订单号:{},当前状态:{}",
                            outTradeNo, PayStatusEnum.getByCode(alipayOrder.getPayStatus()).getDesc());
                }
            } else {
                // 其他状态(如TRADE_CLOSED),可根据业务需求处理(如更新为支付失败)
                log.info("异步通知交易状态非成功,订单号:{},状态:{}", outTradeNo, tradeStatus);
                alipayOrderService.updatePayStatus(
                        outTradeNo, tradeNo, PayStatusEnum.PAY_FAIL, null
                );
            }

            // 7. 处理完成,返回success(支付宝收到后停止重试)
            return "success";
        } catch (Exception e) {
            log.error("异步通知处理异常", e);
            return "fail"; // 异常时返回fail,支付宝重试
        }
    }

    /**
     * 查询支付结果
     */
    @GetMapping("/query")
    @Operation(summary = "查询支付结果", description = "根据商户订单号查询支付状态")
    @ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = AlipayTradeQueryResponse.class)))
    public Map<String, Object> queryPayResult(
            @Parameter(description = "商户订单号", required = true) @RequestParam String orderNo
    ) throws AlipayApiException {
        // 1. 调用支付宝API查询
        AlipayTradeQueryResponse queryResponse = alipayUtils.queryPayResult(orderNo, null);

        // 2. 同步更新本地订单状态(兜底,防止异步通知丢失)
        if ("TRADE_SUCCESS".equals(queryResponse.getTradeStatus())) {
            AlipayOrder alipayOrder = alipayOrderService.getByOrderNo(orderNo);
            if (alipayOrder != null && PayStatusEnum.UN_PAY.getCode().equals(alipayOrder.getPayStatus())) {
                alipayOrderService.updatePayStatus(
                        orderNo, queryResponse.getTradeNo(), PayStatusEnum.PAY_SUCCESS, queryResponse.getGmtPayment()
                );
            }
        }

        return Map.of(
                "code", 200,
                "message", "查询成功",
                "data", Map.of(
                        "orderNo", orderNo,
                        "alipayTradeNo", queryResponse.getTradeNo(),
                        "tradeStatus", queryResponse.getTradeStatus(),
                        "totalAmount", queryResponse.getTotalAmount(),
                        "gmtPayment", queryResponse.getGmtPayment(),
                        "merchantOrderStatus", alipayOrderService.getByOrderNo(orderNo).getPayStatus()
                )
        );
    }

    /**
     * 退款
     */
    @PostMapping("/refund")
    @Operation(summary = "退款", description = "根据商户订单号发起退款,仅已支付订单可退款")
    @ApiResponse(responseCode = "200", description = "退款成功", content = @Content(schema = @Schema(implementation = AlipayTradeRefundResponse.class)))
    public Map<String, Object> refund(
            @Parameter(description = "商户订单号", required = true) @RequestParam String orderNo,
            @Parameter(description = "退款金额(元)", required = true) @RequestParam BigDecimal refundAmount,
            @Parameter(description = "退款原因", required = true) @RequestParam String refundReason
    ) throws AlipayApiException {
        // 1. 调用支付宝API发起退款
        AlipayTradeRefundResponse refundResponse = alipayUtils.refund(orderNo, null, refundAmount, refundReason);

        // 2. 更新本地订单退款状态
        boolean updateSuccess = alipayOrderService.updateRefundStatus(
                orderNo, refundAmount, refundResponse.getGmtRefundPay()
        );
        if (!updateSuccess) {
            log.error("退款后更新订单状态失败,订单号:{}", orderNo);
            throw new RuntimeException("退款成功,但更新订单状态失败,请核对订单信息");
        }

        return Map.of(
                "code", 200,
                "message", "退款成功",
                "data", Map.of(
                        "orderNo", orderNo,
                        "alipayTradeNo", refundResponse.getTradeNo(),
                        "refundAmount", refundAmount,
                        "refundReason", refundReason,
                        "gmtRefund", refundResponse.getGmtRefundPay()
                )
        );
    }
}

6.3 启动类(AlipayDemoApplication)

复制代码
package com.jam.demo;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

/**
 * 支付宝支付demo启动类
 * @author ken
 */
@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "支付宝支付API文档", version = "1.0", description = "企业级支付宝支付方案接口文档"))
public class AlipayDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(AlipayDemoApplication.class, args);
        log.info("AlipayDemoApplication started successfully!");
    }

    /**
     * MyBatis-Plus插件配置(分页、乐观锁、防全表更新/删除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件(MySQL)
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 乐观锁插件(用于并发更新场景)
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        // 防全表更新/删除插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}

七、企业级支付核心优化:高可用与避坑指南

7.1 幂等性设计(核心避坑点)

支付系统最核心的问题之一是「幂等性」,尤其是异步通知场景,支付宝会多次重试,必须保证同一通知多次处理的结果一致。核心实现方案:

  1. 订单状态校验:更新订单状态前,先校验当前状态(如仅未支付订单可更新为已支付);

  2. 唯一索引约束:商户订单号(order_no)设置唯一索引,防止重复创建订单;

  3. 分布式锁:高并发场景下,使用Redis分布式锁包裹订单更新逻辑(如Redisson)。

7.2 高可用设计:异步通知丢失兜底

异步通知可能因网络异常、系统故障导致丢失,需设计兜底方案:

  1. 定时任务查询:每5分钟查询一次「未支付但创建时间超过30分钟」的订单,调用支付宝查询接口核对状态;

  2. 最大查询次数限制:同一订单最多查询10次,仍未支付则标记为「支付超时」。

定时任务实现示例(基于Spring Schedule):

复制代码
package com.jam.demo.task;

import com.alipay.api.AlipayApiException;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.jam.demo.entity.AlipayOrder;
import com.jam.demo.enums.PayStatusEnum;
import com.jam.demo.service.AlipayOrderService;
import com.jam.demo.util.AlipayUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.util.List;

/**
 * 支付订单兜底查询任务
 * @author ken
 */
@Slf4j
@Component
public class AlipayOrderQueryTask {

    @Autowired
    private AlipayOrderService alipayOrderService;

    @Autowired
    private AlipayUtils alipayUtils;

    /**
     * 每5分钟执行一次,查询30分钟前创建的未支付订单
     */
    @Scheduled(cron = "0 0/5 * * * ?")
    public void queryUnPayOrder() {
        log.info("开始执行支付订单兜底查询任务");
        // 1. 查询30分钟前创建的未支付订单
        LocalDateTime queryTime = LocalDateTime.now().minusMinutes(30);
        List<AlipayOrder> unPayOrders = alipayOrderService.list(
                com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.<AlipayOrder>lambdaQuery()
                        .eq(AlipayOrder::getPayStatus, PayStatusEnum.UN_PAY.getCode())
                        .lt(AlipayOrder::getCreateTime, queryTime)
        );

        if (CollectionUtils.isEmpty(unPayOrders)) {
            log.info("无需要兜底查询的订单");
            return;
        }

        // 2. 逐个查询支付宝状态
        for (AlipayOrder order : unPayOrders) {
            try {
                AlipayTradeQueryResponse queryResponse = alipayUtils.queryPayResult(order.getOrderNo(), null);
                // 3. 同步更新订单状态
                if ("TRADE_SUCCESS".equals(queryResponse.getTradeStatus())) {
                    alipayOrderService.updatePayStatus(
                            order.getOrderNo(), queryResponse.getTradeNo(), PayStatusEnum.PAY_SUCCESS, queryResponse.getGmtPayment()
                    );
                } else if ("TRADE_CLOSED".equals(queryResponse.getTradeStatus())) {
                    alipayOrderService.updatePayStatus(
                            order.getOrderNo(), queryResponse.getTradeNo(), PayStatusEnum.PAY_FAIL, null
                    );
                }
            } catch (AlipayApiException e) {
                log.error("兜底查询订单失败,订单号:{}", order.getOrderNo(), e);
                // 记录失败日志,后续人工核对
            }
        }

        log.info("支付订单兜底查询任务执行完成,共查询订单数:{}", unPayOrders.size());
    }
}

7.3 常见坑与解决方案(实战避坑)

常见问题 原因分析 解决方案
签名验证失败 1. 密钥格式错误(如商户私钥非PKCS8格式);2. 公钥配置错误(商户公钥未正确上传到开放平台);3. 编码不一致 1. 使用支付宝密钥生成工具生成PKCS8格式密钥;2. 重新核对开放平台配置的公钥;3. 统一使用UTF-8编码
异步通知接收不到 1. 通知地址非公网可访问;2. 端口被防火墙拦截;3. 接口返回非"success" 1. 使用内网穿透工具(如ngrok)暴露本地地址;2. 开放对应端口;3. 确保通知处理成功后返回"success"
订单状态更新失败 1. 幂等性处理缺失;2. 分布式并发更新;3. 参数校验不严格 1. 增加状态校验和唯一索引;2. 使用分布式锁;3. 严格校验订单号、金额等核心参数
退款失败 1. 订单未支付;2. 退款金额超过支付金额;3. 退款原因过长 1. 退款前校验订单状态;2. 校验退款金额≤支付金额;3. 退款原因控制在50字以内

7.4 安全加固:敏感信息处理

  1. 密钥安全:商户私钥不硬编码,存储在配置中心(如Nacos/Apollo),禁止明文存储;

  2. 日志脱敏:支付金额、用户信息等敏感数据在日志中脱敏(如订单号后4位脱敏);

  3. 接口权限:支付、退款等核心接口增加权限校验(如OAuth2.0),防止恶意调用;

  4. 数据加密:数据库中订单信息可对敏感字段加密(如用户ID),使用AES加密算法。

八、完整测试流程(确保可运行)

8.1 测试环境准备

  1. 支付宝开放平台创建「沙箱应用」,获取沙箱APPID、沙箱密钥(商户私钥、支付宝公钥);

  2. 配置application.yml:gateway-url改为沙箱网关(https://openapi.alipaydev.com/gateway.do),填写沙箱APPID和密钥;

  3. 启动MySQL8.0,执行章节3.2.3的SQL创建表;

  4. 启动Spring Boot应用(端口8080),访问Swagger3文档:http://localhost:8080/swagger-ui.html。

8.2 测试步骤(全链路)

步骤1:创建订单
  • 调用接口:/api/alipay/createOrder

  • 参数:orderNo=TEST20250520001,totalAmount=0.01,subject=测试订单,body=电脑网站支付测试

  • 预期结果:返回创建成功,订单状态为0(未支付)

步骤2:电脑网站支付
  • 调用接口:/api/alipay/pagePay?orderNo=TEST20250520001

  • 预期结果:返回HTML,前端渲染后自动跳转到支付宝沙箱支付页面

  • 操作:使用沙箱买家账号登录,完成支付(沙箱账号可在开放平台获取)

步骤3:异步通知处理
  • 支付完成后,支付宝沙箱会向配置的notify-url发送异步通知

  • 预期结果:系统验证签名通过,更新订单状态为1(已支付),记录支付宝交易号和支付时间

步骤4:查询支付结果
  • 调用接口:/api/alipay/query?orderNo=TEST20250520001

  • 预期结果:返回支付成功状态,商户订单状态与支付宝状态一致

步骤5:退款
  • 调用接口:/api/alipay/refund

  • 参数:orderNo=TEST20250520001,refundAmount=0.01,refundReason=测试退款

  • 预期结果:退款成功,订单状态更新为2(已退款)

九、总结:企业级支付落地核心要点

本文从底层逻辑到实战代码,实现了基于JDK17的企业级支付宝支付方案,核心要点可总结为:

  1. 底层逻辑:搞懂签名机制、支付链路、核心场景区分,是实现支付的基础;

  2. 代码规范:严格遵循阿里巴巴Java开发手册,使用指定工具类,保证代码可维护性;

  3. 高可用设计:异步通知+定时兜底,确保支付结果不丢失;

  4. 幂等性与安全:状态校验、唯一索引、分布式锁保证幂等,密钥安全、接口权限保证系统安全;

  5. 测试验证:使用沙箱环境完成全链路测试,确保代码可直接落地生产。

相关推荐
SuperHeroWu77 个月前
【HarmonyOS 5】鸿蒙mPaaS详解
华为·harmonyos·鸿蒙·mpaas·alipay