微信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. 去后台