简言:
前段时间在自己做的小项目上添加了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 就可以实现支付宝的回调了
作者:lincaomei