簡史 官方文檔說的很清楚,商戶已有H5商城網站,用戶通過消息或掃描二維碼在微信內打開網頁時,可以調用微信支付完成下單購買的流程。 當然,最近微信支付平臺也加入了純H5支付,也就是說用戶可以在微信以外的手機瀏覽器請求微信支付的場景喚起微信支付。 當然,今天的主角是微信公眾號支付,其實也不一定非在公眾號 ...
簡史
官方文檔說的很清楚,商戶已有H5商城網站,用戶通過消息或掃描二維碼在微信內打開網頁時,可以調用微信支付完成下單購買的流程。
當然,最近微信支付平臺也加入了純H5支付,也就是說用戶可以在微信以外的手機瀏覽器請求微信支付的場景喚起微信支付。
當然,今天的主角是微信公眾號支付,其實也不一定非在公眾號中打開,只要在微信中打開就可以使用。
實現
項目使用的springboot微服務來實現,以下都是簡單的偽代碼實現,具體邏輯見碼雲。
Main
其實就是一個初始化下單操作,前臺業務邏輯在這就不展示了,這個就是接收前臺參數的方法:
@RequestMapping("/pay") public String pay(Product product,ModelMap map) { logger.info("H5支付(需要公眾號內支付)"); String url = weixinPayService.weixinPayMobile(product); return "redirect:"+url; }
產品實體Bean:
/** * 產品訂單信息 * 創建者 科幫網 * 創建時間 2017年7月27日 */ @Data @NoArgsConstructor @AllArgsConstructor public class Product implements Serializable { private static final long serialVersionUID = 1L; private String productId;// 商品ID private String subject;//訂單名稱 private String body;// 商品描述 private String totalFee;// 總金額(單位是分) private String outTradeNo;// 訂單號(唯一) private String spbillCreateIp;// 發起人IP地址 private String attach;// 附件數據主要用於商戶攜帶訂單的自定義數據 private Short payType;// 支付類型(1:支付寶 2:微信 3:銀聯) private Short payWay;// 支付方式 (1:PC,平板 2:手機) private String frontUrl;// 前臺回調地址 非掃碼支付使用 }
由於整合了Dubbo,使用PRC的方式調用,這裡定義一個service:
@Override public String weixinPayMobile(Product product) { StringBuffer url = new StringBuffer(); String totalFee = product.getTotalFee(); //redirect_uri 需要在微信支付端添加認證網址 totalFee = CommonUtil.subZeroAndDot(totalFee); url.append("http://open.weixin.qq.com/connect/oauth2/authorize?"); url.append("appid="+ConfigUtil.APP_ID); url.append("&redirect_uri="+server_url+"weixinMobile/dopay?"); //註意 此處 get請求 拼接相關參數 用於redirect_uri獲取 url.append("outTradeNo="+product.getOutTradeNo()+"&totalFee="+totalFee); url.append("&response_type=code&scope=snsapi_base&state="); url.append("#wechat_redirect"); return url.toString(); }
Topay
大家有沒有註意到redirect_uri參數中,我們定義了我們自己系統中的url請求,如下:
@RequestMapping(value = "dopay") public String dopay(HttpServletRequest request, HttpServletResponse response) throws Exception { //此處為weixinPayMobile方法中拼接的參數 String orderNo = request.getParameter("outTradeNo"); String totalFee = request.getParameter("totalFee"); //獲取code 這個在微信支付調用時會自動加上這個參數無須設置 String code = request.getParameter("code"); //獲取用戶openID(JSAPI支付必須傳openid) String openId = MobileUtil.getOpenId(code); String notify_url =server_url+"/weixinMobile/WXPayBack";//回調介面 String trade_type = "JSAPI";// 交易類型H5支付 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); ConfigUtil.commonParams(packageParams); packageParams.put("body","報告");// 商品描述 packageParams.put("out_trade_no", orderNo);// 商戶訂單號 packageParams.put("total_fee", totalFee);// 總金額 packageParams.put("spbill_create_ip", AddressUtils.getIpAddr(request));// 發起人IP地址 packageParams.put("notify_url", notify_url);// 回調地址 packageParams.put("trade_type", trade_type);// 交易類型 packageParams.put("openid", openId);//用戶openID String sign = PayCommonUtil.createSign("UTF-8", packageParams,ConfigUtil.API_KEY); packageParams.put("sign", sign);// 簽名 String requestXML = PayCommonUtil.getRequestXml(packageParams); String resXml = HttpUtil.postData(ConfigUtil.UNIFIED_ORDER_URL, requestXML); Map map = XMLUtil.doXMLParse(resXml); String returnCode = (String) map.get("return_code"); String returnMsg = (String) map.get("return_msg"); StringBuffer url = new StringBuffer(); if("SUCCESS".equals(returnCode)){ String resultCode = (String) map.get("result_code"); String errCodeDes = (String) map.get("err_code_des"); if("SUCCESS".equals(resultCode)){ //獲取預支付交易會話標識 String prepay_id = (String) map.get("prepay_id"); String prepay_id2 = "prepay_id=" + prepay_id; String packages = prepay_id2; SortedMap<Object, Object> finalpackage = new TreeMap<Object, Object>(); String timestamp = DateUtil.getTimestamp(); String nonceStr = packageParams.get("nonce_str").toString(); finalpackage.put("appId", ConfigUtil.APP_ID); finalpackage.put("timeStamp", timestamp); finalpackage.put("nonceStr", nonceStr); finalpackage.put("package", packages); finalpackage.put("signType", "MD5"); //這裡很重要 參數一定要正確 狗日的騰訊 參數到這裡就成大寫了 //可能報錯信息(支付驗證簽名失敗 get_brand_wcpay_request:fail) sign = PayCommonUtil.createSign("UTF-8", finalpackage,ConfigUtil.API_KEY); url.append("redirect:/weixinMobile/payPage?"); url.append("timeStamp="+timestamp+"&nonceStr=" + nonceStr + "&package=" + packages); url.append("&signType=MD5" + "&paySign=" + sign+"&appid="+ ConfigUtil.APP_ID); url.append("&orderNo="+orderNo+"&totalFee="+totalFee); }else{ logger.info("訂單號:{}錯誤信息:{}",orderNo,errCodeDes); url.append("redirect:/weixinMobile/error?code=0&orderNo="+orderNo);//該訂單已支付 } }else{ logger.info("訂單號:{}錯誤信息:{}",orderNo,returnMsg); url.append("redirect:/weixinMobile/error?code=1&orderNo="+orderNo);//系統錯誤 } return url.toString(); }
其實,以上代碼就是一個認證(獲取openid)、下單的過程,最終獲取相關參數再重定向到pay頁面,也就是我們定義的 redirect:/weixinMobile/payPage。
//公眾號H5支付主頁 @RequestMapping(value = "payPage") public String pay(HttpServletRequest request, HttpServletResponse response) throws Exception { return "weixin/pay"; }
然後轉發到pay.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; //相關參數 String appId = request.getParameter("appid"); String timeStamp = request.getParameter("timeStamp"); String nonceStr = request.getParameter("nonceStr"); String packageValue = request.getParameter("package"); String paySign = request.getParameter("paySign"); String orderNo = request.getParameter("orderNo"); String totalFee = request.getParameter("totalFee"); %> <!DOCTYPE html> <html> <head> <title>微信支付</title> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/> <meta name="apple-mobile-web-app-capable" content="yes"/> <meta name="apple-mobile-web-app-status-bar-style" content="black"/> <meta name="format-detection" content="telephone=no"/> <script type="text/javascript" src="<%=basePath%>static/js/jquery-1.10.2.min.js"></script> <!-- 引入 jweixin-1.0.0.js--> <script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script> </head> <body> <article class="order-main "> <div class="ph_order"> <div class=" affirm-info"> <h4 id="orderNo"></h4> <h3 id="totalFee"></h3> <div class="detail-dl"> <dl> <dt>收款方</dt> <dd>科幫網</dd> </dl> <dl> <dt>商 品</dt> <dd id="productName">充值幫幣</dd> </dl> </div> <div onclick="callpay()" class="pay-info">立即支付</div> </div> </div> </article> </body> <script type="text/javascript"> var orderNo = '<%=orderNo%>'; var totalFee = '<%=totalFee%>'; $(function(){ init(); }); function onBridgeReady(){ WeixinJSBridge.invoke('getBrandWCPayRequest',{ "appId" : "<%=appId%>", "timeStamp" : "<%=timeStamp%>", "nonceStr" : "<%=nonceStr%>", "package" : "<%=packageValue%>", "signType" : "MD5", "paySign" : "<%=paySign%>" },function(res){ //使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在用戶支付成功後返回 ok,但並不保證它絕對可靠。 if(res.err_msg == "get_brand_wcpay_request:ok"){ window.location.href="http://前臺回調地址"; }else if(res.err_msg == "get_brand_wcpay_request:cancel"){ alert("用戶取消支付!"); }else if(res.err_msg == "get_brand_wcpay_request:fail"){ alert("支付失敗!"); } }) } function callpay(){ if (typeof WeixinJSBridge == "undefined"){ if( document.addEventListener ){ document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false); }else if (document.attachEvent){ document.attachEvent('WeixinJSBridgeReady', onBridgeReady); document.attachEvent('onWeixinJSBridgeReady', onBridgeReady); } }else{ onBridgeReady(); } } function init(){ $("#orderNo").html("科幫網-訂單編號:"+orderNo); totalFee = accDiv(totalFee,100); $("#totalFee").html("¥"+totalFee); } function accDiv(arg1,arg2){ var t1=0,t2=0,r1,r2; try{t1=arg1.toString().split(".")[1].length;}catch(e){} try{t2=arg2.toString().split(".")[1].length;}catch(e){} with(Math){ r1=Number(arg1.toString().replace(".","")); r2=Number(arg2.toString().replace(".","")); return (r1/r2)*pow(10,t2-t1); } } </script> </html>
Notify
其實,這就是一個回調通知,用戶支付成功以後,微信會通知我們後臺支付狀態,然後我們根據訂單信息完成下一步業務邏輯。
@RequestMapping(value = "WXPayBack") public void WXPayBack(HttpServletRequest request, HttpServletResponse response){ String resXml = ""; try { //解析XML Map<String, String> map = MobileUtil.parseXml(request); String return_code = map.get("return_code");//狀態 String out_trade_no = map.get("out_trade_no");//訂單號 if (return_code.equals("SUCCESS")) { if (out_trade_no != null) { //處理訂單邏輯 logger.info("微信手機支付回調成功訂單號:{}",out_trade_no); resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "; } }else{ logger.info("微信手機支付回調失敗訂單號:{}",out_trade_no); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[報文為空]]></return_msg>" + "</xml> "; } } catch (Exception e) { logger.error("手機支付回調通知失敗",e); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[報文為空]]></return_msg>" + "</xml> "; } try { // ------------------------------ // 處理業務完畢 // ------------------------------ BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); } }
坑
其實,當你完成集成測試的那一刻,也就沒啥子坑了,相關的註意事項都在代碼中有體現。
詳細代碼見:碼雲