Spring boot整合支付宝沙盒
1.这里没有介绍对支付宝沙盒的密钥和公钥的配置,如没配置请先配置后在看此博客, 有很多不足之处,请多多包涵
2.我使用的是扫码支付,也是就是生成二维码,当然不止有这个, 还有另外几种的支付能力,如:网站的,app的, 我使用的是当面付时创建二维码
3.SDK我使用的是alipay-easysdk
alipay-easysdk文档: https://github.com/alipay/alipay-easysdk/blob/066388d02c6f55fe0919d75b386456d80801fec2/APIDoc.md
4.开发工具IDEA, jdk1.8, 和一个内网穿透工具natapp
5.前端使用的是vue, 利用webscoket来传输支付是否完成
好了这就是基本的介绍,结果图如下:
让我们开始实现这个功能吧! 这里省略了spring boot的搭建,直接进入maven依赖的导入
配置
1.pom.xml依赖的导入
<!--支付宝沙盒的依赖-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-easysdk</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 二维码zxing -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.0</version>
</dependency>
<!-- WebSocekt-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
一些spring boot的依赖就省略了, 这里就贴出了生成二维码的,和支付宝沙盒sdk. websoket
2.application.yml 支付宝的配置
alipay:
#商户ID
appId:
#自己的支付宝私钥
privateKey:
#自己的公钥
publicKey:
#支付宝网关
gateway: openapi.alipaydev.com
#支付完成后需要返回的url映射地址
returnUrl:
#异步支付通知
notifyUrl:
3.支付宝的初始配置类
他主要的作用是读取application.yml文件中关于alipay的属性值, 实现了ApplicationRunner接口方法
@Component
public class 你的类名 implements ApplicationRunner {
//应用id
@Value("${alipay.appId}")
private String appId;
//私钥
@Value("${alipay.privateKey}")
private String privateKey;
//公钥
@Value("${alipay.publicKey}")
private String publicKey;
//支付宝的网关
@Value("${alipay.gateway}")
private String gateway;
//支付成功后的接口回调地址
@Value("${alipay.notifyUrl}")
private String notifyUrl;
/**
* Spring boot项目启动时初始化支付宝沙盒配置
*
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
Factory.setOptions(getOptions());
System.out.println("********支付宝SDK初始化完成!******");
}
public Config getOptions() {
Config config = new Config();
config.protocol = "https";
config.gatewayHost = this.gateway;
config.signType = "RSA2";
//支付宝
config.appId = this.appId;
config.merchantPrivateKey = this.privateKey;
config.alipayPublicKey = this.publicKey;
config.notifyUrl = notifyUrl;
return config;
}
}
到这里代表你的支付宝已经配置差不多了, 运行spring boot时会在终端出现********支付宝SDK初始化完成!******
就代表没问题了
支付能力Payment
我使用的是alipay-easysdk,他的官方文档写的也很清楚:https://github.com/alipay/alipay-easysdk/blob/066388d02c6f55fe0919d75b386456d80801fec2/APIDoc.md, 可参考Payment章节
我主要介绍的是交易预创建,生成正扫二维吗,如下表格
- API声明
precreate(subject: string, outTradeNo: string, totalAmount: string)
- 入参:
字段名 | 类型 | 必填 | 说明 |
---|---|---|---|
subject | string | 是 | 订单标题 |
outTradeNo | string | 是 | 交易创建时传入的商户订单号 |
totalAmount | string | 是 | 订单总金额,单位为元,精确到小数点后两位,取值范围[0.01,100000000] |
-
出参
我们只需要要拿到qrcode这个参数, 这是生成二维码的主要数据
其他详细出参请参考:https://opendocs.alipay.com/apis/api_1/alipay.trade.precreate
支付能力就介绍完了, 下面就是实现了
创建一个类,用于支付宝的支付业务层的实现
@Service
public class PayService {
@Autowired
//订单编号生成的工具类
private UtilsCode utilsCode;
public String payQr(Order1 order, String userId) throws Exception {
String serialNumaber = utilsCode.orderUUID();
Integer number = order.getNumber();
Integer goodsId = order.getGoodsId();
//重点异步: 使用的notify接口 在natapp上拿到链接, 到这上面, 支付完记得看终端 200正常
// userId 因为webscoket需要获得到用户的session才能发送消息,
//number 是商品数量 goodsId 是商品id, 用于修改 数据库中的数据, 可以根据自己的需求进行修改
String notify = "http://qjaxf4.natappfree.cc/back/v1/"+notify?userId="
+userId+"&number="+number+"&goodsId="+goodsId;
AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace()
//异步, 用于查看是否支付成功
.asyncNotify(notify)
/* 标题 订单号, 订单金额string 类型 */
.preCreate("标题, 可以随便写", serialNumaber, String.valueOf(order.getMoney()));
//这是我们需要拿到的数据
return response.getQrCode();
}
/*
测试
public String payQr() throws Exception {
String serialNumaber = utilsCode.orderUUID();
//重点异步: 在natapp上拿到链接, 到这上面, 支付完记得看终端 200正常
// userId 因为webscoket需要获得到用户的session才能发送消息,
String notify = "http://qjaxf4.natappfree.cc/back/v1/notify?userId="
+userId+"&number="+number+"&goodsId="+goodsId;
AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace()
//异步, 用于查看是否支付成功
.asyncNotify(notify)
.preCreate("标题, 可以随便写", serialNumaber, "123");
//这是我们需要拿到的数据
return response.getQrCode();
}
*/
}
@Component
public class UtilsCode {
//生成唯一订单号
public String orderUUID() {
//生成8位数的随机数 import org.apache.commons.lang3.RandomStringUtils;
String randomString = RandomStringUtils.randomNumeric(8);
String orderIdStr = "wch-" + datestr() + "-" + randomString;
//返回完整的字符串
return orderIdStr;
}
}
编写前端控制器
/**
* 支付宝沙盒接口的调用,生成二维码
*/
@Autowired
private PayService payService;
//produces = MediaType.IMAGE_JPEG_VALUE必须加上去
@PostMapping(value="pay", produces = MediaType.IMAGE_JPEG_VALUE)
/*如果你想测试,可以把方法里的参数给去掉, 对应的你调用的方法也可以去掉如
public BufferedImage pay() throws Exception {
String result = this.payService.payQr();
BufferedImage bufferedImage = QrCodeUtils.createQr(result);
return bufferedImage;
}
*/
public BufferedImage pay(@RequestBody Order1 order, String userId) throws Exception {
String result = this.payService.payQr(order, userId);
BufferedImage bufferedImage = QrCodeUtils.createQr(result);
return bufferedImage;
}
/**
* 生成BuffereImage, 加载配置类
* @return
*/
@Bean
public BufferedImageHttpMessageConverter addConverter(){
return new BufferedImageHttpMessageConverter();
}
//异步,支付是否成功
@Autowired
private WebScoketAlipay webScoketAlipay;
/**
* 异步通知处理
*/
@PostMapping("/notify")
public void asyncNotify(@RequestBody String notifyData, String userId, /*可根据自己的需求删掉*/String goodsId
,String number) throws IOException {
if (notifyData != null) {
webScoketAlipay.sendMessageToFinduserId("支付成功", userId);
//如测试下面可以不用, 可根据自己的需求
Slideshow slideshow = new Slideshow();
slideshow.setShowDay(Integer.parseInt(number));
slideshow.setImgId(Integer.parseInt(goodsId));
this.slideshowService.topUpSildeShow(slideshow);
}
}
二维码的生成
需要使用到zxing这个sdk, 直接上关键代码, 如需要图片请自行百度
public class QrCodeUtils {
private static final String CHARSET = "utf-8";
public static final String FORMAT = "JPG";
// 二维码尺寸
private static final int QRCODE_SIZE = 350;
// LOGO宽度
private static final int LOGO_WIDTH = 60;
// LOGO高度
private static final int LOGO_HEIGHT = 60;
public static BufferedImage createQr(String contenxt) {
BufferedImage img = null;
try {
BitMatrix bitMatrix = new MultiFormatWriter().encode(contenxt, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE);
img= MatrixToImageWriter.toBufferedImage(bitMatrix);
} catch (WriterException e) {
e.printStackTrace();
}
return img;
}
}
好了这样就完成一半了, 可以用postman测试下,你就会发现生成的二维码图片了, 下面就是我们的前端了
webscoket的配置与使用
配置
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
使用
@Component
@ServerEndpoint("/notfiyPay/{id}")
public class WebScoketAlipay {
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, WebScoketAlipay> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String id = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("id") String id) {
this.session = session;
this.id = id;
//从map数据中取出key,判断是否存在
if (webSocketMap.containsKey(id)) {
//不能够二次登录
webSocketMap.remove(id);
webSocketMap.put(id, this);
} else {
webSocketMap.put(id, this);
//加入set中
addOnlineCount();
//在线数加1
}
System.out.println("用户连接:" + id + ",当前在线人数为:" + getOnlineCount());
//sendMessage("连接成功");
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(id)) {
webSocketMap.remove(id);
//从set中删除
subOnlineCount();
}
System.out.println("用户退出:" + id + ",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("用户消息:" + this.id + ",报文:" + message);
//可以群发消息
//消息保存到数据库、redis
if (StringUtils.isNotBlank(message)) {
try {
//解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
//追加发送人(防止串改)
jsonObject.put("fromUserId", this.id);
String toUserId = jsonObject.getString("toUserId");
String text = jsonObject.getString("contentText");
//判断发送方是否在线
if (StringUtils.isNotBlank(toUserId) && webSocketMap.containsKey(toUserId)) {
//发送消息
webSocketMap.get(toUserId).sendMessage(text);
} else {
System.out.println("请求的userId:" + toUserId + "不在该服务器上");
//否则不在这个服务器上,发送到mysql或者redis
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("用户错误:" + this.id + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessageToFinduserId(String message, String userId) {
try {
//websocket session发送文本消息方法:getAsyncRemote()
// for(WebScoketAlipay map: webSocketMap.values()){
// map.session.getAsyncRemote().sendText(message);
// }
webSocketMap.get(userId).session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
public void sendMessage(String message){
try {
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
++WebScoketAlipay.onlineCount;
}
public static synchronized void subOnlineCount() {
WebScoketAlipay.onlineCount--;
}
}
好了,后端基本就已经完成了
前端vue界面
<template>
<el-dialog title="续费" :visible.sync="open" width="500px" @close="closeDialog()" :close-on-click-modal="false">
<el-form ref="form" :model="order" label-width="80px">
<div style="display: flex">
<div style="border-right: 1px dashed #D5D7D9">
<el-form-item label="商家名称:" style="width: 220px; font-size: 16px;">
<span>{{edit.shopName}}</span>
</el-form-item>
<el-form-item label="剩余时间:">
<span>{{edit.remain}}</span>
</el-form-item>
<el-form-item label="使用情况">
<span>{{edit.imgCase}}</span>
</el-form-item>
</div>
<div>
<el-form-item label="审批进度:">
<span>{{edit.useImg}}</span>
</el-form-item>
<el-form-item label="结束时间">
<span style="">{{edit.imgData}}</span>
</el-form-item>
<el-form-item label="广告图片:">
<img v-if="edit.homeImg" :src="edit.homeImg" style="width:130px; height: 70px;">
</el-form-item>
</div>
</div>
<!-- 重点看这些 -->
<el-form-item label="天数充值">
<el-input-number v-model="order.number"
controls-position="right"
:min="0" :max="1860" style="width: 370px"></el-input-number>
<el-radio-group v-model="order.number">
<el-radio-button :label="31">1个月</el-radio-button>
<el-radio-button :label="62">2个月</el-radio-button>
<el-radio-button :label="93">3个月</el-radio-button>
<el-radio-button :label="124">4个月</el-radio-button>
<el-radio-button :label="372">1年</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="支付方式">
<el-radio-group v-model="radio">
<el-radio :label="1" @change="wallet('form')">余额</el-radio>
<el-radio :label="2" @change="alipay()">支付宝支付</el-radio>
<el-radio :label="3" @change="wechat()">微信支付(功能待开发)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="payUrl != ''" label="扫码支付">
<img :src="payUrl" style="width:120px; height: 120px;">
<span style="color: red">总计需支付{{order.money}}(元)</span>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
export default {
name: "topUp",
data(){
return{
edit: {
imgId: "",
homeImg: "",
imgUrl: "",
shopId: "",
useImg: "",
ImgCase: "",
showDay: "",
remain: ""
},
// 表单参数 edit可以删掉
order:{
title: "",
goodsId:"",
number: 1, //数量
money: "", //总计多少元
price: 0.5
},
payUrl: "",
radio: 0,
// 是否显示弹出层
open: false,
}
},
methods:{
// 窗口初始化方法,nextTick方法可以添加逻辑,如打开窗口时查询数据填充
init(id){
this.$nextTick(() => {
let vm = this
this.$post(vm.API.API_URL_FINDBYIDSLIDESHOW+id).then(res =>{
if(res.errorCode == 0){
vm.edit = res.data
vm.edit.showDay = 0
vm.edit.homeImg = vm.API.BASE_SERVER_URL + res.data.homeImg
}
})
this.open = true
})
},
//系统余额钱包支付
wallet(form){
this.payUrl = ''
},
//支付宝支付 重点
alipay(){
let vm = this
this.order.goodsId = this.edit.imgId
this.order.title = this.edit.shopName
this.order.money = this.order.number * this.order.price
//id根据自己系统啊!!!! 测试的话记得随便写个值
let id = JSON.parse(window.sessionStorage.getItem('accese-usename')).userId
console.log(id)
//请求后端 返回图片 {responseType: 'arraybuffer'}参数必须要
axios.post("http://localhost:4099/back/v1/pay?userId="+id, vm.order,{
responseType: 'arraybuffer'
}).then(res =>{
//将图片以base64显示
let bytes = new Uint8Array(res.data);
let data = "";
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
data += String.fromCharCode(bytes[i]);
}
vm.payUrl = "data:image/jpg;base64," + window.btoa(data);
})
this.initWebSocket(id)
},
//关闭对话框清除数
closeDialog(){
this.edit = this.$options.data().edit
this.order = this.$options.data().order
this.payUrl = ''
this.radio = 0
},
//websocket和axiose的轮询 来查看支付信息是否成功
initWebSocket(id){
if(typeof WebSocket == "undefined"){
alert("浏览器不支持WebSocket")
//如浏览器不支持
}else{
//填自己的webscoket地址
let socketUrl = "http://localhost:4099/notfiyPay/"+id;
socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
this.websocket = new WebSocket(socketUrl);
this.websocket.onopen = this.websocketonopen;
this.websocket.onerror = this.websocketonerror;
this.websocket.onmessage = this.websocketonmessage;
this.websocket.onclose = this.websocketclose();
}
},
websocketonopen(){
console.log("连接成功")
},
websocketonerror(e){
console.log("连接发生错误")
},
websocketonmessage(e){
this.$notify({
title: "消息",
message: e.data
})
if(e.data == "支付成功"){
this.open = false
this.payUrl = ''
//支付成功关闭websocket,
this.websocket.close()
}
},
websocketclose(){
console.log("连接关闭")
}
}
}
</script>
<style scoped>
.el-form-item {
margin: 1px;
}
</style>