SpringBoot 集成支付寶支付,看這篇就夠了!
兄弟們,做 Java 開發(fā)的誰沒踩過 “支付集成” 的坑?。壳瓣囎游彝滦⊥?,為了給公司項(xiàng)目加個支付寶支付,翻遍了全網(wǎng)教程:有的只給半段代碼,關(guān)鍵配置藏著掖著;有的上來就甩專業(yè)術(shù)語,“簽名驗(yàn)簽”“異步回調(diào)” 說得跟天書似的;最氣人的是某篇 “實(shí)戰(zhàn)文”,步驟跳得比兔子還快,結(jié)果小王照著做,測試環(huán)境付了 1 塊錢,后臺沒收到回調(diào),查日志查到凌晨兩點(diǎn) —— 最后發(fā)現(xiàn)是公鑰貼錯了。
所以今天這篇,咱不搞虛的:從支付寶開放平臺怎么注冊,到 SpringBoot 項(xiàng)目怎么配置,再到支付、回調(diào)、查訂單全流程代碼,每一步都給你講得明明白白。咱不用 “高深術(shù)語”,就用大白話,連 “私鑰怎么生成” 這種細(xì)節(jié)都給你標(biāo)出來。
一、先搞懂:集成支付寶要準(zhǔn)備啥?
在寫代碼之前,得先把 “前置工作” 做足 —— 就像做飯前要先買菜一樣,咱得先從支付寶那邊拿到 “入場券”。這里要區(qū)分兩種情況:你是個人開發(fā)測試,還是公司項(xiàng)目上線?咱先從最常用的 “個人測試”(沙箱環(huán)境)說起,后面再提生產(chǎn)環(huán)境的注意點(diǎn)。
1.1 支付寶開放平臺 “薅羊毛”:沙箱環(huán)境開通
支付寶給開發(fā)者提供了 “沙箱環(huán)境”—— 簡單說就是個 “模擬支付環(huán)境”,你在這測支付,花的是 “假錢”,回調(diào)也是支付寶模擬的,不用真的營業(yè)執(zhí)照,個人就能玩。
步驟咱一步一步來,別著急:
- 先登支付寶開放平臺:百度搜 “支付寶開放平臺”,進(jìn)官網(wǎng)(注意看域名是alipay.com,別進(jìn)錯釣魚站),用你自己的支付寶賬號登錄就行(個人賬號就行,不用企業(yè)號)。
 - 找到 “沙箱環(huán)境” 入口:登錄后,右上角點(diǎn) “控制臺”,然后左邊菜單欄找 “研發(fā)服務(wù)”→“沙箱環(huán)境”—— 對,就是那個黃底黑字的入口,很好找。
 - 記好這幾個 “關(guān)鍵信息”:沙箱環(huán)境里有幾個東西是 “命根子”,等下寫代碼要用到,先抄在小本本上:
 
- APPID:相當(dāng)于你這個 “測試應(yīng)用” 的身份證,比如 “2021000000000000” 這種格式;
 - 支付寶網(wǎng)關(guān):沙箱環(huán)境的網(wǎng)關(guān)是固定的 “https://openapi.alipaydev.com/gateway.do”(生產(chǎn)環(huán)境不一樣,別搞混);
 - 沙箱賬號:包括 “商家賬號” 和 “買家賬號”—— 等下測試支付,你要用 “買家賬號” 登錄支付寶(沙箱版)付款,用 “商家賬號” 看收款。
 
這里插個小提醒:沙箱環(huán)境的 “買家支付寶” 不是你平時用的支付寶 APP!得用支付寶 APP 掃沙箱頁面里的 “沙箱支付寶二維碼”,下載個專門的測試版(別擔(dān)心,正規(guī)的,支付寶官方的),不然你用真實(shí)支付寶付不了測試款。
1.2 核心中的核心:密鑰怎么搞?
這一步是大多數(shù)人踩坑的重災(zāi)區(qū) ——“私鑰”“公鑰”“支付寶公鑰”,三個東西繞來繞去,很多人直接貼錯,結(jié)果支付成功了,回調(diào)驗(yàn)簽失敗,查半天查不出問題。
咱先掰扯清楚:這三個密鑰是干啥的?
- 你本地生成 “商戶私鑰” 和 “商戶公鑰”:私鑰你自己藏好,別給任何人;公鑰要上傳到支付寶,相當(dāng)于 “給支付寶一把鎖”;
 - 支付寶給你 “支付寶公鑰”:相當(dāng)于 “支付寶給你一把鑰匙”,你用這把鑰匙驗(yàn)證 “回調(diào)是不是支付寶發(fā)的”(防止別人偽造回調(diào)騙錢)。
 
好,現(xiàn)在教你怎么生成密鑰,超簡單,不用裝復(fù)雜工具:
- 下載支付寶密鑰生成工具:沙箱環(huán)境頁面里,有個 “密鑰工具” 入口,直接下載(Windows、Mac 都有),不用注冊,雙擊就能用;
 - 生成密鑰:打開工具,選擇 “RSA2”(別選 RSA,支付寶現(xiàn)在推薦 RSA2,安全性更高),然后點(diǎn) “生成密鑰”—— 瞬間就出來 “商戶私鑰” 和 “商戶公鑰”;
 - 上傳公鑰拿 “支付寶公鑰”:把生成的 “商戶公鑰” 復(fù)制下來(注意別漏了開頭的 “-----BEGIN PUBLIC KEY-----” 和結(jié)尾的 “-----END PUBLIC KEY-----”),回到沙箱環(huán)境頁面,找到 “設(shè)置 / 查看”(在 APPID 下面),把公鑰貼進(jìn)去,點(diǎn)保存 —— 保存后,支付寶會給你返回一個 “支付寶公鑰”,這個要抄下來,等下配置要用。
 
這里劃重點(diǎn):
- 私鑰別亂傳!別往 GitHub 上提交,最好放配置文件里,用環(huán)境變量或者配置中心管理(后面會講怎么安全配置);
 - 復(fù)制密鑰的時候,別多帶空格、別漏行!有次我復(fù)制的時候多了個換行,結(jié)果簽名一直失敗,查了半小時才發(fā)現(xiàn) —— 別犯這種低級錯誤。
 
1.3 生產(chǎn)環(huán)境注意點(diǎn)(個人開發(fā)也得了解)
如果是公司項(xiàng)目要上線,光有沙箱環(huán)境還不夠,得走 “正式流程”:
- 要用企業(yè)支付寶賬號登錄開放平臺,申請 “應(yīng)用”(選 “自研應(yīng)用”,類型看你項(xiàng)目是 APP、網(wǎng)站還是小程序);
 - 申請 “當(dāng)面付” 或 “電腦網(wǎng)站支付” 等產(chǎn)品(要提交營業(yè)執(zhí)照,支付寶會審核,一般 1-2 天);
 - 正式環(huán)境的密鑰生成方式和沙箱一樣,但網(wǎng)關(guān)是 “https://openapi.alipay.com/gateway.do”,別用沙箱的網(wǎng)關(guān);
 - 正式環(huán)境一定要做 “生產(chǎn)環(huán)境測試”(支付寶有 “生產(chǎn)環(huán)境聯(lián)調(diào)” 入口),別直接上線 —— 不然出問題就是真錢損失了。
 
二、SpringBoot 項(xiàng)目搭建:從 0 到 1 配置
前置工作搞定,現(xiàn)在進(jìn)入正題:SpringBoot 怎么集成支付寶?咱從 “新建項(xiàng)目” 開始,一步都不落下,哪怕你是剛學(xué) SpringBoot 的新手,也能跟上。
2.1 新建 SpringBoot 項(xiàng)目(超簡單)
用 IDEA 或者 Eclipse 都行,咱以 IDEA 為例:
- 打開 IDEA,選 “New Project”→“Spring Initializr”,然后 Group、Artifact 隨便填(比如 Group 填 com.xxx.pay,Artifact 填 alipay-demo);
 - 選依賴的時候,勾上這幾個:
 
- Spring Web:因?yàn)橐獙懡涌冢ㄖЦ督涌?、回調(diào)接口);
 - Lombok:省點(diǎn) getter/setter 代碼,誰用誰知道香;
 - Spring Boot DevTools:熱部署,改代碼不用重啟項(xiàng)目(可選,但推薦);
 
- 點(diǎn) “Finish”,項(xiàng)目就建好了 —— 是不是比想象中簡單?
 
2.2 引入支付寶 SDK 依賴
別自己寫 HTTP 請求調(diào)用支付寶接口!支付寶官方給了 Java SDK,直接引依賴就行,省得處理簽名、參數(shù)封裝這些破事。
打開項(xiàng)目的 pom.xml,在<dependencies>里加這段:
<!-- 支付寶支付SDK -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.38.0.ALL</version> <!-- 版本可以選最新的,去Maven倉庫查就行 -->
</dependency>
<!-- 工具類:處理JSON(支付寶回調(diào)返回的是JSON) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.32</version>
</dependency>
<!-- 日志:后面查問題要用到 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>加完之后,點(diǎn)一下 IDEA 的 “刷新依賴” 按鈕(就是那個小 maven 圖標(biāo)),等 SDK 下載好 —— 這里提醒下,要是下載慢,把 Maven 鏡像換成阿里云的,別卡半天。
2.3 配置文件:把支付寶信息填進(jìn)去
別把 APPID、私鑰這些敏感信息硬編碼在代碼里!咱用 SpringBoot 的配置文件(application.yml)來管理,后面改環(huán)境也方便。
在 resources 目錄下,把 application.properties 改成 application.yml(yml 格式更清晰,習(xí)慣用 properties 也沒事,格式對應(yīng)上就行),然后填這些內(nèi)容:
server:
  port: 8080 # 項(xiàng)目端口,后面測試要用到
# 支付寶相關(guān)配置
alipay:
  app-id: 2021000000000000 # 你沙箱環(huán)境的APPID
  merchant-private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQ... # 你生成的商戶私鑰(完整的,包括開頭結(jié)尾)
  alipay-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... # 支付寶給你的公鑰(完整的)
  gateway-url: https://openapi.alipaydev.com/gateway.do # 沙箱網(wǎng)關(guān)(生產(chǎn)環(huán)境改正式網(wǎng)關(guān))
  sign-type: RSA2 # 簽名類型,固定RSA2
  format: json # 參數(shù)格式,固定json
  charset: UTF-8 # 編碼,固定UTF-8這里有個小技巧:如果怕私鑰太長,在 yml 里寫著亂,可以用 “|” 換行,比如:
alipay:
  merchant-private-key: |
    -----BEGIN PRIVATE KEY-----
    MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQ...
    ...(中間省略)...
    -----END PRIVATE KEY-----這樣看起來更整齊,也不容易漏字符。
2.4 寫個 “支付寶工具類”:封裝重復(fù)邏輯
支付、查訂單、退款這些操作,都要先初始化 “AlipayClient” 對象,還要處理參數(shù)、簽名 —— 咱把這些重復(fù)代碼抽到一個工具類里,后面用的時候直接調(diào),省得寫重復(fù)代碼。
新建一個 util 包,里面建 AlipayUtil.java:
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.response.AlipayTradePagePayResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
 * 支付寶支付工具類
 * 咱把初始化、支付、查訂單的方法都放這
 */
@Slf4j
@Component
public class AlipayUtil {
    // 從配置文件讀取支付寶參數(shù)
    @Value("${alipay.app-id}")
    private String appId;
    @Value("${alipay.merchant-private-key}")
    private String merchantPrivateKey;
    @Value("${alipay.alipay-public-key}")
    private String alipayPublicKey;
    @Value("${alipay.gateway-url}")
    private String gatewayUrl;
    @Value("${alipay.sign-type}")
    private String signType;
    @Value("${alipay.format}")
    private String format;
    @Value("${alipay.charset}")
    private String charset;
    // 支付寶客戶端對象(初始化一次就行,不用每次調(diào)用都新建)
    private AlipayClient alipayClient;
    /**
     * 項(xiàng)目啟動時初始化AlipayClient
     * @PostConstruct:Spring加載完這個Bean后,自動執(zhí)行這個方法
     */
    @PostConstruct
    public void initAlipayClient() {
        alipayClient = new DefaultAlipayClient(
                gatewayUrl,
                appId,
                merchantPrivateKey,
                format,
                charset,
                alipayPublicKey,
                signType
        );
        log.info("支付寶客戶端初始化完成!appId:{}", appId);
    }
    /**
     * 發(fā)起電腦網(wǎng)站支付(最常用的,比如PC端點(diǎn)擊“支付寶支付”跳轉(zhuǎn)到支付寶頁面)
     * @param outTradeNo 商戶訂單號(自己生成,唯一,比如“ORDER20240520001”)
     * @param totalAmount 訂單金額(單位:元,比如1.00)
     * @param subject 訂單標(biāo)題(比如“測試訂單-20240520”)
     * @param returnUrl 支付成功后,支付寶跳轉(zhuǎn)回你的頁面(比如“http://localhost:8080/pay/success”)
     * @param notifyUrl 支付成功后,支付寶異步通知你的接口(比如“http://xxx.xxx.xxx.xxx:8080/pay/notify”)
     * @return 支付寶返回的HTML表單(前端渲染這個表單,就能跳轉(zhuǎn)到支付寶支付頁)
     */
    public String createPagePay(String outTradeNo, String totalAmount, String subject, String returnUrl, String notifyUrl) throws AlipayApiException {
        // 1. 創(chuàng)建支付請求對象(電腦網(wǎng)站支付用AlipayTradePagePayRequest)
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        
        // 2. 設(shè)置回調(diào)地址
        request.setReturnUrl(returnUrl); // 同步回調(diào)(給用戶看的,跳轉(zhuǎn)用)
        request.setNotifyUrl(notifyUrl); // 異步回調(diào)(給后臺用的,真正處理訂單狀態(tài)的)
        
        // 3. 封裝支付參數(shù)(JSON格式)
        String bizContent = String.format(
                "{" +
                        "\"out_trade_no\":\"%s\"," +
                        "\"total_amount\":\"%s\"," +
                        "\"subject\":\"%s\"," +
                        "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"" + // 產(chǎn)品碼,電腦網(wǎng)站支付固定這個
                "}",
                outTradeNo, totalAmount, subject
        );
        request.setBizContent(bizContent);
        log.info("發(fā)起支付寶支付請求!outTradeNo:{},totalAmount:{},bizContent:{}", outTradeNo, totalAmount, bizContent);
        // 4. 調(diào)用支付寶接口,獲取支付頁面HTML
        AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
        
        // 5. 判斷請求是否成功(支付寶返回的code是10000表示成功)
        if (response.isSuccess()) {
            log.info("支付寶支付請求成功!outTradeNo:{},支付寶交易號:{}", outTradeNo, response.getTradeNo());
            return response.getBody(); // 返回HTML表單,前端渲染后跳轉(zhuǎn)
        } else {
            log.error("支付寶支付請求失??!outTradeNo:{},錯誤信息:{}", outTradeNo, response.getMsg());
            throw new AlipayApiException("支付寶支付請求失敗:" + response.getMsg());
        }
    }
    /**
     * 查詢訂單支付狀態(tài)(比如用戶付了錢,但同步回調(diào)沒跳回來,就查一下訂單是不是真的付了)
     * @param outTradeNo 商戶訂單號
     * @return 支付狀態(tài)(比如“TRADE_SUCCESS”表示支付成功)
     */
    public String queryTradeStatus(String outTradeNo) throws AlipayApiException {
        // 1. 創(chuàng)建查詢請求對象
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        
        // 2. 封裝查詢參數(shù)
        String bizContent = String.format(
                "{" +
                        "\"out_trade_no\":\"%s\"" +
                "}",
                outTradeNo
        );
        request.setBizContent(bizContent);
        log.info("查詢支付寶訂單狀態(tài)!outTradeNo:{},bizContent:{}", outTradeNo, bizContent);
        // 3. 調(diào)用查詢接口
        AlipayTradeQueryResponse response = alipayClient.execute(request);
        
        // 4. 處理查詢結(jié)果
        if (response.isSuccess()) {
            String tradeStatus = response.getTradeStatus();
            log.info("訂單查詢成功!outTradeNo:{},支付狀態(tài):{},支付寶交易號:{}", outTradeNo, tradeStatus, response.getTradeNo());
            return tradeStatus;
        } else {
            log.error("訂單查詢失??!outTradeNo:{},錯誤信息:{}", outTradeNo, response.getMsg());
            throw new AlipayApiException("訂單查詢失?。? + response.getMsg());
        }
    }
}這個工具類里,我加了詳細(xì)注釋,你一看就懂。這里重點(diǎn)說兩個點(diǎn):
- AlipayClient 初始化:用 @PostConstruct 注解,項(xiàng)目啟動時只初始化一次,不用每次調(diào)用都新建 —— 這是個小優(yōu)化,避免重復(fù)創(chuàng)建對象浪費(fèi)資源;
 - 同步回調(diào) vs 異步回調(diào):
 
- 同步回調(diào)(returnUrl):用戶支付成功后,支付寶會跳回這個地址,比如你項(xiàng)目的 “支付成功頁”—— 這個回調(diào)不可靠!比如用戶支付后沒等跳轉(zhuǎn)就關(guān)了頁面,同步回調(diào)就不會觸發(fā);
 - 異步回調(diào)(notifyUrl):不管用戶關(guān)沒關(guān)頁面,支付寶都會往這個地址發(fā)請求,告訴你 “這個訂單付了”—— 這才是可靠的,后臺處理訂單狀態(tài)(比如把 “待支付” 改成 “已支付”)一定要用異步回調(diào)!
 
三、核心功能實(shí)現(xiàn):支付、回調(diào)、查訂單
工具類寫好了,現(xiàn)在來寫接口 —— 咱實(shí)現(xiàn)三個核心功能:創(chuàng)建訂單發(fā)起支付、處理異步回調(diào)、處理同步回調(diào),再加個查詢訂單狀態(tài)的接口,齊活。
3.1 先搞個 “訂單服務(wù)”:模擬生成訂單
實(shí)際項(xiàng)目里,訂單肯定要存數(shù)據(jù)庫,但咱這篇主打 “支付集成”,就簡化一下:用一個 Map 模擬數(shù)據(jù)庫,存訂單信息。
新建 service 包,里面建 OrderService.java:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
 * 訂單服務(wù)(模擬)
 * 實(shí)際項(xiàng)目里這里要連數(shù)據(jù)庫,咱先用Map存
 */
@Slf4j
@Service
public class OrderService {
    // 模擬數(shù)據(jù)庫:key=商戶訂單號,value=訂單信息(用Map存訂單的各種字段)
    private static final Map<String, Map<String, String>> ORDER_MAP = new HashMap<>();
    /**
     * 創(chuàng)建訂單(生成唯一訂單號,存到Map里)
     * @param amount 訂單金額(元)
     * @param title 訂單標(biāo)題
     * @return 商戶訂單號
     */
    public String createOrder(String amount, String title) {
        // 1. 生成唯一商戶訂單號(用UUID,再截取一下,避免太長)
        String outTradeNo = "ORDER_" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        
        // 2. 封裝訂單信息
        Map<String, String> order = new HashMap<>();
        order.put("outTradeNo", outTradeNo);
        order.put("totalAmount", amount);
        order.put("subject", title);
        order.put("status", "WAIT_PAY"); // 訂單狀態(tài):WAIT_PAY(待支付)、TRADE_SUCCESS(已支付)、CLOSED(已關(guān)閉)
        
        // 3. 存到“模擬數(shù)據(jù)庫”
        ORDER_MAP.put(outTradeNo, order);
        log.info("創(chuàng)建訂單成功!outTradeNo:{},訂單信息:{}", outTradeNo, order);
        
        return outTradeNo;
    }
    /**
     * 更新訂單狀態(tài)(比如支付成功后,把狀態(tài)改成TRADE_SUCCESS)
     * @param outTradeNo 商戶訂單號
     * @param newStatus 新狀態(tài)
     * @return 是否更新成功
     */
    public boolean updateOrderStatus(String outTradeNo, String newStatus) {
        Map<String, String> order = ORDER_MAP.get(outTradeNo);
        if (order == null) {
            log.error("更新訂單狀態(tài)失?。∮唵尾淮嬖冢簕}", outTradeNo);
            return false;
        }
        // 記錄舊狀態(tài),方便日志排查
        String oldStatus = order.get("status");
        order.put("status", newStatus);
        log.info("更新訂單狀態(tài)成功!outTradeNo:{},舊狀態(tài):{},新狀態(tài):{}", outTradeNo, oldStatus, newStatus);
        return true;
    }
    /**
     * 查詢訂單信息
     * @param outTradeNo 商戶訂單號
     * @return 訂單信息
     */
    public Map<String, String> getOrder(String outTradeNo) {
        return ORDER_MAP.get(outTradeNo);
    }
}這個服務(wù)很簡單,就是模擬訂單的創(chuàng)建、更新、查詢 —— 實(shí)際項(xiàng)目里,你把 Map 換成 MySQL、Redis 就行,邏輯差不多。
3.2 寫支付接口:發(fā)起支付請求
新建 controller 包,里面建 PayController.java,先寫 “創(chuàng)建支付” 的接口:
import com.alipay.api.AlipayApiException;
import com.xxx.pay.service.OrderService;
import com.xxx.pay.util.AlipayUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * 支付相關(guān)接口
 */
@Slf4j
@RestController
@RequestMapping("/pay")
public class PayController {
    @Autowired
    private AlipayUtil alipayUtil;
    @Autowired
    private OrderService orderService;
    /**
     * 發(fā)起支付寶支付(前端調(diào)用這個接口,獲取跳轉(zhuǎn)鏈接)
     * @param amount 訂單金額(比如1.00)
     * @param title 訂單標(biāo)題(比如“測試訂單”)
     * @param response 用response把支付寶返回的HTML寫出去,前端直接跳轉(zhuǎn)
     */
    @GetMapping("/create")
    public void createPay(
            @RequestParam String amount,
            @RequestParam String title,
            HttpServletResponse response
    ) {
        try {
            // 1. 先創(chuàng)建訂單(生成商戶訂單號)
            String outTradeNo = orderService.createOrder(amount, title);
            
            // 2. 配置回調(diào)地址(注意:沙箱測試時,notifyUrl要能被外網(wǎng)訪問!后面講怎么解決)
            String returnUrl = "http://localhost:8080/pay/success"; // 同步回調(diào):本地測試用localhost
            String notifyUrl = "http://xxx.xxx.xxx.xxx:8080/pay/notify"; // 異步回調(diào):要外網(wǎng)能訪問的地址
            
            // 3. 調(diào)用支付寶工具類,獲取支付HTML
            String payHtml = alipayUtil.createPagePay(outTradeNo, amount, title, returnUrl, notifyUrl);
            
            // 4. 把HTML寫回前端(前端渲染后會自動跳轉(zhuǎn)到支付寶支付頁)
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.write(payHtml);
            out.flush();
            out.close();
            
        } catch (AlipayApiException e) {
            log.error("發(fā)起支付失??!金額:{},標(biāo)題:{}", amount, title, e);
            throw new RuntimeException("支付發(fā)起失敗,請稍后再試");
        } catch (IOException e) {
            log.error("輸出支付HTML失敗!", e);
            throw new RuntimeException("支付頁面加載失敗,請稍后再試");
        }
    }
}這里有個 “大坑” 要提前說:異步回調(diào)地址(notifyUrl)必須能被外網(wǎng)訪問!因?yàn)橹Ц秾毜姆?wù)器在公網(wǎng),它要往你的 notifyUrl 發(fā)請求 —— 如果你的項(xiàng)目跑在本地(localhost),支付寶根本訪問不到,異步回調(diào)就不會觸發(fā)。怎么解決?兩種方法:
- 用 “內(nèi)網(wǎng)穿透工具”:比如花生殼、Ngrok、Sunny-Ngrok—— 把你本地的 8080 端口映射到公網(wǎng)上,生成一個外網(wǎng)地址(比如 “http://abc123.ngrok.io”),然后 notifyUrl 就填 “http://abc123.ngrok.io/pay/notify”;
 - 把項(xiàng)目部署到云服務(wù)器:比如阿里云、騰訊云,用服務(wù)器的公網(wǎng) IP 當(dāng) notifyUrl—— 新手推薦用內(nèi)網(wǎng)穿透,簡單快捷,不用買服務(wù)器。
 
3.3 處理同步回調(diào):給用戶看 “支付成功頁”
用戶支付成功后,支付寶會跳回 returnUrl 對應(yīng)的接口 —— 這個接口不用處理復(fù)雜邏輯,主要是給用戶展示 “支付成功” 的頁面,順便可以查一下訂單狀態(tài),確認(rèn)是不是真的支付成功了。
在 PayController 里加這個接口:
/**
 * 同步回調(diào)接口(支付成功后,支付寶跳回這里)
 * 注意:這個接口的參數(shù)名是固定的,支付寶會把訂單信息以GET參數(shù)的形式傳過來
 * @param outTradeNo 商戶訂單號
 * @param tradeNo 支付寶交易號
 * @param tradeStatus 支付狀態(tài)
 * @return 支付成功頁面(這里用字符串模擬,實(shí)際項(xiàng)目里返回HTML頁面)
 */
@GetMapping("/success")
public String paySuccess(
        @RequestParam String outTradeNo,
        @RequestParam String tradeNo,
        @RequestParam String tradeStatus
) {
    log.info("收到同步回調(diào)!outTradeNo:{},tradeNo:{},tradeStatus:{}", outTradeNo, tradeNo, tradeStatus);
    
    // 這里可以查一下訂單狀態(tài),確認(rèn)是不是真的支付成功(防止偽造同步回調(diào))
    Map<String, String> order = orderService.getOrder(outTradeNo);
    if (order == null) {
        log.error("同步回調(diào):訂單不存在!outTradeNo:{}", outTradeNo);
        return "支付失敗:訂單不存在";
    }
    
    // 只有當(dāng)訂單狀態(tài)是TRADE_SUCCESS,才認(rèn)為是真的支付成功
    if ("TRADE_SUCCESS".equals(tradeStatus)) {
        return String.format("""
                <h1>支付成功啦!</h1>
                <p>訂單號:%s</p>
                <p>支付寶交易號:%s</p>
                <p>訂單金額:%s 元</p>
                <p>點(diǎn)擊<a href="http://localhost:8080">返回首頁</a></p>
                """, outTradeNo, tradeNo, order.get("totalAmount"));
    } else {
        log.warn("同步回調(diào):支付狀態(tài)異常!outTradeNo:{},tradeStatus:{}", outTradeNo, tradeStatus);
        return "支付狀態(tài)異常,請聯(lián)系客服";
    }
}這里要注意:同步回調(diào)的參數(shù)名是支付寶固定的,比如 outTradeNo、tradeNo、tradeStatus—— 你不能自己改參數(shù)名,不然接收不到值。實(shí)際項(xiàng)目里,這個接口應(yīng)該返回一個 HTML 頁面(比如用 Thymeleaf、Freemarker),這里用字符串模擬一下,意思到了就行。
3.4 處理異步回調(diào):后臺真正的 “訂單狀態(tài)更新”
這是整個支付流程里最關(guān)鍵的一步!不管用戶有沒有看到同步回調(diào)的頁面,支付寶都會往 notifyUrl 發(fā)請求 —— 這個請求是支付寶服務(wù)器發(fā)的,可靠度極高,后臺必須在這里處理 “訂單狀態(tài)更新”“庫存扣減”“發(fā)短信通知” 這些核心邏輯。
在 PayController 里加異步回調(diào)接口,重點(diǎn)注意:
- 異步回調(diào)是 POST 請求;
 - 必須驗(yàn)證 “這個請求是不是真的來自支付寶”(驗(yàn)簽);
 - 處理完后,必須返回 “success” 字符串給支付寶,不然支付寶會一直重試(默認(rèn)重試 25 小時)。
 
先寫異步回調(diào)接口,然后咱再講驗(yàn)簽:
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.internal.util.AlipaySignature;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
 * 異步回調(diào)接口(支付寶主動發(fā)請求到這里,處理訂單狀態(tài))
 * 注意:這個接口必須是POST,且不能有登錄攔截(支付寶發(fā)請求不會帶token)
 */
@PostMapping("/notify")
public String payNotify(HttpServletRequest request) {
    try {
        // 1. 從request中獲取所有參數(shù)(支付寶會把訂單信息以POST參數(shù)的形式傳過來)
        Map<String, String> params = new HashMap<>();
        Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String name = parameterNames.nextElement();
            String value = request.getParameter(name);
            params.put(name, value);
        }
        log.info("收到異步回調(diào)請求!參數(shù):{}", JSONObject.toJSONString(params));
        // 2. 驗(yàn)簽:驗(yàn)證這個請求是不是真的來自支付寶(關(guān)鍵!防止別人偽造回調(diào))
        // 注意:要排除sign_type參數(shù)(支付寶驗(yàn)簽時不需要這個)
        String sign = params.get("sign");
        params.remove("sign_type");
        boolean verifyResult = AlipaySignature.rsaCheckV1(
                params,
                alipayPublicKey, // 支付寶公鑰(從配置文件拿,這里咱先直接用,后面優(yōu)化)
                charset, // 編碼
                signType // 簽名類型(RSA2)
        );
        // 3. 驗(yàn)簽失?。悍祷豧ail,支付寶會重試
        if (!verifyResult) {
            log.error("異步回調(diào)驗(yàn)簽失??!參數(shù):{}", JSONObject.toJSONString(params));
            return "fail";
        }
        // 4. 驗(yàn)簽成功:處理訂單邏輯
        String outTradeNo = params.get("out_trade_no"); // 商戶訂單號
        String tradeStatus = params.get("trade_status"); // 支付狀態(tài)
        String totalAmount = params.get("total_amount"); // 支付金額(可以和訂單金額對比,防止篡改)
        // 4.1 先查訂單是否存在
        Map<String, String> order = orderService.getOrder(outTradeNo);
        if (order == null) {
            log.error("異步回調(diào):訂單不存在!outTradeNo:{}", outTradeNo);
            return "fail"; // 返回fail,支付寶會重試(可能是訂單還沒創(chuàng)建好)
        }
        // 4.2 對比支付金額和訂單金額(防止別人改金額,比如訂單1元,實(shí)際付了0.1元)
        String orderAmount = order.get("totalAmount");
        if (!orderAmount.equals(totalAmount)) {
            log.error("異步回調(diào):金額不匹配!outTradeNo:{},訂單金額:{},支付金額:{}", outTradeNo, orderAmount, totalAmount);
            return "fail";
        }
        // 4.3 根據(jù)支付狀態(tài)處理訂單(TRADE_SUCCESS表示支付成功)
        if ("TRADE_SUCCESS".equals(tradeStatus)) {
            // 4.3.1 檢查訂單當(dāng)前狀態(tài):如果已經(jīng)是已支付,就不用再處理了(防止重復(fù)回調(diào))
            if ("TRADE_SUCCESS".equals(order.get("status"))) {
                log.info("異步回調(diào):訂單已處理過!outTradeNo:{}", outTradeNo);
                return "success"; // 返回success,告訴支付寶別再發(fā)了
            }
            // 4.3.2 處理核心邏輯:更新訂單狀態(tài)、扣庫存、發(fā)短信等(實(shí)際項(xiàng)目里這里要加事務(wù))
            boolean updateResult = orderService.updateOrderStatus(outTradeNo, "TRADE_SUCCESS");
            if (updateResult) {
                log.info("異步回調(diào):訂單處理成功!outTradeNo:{},tradeStatus:{}", outTradeNo, tradeStatus);
                // 這里可以加其他邏輯:比如調(diào)用庫存服務(wù)扣庫存、調(diào)用短信服務(wù)發(fā)通知
                // stockService.reduceStock(outTradeNo);
                // smsService.sendPaySuccessSms(order.get("phone"));
                return "success"; // 處理成功,返回success
            } else {
                log.error("異步回調(diào):更新訂單狀態(tài)失??!outTradeNo:{}", outTradeNo);
                return "fail"; // 返回fail,支付寶重試
            }
        } else if ("TRADE_CLOSED".equals(tradeStatus)) {
            // 訂單關(guān)閉(比如用戶超時未支付,支付寶關(guān)閉訂單)
            orderService.updateOrderStatus(outTradeNo, "CLOSED");
            log.info("異步回調(diào):訂單已關(guān)閉!outTradeNo:{}", outTradeNo);
            return "success";
        }
        // 其他狀態(tài)(比如TRADE_PENDING,待支付):不用處理,返回success
        log.info("異步回調(diào):無需處理的狀態(tài)!outTradeNo:{},tradeStatus:{}", outTradeNo, tradeStatus);
        return "success";
    } catch (Exception e) {
        log.error("異步回調(diào)處理異常!", e);
        return "fail"; // 出現(xiàn)異常,返回fail,支付寶重試
    }
}這里有幾個 “保命級” 注意點(diǎn),必須記牢:
- 驗(yàn)簽絕對不能少!如果不驗(yàn)簽,別人隨便發(fā)個 POST 請求到你的 notifyUrl,就能偽造 “支付成功”,把你數(shù)據(jù)庫里的訂單改成 “已支付”—— 這要是電商項(xiàng)目,損失就大了;
 - 處理重復(fù)回調(diào):支付寶可能會因?yàn)榫W(wǎng)絡(luò)問題,多次發(fā)同一個異步回調(diào)(比如第一次你沒返回 success),所以必須先查訂單當(dāng)前狀態(tài),已經(jīng)處理過的就直接返回 success;
 - 金額校驗(yàn):要對比支付寶傳過來的 “total_amount” 和你訂單里的金額,防止別人篡改金額(比如用 Fiddler 改請求參數(shù),把 100 元改成 1 元);
 - 返回值必須是 “success” 或 “fail”:不能返回其他字符串(比如 “ok”“成功”),不然支付寶會認(rèn)為你沒收到,一直重試 —— 我之前見過有人返回 “success!”(多了個感嘆號),結(jié)果支付寶重試了 25 小時,數(shù)據(jù)庫里多了一堆重復(fù)記錄;
 - 接口不能有攔截器:比如你的項(xiàng)目加了登錄攔截(Token 驗(yàn)證),支付寶發(fā)請求不會帶 Token,會被攔截 —— 所以要把 notifyUrl 排除在攔截器之外(比如用 SpringSecurity 的 permitAll ())。
 
3.5 寫個 “查詢訂單” 接口:排查問題用
有時候可能會出現(xiàn) “用戶說付了錢,但訂單還是待支付” 的情況 —— 這時候就需要查一下支付寶那邊的訂單狀態(tài),看看是真沒付,還是回調(diào)沒收到。
在 PayController 里加查詢接口:
/**
 * 查詢訂單支付狀態(tài)(給前端用,或者自己排查問題用)
 * @param outTradeNo 商戶訂單號
 * @return 訂單信息和支付狀態(tài)
 */
@GetMapping("/query")
public Map<String, String> queryOrder(@RequestParam String outTradeNo) {
    try {
        // 1. 先查本地訂單
        Map<String, String> order = orderService.getOrder(outTradeNo);
        if (order == null) {
            Map<String, String> result = new HashMap<>();
            result.put("code", "404");
            result.put("msg", "訂單不存在");
            return result;
        }
        // 2. 查支付寶那邊的支付狀態(tài)(如果本地是待支付,就查一下支付寶)
        if ("WAIT_PAY".equals(order.get("status"))) {
            String alipayStatus = alipayUtil.queryTradeStatus(outTradeNo);
            // 如果支付寶那邊已經(jīng)支付成功,就更新本地訂單狀態(tài)
            if ("TRADE_SUCCESS".equals(alipayStatus)) {
                orderService.updateOrderStatus(outTradeNo, "TRADE_SUCCESS");
                order.put("status", "TRADE_SUCCESS");
            }
            order.put("alipay_status", alipayStatus); // 加上支付寶的狀態(tài)
        }
        // 3. 返回訂單信息
        Map<String, String> result = new HashMap<>();
        result.put("code", "200");
        result.put("msg", "查詢成功");
        result.put("order", JSONObject.toJSONString(order));
        return result;
    } catch (AlipayApiException e) {
        log.error("查詢訂單失??!outTradeNo:{}", outTradeNo, e);
        Map<String, String> result = new HashMap<>();
        result.put("code", "500");
        result.put("msg", "查詢失?。? + e.getMessage());
        return result;
    }
}這個接口很實(shí)用:比如用戶支付后,同步回調(diào)沒跳回來,前端可以每隔 3 秒調(diào)用一次這個接口,查訂單狀態(tài) —— 如果查到 “已支付”,就給用戶展示成功頁面。
四、測試環(huán)節(jié):從支付到回調(diào)全流程跑通
代碼寫好了,現(xiàn)在咱來測試一下 —— 別等上線了才發(fā)現(xiàn)問題,沙箱環(huán)境就是用來 “踩坑” 的。
4.1 準(zhǔn)備工作:內(nèi)網(wǎng)穿透(關(guān)鍵?。?/h3>
前面說了,異步回調(diào)需要外網(wǎng)地址,所以咱先搞個內(nèi)網(wǎng)穿透:
- 下載 “Sunny-Ngrok”(新手友好,免費(fèi)版夠用),注冊賬號后,新建一個 “隧道”:
 
- 隧道類型:HTTP;
 - 本地端口:8080(你項(xiàng)目的端口);
 - 本地 IP:127.0.0.1;
 
- 啟動隧道,會生成一個外網(wǎng)地址,比如 “http://abc123.sunnynet.cc”;
 - 把 PayController 里的 notifyUrl 改成 “http://abc123.sunnynet.cc/pay/notify”—— 同步回調(diào)的 returnUrl 可以用localhost,因?yàn)槭怯脩魹g覽器跳轉(zhuǎn),不用外網(wǎng)。
 
4.2 啟動項(xiàng)目,發(fā)起支付
- 啟動 SpringBoot 項(xiàng)目,看日志有沒有 “支付寶客戶端初始化完成” 的提示 —— 有就說明配置沒問題;
 - 用瀏覽器訪問支付接口:“http://localhost:8080/pay/create?amount=1.00&title=測試訂單 - 20240520”;
 - 訪問后,瀏覽器會自動跳轉(zhuǎn)到支付寶沙箱支付頁 —— 這時候用沙箱環(huán)境的 “買家賬號” 登錄(沙箱支付寶 APP);
 - 登錄后,輸入沙箱買家的支付密碼(沙箱頁面里有,比如 “111111”),點(diǎn)擊 “確認(rèn)支付”。
 
4.3 查看回調(diào)和訂單狀態(tài)
- 支付成功后,瀏覽器會跳回同步回調(diào)地址(http://localhost:8080/pay/success),展示 “支付成功啦!” 的頁面 —— 這說明同步回調(diào)沒問題;
 - 看項(xiàng)目日志,會有 “收到異步回調(diào)請求!” 的日志,并且會打印 “訂單處理成功”—— 這說明異步回調(diào)也沒問題;
 - 調(diào)用查詢接口:“http://localhost:8080/pay/query?outTradeNo=你剛才的訂單號”(訂單號在日志里能找到,比如 “ORDER_1234567890abcdef”),返回的訂單狀態(tài)應(yīng)該是 “TRADE_SUCCESS”。
 
如果這三步都沒問題,說明整個流程跑通了!要是中間出了問題,先看日志:
- 跳不到支付寶頁:檢查 APPID、私鑰是不是填錯了,網(wǎng)關(guān)是不是沙箱的;
 - 支付成功后沒異步回調(diào):檢查 notifyUrl 是不是外網(wǎng)能訪問,有沒有被攔截器擋住;
 - 驗(yàn)簽失?。簷z查支付寶公鑰是不是填錯了,參數(shù)里有沒有多傳 sign_type。
 
五、實(shí)際項(xiàng)目里的 “進(jìn)階優(yōu)化”:別只滿足于 “能用”
上面的代碼能跑通,但在實(shí)際項(xiàng)目里,還有很多要優(yōu)化的地方 —— 咱不能只滿足于 “能用”,還要考慮 “安全”“性能”“可維護(hù)性”。
5.1 敏感信息加密:別把私鑰明文存配置
前面咱把商戶私鑰明文存在 application.yml 里 —— 這很危險!如果配置文件被泄露,別人就能用你的私鑰偽造支付請求,損失就大了。
優(yōu)化方案:
- 用配置中心:比如 Nacos、Apollo,把敏感信息存在配置中心,并且加密存儲(Nacos 支持配置加密);
 - 本地加密:如果不用配置中心,就把私鑰加密后存在配置文件,項(xiàng)目啟動時解密 —— 比如用 AES 加密,密鑰存在環(huán)境變量里;
 - 用 Spring Cloud Config:結(jié)合 Git,把配置文件存在 Git 倉庫,并且加密敏感字段。
 
舉個簡單的例子,用 Spring 的 “EnvironmentPostProcessor” 解密配置:
// 新建一個解密處理器,項(xiàng)目啟動時解密私鑰
publicclass AlipayConfigDecryptProcessor implements EnvironmentPostProcessor {
    @Override
    publicvoid postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 從環(huán)境變量拿解密密鑰(別存在代碼里)
        String decryptKey = System.getenv("ALIPAY_DECRYPT_KEY");
        
        // 從配置里拿加密后的私鑰
        String encryptedPrivateKey = environment.getProperty("alipay.merchant-private-key");
        
        // 解密(這里用AES解密,實(shí)際項(xiàng)目里用更安全的算法)
        String decryptedPrivateKey = AESUtil.decrypt(encryptedPrivateKey, decryptKey);
        
        // 把解密后的私鑰放回環(huán)境變量
        Map<String, Object> map = new HashMap<>();
        map.put("alipay.merchant-private-key", decryptedPrivateKey);
        MapPropertySource source = new MapPropertySource("alipayDecryptSource", map);
        environment.getPropertySources().addFirst(source);
    }
}然后在 META-INF/spring.factories 里注冊這個處理器,項(xiàng)目啟動時就會自動解密私鑰。
5.2 加分布式鎖:防止重復(fù)回調(diào)導(dǎo)致的 “超賣”
如果你的項(xiàng)目是分布式部署(多臺服務(wù)器),可能會出現(xiàn) “支付寶同時給兩臺服務(wù)器發(fā)同一個異步回調(diào)” 的情況 —— 這時候如果沒加鎖,就可能導(dǎo)致 “超賣”(比如一個訂單扣兩次庫存)。
優(yōu)化方案:用分布式鎖,比如 Redis 分布式鎖、ZooKeeper 分布式鎖,在處理異步回調(diào)前先加鎖,處理完再釋放鎖。
舉個 Redis 分布式鎖的例子(用 Redisson):
// 在PayController里注入RedissonClient
@Autowired
private RedissonClient redissonClient;
// 異步回調(diào)接口里加鎖
@PostMapping("/notify")
public String payNotify(HttpServletRequest request) {
    String outTradeNo = null;
    RLock lock = null;
    try {
        // 前面的參數(shù)獲取、驗(yàn)簽代碼...
        outTradeNo = params.get("out_trade_no");
        
        // 加分布式鎖:鎖的key用訂單號,確保一個訂單只有一個線程處理
        lock = redissonClient.getLock("alipay:notify:lock:" + outTradeNo);
        boolean isLock = lock.tryLock(5, 30, TimeUnit.SECONDS); // 等待5秒,持有30秒
        if (!isLock) {
            log.warn("異步回調(diào):獲取鎖失敗,可能有其他線程在處理!outTradeNo:{}", outTradeNo);
            return"fail"; // 返回fail,支付寶重試
        }
        
        // 加鎖成功后,再查一次訂單狀態(tài)(防止其他線程已經(jīng)處理了)
        Map<String, String> order = orderService.getOrder(outTradeNo);
        if ("TRADE_SUCCESS".equals(order.get("status"))) {
            return"success";
        }
        
        // 處理訂單邏輯...
        
    } catch (Exception e) {
        // 異常處理...
    } finally {
        // 釋放鎖(只有持有鎖的線程才能釋放)
        if (lock != null && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
    return"success";
}5.3 日志和監(jiān)控:出問題能快速定位
支付模塊是核心模塊,出了問題要能快速定位 —— 所以日志和監(jiān)控必須做好。
優(yōu)化方案:
- 詳細(xì)日志:在支付、回調(diào)、查詢的關(guān)鍵步驟加日志,打印訂單號、參數(shù)、結(jié)果 —— 比如 “發(fā)起支付請求”“異步回調(diào)驗(yàn)簽成功”“訂單處理失敗”,方便后面查問題;
 - 日志歸檔:用 ELK(Elasticsearch+Logstash+Kibana)把日志收集起來,支持按訂單號、時間范圍查詢 —— 比如用戶說 “我付了錢沒到賬”,你用訂單號在 Kibana 里搜日志,就能快速看到哪里出了問題;
 - 監(jiān)控告警:用 Prometheus+Grafana 監(jiān)控支付接口的成功率、響應(yīng)時間 —— 如果支付成功率突然下降,或者回調(diào)失敗率升高,就發(fā)告警(比如短信、釘釘),及時處理。
 
5.4 異常處理和重試:別讓訂單 “卡住”
實(shí)際項(xiàng)目里,可能會出現(xiàn) “支付寶回調(diào)時,數(shù)據(jù)庫剛好宕機(jī)”“扣庫存時服務(wù)超時” 的情況 —— 這時候要做好異常處理和重試,別讓訂單一直 “卡住” 在待支付狀態(tài)。
優(yōu)化方案:
- 本地重試:對于非致命異常(比如數(shù)據(jù)庫連接超時),可以本地重試幾次(比如重試 3 次,每次間隔 1 秒);
 - 定時任務(wù)補(bǔ)償:寫一個定時任務(wù),每隔 5 分鐘查詢一次 “待支付” 狀態(tài)的訂單,調(diào)用支付寶查詢接口查支付狀態(tài) —— 如果支付寶那邊已經(jīng)支付成功,就手動更新本地訂單狀態(tài);
 - 死信隊(duì)列:對于重試多次還是失敗的訂單,把訂單號放進(jìn)死信隊(duì)列,由人工處理(比如發(fā)郵件給運(yùn)維,讓運(yùn)維排查問題)。
 
六、總結(jié)
看到這里,你已經(jīng)掌握了 SpringBoot 集成支付寶支付的全流程 —— 咱再回顧一下:
- 前置準(zhǔn)備:開通支付寶沙箱環(huán)境,生成密鑰,拿到 APPID、公鑰、網(wǎng)關(guān);
 - 項(xiàng)目配置:引入支付寶 SDK,寫配置文件,封裝工具類;
 - 核心功能:發(fā)起支付、處理同步回調(diào)、處理異步回調(diào)、查詢訂單;
 - 測試驗(yàn)證:用內(nèi)網(wǎng)穿透解決異步回調(diào)問題,跑通全流程;
 - 進(jìn)階優(yōu)化:敏感信息加密、分布式鎖、日志監(jiān)控、異常重試。
 
支付集成不難,難的是 “考慮周全”, 比如安全、異常、重試。希望這篇文章能幫你少踩坑,下次再遇到支付寶支付需求,直接翻出來用就行。















 
 
 











 
 
 
 