一、前言:为什么必须掌握支付宝支付?
在移动支付主导的商业生态中,支付宝作为国内头部支付解决方案,覆盖了超10亿用户与数千万商户。对于Java开发者而言,掌握支付宝支付的企业级实现方案,不仅是核心业务落地的必备技能,更是应对高并发、高可用支付场景的关键能力。
二、支付宝支付核心认知:先搞懂这些底层逻辑
2.1 支付宝支付的核心参与者与链路
支付宝支付本质是「多方协同的资金流转链路」,核心参与者包括:商户系统、支付宝开放平台、用户、银行/清算机构。其核心链路可概括为:商户发起支付请求→支付宝验证并引导用户付款→用户完成支付→支付宝同步/异步通知商户→商户更新订单状态。
2.2 核心支付场景区分(易混淆点)
支付宝开放平台提供多种支付产品,核心场景需明确区分:
- 电脑网站支付:适用于PC端网页,用户在电脑上完成支付(跳转支付宝网页);
- 手机网站支付:适用于H5页面,用户在手机浏览器中完成支付;
- APP支付:适用于商户自有APP,用户通过APP调用支付宝SDK完成支付;
- 小程序支付:适用于支付宝小程序内的支付场景。
本文重点覆盖「电脑网站支付」「APP支付」「异步通知处理」「退款」四大核心场景,覆盖80%企业级支付需求。
2.3 支付宝支付的核心安全机制:签名与验签
支付宝支付的安全性核心依赖「RSA2非对称加密」,所有交互数据均需通过签名验证,防止数据被篡改。核心逻辑如下:
- 商户侧:使用商户私钥对请求参数进行签名,将签名与参数一同发送给支付宝;
- 支付宝侧:使用商户公钥(商户提前配置到开放平台)验证签名合法性,验证通过后处理请求;
- 支付宝响应/通知:使用支付宝私钥签名,商户侧使用支付宝公钥验证签名。
关键区别:RSA与RSA2的差异——RSA2(SHA256WithRSA)是支付宝推荐的签名算法,密钥长度2048位,安全性更高;RSA(SHA1WithRSA)为旧版算法,不推荐新系统使用。本文全程采用RSA2算法。
2.4 支付流程核心链路流程图
三、前置准备:支付宝开放平台配置与环境搭建
3.1 支付宝开放平台配置步骤(关键操作)
- 注册支付宝商户账号并完成实名认证(https://b.alipay.com/);
- 登录支付宝开放平台(https://open.alipay.com/),创建应用(选择「自研应用」);
- 应用审核通过后,开通「电脑网站支付」「APP支付」「退款」等所需功能;
- 配置密钥:生成RSA2密钥对(商户私钥、商户公钥),将商户公钥上传至开放平台,获取支付宝公钥;
- 密钥生成工具:支付宝开放平台提供的「密钥生成工具」(支持Windows/Mac);
- 注意:商户私钥需妥善保管,不可泄露;
- 配置异步通知地址(Notify Url)与同步回调地址(Return Url):
- 异步通知地址:用于接收支付宝支付结果的异步通知(必须为公网可访问地址);
- 同步回调地址:用户支付完成后跳转的商户页面(仅用于展示结果,不可作为订单状态更新依据);
- 记录核心配置参数: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 幂等性设计(核心避坑点)
支付系统最核心的问题之一是「幂等性」,尤其是异步通知场景,支付宝会多次重试,必须保证同一通知多次处理的结果一致。核心实现方案:
- 订单状态校验:更新订单状态前,先校验当前状态(如仅未支付订单可更新为已支付);
- 唯一索引约束:商户订单号(order_no)设置唯一索引,防止重复创建订单;
- 分布式锁:高并发场景下,使用Redis分布式锁包裹订单更新逻辑(如Redisson)。
7.2 高可用设计:异步通知丢失兜底
异步通知可能因网络异常、系统故障导致丢失,需设计兜底方案:
- 定时任务查询:每5分钟查询一次「未支付但创建时间超过30分钟」的订单,调用支付宝查询接口核对状态;
- 最大查询次数限制:同一订单最多查询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 安全加固:敏感信息处理
- 密钥安全:商户私钥不硬编码,存储在配置中心(如Nacos/Apollo),禁止明文存储;
- 日志脱敏:支付金额、用户信息等敏感数据在日志中脱敏(如订单号后4位脱敏);
- 接口权限:支付、退款等核心接口增加权限校验(如OAuth2.0),防止恶意调用;
- 数据加密:数据库中订单信息可对敏感字段加密(如用户ID),使用AES加密算法。
八、完整测试流程(确保可运行)
8.1 测试环境准备
- 支付宝开放平台创建「沙箱应用」,获取沙箱APPID、沙箱密钥(商户私钥、支付宝公钥);
- 配置application.yml:gateway-url改为沙箱网关(https://openapi.alipaydev.com/gateway.do),填写沙箱APPID和密钥;
- 启动MySQL8.0,执行章节3.2.3的SQL创建表;
- 启动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的企业级支付宝支付方案,核心要点可总结为:
- 底层逻辑:搞懂签名机制、支付链路、核心场景区分,是实现支付的基础;
- 代码规范:严格遵循阿里巴巴Java开发手册,使用指定工具类,保证代码可维护性;
- 高可用设计:异步通知+定时兜底,确保支付结果不丢失;
- 幂等性与安全:状态校验、唯一索引、分布式锁保证幂等,密钥安全、接口权限保证系统安全;
- 测试验证:使用沙箱环境完成全链路测试,确保代码可直接落地生产。