微信JSAPI支付小结
公司项目用到微信公众号支付,所以,从看文档,到完成,整整用了两天,好多坑,有的坑爬出来,虽然能用了,但是还没去深究原因,这里我先整理一下从头到尾的流程。
上面是官方给的时序图,我也是跟着这个流程走的。
这里我省略了从微信公众号点击自定义菜单获取用户openid的过程,这个打算另外记录。
另外的一些支付的先决条件,这里简单说一下:(没做过微信的其他支付方式,这里只是说微信公众号的)
1. 微信支付商户号(下图中被我打马赛克的地方)
2. 商户支付密钥
3. 需要支付的微信公众号要跟微信商户绑定
4. 微信公众号的appId和密钥
有了这些以后,就可以开发了:
1. 需要支付的页面引入js:(这个官方有)
<script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
2. 需要支付的页面鉴权:
进来该页面的controller里面:
model.addAttribute("jsauthmap",wxOfficalJsApiService.getAuthMap(request));
public Map getAuthMap(HttpServletRequest request) { String url = pageUrlUtils.getPageUrl(request); return getAuthMap(url); }
//这个方法是获取url的,在我的一个工具包中,我摘出来了:
public String getPageUrl(HttpServletRequest request){
String queryString = request.getQueryString();
if(!StringUtils.isEmpty(queryString) || "null".equals(queryString)){
queryString = "?"+queryString;
}else{
queryString = "";
}
String domain = request.getScheme()
+"://"+ microAttr.getDomain()
+ request.getRequestURI()
+queryString;
return domain;
}
public Map getAuthMap(String url){
log.info("获取微信公众号jsapi鉴权数据,,,,,,url:"+url);
//获取随机字符串,这里网上搜索一大堆,我就不晒具体代码了
String nonceStr = RandomStr.getRandom(7, RandomStr.TYPE.LETTER);
long timeStamp = System.currentTimeMillis() / 1000;
//下面是去获取jsapi_ticket
String ticket = wxOfficalJsApiService.getWxOfficalJsApiTicket();
String signature = null;
try {
signature = sign(ticket, nonceStr, timeStamp, url);
} catch (OApiException e) {
e.printStackTrace();
}
Map<String, Object> configValue = new HashMap<>();
configValue.put("jsticket", ticket);
configValue.put("signature", signature);
configValue.put("nonceStr", nonceStr);
configValue.put("timeStamp", timeStamp);
return configValue;
}
/**
* 获取jsapi_ticket
* 这里用到了缓存,第一句是从缓存中取,如果没有就使用accessToken去获取
* 访问微信服务区获取jsapi_ticket我使用了restTemplate,只是发起远程调用的一个工具,可以任意换
*/
public String getWxOfficalJsApiTicket(){
String ticket = cacheHandle.getStr(RedisKeys.REDISKEY_WXOFFICAL_JSAPI_TICKET);
if(StringUtils.isEmpty(ticket)){
//缓存中没有,使用accessToken去微信服务器中获取:
String wxOfficalAccessToken = moltechToken.getToken();
if(StringUtils.isEmpty(wxOfficalAccessToken)){
log.warning("获取微信公众号jsapiticket时,获取微信公众号accessToken失败!");
return "";
}
String jsapiResultStr = restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + wxOfficalAccessToken + "&type=jsapi", String.class);
log.info("获取微信公众号jsapiTicket返回的结果为:");
log.info(jsapiResultStr);
if(StringUtils.isEmpty(jsapiResultStr)){
log.warning("获取微信公众号的jsapiticket,返回值为空!");
return "";
}
JSONObject jsonObject = JSONObject.parseObject(jsapiResultStr);
if("ok".equals(jsonObject.getString("errmsg")) && StringUtils.isNotEmpty(jsonObject.getString("ticket"))){
log.info("访问微信服务器获取到的jsapi_ticket为:"+jsonObject.getString("ticket"));
ticket = jsonObject.getString("ticket");
cacheHandle.saveStr(RedisKeys.REDISKEY_WXOFFICAL_JSAPI_TICKET,7000,ticket);
}
}
return ticket;
}
上面的代码就是后台获取前端页面需要用到的鉴权数据的逻辑,获取到以后存到model中,然后看前端页面:
//页面加载完成后执行 window.onload = function(){ console.log("微信公众号,获取到的JS-SDK鉴权数据:"); var jsauthmap = [[${jsauthmap}]]; wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: jsauthmap.appId, // 必填,公众号的唯一标识 timestamp: jsauthmap.timestamp, // 必填,生成签名的时间戳 nonceStr: jsauthmap.nonceStr, // 必填,生成签名的随机串 signature: jsauthmap.signature,// 必填,签名 jsApiList: [ 'chooseWXPay' ] // 必填,需要使用的JS接口列表 }); //微信公众号登录的,使用微信jsapi支付: //pay_btn是支付按钮的id,这里使用jquery给它绑定点击事件 $("#pay-btn").on('click',function(){ //支付1.去后端获取支付信息 2,带着支付信息调用微信统一支付接口 //禁用支付,防止多次支付 var theBtn = $("#pay-btn"); theBtn.attr('disabled','disable'); //money是要支付的金额,,payfor是自定义的支付用途 var money = $("#cost_span").text().trim(); wx.ready(function(){ // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 /* 微信公众号支付支付 */ $.get("/wxPay/webPay", { totalFee: money, payfor: '1' }, function (res) { if (res.code == 0) { let data = $.parseJSON(res.data.jsonStr); let orderid = res.data.outTradeNo; if (typeof WeixinJSBridge == "undefined") { if (document.addEventListener) { document.addEventListener('WeixinJSBridgeReady', onBridgeReady(data,orderid), false); } else if (document.attachEvent) { document.attachEvent('WeixinJSBridgeReady', onBridgeReady(data,orderid)); document.attachEvent('onWeixinJSBridgeReady', onBridgeReady(data,orderid)); } } else { onBridgeReady(data,orderid); } } else { if (res.code == 2) { layer.alert(res.message); } else { layer.msg("error:" + res.message, { shift: 6 }); } } }); /* 微信公众号支付支付 END */ }); }) }
function onBridgeReady(json,orderid) {
WeixinJSBridge.invoke('getBrandWCPayRequest', json, function (res) {
// 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
// alert(JSON.stringify(res));
if (res.err_msg == "get_brand_wcpay_request:ok") {
//支付成功的逻辑:
// layer.msg("支付成功", {
// shift: 6
// });
//支付成功跳转到认证页面:
// setTimeout(function(){
// window.location.href = '/attr/zhanlve';
// },1000);
//我自己写的没隔5秒去后端获取支付状态的逻辑,后面带着支付成功后跳转的路径
getOrderStatus(orderid,5,'/auth/attr/zhanlve');
// self.location = "#(ctxPath)/success";
} else {
layer.msg("支付失败", {
shift: 6
});
}
});
}
/wxPay/webPay:
/ * 公众号支付
*/ @RequestMapping(value = "/webPay", method = {RequestMethod.POST, RequestMethod.GET}) @ResponseBody public AjaxResult webPay(HttpServletRequest request, @RequestParam("totalFee") String totalFee,@RequestParam("payfor") String payfor,String purchaseId) { // openId,采用 网页授权获取 access_token API:SnsAccessTokenApi获取 log.info("/wxPay/webPay....totalFee:"+totalFee+",,,payfor:"+payfor+",,,purchaseId:"+purchaseId); // log.info("wxPayBean:"); // log.info(wxPayBean.toString()); //获取用户在微信公众号中的openid,这个是我之前用户通过微信公众号的自定义菜单进来以后我存入session中的 String openId = (String) request.getSession().getAttribute("wxOpenId"); if (openId == null || StrUtil.isEmpty(openId)) { return new AjaxResult().addError("openId is null"); } if (StrUtil.isEmpty(totalFee)) { return new AjaxResult().addError("请输入数字金额"); }
//微信支付,金额的单位是分,所以要乘以100 Integer totalFeeInt = (int)(Double.valueOf(totalFee)*100); String ip = IpKit.getRealIp(request); if (StrUtil.isEmpty(ip)) { ip = "127.0.0.1"; }
//我这里封装的map是打算在微信支付的回调事件里面使用,用于业务,如果有需要自定义的东西跟随支付的最后,可以这样做 Map paraMap = new HashMap(); paraMap.put("payFor", payfor); paraMap.put("supplierId",supplier.getPkSupplier()); if(payfor.equals(PayContants.ORDER_PAY_FOR_EXPERT_REVIEW_AND_CONTRACT_FEE)){ if(StringUtils.isBlank(purchaseId)){ return new AjaxResult().addError("支付合同费用时订单id不能为空!"); } paraMap.put("purchaseId",purchaseId); } // WxPayApiConfig wxPayApiConfig = WxPayApiConfigKit.getWxPayApiConfig(); //获取随机字符串充当商户单号 String outTradeNo = WxPayKit.generateStr(); UnifiedOrderModel build = UnifiedOrderModel .builder() .appid(wxPayBean.getAppId()) //微信公众号的appid .mch_id(wxPayBean.getMchId()) //微信支付商户号 .nonce_str(WxPayKit.generateStr()) //随机字符串 .body(body) //商品名称 .attach(JSONObject.toJSONString(paraMap)) //填充自定义参数的地方 .out_trade_no(outTradeNo) //商户单号, .total_fee(totalFeeInt + "") //支付金额,这里一定要传字符串,否则会报错 .spbill_create_ip(ip) //这个ip我的理解是服务器的ip .notify_url("http://xxxx/wxPay/payNotify") //支付结果异步回调接口地址 .trade_type(TradeType.JSAPI.getTradeType()) //微信JSAPI支付标识 .openid(openId) //用户的微信公众号的openid .build(); log.info(JSON.toJSONString(build)); /** * {"appid":"wx12f87136ac56ba6e", * "attach":"1", * "body":"战略供应商服务费用", * "mch_id":"1489352152", * "nonce_str":"0a39fadd2abe4145b44ce6163abdd089", * "notify_url":"http://ding.starint.cn/wxPay/payNotify", * "openid":"o_VB40gpnCnTZJHE9R1rZDQj5Xto", * "out_trade_no":"b1f49c61c794470daf09de9d247a6f92", * "spbill_create_ip":"223.99.0.13", * "total_fee":"1", * "trade_type":"JSAPI"} */ Map<String, String> params = build.createSign(wxPayBean.getPartnerKey(), SignType.HMACSHA256); String xmlResult = WxPayApi.pushOrder(false, params); log.info("获取预支付订单信息:"); log.info(xmlResult); /** * <xml> * <return_code><![CDATA[SUCCESS]]></return_code> * <return_msg><![CDATA[OK]]></return_msg> * <appid><![CDATA[wx12f87136ac56ba6e]]></appid> * <mch_id><![CDATA[1489352152]]></mch_id> * <nonce_str><![CDATA[miebAaIw7PLx7ZV6]]></nonce_str> * <sign><![CDATA[075360B8CA6D2A8C378764041915DA6CD809558543CBDBF3CD4DC7CBF66408BF]]></sign> * <result_code><![CDATA[SUCCESS]]></result_code> * <prepay_id><![CDATA[wx1110261039192337a6b817c4ba1f4e0000]]></prepay_id> * <trade_type><![CDATA[JSAPI]]></trade_type> * </xml> */ Map<String, String> resultMap = WxPayKit.xmlToMap(xmlResult); String returnCode = resultMap.get("return_code"); String returnMsg = resultMap.get("return_msg"); if (!WxPayKit.codeIsOk(returnCode)) { return new AjaxResult().addError(returnMsg); } String resultCode = resultMap.get("result_code"); if (!WxPayKit.codeIsOk(resultCode)) { return new AjaxResult().addError(returnMsg); } // 以下字段在 return_code 和 result_code 都为 SUCCESS 的时候有返回 String prepayId = resultMap.get("prepay_id"); Map<String, String> packageParams = WxPayKit.prepayIdCreateSign(prepayId, wxPayBean.getAppId(), wxPayBean.getPartnerKey(), SignType.HMACSHA256); String jsonStr = JSON.toJSONString(packageParams); log.info("生成的前台页面需要传递的参数及sign为:"); log.info(jsonStr); Map resultMap2 = new HashMap(); resultMap2.put("jsonStr",jsonStr); resultMap2.put("outTradeNo",outTradeNo); /** * { * "timeStamp":"1599791528", * "signType":"HMAC-SHA256", * "package":"prepay_id=wx1110320782721960a9b0c3912d67670000", * "paySign":"3F9C32F7DADE0F65C4053CC1CE0CF6FBABC3549FF745BBA4AE45853331F80B98", * "nonceStr":"1599791528139", * "appId":"wx12f87136ac56ba6e" * } */ return new AjaxResult().success(resultMap2); }
回调时间略
里面的一些工具类请看这个开源项目:https://gitee.com/javen205/IJPay
里面有一个springboot的Demo,下下来比较着做就OK;
在此特别感谢作者开源,方便他人。
看完开源框架里面的内容,然后在跟着我的代码比较一下,微信支付基本就没啥问题了。
2. 去后台