SpringBoot整合支付寶

2023-06-15 06:01:16

最近在做一個網站,後端採用了SpringBoot,需要整合支付寶進行線上支付,在這個過程中研究了大量支付寶的整合資料,也走了一些彎路,現在總結出來,相信你讀完也能輕鬆整合支付寶支付。

在開始整合支付寶支付之前,我們需要準備一個支付寶商家賬戶,如果是個人開發者,可以通過註冊公司或者讓有公司資質的單位進行授權,後續在整合相關API的時候需要提供這些資訊。

下面我以電腦網頁端線上支付為例,介紹整個從整合、測試到上線的具體流程。

1. 預期效果展示

在開始之前我們先看下我們要達到的最後效果,具體如下:

  1. 前端點選支付跳轉到支付寶介面
  2. 支付寶介面展示付款二維條碼
  3. 使用者手機端支付
  4. 完成支付,支付寶回撥開發者指定的url。

2. 開發流程

2.1 沙盒偵錯

支付寶為我們準備了完善的沙盒開發環境,我們可以先在沙盒環境偵錯好程式,後續新建好應用併成功上線後,把程式中對應的引數替換為線上引數即可。

1. 建立沙盒應用

直接進入 https://open.alipay.com/develop/sandbox/app 建立沙盒應用即可,

這裡因為是測試環境,我們就選擇系統預設金鑰就行了,下面選擇公鑰模式,另外應用閘道器地址就是使用者完成支付之後,支付寶會回撥的url。在開發環境中,我們可以採用內網穿透的方式,將我們本機的埠暴露在某個公網地址上,這裡推薦 https://natapp.cn/ ,可以免費註冊使用。

2. SpringBoot程式碼實現

在建立好沙盒應用,獲取到金鑰,APPID,商家賬戶PID等資訊之後,就可以在測試環境開發整合對應的API了。這裡我以電腦端支付API為例,介紹如何進行整合。

關於電腦網站支付的詳細產品介紹和API接入檔案可以參考:https://opendocs.alipay.com/open/repo-0038oa?ref=apihttps://opendocs.alipay.com/open/270/01didh?ref=api

  • 步驟1, 新增alipay sdk對應的Maven依賴。
<!-- alipay -->  
<dependency>  
   <groupId>com.alipay.sdk</groupId>  
   <artifactId>alipay-sdk-java</artifactId>  
   <version>4.35.132.ALL</version>  
</dependency>
  • 步驟2,新增支付寶下單、支付成功後同步呼叫和非同步呼叫的介面。

這裡需要注意,同步介面是使用者完成支付後會自動跳轉的地址,因此需要是Get請求。非同步介面,是使用者完成支付之後,支付寶會回撥來通知支付結果的地址,所以是POST請求。

@RestController  
@RequestMapping("/alipay")  
public class AliPayController {  
  
    @Autowired  
    AliPayService aliPayService;  
  
    @PostMapping("/order")  
    public GenericResponse<Object> placeOrderForPCWeb(@RequestBody AliPayRequest aliPayRequest) {  
        try {  
            return aliPayService.placeOrderForPCWeb(aliPayRequest);  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @PostMapping("/callback/async")  
    public String asyncCallback(HttpServletRequest request) {  
        return aliPayService.orderCallbackInAsync(request);  
    }  
  
    @GetMapping("/callback/sync")  
    public void syncCallback(HttpServletRequest request, HttpServletResponse response) {  
        aliPayService.orderCallbackInSync(request, response);  
    }  

}
  • 步驟3,實現Service層程式碼

這裡針對上面controller中的三個介面,分別完成service層對應的方法。下面是整個支付的核心流程,其中有些地方需要根據你自己的實際情況進行儲存訂單到DB或者檢查訂單狀態的操作,這個可以根據實際業務需求進行設計。

public class AliPayService {  
  
    @Autowired  
    AliPayHelper aliPayHelper;  
  
    @Resource  
    AlipayConfig alipayConfig;  
  
    @Transactional(rollbackFor = Exception.class)  
    public GenericResponse<Object> placeOrderForPCWeb(AliPayRequest aliPayRequest) throws IOException {  
        log.info("【請求開始-線上購買-交易建立】*********統一下單開始*********");  
  
        String tradeNo = aliPayHelper.generateTradeNumber();  
    
        String subject = "購買套餐1";  
        Map<String, Object> map = aliPayHelper.placeOrderAndPayForPCWeb(tradeNo, 100, subject);  
  
        if (Boolean.parseBoolean(String.valueOf(map.get("isSuccess")))) {  
            log.info("【請求開始-線上購買-交易建立】統一下單成功,開始儲存訂單資料");  
  
            //儲存訂單資訊  
            // 新增你自己的業務邏輯,主要是儲存訂單資料
  
            log.info("【請求成功-線上購買-交易建立】*********統一下單結束*********");  
            return new GenericResponse<>(ResponseCode.SUCCESS, map.get("body"));  
        }else{  
            log.info("【失敗:請求失敗-線上購買-交易建立】*********統一下單結束*********");  
            return new GenericResponse<>(ResponseCode.INTERNAL_ERROR, String.valueOf(map.get("subMsg")));  
        }  
    }  
  
    // sync return page  
    public void orderCallbackInSync(HttpServletRequest request, HttpServletResponse response) {  
        try {  
            OutputStream outputStream = response.getOutputStream();  
            //通過設定響應頭控制瀏覽器以UTF-8的編碼顯示資料,如果不加這句話,那麼瀏覽器顯示的將是亂碼  
            response.setHeader("content-type", "text/html;charset=UTF-8");  
            String outputData = "支付成功,請返回網站並重新整理頁面。";  
  
            /**  
             * data.getBytes()是一個將字元轉換成位元組陣列的過程,這個過程中一定會去查碼錶,  
             * 如果是中文的作業系統環境,預設就是查詢查GB2312的碼錶,  
             */  
            byte[] dataByteArr = outputData.getBytes("UTF-8");//將字元轉換成位元組陣列,指定以UTF-8編碼進行轉換  
            outputStream.write(dataByteArr);//使用OutputStream流向使用者端輸出位元組陣列  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    public String orderCallbackInAsync(HttpServletRequest request) {  
        try {  
            Map<String, String> map = aliPayHelper.paramstoMap(request);  
            String tradeNo = map.get("out_trade_no");  
            String sign = map.get("sign");  
            String content = AlipaySignature.getSignCheckContentV1(map);  
            boolean signVerified = aliPayHelper.CheckSignIn(sign, content);  
  
            // check order status  
            // 這裡在DB中檢查order的狀態,如果已經支付成功,無需再次驗證。
            if(從DB中拿到order,並且判斷order是否支付成功過){  
                log.info("訂單:" + tradeNo + " 已經支付成功,無需再次驗證。");  
                return "success";  
            }  
  
            //驗證業務資料是否一致  
            if(!checkData(map, order)){  
                log.error("返回業務資料驗證失敗,訂單:" + tradeNo );  
                return "返回業務資料驗證失敗";  
            }  
            //簽名驗證成功  
            if(signVerified){  
                log.info("支付寶簽名驗證成功,訂單:" + tradeNo);  
                // 驗證支付狀態  
                String tradeStatus = request.getParameter("trade_status");  
                if(tradeStatus.equals("TRADE_SUCCESS")){  
                    log.info("支付成功,訂單:"+tradeNo);  
			        // 更新訂單狀態,執行一些業務邏輯

                    return "success";  
                }else{  
                    System.out.println("支付失敗,訂單:" + tradeNo );  
                    return "支付失敗";  
                }  
            }else{  
                log.error("簽名驗證失敗,訂單:" + tradeNo );  
                return "簽名驗證失敗.";  
            }  
        } catch (IOException e) {  
            log.error("IO exception happened ", e);  
            throw new RuntimeException(ResponseCode.INTERNAL_ERROR, e.getMessage());  
        }  
    }  
  
  
    public boolean checkData(Map<String, String> map, OrderInfo order) {  
        log.info("【請求開始-交易回撥-訂單確認】*********校驗訂單確認開始*********");  
  
        //驗證訂單號是否準確,並且訂單狀態為待支付  
        if(驗證訂單號是否準確,並且訂單狀態為待支付){  
            float amount1 = Float.parseFloat(map.get("total_amount"));  
            float amount2 = (float) order.getOrderAmount();  
            //判斷金額是否相等  
            if(amount1 == amount2){  
                //驗證收款商戶id是否一致  
                if(map.get("seller_id").equals(alipayConfig.getPid())){  
                    //判斷appid是否一致  
                    if(map.get("app_id").equals(alipayConfig.getAppid())){  
                        log.info("【成功:請求開始-交易回撥-訂單確認】*********校驗訂單確認成功*********");  
                        return true;                    }  
                }  
            }  
        }  
        log.info("【失敗:請求開始-交易回撥-訂單確認】*********校驗訂單確認失敗*********");  
        return false;    }  
}
  • 步驟4,實現alipayHelper類。這個類裡面對支付寶的介面進行封裝。
public class AliPayHelper {  
  
    @Resource  
    private AlipayConfig alipayConfig;  
  
    //返回資料格式  
    private static final String FORMAT = "json";  
    //編碼型別  
    private static final String CHART_TYPE = "utf-8";  
    //簽名型別  
    private static final String SIGN_TYPE = "RSA2";  
  
    /*支付銷售產品碼,目前支付寶只支援FAST_INSTANT_TRADE_PAY*/  
    public static final String PRODUCT_CODE = "FAST_INSTANT_TRADE_PAY";  
  
    private static AlipayClient alipayClient = null;  
  
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");  
    private static final Random random = new Random();  
  
    @PostConstruct  
    public void init(){  
        alipayClient = new DefaultAlipayClient(  
                alipayConfig.getGateway(),  
                alipayConfig.getAppid(),  
                alipayConfig.getPrivateKey(),  
                FORMAT,  
                CHART_TYPE,  
                alipayConfig.getPublicKey(),  
                SIGN_TYPE);  
    };  
  
    /*================PC網頁支付====================*/  
    /**  
     * 統一下單並呼叫支付頁面介面  
     * @param outTradeNo  
     * @param totalAmount  
     * @param subject  
     * @return  
     */  
    public Map<String, Object> placeOrderAndPayForPCWeb(String outTradeNo, float totalAmount, String subject){  
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();  
        request.setNotifyUrl(alipayConfig.getNotifyUrl());  
        request.setReturnUrl(alipayConfig.getReturnUrl());  
        JSONObject bizContent = new JSONObject();  
        bizContent.put("out_trade_no", outTradeNo);  
        bizContent.put("total_amount", totalAmount);  
        bizContent.put("subject", subject);  
        bizContent.put("product_code", PRODUCT_CODE);  
  
        request.setBizContent(bizContent.toString());  
        AlipayTradePagePayResponse response = null;  
        try {  
            response = alipayClient.pageExecute(request);  
        } catch (AlipayApiException e) {  
            e.printStackTrace();  
        }  
        Map<String, Object> resultMap = new HashMap<>();  
        resultMap.put("isSuccess", response.isSuccess());  
        if(response.isSuccess()){  
            log.info("呼叫成功");  
            log.info(JSON.toJSONString(response));  
            resultMap.put("body", response.getBody());  
        } else {  
            log.error("呼叫失敗");  
            log.error(response.getSubMsg());  
            resultMap.put("subMsg", response.getSubMsg());  
        }  
        return resultMap;  
    }  
  
    /**  
     * 交易訂單查詢  
     * @param out_trade_no  
     * @return  
     */  
    public Map<String, Object> tradeQueryForPCWeb(String out_trade_no){  
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();  
        JSONObject bizContent = new JSONObject();  
        bizContent.put("trade_no", out_trade_no);  
        request.setBizContent(bizContent.toString());  
        AlipayTradeQueryResponse response = null;  
        try {  
            response = alipayClient.execute(request);  
        } catch (AlipayApiException e) {  
            e.printStackTrace();  
        }  
        Map<String, Object> resultMap = new HashMap<>();  
        resultMap.put("isSuccess", response.isSuccess());  
        if(response.isSuccess()){  
            System.out.println("呼叫成功");  
            System.out.println(JSON.toJSONString(response));  
            resultMap.put("status", response.getTradeStatus());  
        } else {  
            System.out.println("呼叫失敗");  
            System.out.println(response.getSubMsg());  
            resultMap.put("subMsg", response.getSubMsg());  
        }  
        return resultMap;  
    }  
  
    /**  
     * 驗證簽名是否正確  
     * @param sign  
     * @param content  
     * @return  
     */  
    public boolean CheckSignIn(String sign, String content){  
        try {  
            return AlipaySignature.rsaCheck(content, sign, alipayConfig.getPublicKey(), CHART_TYPE, SIGN_TYPE);  
        } catch (AlipayApiException e) {  
            e.printStackTrace();  
        }  
        return false;  
    }  
  
    /**  
     * 將非同步通知的引數轉化為Map  
     * @return  
     */  
    public Map<String, String> paramstoMap(HttpServletRequest request) throws UnsupportedEncodingException {  
        Map<String, String> params = new HashMap<String, String>();  
        Map<String, String[]> requestParams = request.getParameterMap();  
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {  
            String name = (String) iter.next();  
            String[] values = (String[]) requestParams.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);  
        }  
        return params;  
    }  

}
  • 步驟5,封裝config類,用於存放所有的設定屬性。
@Data  
@Component  
@ConfigurationProperties(prefix = "alipay")  
public class AlipayConfig {  
  
    private String gateway;  
  
    private String appid;  
  
    private String pid;  
  
    private String privateKey;  
  
    private String publicKey;  
  
    private String returnUrl;  
  
    private String notifyUrl;  
  
}

另外需要在application.properties中,準備好上述對應的屬性。

# alipay config  
alipay.gateway=https://openapi.alipaydev.com/gateway.do  
alipay.appid=your_appid
alipay.pid=your_pid  
alipay.privatekey=your_private_key
alipay.publickey=your_public_key
alipay.returnurl=完成支付後的同步跳轉地址 
alipay.notifyurl=完成支付後,支付寶會非同步回撥的地址

3. 前端程式碼實現

前端程式碼只需要完成兩個功能,

  1. 根據使用者的請求向後端發起支付請求。
  2. 直接提交返回資料完成跳轉。

下面的例子中,我用typescript實現了使用者點選支付之後的功能,

async function onPositiveClick() {  
   paymentLoading.value = true  
  
   const { data } = await placeAlipayOrder<string>({  
	//你的一些請求引數,例如金額等等
   })  
  
   const div = document.createElement('divform')  
   div.innerHTML = data  
   document.body.appendChild(div)  
   document.forms[0].setAttribute('target', '_blank')  
   document.forms[0].submit()  
  
   showModal.value = false  
   paymentLoading.value = false  
}

2.2 建立並上線APP

完成沙盒偵錯沒問題之後,我們需要建立對應的支付寶網頁應用並上線。

登入 https://open.alipay.com/develop/manage 並選擇建立網頁應用,

填寫應用相關資訊:

建立好應用之後,首先在開發設定中,設定好介面加簽方式以及應用閘道器。

注意金鑰選擇RSA2,其他按照上面的操作指南一步步走即可,注意保管好自己的私鑰和公鑰。

之後在產品系結頁,繫結對應的API,比如我們這裡是PC網頁端支付,找到對應的API繫結就可以了。如果第一次繫結,可能需要填寫相關的資訊進行稽核,按需填寫即可,一般稽核一天就通過了。

最後如果一切就緒,我們就可以把APP提交上線了,上線成功之後,我們需要把下面SpringBoot中的properties替換為線上APP的資訊,然後就可以在生產環境呼叫支付寶的介面進行支付了。

# alipay config  
alipay.gateway=https://openapi.alipaydev.com/gateway.do  
alipay.appid=your_appid
alipay.pid=your_pid  
alipay.privatekey=your_private_key
alipay.publickey=your_public_key
alipay.returnurl=完成支付後的同步跳轉地址 
alipay.notifyurl=完成支付後,支付寶會非同步回撥的地址

參考:


歡迎關注公眾號【碼老思】,只講最通俗易懂的原創技術乾貨。