支付宝支付实战全攻略

简介: 本文详细介绍了基于JDK17的企业级支付宝支付实现方案。首先阐述了支付宝支付的核心参与者、安全机制和支付场景区分,重点分析了RSA2签名验证流程。随后提供了完整的开发指南,包括开放平台配置、Maven环境搭建、数据库设计以及核心工具类封装。文章详细展示了基于MyBatis-Plus的持久层实现和Swagger3接口文档集成,并重点讲解了支付回调处理、退款功能等核心业务逻辑的实现。针对企业级应用场景,特别强调了幂等性设计、高可用方案和常见问题的解决方案,包括异步通知丢失兜底机制和安全加固措施。

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

在移动支付主导的商业生态中,支付宝作为国内头部支付解决方案,覆盖了超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 支付流程核心链路流程图

image.png

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

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

  1. 注册支付宝商户账号并完成实名认证(https://b.alipay.com/);
  2. 登录支付宝开放平台(https://open.alipay.com/),创建应用(选择「自研应用」);
  3. 应用审核通过后,开通「电脑网站支付」「APP支付」「退款」等所需功能;
  4. 配置密钥:生成RSA2密钥对(商户私钥、商户公钥),将商户公钥上传至开放平台,获取支付宝公钥;
  • 密钥生成工具:支付宝开放平台提供的「密钥生成工具」(支持Windows/Mac);
  • 注意:商户私钥需妥善保管,不可泄露;
  1. 配置异步通知地址(Notify Url)与同步回调地址(Return Url):
  • 异步通知地址:用于接收支付宝支付结果的异步通知(必须为公网可访问地址);
  • 同步回调地址:用户支付完成后跳转的商户页面(仅用于展示结果,不可作为订单状态更新依据);
  1. 记录核心配置参数: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. 测试验证:使用沙箱环境完成全链路测试,确保代码可直接落地生产。
目录
相关文章
|
2天前
|
云安全 监控 安全
|
7天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
932 5
|
13天前
|
人工智能 Java API
Java 正式进入 Agentic AI 时代:Spring AI Alibaba 1.1 发布背后的技术演进
Spring AI Alibaba 1.1 正式发布,提供极简方式构建企业级AI智能体。基于ReactAgent核心,支持多智能体协作、上下文工程与生产级管控,助力开发者快速打造可靠、可扩展的智能应用。
1097 41
|
9天前
|
机器学习/深度学习 人工智能 数据可视化
1秒生图!6B参数如何“以小博大”生成超真实图像?
Z-Image是6B参数开源图像生成模型,仅需16GB显存即可生成媲美百亿级模型的超真实图像,支持中英双语文本渲染与智能编辑,登顶Hugging Face趋势榜,首日下载破50万。
664 38
|
13天前
|
人工智能 前端开发 算法
大厂CIO独家分享:AI如何重塑开发者未来十年
在 AI 时代,若你还在紧盯代码量、执着于全栈工程师的招聘,或者仅凭技术贡献率来评判价值,执着于业务提效的比例而忽略产研价值,你很可能已经被所谓的“常识”困住了脚步。
758 67
大厂CIO独家分享:AI如何重塑开发者未来十年
|
9天前
|
存储 自然语言处理 测试技术
一行代码,让 Elasticsearch 集群瞬间雪崩——5000W 数据压测下的性能避坑全攻略
本文深入剖析 Elasticsearch 中模糊查询的三大陷阱及性能优化方案。通过5000 万级数据量下做了高压测试,用真实数据复刻事故现场,助力开发者规避“查询雪崩”,为您的业务保驾护航。
473 30
|
16天前
|
数据采集 人工智能 自然语言处理
Meta SAM3开源:让图像分割,听懂你的话
Meta发布并开源SAM 3,首个支持文本或视觉提示的统一图像视频分割模型,可精准分割“红色条纹伞”等开放词汇概念,覆盖400万独特概念,性能达人类水平75%–80%,推动视觉分割新突破。
937 59
Meta SAM3开源:让图像分割,听懂你的话
|
5天前
|
弹性计算 网络协议 Linux
阿里云ECS云服务器详细新手购买流程步骤(图文详解)
新手怎么购买阿里云服务器ECS?今天出一期阿里云服务器ECS自定义购买流程:图文全解析,阿里云服务器ECS购买流程图解,自定义购买ECS的设置选项是最复杂的,以自定义购买云服务器ECS为例,包括付费类型、地域、网络及可用区、实例、镜像、系统盘、数据盘、公网IP、安全组及登录凭证详细设置教程:
204 114