Spring Boot 整合支付寶支付


接入准備

在整合之前,需要你已經注冊成為支付寶開發者。我在 jsp對接支付寶支付接口,實現網站在線支付 這篇文章中有寫到接入流程,可供參考。

支付流程

在整合之前,先來看一下支付寶的支付流程,如下圖所示:
在這里插入圖片描述
調用順序如下:

  • 商戶系統請求支付寶接口 alipay.trade.page.pay,支付寶對商戶請求參數進行校驗,而后重新定向至用戶登錄頁面。

  • 用戶確認支付后,支付寶通過 get 請求 returnUrl(商戶入參傳入),返回同步返回參數。

  • 交易成功后,支付寶通過 post 請求 notifyUrl(商戶入參傳入),返回異步通知參數。

  • 若由於網絡等問題異步通知沒有到達,商戶可自行調用交易查詢接口 alipay.trade.query 進行查詢,根據查詢接口獲取交易以及支付信息(商戶也可以直接調用查詢接口,不需要依賴異步通知)。


支付結果有兩種通知方式:

  • 同步通知
    簡單來講,同步通知就是通知給用戶的。同步通知將支付結果通過界面的方式通知給用戶,如下圖所示的就是同步通知:
    在這里插入圖片描述
  • 異步通知
    異步通知是將支付結果通知給服務器,在我們掃碼支付時,支付寶服務器會每隔一段時間調用這個異步通知接口,直到我們支付完成。

所以,由於同步返回的不可靠性,支付結果必須以異步通知或查詢接口返回為准,不能依賴同步跳轉。

請見官方文檔:支付寶異步通知


了解了這些,接下來就可以整合支付接口了。


Spring Boot 整合

  • 添加 Maven 依賴
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 支付寶 -->
<dependency>
	<groupId>com.alipay.sdk</groupId>
	<artifactId>alipay-sdk-java</artifactId>
	<version>3.7.26.ALL</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 配置
import java.io.FileWriter;
import java.io.IOException;

public class AlipayConfig {

    // 應用ID,您的APPID,收款賬號既是您的APPID對應支付寶賬號
    public static String app_id = "";

    // 商戶私鑰,您的PKCS8格式RSA2私鑰
    public static String merchant_private_key = "";
    // 支付寶公鑰,查看地址:https://openhome.alipay.com/platform/keyManage.htm 對應APPID下的支付寶公鑰。
    public static String alipay_public_key = "";
    // 服務器異步通知頁面路徑  需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問
    public static String notify_url = "";

    // 頁面跳轉同步通知頁面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問
    // 即支付成功之后,需要跳轉到的頁面,一般為網站的首頁
    public static String return_url = "http://www.baidu.com";

    // 簽名方式
    public static String sign_type = "RSA2";

    // 字符編碼格式
    public static String charset = "utf-8";

    // 支付寶網關
    public static String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";

    // 日志存儲路徑
    public static String log_path = "C:\\";


    /**
     * 寫日志,方便測試(看網站需求,也可以改成把記錄存入數據庫)
     * @param sWord 要寫入日志里的文本內容
     */
    public static void logResult(String sWord) {
        FileWriter writer = null;
        try {
            writer = new FileWriter(log_path + "alipay_log_" + System.currentTimeMillis()+".txt");
            writer.write(sWord);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

也可以將這些配置信息放入 application.properties 配置文件中,通過 @ConfigurationProperties(prefix = "xxx") 或者 @Value 的方式進行賦值。

  • 去付款界面 index.html
<form action="/pay/topay">
	<button type="submit">付款</button>
</form>
  • controller
@Controller
@RequestMapping("/pay")
public class PayController {

    @Autowired
    private AlipayService alipayService;

    @GetMapping("/hello")
    public String hello() {
        return "index";
    }

    /**
     * 跳轉到支付界面
     * @return
     * @throws Exception
     */
    @GetMapping("/topay")
    @ResponseBody
    public String pay() throws Exception {
        String form = alipayService.toPay(String.valueOf(new Date().getTime()), 
        	720.0, "易購商城", "訂單描述");
        return form;
    }
}
  • service
@Service
public class AlipayService {

    public String toPay(String orderId, double price, String orderName, String orderDesc) throws Exception{
        //獲得初始化的AlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);

        //設置請求參數
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(AlipayConfig.return_url);
        alipayRequest.setNotifyUrl(AlipayConfig.notify_url);

        //商戶訂單號,商戶網站訂單系統中唯一訂單號,必填
        String out_trade_no = orderId;
        //付款金額,必填
        String total_amount = String.valueOf(price);
        //訂單名稱,必填
        String subject = orderName;
        //商品描述,可空
        String body = orderDesc;

        alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
                + "\"total_amount\":\""+ total_amount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}")

        String form = "";
        AlipayTradePagePayResponse response = alipayClient.pageExecute(alipayRequest);
        if (response.isSuccess()) {
            form = alipayClient.pageExecute(alipayRequest).getBody();
        }
        // 這里返回的 form 是一個字符串,里面封裝了支付的表單信息
        //(即 html 標簽 和 javascript 代碼),直接將這個 form 輸出到頁面即可。
        return form;
    }
}

然后啟動項目,訪問 localhost:8080,如下:
在這里插入圖片描述

點擊付款,就可以到支付寶的支付界面了,如下:
在這里插入圖片描述
掃碼(使用沙箱環境的支付寶,不是真實的支付寶)付款即可。


通過上面的步驟,已經可以進行支付了。但是,這只是同步通知,真實的支付結果需要使用異步通知。

支付結果異步通知

首先,我們需要一個提供異步通知的 URL,並且,這個 URL 必須可以通過 外網 訪問(如果沒有服務器,可以使用內網穿透)。這里推薦一款內網穿透工具 NATAPP


異步通知的特性:

  • 支付寶是用 POST 方式發送通知信息,所有請求信息被保存在 request 中。
  • 服務器間的交互,不像頁面跳轉同步通知可以在頁面上顯示出來,這種交互方式是不可見的
  • 程序執行完后必須打印輸出“success”(不包含引號)。如果商戶反饋給支付寶的字符不是 success 這7個字符,支付寶服務器會不斷重發通知,直到超過24小時22分鍾。一般情況下,25小時以內完成8次通知(通知的間隔頻率一般是:4m,10m,10m,1h,2h,6h,15h)
  • 程序執行完成后,該頁面不能執行頁面跳轉。如果執行頁面跳轉,支付寶會收不到 success 字符,會被支付寶服務器判定為該頁面程序運行出現異常,而重發處理結果通知
  • cookies、session 等在此頁面會失效,即無法獲取這些數據;
  • 該方式的調試與運行必須在服務器上,即互聯網上能訪問;
  • 該方式的作用主要防止訂單丟失,即頁面跳轉同步通知沒有處理訂單更新,所以應該在異步通知中更新訂單;

驗簽:

在異步通知中,有一個驗簽的過程,這個過程就是為了保證我們這個接口的調用方是支付寶的服務器,而不是其他服務器。 驗簽過程如下:

  • 第一步: 在通知返回參數列表中,除去 sign、sign_type 兩個參數外,凡是通知返回回來的參數皆是待驗簽的參數。

  • 第二步: 將剩下參數進行 url_decode,然后進行字典排序,組成字符串,得到待簽名字符串:

  • 第三步: 將簽名參數(sign)使用 base64 解碼為字節碼串。

  • 第四步: 使用 RSA 的驗簽方法,通過簽名字符串、簽名參數(經過 base64 解碼)及支付寶公鑰驗證簽名。

看似復雜,其實只需調用驗簽的 API 即可,如下:

boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, ALIPAY_PUBLIC_KEY, CHARSET, SIGN_TYPE) //調用SDK驗證簽名

前面介紹了異步通知的流程,接下來就是進行異步通知接口的開發。

  • 配置

在支付寶配置類 AlipayConfig.java中配置 notify_url 屬性,這個屬性就是異步通知的 URL。

// 比如
public static String notify_url = "http://xxx.top/pay/callback";
// xxx.top 為你的域名或者外網可訪問的 ip 地址。
// pay/callback 為后台請求路徑,即 controller 中的 RequestMapping。

同時,需要在沙箱環境中配置授權回調地址為異步通知的 URL:
在這里插入圖片描述

  • controller
/**
 * 異步通知支付結果
 * @param request
 * @return
 * @throws AlipayApiException
*/
@PostMapping("/callback")
public String alipayNotify(HttpServletRequest request) throws AlipayApiException {
	String success = "success";
	String failure = "failure";
	//獲取支付寶的請求信息
	Map<String,String> params = new HashMap<>();
	Map<String,String[]> requestParams = request.getParameterMap();
	// 將 Map<String,String[]> 轉為 Map<String,String>
	for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
		String name = iter.next();
		String[] values = requestParams.get(name);
		String valueStr = "";
		for (int i = 0; i < values.length; i++) {
			valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
		}
		params.put(name, valueStr);
	}
	params.remove("sign_type");
	// 驗簽
	boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
	// 驗簽通過
	if (signVerified) {
		System.out.println("通過驗簽");
		// 更新訂單信息
		String result = alipayService.updateOrder(params);
		if ("success".equals(result)) {
			System.out.println("controller支付成功");
			return success;
		}
	}
	return failure;
}
  • service
public String updateOrder(Map<String, String> params) {
    if (params == null || params.isEmpty()){
		return "success";
    }
    String orderId = params.get("out_trade_no");
    System.out.println("service訂單id:" + orderId);
//        PayOrderDTO order = orderService.getOrderById(orderId);
//     如果訂單不存在,則支付操作無意義
//     不讓支付寶再繼續調用異步通知(返回為 SUCCESS 后,支付寶將不再調用)。
//        if (order == null) {
//            return "success";
//        }
        // 判斷訂單狀態是否已經被修改
//        int orderStatus = orderService.getOrderStatus(orderId);
//        if (orderStatus == 1){
//            return "success";
//        }
	String tradeStatus = params.get("trade_status");
        // 支付成功
	if ("TRADE_SUCCESS".equals(tradeStatus)){
        // 更新訂單信息
        // ...
        
		System.out.println("訂單支付成功service");
		return "success";
	}
    return "failure";
}

到此,Spring Boot 就簡單的整合了支付寶。

完整代碼

  • AlipayConfig.java
public class AlipayConfig {

    // 應用ID,您的APPID,收款賬號既是您的APPID對應支付寶賬號
    public static String app_id = "";

    // 商戶私鑰,您的PKCS8格式RSA2私鑰
    public static String merchant_private_key = "";
    // 支付寶公鑰,查看地址:https://openhome.alipay.com/platform/keyManage.htm 對應APPID下的支付寶公鑰。
    public static String alipay_public_key = "";
    // 服務器異步通知頁面路徑  需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問
    public static String notify_url = "";

    // 頁面跳轉同步通知頁面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問
    // 即支付成功之后,需要跳轉到的頁面,一般為網站的首頁
    // 便於測試,直接使用了 baidu
    public static String return_url = "http://www.baidu.com";

    // 簽名方式
    public static String sign_type = "RSA2";

    // 字符編碼格式
    public static String charset = "utf-8";

    // 支付寶網關
    public static String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";

    // 日志存儲路徑
    public static String log_path = "C:\\";


    /**
     * 寫日志,方便測試(看網站需求,也可以改成把記錄存入數據庫)
     * @param sWord 要寫入日志里的文本內容
     */
    public static void logResult(String sWord) {
        FileWriter writer = null;
        try {
            writer = new FileWriter(log_path + "alipay_log_" + System.currentTimeMillis()+".txt");
            writer.write(sWord);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • AlipayService.java
@Service
public class AlipayService {

    // 跳轉到支付界面
    public String toPay(String orderId, double price, String orderName, String orderDesc) throws Exception{
        //獲得初始化的AlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);

        //設置請求參數
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(AlipayConfig.return_url);
        alipayRequest.setNotifyUrl(AlipayConfig.notify_url);

        //商戶訂單號,商戶網站訂單系統中唯一訂單號,必填
        String out_trade_no = orderId;
        //付款金額,必填
        String total_amount = String.valueOf(price);
        //訂單名稱,必填
        String subject = orderName;
        //商品描述,可空
        String body = orderDesc;

        alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
                + "\"total_amount\":\""+ total_amount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}")

        String form = "";
        AlipayTradePagePayResponse response = alipayClient.pageExecute(alipayRequest);
        if (response.isSuccess()) {
            form = alipayClient.pageExecute(alipayRequest).getBody();
        }
        return form;
    }
 
    // 更新訂單
	public String updateOrder(Map<String, String> params) {
	    if (params == null || params.isEmpty()){
			return "success";
	    }
	    String orderId = params.get("out_trade_no");
	    System.out.println("service訂單id:" + orderId);
	//        PayOrderDTO order = orderService.getOrderById(orderId);
	//     如果訂單不存在,則支付操作無意義
	//     不讓支付寶再繼續調用異步通知(返回為 SUCCESS 后,支付寶將不再調用)。
	//        if (order == null) {
	//            return "success";
	//        }
	        // 判斷訂單狀態是否已經被修改
	//        int orderStatus = orderService.getOrderStatus(orderId);
	//        if (orderStatus == 1){
	//            return "success";
	//        }
		String tradeStatus = params.get("trade_status");
	        // 支付成功
		if ("TRADE_SUCCESS".equals(tradeStatus)){
	        // 更新訂單信息
	        // ...
	        
			System.out.println("訂單支付成功service");
			return "success";
		}
	    return "failure";
	}
}
  • PayController.java
@Controller
@RequestMapping("/pay")
public class PayController {

    @Autowired
    private AlipayService alipayService;

    @GetMapping("/hello")
    public String hello() {
        return "index";
    }

    /**
     * 跳轉到支付界面
     * @return
     * @throws Exception
     */
    @GetMapping("/topay")
    @ResponseBody
    public String pay() throws Exception {
        String form = alipayService.toPay(String.valueOf(new Date().getTime()), 
        	720.0, "易購商城", "訂單描述");
        return form;
    }

	/**
	 * 異步通知支付結果
	 * @param request
	 * @return
	 * @throws AlipayApiException
	*/
	@PostMapping("/callback")
	public String alipayNotify(HttpServletRequest request) throws AlipayApiException {
		String success = "success";
		String failure = "failure";
		//獲取支付寶的請求信息
		Map<String,String> params = new HashMap<>();
		Map<String,String[]> requestParams = request.getParameterMap();
		// 將 Map<String,String[]> 轉為 Map<String,String>
		for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
			String name = iter.next();
			String[] values = requestParams.get(name);
			String valueStr = "";
			for (int i = 0; i < values.length; i++) {
				valueStr = (i == values.length - 1) ? valueStr + values[i]
	                        : valueStr + values[i] + ",";
			}
			params.put(name, valueStr);
		}
		params.remove("sign_type");
		// 驗簽
		boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
		// 驗簽通過
		if (signVerified) {
			System.out.println("通過驗簽");
			// 更新訂單信息
			String result = alipayService.updateOrder(params);
			if ("success".equals(result)) {
				System.out.println("controller支付成功");
				return success;
			}
		}
		return failure;
	}
}
  • index.html
 <form action="/pay/topay">
	<button type="submit">付款</button>
</form>

官方文檔:
https://opendocs.alipay.com/open/270/105899
https://opendocs.alipay.com/open/270/105902


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM