天天看點

微信支付服務端開發總結

原文連結:http://blog.csdn.net/baple/article/details/68059283

前言

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

開發流程

首先,看一下微信開方平台關于支付的一個時序圖,如下:

微信支付時序圖

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 = ; i < infoIds.size(); i++) {
            String[] split = infoIds.get(i).toString().split("=");
            sortmap.put(split[], split[]);
        }
        return sortmap;
    }
           

1.對排序後的資料進行MD5簽名,微信服務端會進行校驗,防止資料在網絡傳輸過程中被篡改。

2.拿到微信響應的資料,首先要做的事,也是對擷取的資料進行簽名校驗,理由同上。

3.需要注意的一點,傳回給app用戶端的資料的key一定是小寫,這點微信的api是沒有說明白的,之前和用戶端聯調時耽誤了很多時間,這也是微信支付被很多開發者吐槽的地方api比較難用^-^

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

第二步 調起支付

對排序後的資料進行MD5簽名,微信服務端會進行校驗,防止資料在網絡傳輸過程中被篡改。

拿到微信響應的資料,首先要做的事,也是對擷取的資料進行簽名校驗,理由同上。

需要注意的一點,傳回給app用戶端的資料的key一定是小寫,這點微信的api是沒有說明白的,之前和用戶端聯調時耽誤了很多時間,這也是微信支付被很多開發者吐槽的地方api比較難用^-^

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

/**
     * 微信回調告訴微信支付結果 注意:同樣的通知可能會多次發送給此接口,注意處理重複的通知。
     * 對于支付結果通知的内容做簽名驗證,防止資料洩漏導緻出現“假通知”,造成資金損失。
     * 
     * @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 = ;
                }
                boolean isOk = updateDB(outTradeNo, transactionId, totalPrice,
                        );
                // 告訴微信伺服器,我收到資訊了,不要在調用回調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 [email protected]_ip_address,輸入密碼;

(2) server上檢視一下是否監聽了9999端口,netstat -anltp | grep 9999;

[email protected]:~$ 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() == ) {
            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(, "查詢訂單參數不能為空");
        }
        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(, responseMap.get("return_msg")
                        .toString());
            }
            // 校驗業務結果result_code
            if (WeixinConstant.FAIL.equalsIgnoreCase(responseMap.get(
                    "result_code").toString())) {
                return HttpResult.error(, responseMap.get("err_code")
                        .toString() + "=" + responseMap.get("err_code_des"));
            }
            // 校驗簽名
            if (!PayCommonUtil.checkIsSignValidFromResponseString(responseStr)) {
                logger.error("訂單查詢失敗,簽名可能被篡改");
                return HttpResult.error(, "簽名錯誤");
            }
            // 判斷支付狀态
            String tradeState = responseMap.get("trade_state").toString();
            if (tradeState != null && tradeState.equals("SUCCESS")) {
                return HttpResult.success(, "訂單支付成功");
            } else if (tradeState == null) {
                return HttpResult.error(, "擷取訂單狀态失敗");
            } else if (tradeState.equals("REFUND")) {
                return HttpResult.error(, "轉入退款");
            } else if (tradeState.equals("NOTPAY")) {
                return HttpResult.error(, "未支付");
            } else if (tradeState.equals("CLOSED")) {
                return HttpResult.error(, "已關閉");
            } else if (tradeState.equals("REVOKED")) {
                return HttpResult.error(, "已撤銷(刷卡支付");
            } else if (tradeState.equals("USERPAYING")) {
                return HttpResult.error(, "使用者支付中");
            } else if (tradeState.equals("PAYERROR")) {
                return HttpResult.error(, "支付失敗");
            } else {
                return HttpResult.error(, "未知的失敗狀态");
            }
        } catch (Exception e) {
            logger.error("訂單查詢失敗,查詢參數 = {}", JSONObject.toJSONString(params));
            return HttpResult.success(, "訂單查詢失敗");
        }
    }
           

整個流程就是這樣的,呵呵呵…好久沒寫部落格有點手生了。對于代碼中很多工具類,這裡就不一一貼出來了. Fork me on Github thanks !

from: https://segmentfault.com/a/1190000005795580

繼續閱讀