SpringBoot + vue 實現支付寶掃碼支付功能


簡言:

    前段時間在自己做的小項目上添加了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 就可以實現支付寶的回調了


免責聲明!

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



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