微信支付服務端開發

前言

最近應公司業務需求,把微信支付完成了,固然已經順利上線。可是開發的過程是也是踩了不少坑,下面我就先說說開發流程,以及在開發中遇到的大大小小的坑。javascript

開發流程

首先,看一下微信開方平臺關於支付的一個時序圖,以下:php

微信支付時序圖
https://pay.weixin.qq.com/wiki/doc/api/app/app.phpjava

商戶系統和微信支付系統主要交互說明:
步驟1:用戶在商戶APP中選擇商品,提交訂單,選擇微信支付。 步驟2:商戶後臺收到用戶支付單,調用微信支付統一下單接口。參見【統一下單API】。 步驟3:統一下單接口返回正常的prepay_id,再按簽名規範從新生成簽名後,將數據傳輸給APP。參與簽名的字段名爲appId,partnerId,prepayId,nonceStr,timeStamp,package。注意:package的值格式爲Sign=WXPay 步驟4:商戶APP調起微信支付。api參見本章節【app端開發步驟說明】 步驟5:商戶後臺接收支付通知。api參見【支付結果通知API】 步驟6:商戶後臺查詢支付結果。,api參見【查詢訂單API】

這裏我講解的服務端的開發,那咱們就看服務端須要作什麼工做。android

第一步 統一下單

商戶系統先調用該接口在微信支付服務後臺生成預支付交易單,返回正確的預支付交易回話標識後再在APP裏面調起支付。
首先,準備請求的參數
代碼以下:ios

private SortedMap<String, Object> prepareOrder(String ip, String orderId, int price) { Map<String, Object> oparams = ImmutableMap.<String, Object> builder() .put("appid", ConfigUtil.APPID)//應用號 .put("body", WeixinConstant.PRODUCT_BODY)// 商品描述 .put("mch_id", ConfigUtil.MCH_ID)// 商戶號 .put("nonce_str", PayCommonUtil.CreateNoncestr())// 16隨機字符串(大小寫字母加數字) .put("out_trade_no", orderId)// 商戶訂單號 .put("total_fee", "1")// 銀行幣種支付的錢錢啦 .put("spbill_create_ip", ip)// IP地址 .put("notify_url", ConfigUtil.NOTIFY_URL) // 微信回調地址 .put("trade_type", ConfigUtil.TRADE_TYPE)// 支付類型 APP .build(); return MapUtils.sortMap(oparams); }

接下來將這些請求參數格式化成XML格式的數據 like thisgit

<xml> <appid>wx2421b1c4370ec43b</appid> <attach>支付測試</attach> <body>APP支付測試</body> <mch_id>10000100</mch_id> <nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str> <notify_url>http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php</notify_url> <out_trade_no>1415659990</out_trade_no> <spbill_create_ip>14.23.150.211</spbill_create_ip> <total_fee>1</total_fee> <trade_type>APP</trade_type> <sign>0CB01533B8C1EF103065174F50BCA001</sign> </xml>

請求統一下單地址 https://api.mch.weixin.qq.com/pay/unifiedorder
代碼(部分代碼,完整的代碼請見個人)githubgithub

String requestXML = PayCommonUtil.getRequestXml(parameters);// 生成xml格式字符串 String responseStr = HttpUtil.httpsRequest( ConfigUtil.UNIFIED_ORDER_URL, "POST", requestXML);// 帶上post

完成以後將微信返回的數據進行解析,取出APP客戶端須要的數據,用於喚起微信支付。代碼web

/** * 生成訂單完成,返回給android,ios喚起微信所須要的參數。 * * @param resutlMap * @return * @throws UnsupportedEncodingException */ private SortedMap<String, Object> buildClientJson( Map<String, Object> resutlMap) throws UnsupportedEncodingException { // 獲取微信返回的簽名 /** * backObject.put("appid", appid); * * backObject.put("noncestr", payParams.get("noncestr")); * * backObject.put("package", "Sign=WXPay"); * * backObject.put("partnerid", payParams.get("partnerid")); * * backObject.put("prepayid", payParams.get("prepayid")); * * backObject.put("appkey", this.appkey); * * backObject.put("timestamp",payParams.get("timestamp")); * * backObject.put("sign",payParams.get("sign")); */ Map<String, Object> params = ImmutableMap.<String, Object> builder() .put("appid", ConfigUtil.APPID) .put("noncestr", PayCommonUtil.CreateNoncestr()) .put("package", "Sign=WXPay") .put("partnerid", ConfigUtil.MCH_ID) .put("prepayid", resutlMap.get("prepay_id")) .put("timestamp", DateUtils.getTimeStamp()).build();//取10位時間戳 // key ASCII排序 SortedMap<String, Object> sortMap = MapUtils.sortMap(params); sortMap.put("package", "Sign=WXPay"); // paySign的生成規則和Sign的生成規則同理 String paySign = PayCommonUtil.createSign("UTF-8", sortMap); sortMap.put("sign", paySign); return sortMap; }

整個統一下訂單的邏輯就完成了。這裏小結一下:ubuntu

  1. 請求參數須要按照參數的key進行字母的ASCII碼進行排序,因爲我使用的是map數據結構,這裏提供一個對map集合中的key元素進行排序的工具類swift

/** * 對map根據key進行排序 ASCII 順序 * * @param 無序的map * @return */ public static SortedMap<String, Object> sortMap(Map<String, Object> map) { List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>( map.entrySet()); // 排序 Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() { public int compare(Map.Entry<String, Object> o1, Map.Entry<String, Object> o2) { return (o1.getKey()).toString().compareTo(o2.getKey()); } }); SortedMap<String, Object> sortmap = new TreeMap<String, Object>(); for (int i = 0; i < infoIds.size(); i++) { String[] split = infoIds.get(i).toString().split("="); sortmap.put(split[0], split[1]); } return sortmap; }
  1. 對排序後的數據進行MD5簽名,微信服務端會進行校驗,防止數據在網絡傳輸過程當中被篡改。

  2. 拿到微信響應的數據,首先要作的事,也是對獲取的數據進行簽名校驗,理由同上。

  3. 須要注意的一點,返回給app客戶端的數據的key必定是小寫,這點微信的api是沒有說明白的,以前和客戶端聯調時耽誤了不少時間,這也是微信支付被不少開發者吐槽的地方api比較難用^-^

  4. 注意小細節:返回給客戶端時時間戳要是10位的,太長ios那邊會越界,支付不成功。

第二步 調起支付

支付成功後,微信就會調用你填寫的notify_url的方法,本人微信支付的開發配置中說明了個人notify_url爲http://ip:port/weixin
/pay/callback/pay.action
對後臺通知交互時,若是微信收到商戶的應答不是成功或超時,微信認爲通知失敗,微信會經過必定的策略(如 30 分鐘共 8 次)按期從新發起通知,儘量提升通知的成功率,但微信不保證通知最終能成功。因爲存在從新収送後臺通知的狀況,所以一樣的通知可能會屢次収送給商戶系統。 商戶系統必須可以正確處理重複的通知。推薦的作法是,當收到通知進行處理時,首先檢查對應業務數據的狀態,判斷該通知是否已經處理過,若是沒有處理過再進行處理,若是處理過直接返回結果成功。在對業務數據進行狀態檢查和處理以前,要採用數據鎖進行幵収控制,以免凼數重入形成的數據混亂。判斷完成後,咱們須要通知微信,咱們收到信息了,否則微信就會經過必定的策略按期從新發起通知。

/** * 微信回調告訴微信支付結果 注意:一樣的通知可能會屢次發送給此接口,注意處理重複的通知。 * 對於支付結果通知的內容作簽名驗證,防止數據泄漏致使出現「假通知」,形成資金損失。 * * @param params * @return */ public String callback(HttpRequest request) { try { String responseStr = parseWeixinCallback(request); Map<String, Object> map = XMLUtil.doXMLParse(responseStr); // 校驗簽名 防止數據泄漏致使出現「假通知」,形成資金損失 if (!PayCommonUtil.checkIsSignValidFromResponseString(responseStr)) { logger.error("微信回調失敗,簽名可能被篡改"); return PayCommonUtil.setXML("FAIL", "invalid sign"); } if (WeixinConstant.FAIL.equalsIgnoreCase(map.get("result_code") .toString())) { logger.error("微信回調失敗"); return PayCommonUtil.setXML("FAIL", "weixin pay fail"); } if (WeixinConstant.SUCCESS.equalsIgnoreCase(map.get("result_code") .toString())) { //獲取應用服務器須要的數據進行持久化操做 String outTradeNo = (String) map.get("out_trade_no"); String transactionId = (String) map.get("transaction_id"); String totlaFee = (String) map.get("total_fee"); Integer totalPrice = Integer.valueOf(totlaFee); if (PayApp.theApp.isDebug()) {// 測試時候支付一分錢,買入價值6塊的20分鐘語音 totalPrice = 6; } boolean isOk = updateDB(outTradeNo, transactionId, totalPrice, 2); // 告訴微信服務器,我收到信息了,不要在調用回調action了 if (isOk) { return PayCommonUtil.setXML(WeixinConstant.SUCCESS, "OK"); } else { return PayCommonUtil .setXML(WeixinConstant.FAIL, "pay fail"); } } } catch (Exception e) { logger.debug("支付失敗" + e.getMessage()); return PayCommonUtil.setXML(WeixinConstant.FAIL, "weixin pay server exception"); } return PayCommonUtil.setXML(WeixinConstant.FAIL, "weixin pay fail"); }

小結:

  1. 當在本地作開發時,微信回調是不方便的,這裏提供一種比較快速的方法,不過前提是有云服務器。用ssh創建反向通道。

步驟以下:
(1) ssh -R 9999:localhost:9000 ubuntu@myserver_ip_address,輸入密碼; (2) server上查看一下是否監聽了9999端口,netstat -anltp | grep 9999; ubuntu@VM-39-45-ubuntu:~$ netstat -anltp | grep 9999 (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) tcp 0 0 127.0.0.1:9999 0.0.0.0:* LISTEN - tcp6 0 0 ::1:9999 :::* LISTEN - (3) 在本地9000上開啓web服務; (4) 當微信回調公網服務器時就會被代理到本地9000端口對應的web服務;

這樣就能夠在本地調試了,是否是很方便呢。

2.回調邏輯中記得,將重要數據在應用服務器進行持久化哦。

第三步 查詢訂單

該接口提供全部微信支付訂單的查詢,商戶能夠經過該接口主動查詢訂單狀態,完成下一步的業務邏輯。

須要調用查詢接口的狀況:
◆ 當商戶後臺、網絡、服務器等出現異常,商戶系統最終未接收到支付通知;
◆ 調用支付接口後,返回系統錯誤或未知交易狀態狀況;
◆ 調用被掃支付API,返回USERPAYING的狀態;
◆ 調用關單或撤銷接口API以前,需確認支付狀態;

須要提供兩個參數
outTradeNo 商戶訂單號
transactionId 微信訂單號 
二選一
請求接口 https://api.mch.weixin.qq.com/pay/orderquery
代碼:

/** * 封裝查詢請求數據 * @param outTradeNo * @param transactionId * @return */ private SortedMap<String, Object> prepareQueryData(String outTradeNo, String transactionId) { Map<String, Object> queryParams = null; // 微信的訂單號,優先使用 if (null == outTradeNo || outTradeNo.length() == 0) { queryParams = ImmutableMap.<String, Object> builder() .put("appid", ConfigUtil.APPID) .put("mch_id", ConfigUtil.MCH_ID) .put("transaction_id", transactionId) .put("nonce_str", PayCommonUtil.CreateNoncestr()).build(); } else { queryParams = ImmutableMap.<String, Object> builder() .put("appid", ConfigUtil.APPID) .put("mch_id", ConfigUtil.MCH_ID) .put("out_trade_no", outTradeNo) .put("nonce_str", PayCommonUtil.CreateNoncestr()).build(); } // key ASCII 排序 SortedMap<String, Object> sortMap = MapUtils.sortMap(queryParams); // 簽名 String createSign = PayCommonUtil.createSign("UTF-8", sortMap); sortMap.put("sign", createSign); return sortMap; }

下一步對微信響應的數據進行解析,檢查支付的狀態代碼以下

/** * 查詢訂單狀態 * * @param params * 訂單查詢參數 * @return */ public HttpResult<String> checkOrderStatus(SortedMap<String, Object> params) { if (params == null) { return HttpResult.error(1, "查詢訂單參數不能爲空"); } try { String requestXML = PayCommonUtil.getRequestXml(params);// 生成xml格式字符串 String responseStr = HttpUtil.httpsRequest( ConfigUtil.CHECK_ORDER_URL, "POST", requestXML);// 帶上post SortedMap<String, Object> responseMap = XMLUtil .doXMLParse(responseStr);// 解析響應xml格式字符串 // 校驗響應結果return_code if (WeixinConstant.FAIL.equalsIgnoreCase(responseMap.get( "return_code").toString())) { return HttpResult.error(1, responseMap.get("return_msg") .toString()); } // 校驗業務結果result_code if (WeixinConstant.FAIL.equalsIgnoreCase(responseMap.get( "result_code").toString())) { return HttpResult.error(2, responseMap.get("err_code") .toString() + "=" + responseMap.get("err_code_des")); } // 校驗簽名 if (!PayCommonUtil.checkIsSignValidFromResponseString(responseStr)) { logger.error("訂單查詢失敗,簽名可能被篡改"); return HttpResult.error(3, "簽名錯誤"); } // 判斷支付狀態 String tradeState = responseMap.get("trade_state").toString(); if (tradeState != null && tradeState.equals("SUCCESS")) { return HttpResult.success(0, "訂單支付成功"); } else if (tradeState == null) { return HttpResult.error(4, "獲取訂單狀態失敗"); } else if (tradeState.equals("REFUND")) { return HttpResult.error(5, "轉入退款"); } else if (tradeState.equals("NOTPAY")) { return HttpResult.error(6, "未支付"); } else if (tradeState.equals("CLOSED")) { return HttpResult.error(7, "已關閉"); } else if (tradeState.equals("REVOKED")) { return HttpResult.error(8, "已撤銷(刷卡支付"); } else if (tradeState.equals("USERPAYING")) { return HttpResult.error(9, "用戶支付中"); } else if (tradeState.equals("PAYERROR")) { return HttpResult.error(10, "支付失敗"); } else { return HttpResult.error(11, "未知的失敗狀態"); } } catch (Exception e) { logger.error("訂單查詢失敗,查詢參數 = {}", JSONObject.toJSONString(params)); return HttpResult.success(1, "訂單查詢失敗"); } }
相關文章
相關標籤/搜索