微信支付服務端開發總結
前言
最近應公司業務需求,把微信支付完成了,當然已經順利上線。但是開發的過程是也是踩了很多坑,下面我就先說說開發流程,以及在開發中遇到的大大小小的坑。
開發流程
首先,看一下微信開方平臺關於支付的一個時序圖,如下:
微信支付時序圖
https://pay.weixin.qq.com/wiki/doc/api/app/app.php
商戶系統和微信支付系統主要互動說明:
步驟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】
這裡我講解的服務端的開發,那我們就看服務端需要做什麼工作。
第一步 統一下單
商戶系統先呼叫該介面在微信支付服務後臺生成預支付交易單,返回正確的預支付交易回話標識後再在APP裡面調起支付。
首先,準備請求的引數
程式碼如下:
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 this
<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
程式碼(部分程式碼,完整的程式碼請見我的)github
String requestXML = PayCommonUtil.getRequestXml(parameters);// 生成xml格式字串
String responseStr = HttpUtil.httpsRequest(
ConfigUtil.UNIFIED_ORDER_URL, "POST", requestXML);// 帶上post
完成之後將微信返回的資料進行解析,取出APP客戶端需要的資料,用於喚起微信支付。程式碼
/**
* 生成訂單完成,返回給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;
}
整個統一下訂單的邏輯就完成了。這裡小結一下:
-
請求引數需要按照引數的key進行字母的ASCII碼進行排序,由於我使用的是map資料結構,這裡提供一個對map集合中的key元素進行排序的工具類
/**
* 對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;
}
-
對排序後的資料進行MD5簽名,微信服務端會進行校驗,防止資料在網路傳輸過程中被篡改。
-
拿到微信響應的資料,首先要做的事,也是對獲取的資料進行簽名校驗,理由同上。
-
需要注意的一點,返回給app客戶端的資料的key一定是小寫,這點微信的api是沒有說明白的,之前和客戶端聯調時耽誤了很多時間,這也是微信支付被很多開發者吐槽的地方api比較難用^-^
-
注意小細節:返回給客戶端時時間戳要是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");
}
小結:
-
當在本地做開發時,微信回撥是不方便的,這裡提供一種比較快速的方法,不過前提是有云伺服器。用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, "訂單查詢失敗");
}
}
整個流程就是這樣的,呵呵呵...好久沒寫部落格有點手生了。對於程式碼中很多工具類,這裡就不一一貼出來了. Fork me on Github thanks !
from: https://segmentfault.com/a/1190000005795580