當前位置: 妍妍網 > 碼農

SpringBoot 對接支付寶完成掃碼支付,完整流程梳理!

2024-02-19碼農

點選「 IT碼徒 」, 關註,置頂 公眾號

每日技術幹貨,第一時間送達!

文章目錄

  1. 支付方式選擇

  2. 互動流程

    1. 對接準備

    2. 加密解密 + 簽名驗簽

    3. 沙箱環境

  3. 內網穿透

  4. 二維碼

  5. 下單

  6. 異步通知回呼

  7. 查詢支付結果

  8. 退款

  9. 通用版SDK

需求:系統A對接支付寶,實作支持使用者掃碼支付

1、支付方式選擇

對接的API文件:

  • https://open.alipay.com/api

  • 可選的支付方式有:

  • 掃碼付 :出示付款碼或者使用者掃碼付款

  • APP支付 :在APP中喚起支付寶

  • 手機網站支付 :在移動端網頁中喚起支付寶 App 或支付寶網頁

  • 電腦網站支付 :在PC端喚起支付寶App或者網頁登入支付寶帳戶

  • 刷臉付 :需硬體支持

  • 商家扣款 :類似每月會員扣款

  • 預授權支付 :凍結對應額度,交易完成後給商家

  • JSAPI支付 :小程式

  • 這裏選擇掃碼付的方式,點選下單後,返回支付二維碼,使用者掃碼支付。

    2、互動流程

    畫個下單流程的時序圖:

    大致流程:

  • 使用者下單,系統A組裝資訊後(訂單資訊、回呼地址、簽名),呼叫支付寶預下單介面,返回二維碼連結

  • 系統A將二維碼連結轉二維碼圖片

  • 使用者掃碼,喚醒本地支付寶,完成支付

  • 支付寶返回支付成功資訊給使用者

  • 支付寶異步通知系統A支付成功的訊息(回呼地址),如果使用者支付成功,支付寶就呼叫回呼地址的API,回呼介面中自然是系統A收到使用者支付成功訊息後的動作

  • 上一步如果通知失敗,比如網路異常或支付寶呼叫異步通知介面時系統A正好掛了 ⇒ 可主動調支付寶提供的查詢支付結果介面,或者加定時任務輪詢來查詢交易狀態,如3s-5s

  • 還可以考慮在第一步請求支付寶介面時加上二維碼的有效時間,過期就重新發起

  • 查詢支付結果流程:

    退款流程同上查詢支付結果。PS:註意下單、退款過程中,相關訂單的業務數據落庫到系統A。

    3、對接準備

    1)加密解密 + 簽名驗簽

    支付資訊不能在網路上明文傳輸,以防被篡改。系統A到支付寶的方向,采用:

  • 支付寶公鑰加密 + 系統A的私鑰簽名(系統A做的事)

  • 支付寶私鑰解密 + 系統A的公鑰驗簽(收到資訊後,支付寶做的事)

  • 同理,支付寶返回支付結果時,就是在支付寶中用系統A的公鑰加密+支付寶的私鑰簽名,傳輸到系統A後,則是先用支付寶的公鑰驗簽,再用系統A的私鑰解密支付結果

    2)沙箱環境

    偵錯過程中,可采用支付寶提供的沙箱環境,點選右上角控制台,登入後選擇沙箱:

    這裏有一套可偵錯的APPID、系統A的公鑰、金鑰、支付寶的公鑰、支付寶的閘道器地址,以及商家帳戶和使用者帳戶(用於後續登入沙箱版本支付寶APP完成支付)

    點選【沙箱工具】側資訊看板,下載沙箱版支付寶APP,等於上面的買家帳戶。

    3)內網穿透

    前面提到,使用者支付成功後,支付寶需要回呼系統A介面來通知系統A,但我的開發環境在內網,支付寶存取不到,考慮做內網穿透,讓支付寶通知到一個中轉地址,再由中轉地址到我的內網。穿透工具選擇cpolar,下載地址 https://dashboard.cpolar.com/get-started ,下載後,解壓並安裝msi包

    雙擊exe檔,執行認證:

    cpolar authtoken xxxx

    建立隧道,建立連結:

    cpolar http 9527
    //返回結果
    Forwarding http://maggie.cpolar.io -> localhost:9527
    Forwarding https://maggie.cpolar.io -> localhost:9527

    轉發成功。此時,給支付寶存取forward的地址即可,比如系統A的異步通知介面:

    localhost:9527/notify

    那就是:

    http://maggie.cpolar.io/notify

    4、二維碼

    二維碼是訊息的載體。平時玩可直接在草料二維碼UI頁面,這裏需要給系統A的訂單服務用程式碼生成二維碼。二維碼中的資訊自然是支付寶預下單返回的url。

    Java生成二維碼可整合zxing庫,但這樣得自己兩層for填充方格子,這裏選擇hutool工具類別庫(對zxing的二次封裝),引入依賴:

    <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.22</version>
    </dependency>
    <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.4.1</version>
    </dependency>

    呼叫方式:

    //生成直到url對應的二維碼,寬高均300像素,可到路徑,也可到Http響應
    QrCodeUtil.generate("https://url/path"300300"png", httpServletResponse.getOutPutStream());

    也可引入QrConfig物件,設定其他內容:

    QrConfig config = new QrConfig(300300);
    //糾錯級別
    config.setErrorCorrection(ErrorCorrectionLevel.H);
    //二維碼顏色
    config.setBackColor(Color.BLUE);
    QrCodeUtil.generate("https://www.baidu.com", config, new File("D:\\code.png"));

    5、下單

    支付寶提供的SDK 中已經對加簽驗簽邏輯做了封裝,使用 SDK 時傳入支付寶公鑰等內容可直接透過 SDK 自動進行加驗簽。 SDK文件地址: https://opendocs.alipay.com/open/54/103419?pathHash=d6bc7c2b 。支付寶提供了兩種SDK:

  • 通用版SDK

  • 簡易版SDK

  • 官網有通用版的API程式碼範例,這裏走簡易版的。引入簡易版SDK的依賴:

    <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
    <dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-easysdk</artifactId>
    <version>2.2.0</version>
    </dependency>

    在application.yml配置檔中統一寫金鑰、通知地址等(生產環境不要將私鑰資訊配置在源碼中,例如配置為常量或儲存在配置檔中,這樣源碼一丟,這些保密資訊都泄漏了,放安全區域或伺服器,執行時讀取即可)

    alipay:
    easy:
    protocol:https
    gatewayHost:openapi-sandbox.dl.alipaydev.com
    signType:RSA2
    appId:9021000133624745
    merchantPrivateKey:MIIEvQIBADANBgkqhkiG9w0B
    alipayPublicKey:MIIBIjANBgkqhkiG9w0BAQEFAAOC
    notifyUrl:http://maggie.cpolar.io/notify
    server:
    port:9527

    @ConfigurationProperties註解統一讀到:

    @Configuration
    @Data
    @ConfigurationProperties(prefix = "alipay.easy")
    public classAliPayConfigInfo{
    /**
    * 請求協定
    */

    private String protocol;
    /**
    * 請求閘道器
    */

    private String gatewayHost;
    /**
    * 簽名型別
    */

    private String signType;
    /**
    * 套用ID(來自支付寶申請)
    */

    private String appId;
    /**
    * 套用秘鑰
    */

    private String merchantPrivateKey;
    /**
    * 支付寶公鑰
    */

    private String alipayPublicKey;
    /**
    * 支付結果異步通知的地址
    */

    private String notifyUrl;
    /**
    * 設施AES秘鑰
    */

    private String encryptKey;
    }

    將配置處理成Config型別的Bean,方便後面傳入Config物件:

    @Configuration
    public classAliPayConfig{
    @Bean
    public Config config(AliPayConfigInfo configInfo){
    Config config = new Config();
    config.protocol = configInfo.getProtocol();
    config.gatewayHost = configInfo.getGatewayHost();
    config.signType = configInfo.getSignType();
    config.appId = configInfo.getAppId();
    config.merchantPrivateKey = configInfo.getMerchantPrivateKey();
    config.alipayPublicKey = configInfo.getAlipayPublicKey();
    config.notifyUrl = configInfo.getNotifyUrl();
    config.encryptKey = "";
    return config;
    }
    }

    寫下單介面,響應一個二維碼給前端,這裏業務數據、訂單編號直接寫死,只做示意:

    @RestController
    @Slf4j
    public classPayController{
    @Resource
    private Config config;
    /**
    * 收銀台點選結賬
    * 發起下單請求
    */

    @GetMapping("/pay")
    publicvoidpay(HttpServletRequest request, HttpServletResponse response)throws Exception {
    Factory.setOptions(config);
    //呼叫支付寶的介面
    AlipayTradePrecreateResponse payResponse = Factory.Payment.FaceToFace().preCreate("訂單主題:Mac筆記本""LS123qwe123""19999");
    //參照官方文件響應範例,解析返回結果
    String httpBodyStr = payResponse.getHttpBody();
    JSONObject jsonObject = JSONObject.parseObject(httpBodyStr);
    String qrUrl = jsonObject.getJSONObject("alipay_trade_precreate_response").get("qr_code").toString();
    QrCodeUtil.generate(qrUrl, 300300"png", response.getOutputStream());
    }
    }

    6、異步通知回呼

    異步回呼參考文件: https://opendocs.alipay.com/open/194/103296?pathHash=e43f422e&ref=api ,實作先全放Controller層了:

    @RestController
    @Slf4j
    public classPayController{
    @Resource
    private Config config;
    /**
    * 給支付寶的回呼介面
    */

    @PostMapping("/notify")
    publicvoidnotify(HttpServletRequest request, HttpServletResponse response)throws Exception {
    Map<String, String> params = new HashMap<>();
    //獲取支付寶POST過來反饋資訊,將異步通知中收到的待驗證所有參數都存放到map中
    Map<String, String[]> parameterMap = request.getParameterMap();
    for (String name : parameterMap.keySet()) {
    String[] values = parameterMap.get(name);
    String valueStr = "";
    for (int i = 0; i < values.length; i++) {
    valueStr = (i == values.length - 1) ? valueStr + values[i]
    : valueStr + values[i] + ",";
    }
    //亂碼解決
    valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
    params.put(name, valueStr);
    }
    //驗簽
    Boolean signResult = Factory.Payment.Common().verifyNotify(params);
    if (signResult) {
    log.info("收到支付寶發送的支付結果通知");
    String out_trade_no = request.getParameter("out_trade_no");
    log.info("交易流水號:{}", out_trade_no);
    //交易狀態
    String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
    //交易成功
    switch (trade_status) {
    case"TRADE_SUCCESS":
    //支付成功的業務邏輯,比如落庫,開vip許可權等
    log.info("訂單:{} 交易成功", out_trade_no);
    break;
    case"TRADE_FINISHED":
    log.info("交易結束,不可退款");
    //其余業務邏輯
    break;
    case"TRADE_CLOSED":
    log.info("超時未支付,交易已關閉,或支付完成後全額退款");
    //其余業務邏輯
    break;
    case"WAIT_BUYER_PAY":
    log.info("交易建立,等待買家付款");
    //其余業務邏輯
    break;
    }
    response.getWriter().write("success"); //返回success給支付寶,表示訊息我已收到,不用重調
    else {
    response.getWriter().write("fail"); ///返回fail給支付寶,表示訊息我沒收到,請重試
    }
    }
    }

    到此,看下效果,請求下單介面:

    用沙箱版app掃碼:

    支付,檢視余額:

    7、查詢支付結果

    主動查詢使用者的支付結果,訂單編號依然寫死:

    @RestController
    @Slf4j
    public classPayController{
    @Resource
    private Config config;
    @GetMapping("/query")
    public String query()throws Exception {
    Factory.setOptions(config);
    AlipayTradeQueryResponse result = Factory.Payment.Common().query("LS123qwe123");
    return result.getHttpBody();
    }
    }

    8、退款

    退款操作:

    @RestController
    @Slf4j
    public classPayController{
    @Resource
    private Config config;
    @GetMapping("/refund")
    public String refund()throws Exception {
    Factory.setOptions(config);
    AlipayTradeRefundResponse refundResponse = Factory.Payment.Common().refund("LS123qwe123""19999");
    return refundResponse.getHttpBody();
    }
    }

    9、通用版SDK

    官方文件就是以這個SDK為例的,貼個程式碼範例:

    privatestaticfinal String GATEWAY_URL = "https://openapi.alipaydev.com/gateway.do";
    privatestaticfinal String FORMAT = "JSON";
    privatestaticfinal String CHARSET = "UTF-8";
    //簽名方式
    privatestaticfinal String SIGN_TYPE = "RSA2";
    @Resource
    private AliPayConfig aliPayConfig;
    @Resource
    private OrdersMapper ordersMapper;
    @GetMapping("/pay"// &subject=xxx&traceNo=xxx&totalAmount=xxx
    publicvoidpay(AliPay aliPay, HttpServletResponse httpResponse)throws Exception {
    // 1. 建立Client,通用SDK提供的Client,負責呼叫支付寶的API
    AlipayClient alipayClient = new DefaultAlipayClient(GATEWAY_URL, aliPayConfig.getAppId(),
    aliPayConfig.getAppPrivateKey(), FORMAT, CHARSET, aliPayConfig.getAlipayPublicKey(), SIGN_TYPE);
    // 2. 建立 Request並設定Request參數
    AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 發送請求的 Request類
    request.setNotifyUrl(aliPayConfig.getNotifyUrl());
    JSONObject bizContent = new JSONObject();
    bizContent.set("out_trade_no", aliPay.getTraceNo()); // 我們自己生成的訂單編號
    bizContent.set("total_amount", aliPay.getTotalAmount()); // 訂單的總金額
    bizContent.set("subject", aliPay.getSubject()); // 支付的名稱
    bizContent.set("product_code""FAST_INSTANT_TRADE_PAY"); // 固定配置
    request.setBizContent(bizContent.toString());
    // 執行請求,拿到響應的結果,返回給瀏覽器
    String form = "";
    try {
    form = alipayClient.pageExecute(request).getBody(); // 呼叫SDK生成表單
    catch (AlipayApiException e) {
    e.printStackTrace();
    }
    httpResponse.setContentType("text/html;charset=" + CHARSET);
    httpResponse.getWriter().write(form);// 直接將完整的表單html輸出到頁面
    httpResponse.getWriter().flush();
    httpResponse.getWriter().close();
    }


    具體有業務數據邏輯的對接支付寶介面,可跳轉支付寶業務對接:

  • https://llg-notes.blog.csdn.net/article/details/130357977

  • 來源:blog.csdn.net/llg___/ article/details/135145886

    END

    PS:防止找不到本篇文章,可以收藏點贊,方便翻閱尋找哦。

    往期推薦