H5接入Native支付和Jsapi支付遇到的一些问题
接入微信支付之前的准备工作
提示:这是必须的准备 !这是必须的准备! 这是必须的准备!重要的事情说三遍
1.申请一个商户号
2.准备一个已备案的域名
问题1:简要梳理一下业务场景,然后决定是接入微信的Native支付还是Jsapi支付,我这里是原本是打算用jsapi支付的,但是Native支付比Jsapi支付要方便接入,所以后面改用Native支付,但是后面写完之后发现微信官方把手机端Native支付二维码的长按识别支付这个功能禁用了,至于原因是因为好像是银行风控风险,反正是用不了这个功能,说起来这里也是一个问题不能在长按识别二维码支付的话,对业务的影响还是比较大的,最后还是要jsapi支付的方式。所以还是要结合项目实际业务来决定接入那种支付方式,我是已经踩过了,在这里记录一下
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
上面是我用的依赖,不得不说确实挺好用的,这波必须点个赞
概要:既然Native支付不太符合业务,那么我用jsapi支付咯。然后就开始我的踩坑之路,讲错了开发之路,不重要了。wechatpay-java封装验签等方法直接调就行了,下面说遇到的问题。用jsapi支付的流程大概是
1.获取用户code
2.根据code获取用户openId
3.根据openid等参数生成订单,得到微信支付的预支付ID等
4.返回预支付id、appid、时间戳等参数给前端,让前端调支付控件支付
5.用户支付完成之后,后台接收支付通知,然后处理你的业务,最后响应微信服务器的通知
6.前端变化一下或者提示一下用户支付完成了,然后就是没有然后了
问题2:因为我开发的时候的使用是固定的code获取,而且我的openid是固定的所以就没有出现死循环调用https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${encodeURIComponent(local)}&response_type=code&scope=snsapi_base&state=123#wechat_redirect
然后我在vue的created()调用发现一直在请求调用微信的获取code接口,后面查询微信相关资料,发现微信获取code是重定向转发的加上vue的生命周期的执行顺序等,code值会一直重复获取,解决方案是:
//从url中获取code的值
getQueryVariable(paramName) {
let url = window.location.href;
let regex = new RegExp(`${paramName}=([^&]*)`);
let match = regex.exec(url);
if (match) {
return match[1];
} else {
return null;
}
},
//从url中祛除code,返回没有code的url
ridUrlParam(url, params) {
for (var index = 0; index < params.length; index++) {
var item = params[index];
var fromIndex = url.indexOf(item + "="); //必须加=号,避免参数值中包含item字符串
if (fromIndex !== -1) {
// 通过url特殊符号,计算出=号后面的的字符数,用于生成replace正则
var startIndex = url.indexOf("=", fromIndex);
var endIndex = url.indexOf("&", fromIndex);
var hashIndex = url.indexOf("#", fromIndex);
var reg = "";
if (endIndex !== -1) {
// 后面还有search参数的情况
var num = endIndex - startIndex;
reg = new RegExp(item + "=.{" + num + "}");
url = url.replace(reg, "");
} else if (hashIndex !== -1) {
// 有hash参数的情况
var num = hashIndex - startIndex - 1;
reg = new RegExp("&?" + item + "=.{" + num + "}");
url = url.replace(reg, "");
} else {
// search参数在最后或只有一个参数的情况
reg = new RegExp("&?" + item + "=.+");
url = url.replace(reg, "");
}
}
}
var noSearchParam = url.indexOf("=");
if (noSearchParam === -1) {
url = url.replace(/\?/, ""); // 如果已经没有参数,删除?号
}
return url;
},
这是我调用的方法
getCodeApi() {
let local = window.location.href
let url =
`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${encodeURIComponent(local)}&response_type=code&scope=snsapi_base&state=123#wechat_redirect`
// window.location.replace(url)
// window.location.assign(url)
if (window.location.href.indexOf('code=') != -1) { // 避免一直重复重定向无限获取code
let code = this.getQueryVariable('code') // 从url中获取code的值(此方法文末会贴出)
if (code == sessionStorage.getItem(
'code')) { // 微信获取code会重定向,所以从别的页面返回本页后,返回的其实是重定向之后的url,此时url一定带有上次的code,code只能用一次,这时候要重新获取
let urls = this.ridUrlParam(window.location.href, [
'code'
]) // 从url中祛除code,用没有code的url重新生成code (此方法文末会贴出)
window.location.href = urls
}
sessionStorage.setItem('code', code)
} else {
window.location.href = url
}
},
在vue的created()的调用
created() {
this.fetchPageData();//获取页面数据
this.getScreen();//判断当前是否是pc端
if(this.isPC==false){//手机端(我这里是做了兼容pc的native支付,所以要判断一下使用环境)
this.codeUrl = this.getCodeApi()//获取用户的code
}
}
就这样这个问题2解决了
问题3:wechatpay-java的商户API私钥路径找不到这个路径,这也是一个小问题,不过问题不大,也记录一下,切换成线上的环境时要切换对应的路径,并且这个文件路径只能用一个,要么是本地要么是线上的,注意一下就行
//Native支付回调地址 使用https://notify_url,商户配置的回调地址将会失效
public static final String NotifyUrl = "https://notify_url";//
//Jsapi支付回调地址
public static final String payNotifyUrl = "https://******/parent/order/payNotify";
//商户API私钥路径(线上Linux环境的路径)
public static String privateKeyPath = "/usr/local/app/tomcat-tice_kc/webapps/tice/WEB-INF/classes/apiclient_key.pem";
//商户API私钥路径(本地)
// public static String privateKeyPath = "E://xuguanhui_tice_java//src//main//resources//apiclient_key.pem";
//商户证书序列号
public static String merchantSerialNumber = "4E86A06D6F4370***************";
//商户APIV3密钥
public static String apiV3Key = "k5jqhazzpzdlblw**********";
问题4:接着上一个问题说到商户路径配置好之后,然后就是调接口获取openid,然后微信那边返回请求的用户openid,这一步都没有遇到问题,然后就是封装参数返回给前端,这里有一个问题是关于timeStamp的,因为在Java后台生成时间戳是可以,但是在vue前端里面会被识别number类型,而jsapi支付是需要传入string类型的,所以会报参数缺少错误,解决方法是把timeStamp
tostring一下,如果还是不行,就在前端处理一下,把timeStamp转成string类型
参考代码如下:
//生成订单并且获取jsapi支付的支付参数
public void getOpenId() {//这里是jfinal框架封装的,根据你自己的框架的封装类调整返回值就行
String code = this.getPara("code");
Integer reportId = this.getParaToInt("reportId");
Integer studentId = this.getParaToInt("studentId");
String openIdStr = this.getPara("openId");
log.info("jsapi:code===>" + code);
log.info("jsapi:openIdStr===>" + openIdStr);
log.info("jsapi:reportId===>" + reportId);
log.info("jsapi:studentId===>" + studentId);
if (reportId == null || reportId == 0) {
this.error("未获取到报告信息");
return;
}
if (studentId == null || studentId == 0) {
this.error("未获取到学生信息");
return;
}
ReportStudentMobile studentReport = stuReportService.getStudentReportById(reportId);
if (studentReport == null) {
this.error("未查询到该报告");
return;
}
Student student = studentService.findById(studentId);
if (student == null) {
this.error("未查询到该学生信息");
return;
}
Nursery nursery = nurseryService.findById(student.getNurseryId());
if (nursery == null||nursery.getPrice()==0) {
this.error("未查询到该订单收费金额!请联系客服");
return;
}
//查询是否已经生成订单,存在,返回;不存在,生成
StudentOrder studentOrderData = studentOrderService.getStudentOrderByStudentId(student.getNurseryId(), reportId, student.getParentPhone(), studentId);
log.info("是否已经生成订单了=========>" + studentOrderData);
try {
if (studentOrderData != null && studentOrderData.getPrepayId() != null) {
//封装支付参数
WxPayRespVO wxPayRespVO = new WxPayRespVO();
wxPayRespVO.setAppId(JsapiPayOrderConfig.AppID);
wxPayRespVO.setPrepayId(studentOrderData.getPrepayId());
Long timeStamp = Long.parseLong(studentOrderData.getTimeStamp());
wxPayRespVO.setTimeStamp(timeStamp.toString());
String substring = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
wxPayRespVO.setNonceStr(substring);
String signatureStr = Stream.of(JsapiPayOrderConfig.AppID, String.valueOf(timeStamp), substring, "prepay_id=" + studentOrderData.getPrepayId())
.collect(Collectors.joining("\n", "", "\n"));
String sign = OrderPayUtil.getSign(signatureStr, JsapiPayOrderConfig.privateKeyPath);
wxPayRespVO.setPaySign(sign);
wxPayRespVO.setPackageStr("prepay_id=" + studentOrderData.getPrepayId());
log.info("订单已存在,获取订单信息成功======>" + wxPayRespVO.toString());
this.success("订单已存在,请支付======>", wxPayRespVO);
return;
} else {
JsapiPayOrderConfig jsapiPayOrderConfig = new JsapiPayOrderConfig();
if (studentOrderData != null && studentOrderData.getOpenId() != null) {
openIdStr = studentOrderData.getOpenId();
log.info("获取订单中的用户OpenId=========>" + openIdStr);
} else {
// String openId = JsapiPayOrderConfig.testOpenId;//测试openid
openIdStr = studentOrderService.getStudentOrderOpenId(student.getParentPhone());
log.info("查找用户历史openid=====>" + openIdStr);
if (openIdStr == null) {
log.info("正在获取用户openid信息");
openIdStr = jsapiPayOrderConfig.getOpenId(code);
log.info("获取结果=======>" + openIdStr);
}
}
log.info("获取用户openId======>" + openIdStr);
if (openIdStr == null) {
this.error("获取用户openId失败");
return;
}
//生成订单编号
String orderNo = studentOrderService.createOrderNo();
log.info("创建订单编号=====>" + orderNo);
//生成订单信息
StudentOrder studentOrder = new StudentOrder();
studentOrder.setOrderNo(orderNo);
studentOrder.setStatus(1);
studentOrder.setIsOrg(nursery.getIsOrg());
studentOrder.setMoney(nursery.getPrice());
studentOrder.setNurseryId(student.getNurseryId());
studentOrder.setReportId(studentReport.getId());
studentOrder.setReportName(studentReport.getSchoolName() + studentReport.getClassName());
studentOrder.setParentPhone(student.getParentPhone());
studentOrder.setStudentId(student.getId());
studentOrder.setStudentName(student.getName());
studentOrder.setNurseryId(student.getNurseryId());
studentOrder.setClassId(student.getClassId());
studentOrder.setCreateTime(new Date());
studentOrder.setOpenId(openIdStr);
studentOrder.setOrderType("Jsapi支付");
studentOrder.setCode(code);
String jsapiPayId = jsapiPayOrderConfig.getJsapiPay(studentOrder);
if (jsapiPayId.length() == 0) {
this.error("生成预支付订单id失败");
return;
}
studentOrder.setPrepayId(jsapiPayId);
Long timeStamp = System.currentTimeMillis() / 1000;
studentOrder.setTimeStamp(timeStamp.toString());
studentOrder.save();
WxPayRespVO wxPayRespVO = new WxPayRespVO();
wxPayRespVO.setAppId(JsapiPayOrderConfig.AppID);
wxPayRespVO.setPrepayId(jsapiPayId);
wxPayRespVO.setTimeStamp(timeStamp.toString());
String substring = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
wxPayRespVO.setNonceStr(substring);
String signatureStr = Stream.of(JsapiPayOrderConfig.AppID, String.valueOf(timeStamp), substring, "prepay_id=" + jsapiPayId)
.collect(Collectors.joining("\n", "", "\n"));
String sign = OrderPayUtil.getSign(signatureStr, JsapiPayOrderConfig.privateKeyPath);
wxPayRespVO.setPaySign(sign);
wxPayRespVO.setPackageStr("prepay_id=" + jsapiPayId);
log.info("创建订单成功======>" + wxPayRespVO.toString());
this.success("创建订单成功======>", wxPayRespVO);
}
} catch (Exception e) {
log.error("创建失败======>" + e.getMessage());
}
}
问题5:接问题4说,支付参数都准备好了,那这样就可以调支付控件支付,理论上这样没问题,这里我用的是WeixinJSBridge的方法,但是没有生效,原因是因为我没有按script标签的方式引入,按照script标签引入就行,还有一种可能就是你jsapi支付授权目录没有配置或者说配置不对,我这里是配置了支付授权目录的,所以没有这方面的原因
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<%= require('pages/common/header-link.html') %>
<meta name="viewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<%= require('pages/common/flexible.html') %>
<title>绑定信息</title>
</head>
<body>
<div id="app">
<index></index>
</div>
<%= require('pages/common/footer.html') %>
<%= require('pages/common/footer-script.html') %>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.4.4/build/qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.4.4"></script>
</body>
</html>
问题6:接着问题5说,支付参数正确,支付控件能调起正确,能支付正确,商户也能收到钱正确,业务没有正确的执行,有问题。
用户支付完成之后,微信服务会发送支付回调给服务端,你需要响应他的回调请求并且完成你的业务处理,代码如下
/**
* jsapi支付回调
*
* @throws Exception
*/
public void payNotify() throws Exception {
log.info("===============接收微信支付通知========");
JsapiPayOrderConfig jsapiPayOrderConfig = new JsapiPayOrderConfig();
jsapiPayOrderConfig.payNotifyUrl(this.getRequest(), this.getResponse());
this.success("接收成功");
}
jsapiPayOrderConfig.payNotifyUrl封装方法里面的内容:
public void payNotifyUrl(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
//读取请求体的信息
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String body = stringBuffer.toString();
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
String signType = request.getHeader("Wechatpay-Signature-Type");
String serialNo = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
// 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
// 没有的话,则构造一个
// log.error(com.alibaba.fastjson2.JSON.toJSONString(wxPayV3Bean));
NotificationConfig config = new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(signType)
.body(body)
.build();
Transaction transaction = parser.parse(requestParam, Transaction.class);
Transaction.TradeStateEnum state = transaction.getTradeState();
log.info("body" + body);
log.info("parse = " + transaction);
Writer out = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
if (!StringUtils.equals("SUCCESS", state.toString())) {
log.error("微信回调失败,JsapiPayController.payNotify.transaction:{}");
//通知微信回调失败
// response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>");
out.write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>");
}
//TODO------
//根据自己的需求处理相应的业务逻辑,异步
log.info("报文输出=======>" + requestParam.toString());
StudentOrder orderData = studentOrderService.getStudentOrderByOrderNo(transaction.getOutTradeNo());
if (orderData != null) {
studentOrderService.updateStudentOrderByOrderNo(transaction.getOutTradeNo());
reportStudentMobileService.updateReport(orderData.getReportId());
}
out.write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
log.info("============支付成功==============");
//通知微信回调成功
// response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
} catch (Exception e) {
log.error("支付通知失败");
} finally {
response.getOutputStream().flush();
}
我这里用response.getWriter().write报输出异常,所以改用out.write的方式,finally 无论请求失败还是成功都要执行
问题7:接着问题6来说我这里创建支付订单到支付回调都可以跑通了,然后就是支付之后的操作了,在这里我的想法是用户支付之后直接跳转页面,不需要再点一下,但是从2024年起微信官方默认把支付之后跳转的这个功能关闭了,如果需要的话要去配置这个功能,这个功能的配置可以去看一下微信官方文档,我这里的做法是刷一下页面,然后让用户再点击一下跳转页面
结尾:jsapi支付就这样完成了,如果对你有所帮助记得点赞