简言:
前端时间在自己做的小项目上添加了ZFB的支付功能,并且优化了网页版支付宝的扫码支付,使用的框架是Spring + SpringBoot + SpringMVC + Mybatis + VUE。
准备:
首先需要到支付宝官网申请沙箱测试的资格:https://open.alipay.com/platform/home.htm
点击 查看接入文档 根据自己的操作系统下载密钥生成器,生成应用私钥
步骤一:
引入支付宝的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即可完成内网穿透