簡言:
前段時間在自己做的小項目上添加了ZFB的支付功能,並且優化了網頁版支付寶的掃碼支付,使用的框架是Spring + SpringBoot + SpringMVC + Mybatis + VUE。
准備:
首先需要到支付寶官網申請沙箱測試的資格:https://open.alipay.com/platform/home.htm



點擊 查看接入文檔 根據自己的操作系統下載密鑰生成器,生成應用私鑰

步驟一:
pom.xml 文件引入支付寶的Jar包
<!-- 支付寶 jar--> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.22.67.ALL</version> </dependency>
步驟二:
創建支付寶配置類
package org.lpy.config; import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayClient; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.internal.util.AlipaySignature; import lombok.extern.log4j.Log4j2; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; /** * 支付寶接口配置類 * @author 林草莓233 * @since 2022/04/08 */ @Log4j2 @Configuration public class PayConfig {
// 請填寫您的AppId(必填) public static final String appID = ""; //應用私鑰,這里修改生成的私鑰即可(必填) public static final String privateKey = ""; //支付寶公鑰,不是應用公鑰!!!(必填) public static final String publicKey = ""; //默認即可(必填) public static final String charset = "utf-8"; //默認即可(必填) public static final String signType = "RSA2";
@Bean public AlipayClient alipayClient(){ //沙箱環境使用https://openapi.alipaydev.com/gateway.do,線上環境使用https://openapi.alipay.com/gateway.do return new DefaultAlipayClient("https://openapi.alipaydev.com/gateway.do", appID, privateKey, "json", charset, publicKey, signType); }
/** * 驗簽,是否正確 */ public static boolean checkSign(HttpServletRequest request){ Map<String, String[]> requestMap = request.getParameterMap(); Map<String, String> paramsMap = new HashMap<>(); requestMap.forEach((key, values) -> { StringBuilder str = new StringBuilder(); for(String value : values) { str.append(value); } log.info("ZFB驗簽:" + key + "===>" + str); paramsMap.put(key, str.toString()); }); //調用SDK驗證簽名 try { return AlipaySignature.rsaCheckV1(paramsMap, PayConfig.publicKey, PayConfig.charset, PayConfig.signType); } catch (AlipayApiException e) { // TODO Auto-generated catch block e.printStackTrace(); log.info("*********************驗簽失敗********************"); return false; } } }
步驟三:
創建WeSorcket類,用來實現前后端通信
package org.lpy.util; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.springframework.web.socket.server.standard.ServerEndpointExporter; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; @Component @ServerEndpoint("/webSocket") @Slf4j public class WebSocket { private Session session; private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>(); /** * 新建webSocket配置類 * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } /** * 建立連接 * @param session */ @OnOpen public void onOpen(Session session) { this.session = session; webSockets.add(this); log.info("【新建連接】,連接總數:{}", webSockets.size()); } /** * 斷開連接 */ @OnClose public void onClose(){ webSockets.remove(this); log.info("【斷開連接】,連接總數:{}", webSockets.size()); } /** * 接收到信息 * @param message */ @OnMessage public void onMessage(String message){ log.info("【收到】,客戶端的信息:{},連接總數:{}", message, webSockets.size()); } /** * 發送消息 * @param message */ public void sendMessage(String message){ log.info("【廣播發送】,信息:{},總連接數:{}", message, webSockets.size()); for (WebSocket webSocket : webSockets) { try { webSocket.session.getBasicRemote().sendText(message); } catch (IOException e) { log.info("【廣播發送】,信息異常:{}", e.fillInStackTrace()); } } } }
步驟四:
創建交易控制中心(AliPayHandler)
package org.lpy.handler; import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayClient; import com.alipay.api.request.AlipayTradePrecreateRequest; import com.alipay.api.response.AlipayTradePrecreateResponse; import lombok.extern.slf4j.Slf4j; import org.lpy.config.PayConfig; import org.lpy.pojo.AliReturnPayBean; import org.lpy.util.WebSocket; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigDecimal; /** * 支付交易控制中心 * @author 林草莓233 * @since 2022/04/08 */ @Controller @Slf4j public class AliPayHandler { @Resource private AlipayClient alipayClient; @Resource private WebSocket webSocket; @Value("${company}") private String company; @Value("${timeout}") private String timeout; @RequestMapping("/createQR") @ResponseBody public String send(BigDecimal money,String title) throws AlipayApiException { AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); //創建API對應的request類 //異步回調地址 request.setNotifyUrl("http://127.0.0.1:8081/call"); //同步回調地址 // request.setReturnUrl(""); request.setBizContent( "{"+ "\"out_trade_no\":\""+ System.currentTimeMillis()/1000 + Math.round((Math.random()+1) * 1000) + "\"," + // 商戶訂單號 "\"total_amount\":\""+ money +"\"," +// 商品價格 "\"subject\":\""+ title +"\"," +// 商品標題 "\"store_id\":\"" + company + "\"," + // 組織或公司名 "\"timeout_express\":\"" + timeout + "\"}" ); //支付超時時間 AlipayTradePrecreateResponse response = alipayClient.execute(request); if (response.isSuccess()) { log.info("支付API調用成功"); return response.getQrCode(); } else { log.info("支付API調用失敗"); } return ""; } // 支付寶回調函數 @RequestMapping("/call") public void call(HttpServletRequest request, HttpServletResponse response, AliReturnPayBean returnPay) throws IOException { response.setContentType("type=text/html;charset=UTF-8"); log.info("支付寶的的回調函數被調用"); if (!PayConfig.checkSign(request)) { log.info("驗簽失敗"); response.getWriter().write("failture"); return; } if (returnPay == null) { log.info("支付寶的returnPay返回為空"); response.getWriter().write("success"); return; } log.info("支付寶的returnPay" + returnPay); //表示支付成功狀態下的操作 if (returnPay.getTrade_status().equals("TRADE_SUCCESS")) { log.info("支付寶的支付狀態為TRADE_SUCCESS"); //業務邏輯處理 ,webSocket在下面會有介紹配置 webSocket.sendMessage("true"); } response.getWriter().write("success"); } }
這里要注意!!!!
request.setNotifyUrl("http://127.0.0.1:8081/call");
這里的地址是錯誤的,這里應該要填寫外網可以訪問到的地址后面拼接上應用端口號加上“/call”,這樣支付寶才能調用到我們的call方法,返回支付狀態,因為隱私問題,我用內網的地址做示范,如果需要部署在服務器上,這里應該填寫服務器的公網IP加上"/call",例如你的公網IP為:123.45.6.7,應用端口號為:8081,回調地址應該填"http://123.45.6.7:8081/call",同樣的,之前在支付寶的沙箱應用頁面也需要配置授權回調地址,兩邊填寫一致。如果要在本地上測試支付功能的話,需要借助軟件來完成內網穿透,內網穿透的具體方法我會放在文章最后。
步驟五:
前端頁面,這里使用的是 VUE 框架 + element 組件 + qr 二維碼生成組件
先通過命令加載 qr 組件:
npm install vue-qr --save
前端頁面代碼:
<template> <div> <!-- 支付按鈕,模擬支付操作 --> <van-button type="primary" @click="pay">支付</van-button> <el-dialog :title="paySucc?'支付成功':'掃碼支付'" :visible.sync="dialogVisible" width="16%" center> <!-- 生成二維碼圖片 --> <vueQr :text="text" :size="200" v-if="!paySucc"></vueQr> <!-- 使用websocket監控是否掃描,掃描成功顯示成功並退出界面 --> <span class="iconfont icon-success" style="position: relative;font-size: 100px;color:#42B983;margin-left: 50px;top:-10px;" v-else></span> </el-dialog> </div> </template> <script> import vueQr from 'vue-qr' export default { data() { return { dialogVisible: false, text: "", paySucc: false } }, components: { vueQr }, methods: { pay() { let _this = this; _this.paySucc = false; _this.dialogVisible = true; this.axios.request("http://localhost:8081/createQR") .then((response) => { _this.text = response.data; _this.dialogVisible = true; //使用webSocket發送請求,下面會簡單介紹websocket使用 if ("WebSocket" in window) { // 打開一個 web socket var ws = new WebSocket("ws://localhost:8081/bindingRecord"); ws.onopen = function() { // Web Socket 已連接上,使用 send() 方法發送數據 // ws.send("data"); // alert("數據發送中..."); }; ws.onmessage = function(evt) { var received_msg = evt.data; // alert("數據已接收..." + evt.data); if (Boolean(evt.data)) { _this.paySucc = true; setTimeout(() => { _this.dialogVisible = false; }, 3 * 1000); } ws.close(); }; ws.onclose = function() { // // 關閉 websocket console.log("連接已關閉..."); }; } else { // 瀏覽器不支持 WebSocket alert("您的瀏覽器不支持 WebSocket!"); } }).catch((err) => { console.log(err) }) }, back(dataUrl, id) { console.log(dataUrl, id) } } } </script> <style> .btn { margin-left: 100px; } </style>
示例:

附言:
實現內網穿透我們需要用到專門的工具,這里有兩種,分別是 Sunny-Ngrok 和 NATAPP 這兩個軟件都有免費通道和付費通道,免費的通道不穩定,而且每次開啟域名都會變,但如果只是測試可以湊合着用。
百度搜索NATAPP官網,進去注冊領取免費的隧道,然后配置。


點擊下載客戶端,下載對應系統的natapp.exe文件,然后在natapp.exe文件同目錄下創建config.ini文件,編輯文件內容
[default]
authtoken= #對應一條隧道的authtoken
clienttoken= #對應客戶端的clienttoken,將會忽略authtoken,若無請留空,
log=none #log 日志文件,可指定本地文件, none=不做記錄,stdout=直接屏幕輸出 ,默認為none
loglevel=ERROR #日志等級 DEBUG, INFO, WARNING, ERROR 默認為 DEBUG
http_proxy= #代理設置 如 http://10.123.10.10:3128 非代理上網用戶請務必留空
只需要在authtoken= 后面填上你注冊的隧道的authtoken碼保存,之后直接打開natapp.exe即可完成內網穿透

最后只需要把回調地址改成 http://cv95x3.natappfree.cc/call 就可以實現支付寶的回調了
