別再踩坑了!SpringBoot 集成支付寶支付最全教程
在電商、在線服務(wù)以及會(huì)員系統(tǒng)中,支付是繞不過(guò)去的核心環(huán)節(jié)。支付寶作為國(guó)內(nèi)主流的支付方式,早已開(kāi)放了完善的 API 接口供開(kāi)發(fā)者使用。然而,許多初學(xué)者在 Spring Boot 項(xiàng)目中集成支付寶時(shí),經(jīng)常會(huì)遇到密鑰配置不當(dāng)、回調(diào)驗(yàn)證失敗、訂單狀態(tài)未及時(shí)更新等“坑”。
本文將帶你從 支付寶開(kāi)放平臺(tái)沙箱環(huán)境配置 → Spring Boot 項(xiàng)目接入 → 支付/回調(diào)/退款流程 → 消息隊(duì)列處理超時(shí)訂單 全流程,構(gòu)建一個(gè)健壯的支付模塊。為了便于理解,我們會(huì)逐步展開(kāi)代碼示例,幫助你快速落地。
支付寶開(kāi)放平臺(tái)沙箱環(huán)境配置
在本地調(diào)試之前,我們需要先完成支付寶開(kāi)放平臺(tái) 沙箱環(huán)境 的配置。操作步驟如下:
登錄沙箱環(huán)境
進(jìn)入支付寶開(kāi)發(fā)者平臺(tái),使用開(kāi)發(fā)者賬號(hào)登錄,選擇 沙箱環(huán)境。 在 沙箱控制臺(tái) → 沙箱應(yīng)用 → 產(chǎn)品列表 中,可以看到當(dāng)前沙箱所支持的產(chǎn)品。
配置接口加簽方式
支付寶提供了兩種方式:
- 系統(tǒng)默認(rèn)密鑰/證書(shū)(推薦)
- 自定義密鑰
這里選擇系統(tǒng)默認(rèn)密鑰,因?yàn)楹罄m(xù)使用 API 在線調(diào)試工具時(shí)必須依賴默認(rèn)密鑰。
應(yīng)用網(wǎng)關(guān)配置
應(yīng)用網(wǎng)關(guān)主要用于接收支付寶的異步通知(如交易完成回調(diào))。
- HTTP 訂閱模式下必須配置應(yīng)用網(wǎng)關(guān);
- WebSocket 訂閱模式下則不需要。
生成密鑰
完成應(yīng)用信息配置后,生成一套屬于自己的應(yīng)用密鑰,確保項(xiàng)目后續(xù)能與支付寶沙箱環(huán)境正常交互。
至此,網(wǎng)頁(yè)端配置環(huán)節(jié)結(jié)束。
Spring Boot 項(xiàng)目配置(IDEA 操作部分)
引入依賴
在 pom.xml 中添加支付寶 SDK:
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.22.110.ALL</version>
</dependency>在 application.yml 中配置
alipay:
appId: your-app-id
appPrivateKey: your-private-key
alipayPublicKey: your-alipay-public-key
notifyUrl: http://your-domain.com/alipay/notify封裝配置類
//src/main/java/com/icoderoad/config/AliPayConfig.java
package com.icoderoad.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "alipay")
public class AliPayConfig {
private String appId;
private String appPrivateKey;
private String alipayPublicKey;
private String notifyUrl;
}支付接口開(kāi)發(fā)
//src/main/java/com/icoderoad/controller/AliPayController.java
package com.icoderoad.controller;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.*;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.icoderoad.config.AliPayConfig;
import com.icoderoad.entity.AliPay;
import com.icoderoad.mapper.OrdersMapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
@RestController
public class AliPayController {
private static final String GATEWAY_URL = "https://openapi.alipaydev.com/gateway.do";
private static final String FORMAT = "JSON";
private static final String CHARSET = "UTF-8";
private static final String SIGN_TYPE = "RSA2";
@Resource
private AliPayConfig aliPayConfig;
@Resource
private OrdersMapper ordersMapper;
@GetMapping("/pay")
public void pay(AliPay aliPay, HttpServletResponse httpResponse) throws Exception {
// 1. 創(chuàng)建客戶端
AlipayClient alipayClient = new DefaultAlipayClient(
GATEWAY_URL,
aliPayConfig.getAppId(),
aliPayConfig.getAppPrivateKey(),
FORMAT,
CHARSET,
aliPayConfig.getAlipayPublicKey(),
SIGN_TYPE
);
// 2. 創(chuàng)建支付請(qǐng)求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(aliPayConfig.getNotifyUrl());
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", aliPay.getTraceNo());
bizContent.put("total_amount", aliPay.getTotalAmount());
bizContent.put("subject", aliPay.getSubject());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
// 3. 執(zhí)行請(qǐng)求并返回表單
String form = alipayClient.pageExecute(request).getBody();
httpResponse.setContentType("text/html;charset=" + CHARSET);
httpResponse.getWriter().write(form);
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}
}異步回調(diào)處理
支付寶異步回調(diào)必須使用 公網(wǎng)地址,本地調(diào)試可借助 natapp 內(nèi)網(wǎng)穿透。
@PostMapping("/notify")
public String payNotify(HttpServletRequest request) throws Exception {
if ("TRADE_SUCCESS".equals(request.getParameter("trade_status"))) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> params.put(name, values[0]));
String sign = params.get("sign");
String content = AlipaySignature.getSignCheckContentV1(params);
boolean checkSignature = AlipaySignature.rsa256CheckContent(
content,
sign,
aliPayConfig.getAlipayPublicKey(),
"UTF-8"
);
if (checkSignature) {
ordersMapper.updateState(params.get("out_trade_no"),
"已支付",
params.get("gmt_payment"),
params.get("trade_no"));
}
}
return "success";
}退款流程
退款流程與支付類似:
- 創(chuàng)建
AlipayClient; - 創(chuàng)建
AlipayTradeRefundRequest并設(shè)置參數(shù); - 執(zhí)行請(qǐng)求,根據(jù)
isSuccess判斷結(jié)果; - 成功則更新數(shù)據(jù)庫(kù)狀態(tài)。
退款接口完整代碼
package com.icoderoad.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.DateUnit;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.*;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.icoderoad.config.AliPayConfig;
import com.icoderoad.entity.AliPay;
import com.icoderoad.entity.Orders;
import com.icoderoad.mapper.OrdersMapper;
import com.icoderoad.result.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class RefundController {
private static final String GATEWAY_URL = "https://openapi.alipaydev.com/gateway.do";
private static final String FORMAT = "JSON";
private static final String CHARSET = "UTF-8";
private static final String SIGN_TYPE = "RSA2";
@Resource
private AliPayConfig aliPayConfig;
@Resource
private OrdersMapper ordersMapper;
@GetMapping("/return")
public Result returnPay(AliPay aliPay) throws AlipayApiException {
String now = DateUtil.now();
Orders orders = ordersMapper.getByNo(aliPay.getTraceNo());
if (orders != null) {
long between = DateUtil.between(DateUtil.parseDateTime(orders.getPaymentTime()),
DateUtil.parseDateTime(now), DateUnit.DAY);
if (between > 7) {
return Result.error("-1", "該訂單已超過(guò)7天,不支持退款");
}
}
AlipayClient alipayClient = new DefaultAlipayClient(
GATEWAY_URL,
aliPayConfig.getAppId(),
aliPayConfig.getAppPrivateKey(),
FORMAT,
CHARSET,
aliPayConfig.getAlipayPublicKey(),
SIGN_TYPE
);
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("trade_no", aliPay.getAlipayTraceNo());
bizContent.put("refund_amount", aliPay.getTotalAmount());
bizContent.put("out_request_no", aliPay.getTraceNo());
request.setBizContent(bizContent.toString());
AlipayTradeRefundResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
ordersMapper.updatePayState(aliPay.getTraceNo(), "已退款", now);
return Result.success();
} else {
return Result.error(response.getCode(), response.getBody());
}
}
}核心邏輯就是 使用支付寶回調(diào)訂單號(hào)與金額完成退款。
未支付訂單的自動(dòng)取消(消息隊(duì)列)
為了避免用戶下單后長(zhǎng)時(shí)間不支付,系統(tǒng)需要自動(dòng)取消訂單。
我們使用 RabbitMQ 延遲隊(duì)列 來(lái)實(shí)現(xiàn):
- 下單時(shí)投遞一條延時(shí)消息(30 分鐘);
- 若超時(shí)未消費(fèi),該消息進(jìn)入死信隊(duì)列;
- 消費(fèi)死信消息時(shí),查詢訂單支付狀態(tài);
- 若仍未支付,則更新訂單為“已超時(shí)”;
- 若已支付,則丟棄消息。
這種方式具有 高效、可擴(kuò)展 的優(yōu)勢(shì),但依賴 RabbitMQ 運(yùn)維,增加了一定的系統(tǒng)復(fù)雜度。
RabbitMQ 配置類
//src/main/java/com/icoderoad/config/RabbitConfig.java
package com.icoderoad.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
public static final String ORDER_DELAY_QUEUE = "order.delay.queue";
public static final String ORDER_DELAY_EXCHANGE = "order.delay.exchange";
public static final String ORDER_ROUTING_KEY = "order.delay.routingkey";
public static final String ORDER_DEAD_QUEUE = "order.dead.queue";
public static final String ORDER_DEAD_EXCHANGE = "order.dead.exchange";
public static final String ORDER_DEAD_ROUTING_KEY = "order.dead.routingkey";
@Bean
public DirectExchange orderDelayExchange() {
return new DirectExchange(ORDER_DELAY_EXCHANGE);
}
@Bean
public DirectExchange orderDeadExchange() {
return new DirectExchange(ORDER_DEAD_EXCHANGE);
}
@Bean
public Queue orderDelayQueue() {
return QueueBuilder.durable(ORDER_DELAY_QUEUE)
.withArgument("x-dead-letter-exchange", ORDER_DEAD_EXCHANGE)
.withArgument("x-dead-letter-routing-key", ORDER_DEAD_ROUTING_KEY)
.withArgument("x-message-ttl", 1800000) // 30分鐘
.build();
}
@Bean
public Queue orderDeadQueue() {
return QueueBuilder.durable(ORDER_DEAD_QUEUE).build();
}
@Bean
public Binding orderDelayBinding() {
return BindingBuilder.bind(orderDelayQueue()).to(orderDelayExchange()).with(ORDER_ROUTING_KEY);
}
@Bean
public Binding orderDeadBinding() {
return BindingBuilder.bind(orderDeadQueue()).to(orderDeadExchange()).with(ORDER_DEAD_ROUTING_KEY);
}
}死信隊(duì)列消費(fèi)者
//src/main/java/com/icoderoad/consumer/OrderDeadConsumer.java
package com.icoderoad.consumer;
import com.icoderoad.mapper.OrdersMapper;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import static com.icoderoad.config.RabbitConfig.ORDER_DEAD_QUEUE;
@Component
public class OrderDeadConsumer {
@Resource
private OrdersMapper ordersMapper;
@RabbitListener(queues = ORDER_DEAD_QUEUE)
public void handleDeadMessage(String orderNo) {
String state = ordersMapper.getStateByNo(orderNo);
if ("待支付".equals(state)) {
ordersMapper.updateState(orderNo, "已超時(shí)", null, null);
System.out.println("訂單超時(shí)未支付,已自動(dòng)取消: " + orderNo);
}
}
}結(jié)論
通過(guò)以上完整的流程,我們實(shí)現(xiàn)了一個(gè) 可落地、穩(wěn)定、支持支付/回調(diào)/退款/超時(shí)取消 的支付寶支付模塊:
- 前端沙箱環(huán)境幫助我們快速調(diào)試;
- Spring Boot 配合支付寶 SDK 打通了支付鏈路;
- 異步回調(diào)保障交易狀態(tài)的準(zhǔn)確性;
- RabbitMQ 延遲隊(duì)列確保未支付訂單能夠自動(dòng)取消。
支付寶支付集成并不復(fù)雜,但細(xì)節(jié)環(huán)環(huán)相扣,稍有遺漏就可能導(dǎo)致異常。希望本文能幫你在項(xiàng)目中快速構(gòu)建一套可靠的支付體系,少走彎路!


























