生活随笔
收集整理的這篇文章主要介紹了
微信公众号开发,微信支付功能开发(网页JSAPI调用)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
1、微信支付的流程
如下三張手機截圖,我們在微信網頁端看到的支付,表面上看到的是 “點擊支付按鈕 - 彈出支付框 - 支付成功后出現提示頁面”,實際上的核心處理過程是:
- 點擊支付按鈕時,執行一個Ajax到后臺
- 后臺通過前臺的部分信息(如商品名額,金額等),將其組裝成符合微信要求格式的xml,然后調用微信的“統一下單接口”
- 調用成功后微信會返回一個組裝好的xml,我們提取之中的消息(預支付id也在其中)以JSON形式返回給前臺
- 前臺將該JSON傳參給微信內置JS的方法中,調其微信支付
- 支付成功后,微信會將本次支付相關信息返回給我們的服務器
這些在《 微信支付官方文檔 - 場景介紹》和《 微信支付官方文檔 - 業務流程》都進行了更詳細的說明。
2、微信支付功能開發詳解?
2.1 設置支付目錄和授權域名
登陸公眾號,進行支付相關的目錄和域名設置,詳情參考《 微信支付官方文檔 - 開發步驟》,我這里簡單貼幾張官方的圖就行了,這步比較簡單,就不過多說明了,只提其中一點:對于微信支付授權的目錄,發起微信支付的頁面必須精確地位于授權目錄下,假如支付頁面為 http://www.a.com/wx/pay/a.html,那么授權目錄必須為 http://www.a.com/wx/pay/,其他的如 http://www.a.com/wx/ , https://www.a.com/wx/pay/(http和https是不一樣的),http://a.com/wx/pay/(千萬別忘了www)都是不行的。填寫了這些非法目錄無法調起支付。
2.2 組裝xml,調用統一下單接口,獲取prepay_id
2.2.1 組裝xml
點擊支付按鈕后,寫一個Ajax將前臺部分信息發送給后臺,然后組裝xml,調用統一下單接口。該接口在《 微信支付官方文檔 - 統一下單》進行了很詳細的解釋,我在這里進行部分說明:
| 參數 | 說明? ?? | 備注 |
| appId? ?? | 開發者應用ID,在 “開發 - 基本配置” 查看 | |
| mch_id | 微信支付的商戶號,在 “微信支付 - 商戶信息” 查看 | |
| device_info? ?? | 終端設備號(門店號或收銀設備ID) | PC網頁或公眾號內支付,則傳 “WEB” |
| body | 商品或支付的簡單描述 | |
| trade_type | 可取值JSAPI,NATIVE,APP等,我們這里使用的是JSAPI | JSAPI 公眾號支付;NATIVE 原生掃碼支付;APP app支付 |
| nonce_str | 隨機字符串 | 參考算法:《微信支付官方文檔 - 安全規范》 |
| notify_url | 通知地址,微信支付成功后,微信服務器會發送信息到該url | |
| out_trade_no | 商戶系統內部訂單號,由商戶自定義,訂單號要保持唯一性 | |
| total_fee | 訂單總金額,單位:分 | |
| openid | 用戶標識,用戶在該公眾號下的唯一身份標識 | |
| sign | 簽名 | 參考算法:《微信支付官方文檔 - 安全規范》 |
| key | API密鑰,在 “微信商戶平臺?- 賬戶中心 - API安全 - API密鑰” | |
其他的都比較簡單,重要的在于這兩個涉及算法的參數,nonce_str 和 sign,這里說明一下:
- nonce_str 隨機字符串,用于保證簽名不可預測
- sign 簽名
- 算法:
- 所有發送或接收的數據按參數名ASCII碼從小到大排序,使用鍵值對形式拼接為字符串(如 key1=value1&key2=value2…)
- ASCII碼的字典排序,可以利用TreeMap幫我們自動實現
- 將拼接好的字符串最后,再拼接上API密鑰,即key,得到新的字符串
- 將新的字符串進行MD5加密,并將加密后字符串全部轉換為大寫
按照以上的這些說明,進行xml的拼裝,貼上我自己的測試代碼(注:為方便測試,部分數據我直接寫入方法了,如body、openId等):
String appId = WeChatAPI.getAppID();String body = "JSAPI支付測試";String merchantId = WeChatAPI.getMerchantID();String tradeNo = String.valueOf(new Date().getTime());String nonceStr1 = SignUtil.createNonceStr();String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";String openId = "okAkc0muYuSJUtvMf25UQHnqYvM4";String totalFee = "1";TreeMap<String, String> map = new TreeMap<String, String>();map.put("appid", appId);map.put("mch_id", merchantId);map.put("device_info", "WEB");map.put("body", body);map.put("trade_type", "JSAPI");map.put("nonce_str", nonceStr1);map.put("notify_url", notifyUrl);map.put("out_trade_no", tradeNo);map.put("total_fee", totalFee);map.put("openid", openId);String sign = SignUtil.createSign(map);String xml = "<xml>" + "<appid>" + appId + "</appid>" + "<body>" + body +"</body>" + "<device_info>WEB</device_info>" + "<mch_id>" + merchantId + "</mch_id>" + "<nonce_str>" + nonceStr1 + "</nonce_str>" + "<notify_url>" + notifyUrl +"</notify_url>" + "<openid>" + openId + "</openid>" + "<out_trade_no>" + tradeNo + "</out_trade_no>" + "<total_fee>" + totalFee + "</total_fee>" + "<trade_type>JSAPI</trade_type>" + "<sign>" + sign + "</sign>" + "</xml>";
注意:
- body參數如果直接填寫中文,在調用接口時會出現“簽名錯誤”,要以ISO8859-1編碼
- 所以?String body = new String("body內容字符串".getBytes("ISO8859-1"));
- 但即便如此,在支付完成后微信推送的“微信支付憑證”中,商品詳情中的中文也依然顯示的亂碼
- body參數內容如果包含中文,那么在調用接口時會出現“簽名錯誤”
- 在網上找了很多方法,有了如上刪除線部分的方法,但是仍然是有問題的,因為支付成功后的憑證里中文是亂碼
- 后來終于在網上各種倒騰,找到了原因,確實是編碼問題,但問題不在body是否使用ISO8859-1,而在MD5的加密算法中是否使用UTF-8
- 所以?md.update(sourceStr.getBytes("UTF-8"));
兩個算法的代碼如下:
/** * 生成隨機數 * <p>算法參考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p> * @return 隨機數字符串 */public static String createNonceStr() { SecureRandom random = new SecureRandom(); int randomNum = random.nextInt(); return Integer.toString(randomNum);}/** * 生成簽名,用于在微信支付前,獲取預支付時候需要使用的參數sign * <p>算法參考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p> * @param params 需要發送的所有數據設置為的Map * @return 簽名sign */public static String createSign(TreeMap<String, String> params) { String signValue = ""; String stringSignTemp = ""; String stringA = ""; //獲得stringA Set<String> keys = params.keySet(); for (String key : keys) { stringA += (key + "=" + params.get(key) + "&"); } stringA = stringA.substring(0, stringA.length() - 1); //獲得stringSignTemp stringSignTemp = stringA + "&key=" + WeChatAPI.getMerchantKey(); //獲得signValue signValue = encryptByMD5(stringSignTemp).toUpperCase(); log.debug("預支付簽名:" + signValue); return signValue;}/** * MD5加密 * * @param sourceStr * @return */public static String encryptByMD5(String sourceStr) { String result = ""; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(sourceStr.getBytes("UTF-8")); byte b[] = md.digest(); int i; StringBuffer buf = new StringBuffer(""); for (int offset = 0; offset < b.length; offset++) { i = b[offset]; if (i < 0) i += 256; if (i < 16) buf.append("0"); buf.append(Integer.toHexString(i)); } result = buf.toString(); } catch (NoSuchAlgorithmException e) { System.out.println(e); } return result;}
2.2.2 調用統一下單接口,獲取prepay_id
有了組裝好的xml,現在我們直接使用POST方式的請求發送給微信提供的接口就可以了,如果一切順利,我們會收到微信返回的xml字符串,格式示例如下:
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> <appid><![CDATA[wx2421b1c4370ec43b]]></appid> <mch_id><![CDATA[10000100]]></mch_id> <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str> <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid> <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign> <result_code><![CDATA[SUCCESS]]></result_code> <prepay_id><![CDATA[wx201411101639507cbf6ffd8b07629950874]]></prepay_id> <trade_type><![CDATA[JSAPI]]></trade_type></xml> 其中我們最需要的就是 prepay_id,這個值在后續需要用到。這段過程比較簡單,其中提取prepay_id我是用的正則,我直接貼代碼好了:
String url = WeChatAPI.getUrl_prePay();String result = NetUtil.sendRequest(url, "POST", xml);String reg = "<prepay_id><!\\[CDATA\\[(\\w+)\\]\\]></prepay_id>";Pattern pattern = Pattern.compile(reg);Matcher matcher = pattern.matcher(result);String prepayId = "";while (matcher.find()) { prepayId = matcher.group(1); log.debug("預支付ID:" + prepayId);} 2.3 回傳參數,調起微信支付JS
2.3.1 回傳參數
這時候,已經有了預支付ID,但是后臺的處理還沒有結束,我們還沒有把該有的信息返回給前臺。那么前臺需要哪些東西呢?《 微信支付官方文檔 - 微信內H5調起支付》有詳細的解釋,這里再貼一下:
| 參數???? | 說明? ?? | 備注 |
| appId | 開發者應用ID,在 “開發 - 基本配置” 查看 | |
| timeStamp? ?? | 時間戳,標準北京時間,秒級(10位數字) | |
| nonceStr? ?? | 隨機字符串 | 參考算法:《微信支付官方文檔 - 安全規范》 |
| package? ?? | 訂單詳情擴展字符串,其實就是預支付ID | 示例: prepay_id=*** |
| signType? ?? | 簽名方式,暫支持MD5 | |
| paySign? ?? | 簽名 | 參考算法:《微信支付官方文檔 - 安全規范》 |
有了之前的經驗,想必到這里對這些的獲取已經沒有什么問題了,但是仍然有幾個
注意的地方:
- package的值是 “prepay_id=***”,而不是 "***" 的方式(***表示之前獲取的prepay_id)
- timeStamp注意使用標準北京時間,可以使用Calendar設置Locale為CHINA,因為是秒級所以記得除以1000
- paySign簽名要重新生成,算法還是之前的,但是參數需要除自己以外的?appId、timeStamp、nonceStr、package、signType
- 之前xml中參數appid是小寫,這里的appId是大寫的I
好了,因為前臺接受到參數以后會以JSON的形式發送給微信服務器,所以我們這里后臺,直接就把這些參數封裝到一個JSONObject中就行了,然后轉成JSON的形式發給前臺。下面貼一下我的測試代碼,簽名算法和之前一樣,我這里就不重復貼出來了:
Date beijingDate = Calendar.getInstance(Locale.CHINA).getTime();
String nonceStr2 = SignUtil.createNonceStr();JSONObject json = new JSONObject();json.put("appId", appId);json.put("timeStamp", beijingDate.getTime() / 1000);json.put("nonceStr", nonceStr2);json.put("package", "prepay_id=" + prepayId);json.put("signType", "MD5");TreeMap<String, String> map2 = new TreeMap<String, String>();map2.put("appId", appId);map2.put("timeStamp", String.valueOf(beijingDate.getTime() / 1000));map2.put("nonceStr", nonceStr2);map2.put("package", "prepay_id=" + prepayId);map2.put("signType", "MD5");String paySign = SignUtil.createSign(map2);json.put("paySign", paySign);String re = json.toJSONString();AjaxSupport.sendSuccessText(null, re);
2.3.2 使用微信內置的JS調起微信支付
前臺的調用就很簡單了,看下官方給的示例代碼:
function onBridgeReady(){ WeixinJSBridge.invoke( 'getBrandWCPayRequest', { "appId":"wx2421bk1c4370c43b", //公眾號名稱,由商戶傳入 "timeStamp":"1395712654", //時間戳,自1970年以來的秒數 "nonceStr":"e61463f8efa94090b1f366cccfbbb444", //隨機串 "package":"prepay_id=u802345jfgjsdfgsdg888", "signType":"MD5", //微信簽名方式: "paySign":"70EA570631E4B79628FBCS90534C63FF7FADD89" //微信簽名 }, function(res){ if(res.err_msg == "get_brand_wcpay_request:ok" ) {} ? // 使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在用戶支付成功后返回ok,但并不保證它絕對可靠。 } ); }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();}
使用時直接替換掉invoke方法中的參數即可,實際上如果后臺直接是傳遞的JSON字符串到前臺,可以直接解析為JS對象作為參數,下面貼我自己的代碼:
$().invoke("/pay/do/pay.q", null, function (re) { var result = JSON.parse(re); function onBridgeReady(){ WeixinJSBridge.invoke( 'getBrandWCPayRequest', result, function(res){ alert(JSON.stringify(res)); if(res.err_msg == "get_brand_wcpay_request:ok" ) { //doit 這里處理支付成功后的邏輯,通常為頁面跳轉 } } ); } 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(); }});
這里還有個
坑,是iOS和Android系統不同導致的,如上代碼:
- 如果你在 var result = JSON.parse(re); 之前再添加一個用于debug的輸出語句 ?alert(re);
- 你可以看到傳過來的各項參數,其中timeStamp的值是沒有雙引號的,這會導致在iOS中支付出現錯誤,提示缺少timeStamp參數
? ? 所以為了兼容,必須要將這個轉換成字符串,帶上雙引號:
$().invoke("/pay/do/pay.q", null, function (re) { var result = JSON.parse(re); result['timeStamp'] = result['timeStamp'] + ""; function onBridgeReady(){ WeixinJSBridge.invoke( 'getBrandWCPayRequest', result, function(res){ alert(JSON.stringify(res)); if(res.err_msg == "get_brand_wcpay_request:ok" ) { //doit 這里處理支付成功后的邏輯,通常為頁面跳轉 } } ); } 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(); }});
另外,在這個頁面調試有個小技巧,將微信回調的JS對象序列化為JSON字符串,進行彈窗顯示:alert(JSON.stringify(res));
2.4 校驗信息的正確性
實際上在完成上面的步驟以后,已經可以進行微信支付了。這最后一步主要是為了確認支付信息的正確性,以及傳遞給我們本次支付的一些信息,以便業務處理。
支付成功后,微信會將本次支付的相關信息,以流的方式發送給我們指定的url地址,而我們指定的url地址,就是第一次組裝xml時 <notify_url> 中填寫的地址,下面我們可以先回顧一下:
...String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";...String xml = "<xml>" + "<appid>" + appId + "</appid>" + "<body>" + body +"</body>" + "<device_info>WEB</device_info>" + "<mch_id>" + merchantId + "</mch_id>" + "<nonce_str>" + nonceStr1 + "</nonce_str>" + "<notify_url>" + notifyUrl +"</notify_url>" + "<openid>" + openId + "</openid>" + "<out_trade_no>" + tradeNo + "</out_trade_no>" + "<total_fee>" + totalFee + "</total_fee>" + "<trade_type>JSAPI</trade_type>" + "<sign>" + sign + "</sign>" + "</xml>";
而我們要做的,就是接受到這些信息后,進行處理,并對微信服務器做出應答。如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,盡可能提高通知的成功率,但微信不保證通知最終能成功。詳情請參考《 微信支付官方文檔 - 支付結果通知》
需要做三件事:
- 解析微信發來的信息,通過重新簽名的方式驗證信息的正確性,確認信息是否是微信所發
- return_code和result_code都是SUCCESS的話,處理商戶自己的業務邏輯
- 應答微信,告訴它說我們收到信息了,不用再發了(如果不進行應答,則微信服務器會通過一定的策略定期重新發起通知)
過程也很簡單,將微信發來的流信息解析出來之后,再次調用之前的簽名算法,用計算出來的算法,和微信發來的xml中的簽名sign進行對比,如果相同,則說明是微信返回的通知,響應給微信即可。
注意:驗證調用返回或微信主動通知簽名時,傳送的sign參數不參與簽名,而是將生成的簽名與該sign值作校驗。也就是說,微信發來的xml中包含元素sign,該元素內容不參與簽名算法之中,而是和最后算法的結果進行比較的,所以傳參進行算法的時候不用加入sign值。
好了,現在我們先看下微信發回來的流信息是什么,實際上文檔里有說明,就是個xml,我們看下官方的示例:
<xml> <appid><![CDATA[wx2421b1c4370ec43b]]></appid> <attach><![CDATA[支付測試]]></attach> <bank_type><![CDATA[CFT]]></bank_type> <fee_type><![CDATA[CNY]]></fee_type> <is_subscribe><![CDATA[Y]]></is_subscribe> <mch_id><![CDATA[10000100]]></mch_id> <nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str> <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid> <out_trade_no><![CDATA[1409811653]]></out_trade_no> <result_code><![CDATA[SUCCESS]]></result_code> <return_code><![CDATA[SUCCESS]]></return_code> <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign> <sub_mch_id><![CDATA[10000100]]></sub_mch_id> <time_end><![CDATA[20140903131540]]></time_end> <total_fee>1</total_fee> <trade_type><![CDATA[JSAPI]]></trade_type> <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id></xml>
其中除了sign的值,其他的值需要做成集合進行簽名算法,然后結果和sign值對比,相同的話,給微信一個應答,應答的格式官方也給出了示例,如下:
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg></xml>
總之,這一部分還是很簡單的,就直接上我的代碼了:
/** * 支付成功后的處理 * <p>微信支付成功后,對微信返回的信息進行校驗</p> * @return */public String afterPaySuccess() { HttpServletRequest request = ServletActionContext.getRequest(); HttpServletResponse response = ServletActionContext.getResponse(); TreeMap<String, String> map = new TreeMap<String, String>(); try { //解析xml,存入map InputStream inputStream = request.getInputStream(); SAXReader saxReader = new SAXReader(); Document document = saxReader.read(inputStream); Element rootElement = document.getRootElement(); List<Element> elements = rootElement.elements(); String reg = "<!\\[CDATA\\[(.+)\\]\\]>"; Pattern pattern = Pattern.compile(reg); for (Element element : elements) { String key = element.getName(); String value = element.getText(); Matcher matcher = pattern.matcher(value); while (matcher.find()) { value = matcher.group(1); } map.put(key, value); } //如果微信結果通知為失敗 if ("FAIL".equals(map.get("return_code"))) { log.debug(map.get("return_msg")); return NONE; } //doit 處理商戶業務邏輯 //簽名對比,應答微信服務器 String signFromWechat = map.get("sign"); map.remove("sign"); String sign = SignUtil.createSign(map); if (sign.equals(signFromWechat)) { String responseXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>"; response.getWriter().write(responseXml); } } catch (IOException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } return NONE;}
另外,如果在執行支付流程中,有部分數據希望能放在支付完成后再處理,可以在組裝xml的時候放置在attach標簽中;然后在支付完成后微信發送來的xml中,會將原數據在此返回。需要注意的是,該attach有字符串的長度限制(詳見文檔),所以試圖直接在支付處理時直接把某個類的JSON格式放進來留做事后處理,是會出錯的(我就是這樣踩了坑),所以用來傳遞一些核心數據就行了。
再另外,對于最后這部分,看看微信推薦我們的做法是:當收到通知進行處理時,首先檢查對應業務數據的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。在對業務數據進行狀態檢查和處理之前,要采用數據鎖進行并發控制,以避免函數重入造成的數據混亂。另,商戶系統對于支付結果通知的內容一定要做簽名驗證,并校驗返回的訂單金額是否與商戶側的訂單金額一致,防止數據泄漏導致出現“假通知”,造成資金損失。
3、參考鏈接
- 微信支付之JSAPI開發第一篇-基本概念
- 微信公眾號支付開發全過程 --JAVA
- 原文鏈接 http://www.cnblogs.com/deng-cc/p/7183239.html
總結
以上是生活随笔為你收集整理的微信公众号开发,微信支付功能开发(网页JSAPI调用)的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。