淺析微信支付:申請退款、退款回撥介面、查詢退款

YClimb發表於2018-11-08

本文是【淺析微信支付】系列文章的第八篇,主要講解商戶如何處理微信申請退款、退款回撥、查詢退款介面,其中有一些坑的地方,會著重強調。


淺析微信支付系列已經更新七篇了喲~,沒有看過的朋友們可以看一下哦。

淺析微信支付:查詢訂單和關閉訂單

淺析微信支付:支付結果通知

淺析微信支付:統一下單介面

在實際場景中,申請退款和退款回撥介面是比較常用到的微信支付介面,這裡我們會講原路返回方式的退款,還有的是使用直接為使用者付款到零錢現金紅包等方式來退款,此種情況主要會出現在客服退款時,不是全部退款的情況,也有的會出現在使用了微信代金券-單品券的時候,因為單品券不能部分退款,所以只能走企業付款使用者的方式,以下我們主要講原路返回退款。

PS:原路返回的意思就是,從你支付時的關聯支付單中扣款,微信會記錄相關資料,可以在客戶端通知中展示。

1、申請退款介面

以下為微信官方的申請退款文件:

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
複製程式碼

1.1. 應用場景

當交易發生之後一段時間內,由於買家或者賣家的原因需要退款時,賣家可以通過退款介面將支付款退還給買家,微信支付將在收到退款請求並且驗證成功之後,按照退款規則將支付款按原路退到買家帳號上。

注意:
1、交易時間超過一年的訂單無法提交退款
2、微信支付退款支援單筆交易分多次退款,多次退款需要提交原支付訂單的商戶訂單號和設定不同的退款單號。申請退款總金額不能超過訂單金額。 一筆退款失敗後重新提交,請不要更換退款單號,請使用原商戶退款單號
3、請求頻率限制:150qps,即每秒鐘正常的申請退款請求次數不超過150次
錯誤或無效請求頻率限制:6qps,即每秒鐘異常或錯誤的退款申請請求不超過6次
4、每個支付訂單的部分退款次數不能超過50次
複製程式碼

PS:以上限制一般情況下不會出現,但我們也必須寫入系統異常場景處理中,請求頻率可以使用佇列或增加延遲等方式來處理,部分退款此時不要超過微信的限制。

1.2. 介面連結

https://api.mch.weixin.qq.com/secapi/pay/refund
複製程式碼

1.3. 是否需要證照

請求需要雙向證照。

PS:關於微信證照,可以在 [商戶平臺-賬戶中心-API安全] 去下載,此證照很多支付介面均需要使用,請將證照地址配置為常量,具體實現可以參考作者github原始碼。

1.4. 呼叫介面

先看原始碼,如下:

/**
 * [微信退款介面] - 儲存呼叫的相關記錄
 * @param refundPayment 退款訂單的支付記錄
 * @param tradePayment 歷史付款單
 * @return map
 * @throws Exception e
 *
 * @author yclimb
 * @date 2018/6/21
 */
public Map<String,String> saveWxPayRefund(Payment refundPayment, Payment tradePayment) throws Exception {
    if (refundPayment == null || tradePayment == null) {
        return null;
    }

    // 微信訂單號/商戶訂單號,必須傳入其中一個,此處預設傳入商戶訂單號
    // 微信訂單號,微信生成的訂單號,在支付通知中有返回
    // String transaction_id = null;
    // 商戶訂單號,商戶系統內部訂單號,要求32個字元內,只能是數字、大小寫字母_-|*@ ,且在同一個商戶號下唯一。
    String out_trade_no = tradePayment.getFlowNumer();
    // 商戶退款單號,商戶系統內部的退款單號,商戶系統內部唯一,只能是數字、大小寫字母_-|*@ ,同一退款單號多次請求只退一筆。
    String out_refund_no = refundPayment.getFlowNumer();
    // 訂單總金額,傳入引數單位為:元
    String total_fee = String.valueOf(tradePayment.getAmount());
    // 退款總金額,訂單總金額,傳入引數單位為:元
    String refund_fee = String.valueOf(refundPayment.getAmount());
    // 退款原因,若商戶傳入,會在下發給使用者的退款訊息中體現退款原因
    String refund_desc = refundPayment.getBody();

    // 微信支付物件
    WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance());

    // 微信退款介面
    Map<String, String> resultMap = wxPay.refund(refundUrl, null, out_trade_no, out_refund_no, total_fee, refund_fee, refund_desc);
    logger.info("saveWxPayRefund:resultMap:" + resultMap.toString());

    // 記錄付款流水


    // 下單失敗,進行處理
    if (WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RETURN_CODE)) ||
            WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RESULT_CODE))) {

        // 處理結果返回,無需繼續執行
        resultMap.put(WXPayConstants.RESULT_CODE, WXPayConstants.FAIL);
        resultMap.put(WXPayConstants.ERR_CODE_DES, resultMap.get(WXPayConstants.RETURN_MSG));
        return resultMap;
    }

    return resultMap;
}
複製程式碼

以上為sdk退款呼叫示例程式碼,有幾個引數需要我們注意:

欄位名 變數名 必填 型別 描述
微信訂單號 transaction_id String(32) 微信生成的訂單號,在支付通知中有返回
商戶訂單號 out_trade_no String(32) 商戶系統內部訂單號,要求32個字元內,只能是數字、大小寫字母_-
商戶退款單號 out_refund_no String(64) 商戶系統內部的退款單號,商戶系統內部唯一,只能是數字、大小寫字母_-
退款金額 refund_fee Int 退款總金額,訂單總金額,單位為分,只能為整數
退款結果通知url notify_url String(256) 非同步接收微信支付退款結果通知的回撥地址,通知URL必須為外網可訪問的url,不允許帶引數,如果引數中傳了notify_url,則商戶平臺上配置的回撥地址將不會生效。

PS:推薦以上的引數都必填,notify_url引數可配置為環境常量,根據環境的不同配置呼叫不會的回撥地址。

下面為具體的實際sdkwxPay.refund呼叫程式碼:

/**
 * 作用:申請退款<br>
 * 場景:當交易發生之後一段時間內,由於買家或者賣家的原因需要退款時,賣家可以通過退款介面將支付款退還給買家,
 * 微信支付將在收到退款請求並且驗證成功之後,按照退款規則將支付款按原路退到買家帳號上。
 * 介面文件地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4
 *
 * @param notify_url     回撥地址
 * @param transaction_id 微信生成的訂單號,在支付通知中有返回
 * @param out_trade_no   商戶系統內部訂單號,要求32個字元內,只能是數字、大小寫字母_-|*@ ,且在同一個商戶號下唯一。
 * @param out_refund_no  商戶系統內部的退款單號,商戶系統內部唯一,只能是數字、大小寫字母_-|*@ ,同一退款單號多次請求只退一筆。
 * @param total_fee      訂單總金額,傳入引數單位為:元
 * @param refund_fee     退款總金額,訂單總金額,傳入引數單位為:元
 * @param refund_desc    退款原因,若商戶傳入,會在下發給使用者的退款訊息中體現退款原因
 * @return API返回資料
 * @throws Exception e
 */
public Map<String, String> refund(String notify_url, String transaction_id, String out_trade_no, String out_refund_no,
                                  String total_fee, String refund_fee, String refund_desc) throws Exception {

    /** 構造請求引數資料 **/
    Map<String, String> data = new HashMap<>();

    // 變數名		欄位名	必填	型別	示例值	描述
    // 微信訂單號	二選一	String(32)	1.21775E+27	微信生成的訂單號,在支付通知中有返回
    if (transaction_id != null) {
        data.put("transaction_id", transaction_id);
    }
    // 商戶訂單號	String(32)	1.21775E+27	商戶系統內部訂單號,要求32個字元內,只能是數字、大小寫字母_-|*@ ,且在同一個商戶號下唯一。
    data.put("out_trade_no", out_trade_no);
    // 商戶退款單號	是	String(64)	1.21775E+27	商戶系統內部的退款單號,商戶系統內部唯一,只能是數字、大小寫字母_-|*@ ,同一退款單號多次請求只退一筆。
    data.put("out_refund_no", out_refund_no);
    // 訂單金額	是	Int	100	訂單總金額,單位為分,只能為整數,詳見支付金額
    data.put("total_fee", String.valueOf(new BigDecimal(total_fee).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()));
    // 退款金額	是	Int	100	退款總金額,訂單總金額,單位為分,只能為整數,詳見支付金額
    // 預設單位為分,系統是元,所以需要*100
    data.put("refund_fee", String.valueOf(new BigDecimal(refund_fee).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()));
    // 退款原因	否	String(80)	商品已售完	若商戶傳入,會在下發給使用者的退款訊息中體現退款原因
    data.put("refund_desc", refund_desc);
    // 貨幣種類	否	String(8)	CNY	貨幣型別,符合ISO 4217標準的三位字母程式碼,預設人民幣:CNY,其他值列表詳見貨幣型別
    data.put("refund_fee_type", WXPayConstants.FEE_TYPE_CNY);
    // 退款結果通知url	否	String(256)	https://weixin.qq.com/notify/	非同步接收微信支付退款結果通知的回撥地址,通知URL必須為外網可訪問的url,不允許帶引數,如果引數中傳了notify_url,則商戶平臺上配置的回撥地址將不會生效。
    data.put("notify_url", notify_url);

    /** 以下引數為非必填引數 **/
    // 退款資金來源	否	String(30)	REFUND_SOURCE_RECHARGE_FUNDS	僅針對老資金流商戶使用;REFUND_SOURCE_UNSETTLED_FUNDS---未結算資金退款(預設使用未結算資金退款);REFUND_SOURCE_RECHARGE_FUNDS---可用餘額退款
    // data.put("refund_account", null);


    /** 以下五個引數,在 this.fillRequestData 方法中會自動賦值 **/
	/*// 小程式ID	appid	是	String(32)	wxd678efh567hg6787	微信分配的小程式ID
    data.put("appid", WXPayConstants.APP_ID);
    // 商戶號	mch_id	是	String(32)	1230000109	微信支付分配的商戶號
    data.put("mch_id", WXPayConstants.MCH_ID);
	// 隨機字串	nonce_str	是	String(32)	5K8264ILTKCH16CQ2502SI8ZNMTM67VS	隨機字串,長度要求在32位以內。推薦隨機數生成演算法
	data.put("nonce_str", nonce_str);
	// 簽名型別	sign_type	否	String(32)	MD5	簽名型別,預設為MD5,支援HMAC-SHA256和MD5。
    data.put("sign_type", WXPayConstants.MD5);
	// 簽名	sign	是	String(32)	C380BEC2BFD727A4B6845133519F3AD6	通過簽名演算法計算得出的簽名值,詳見簽名生成演算法
	data.put("sign", sign);*/

    // 微信退款介面
    Map<String, String> resultMap = this.refund(data);

    WXPayUtil.getLogger().info("wxPay.refund:" + resultMap);

    return resultMap;
}
複製程式碼

以上已經詳細說明的具體的欄位含義,有不明白的同學可以檢視微信的官方文件,具體的原始碼可以檢視作者的github。

這裡有一個比較需要注意的點,在我們呼叫退款之後,會返回一些異常處理情況,官方文件中收錄了一系列錯誤碼code,我們可以在系統中對其進行處理,這裡就不細說了。

2、退款回撥介面

以下為微信官方的退款結果通知文件:

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10
複製程式碼

2.1. 應用場景

當商戶申請的退款有結果後,微信會把相關結果傳送給商戶,商戶需要接收處理,並返回應答。 對後臺通知互動時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,儘可能提高通知的成功率,但微信不保證通知最終能成功。

(通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒)

注意:同樣的通知可能會多次傳送給商戶系統。商戶系統必須能夠正確處理重複的通知。 推薦的做法是,當收到通知進行處理時,首先檢查對應業務資料的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。在對業務資料進行狀態檢查和處理之前,要採用資料鎖進行併發控制,以避免函式重入造成的資料混亂。

特別說明:退款結果對重要的資料進行了加密,商戶需要用商戶祕鑰進行解密後才能獲得結果通知的內容

2.2. 介面連結

在申請退款介面中上傳引數“notify_url”以開通該功能 如果連結無法訪問,商戶將無法接收到微信通知。 通知url必須為直接可訪問的url,不能攜帶引數。

示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
複製程式碼

2.3. 解密方式

解密步驟如下: 
(1)對加密串A做base64解碼,得到加密串B
(2)對商戶key做md5,得到32位小寫key* ( key設定路徑:微信商戶平臺-->賬戶設定-->API安全-->金鑰設定 )
(3)用key*對加密串B做AES-256-ECB解密(PKCS7Padding)
複製程式碼

PS:特別注意,如果要進行微信AES解密,因為GJ的進口管制限制,Java釋出的執行環境包中的加解密有一定的限制。預設不允許256位金鑰的AES加解密,解決方法就是修改策略檔案,我們需要從官方網站下載無限制許可權策略檔案,注意自己JDK的版本別下錯了。

jdk8的jce下載地址:https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html

將local_policy.jar和US_export_policy.jar這兩個檔案替換%JRE_HOME%\lib\security和%JDK_HOME%\jre\lib\security下原來的檔案,注意先備份原檔案。

如果是jdk8,可能會遇到安全目錄下有policy資料夾的情況,拿作者的電腦舉例,jdk路徑為/opt/jdk1.8.0_152/jre/lib/security/policy,此目錄下有兩個子資料夾limitedlimited,需要替換limited資料夾下的檔案 local_policy.jarUS_export_policy.jar這兩個,最好先備份哦~!!!替換後重啟專案即可。

2.4. 呼叫介面

因為退款回撥介面是咋們系統被動接收微信的訊息,所以此處和支付回撥介面一致,也是使用了流的方式,格式為xml,下面我們來看程式碼:


/**
 * 退款結果通知
 * <p>
 * 在申請退款介面中上傳引數“notify_url”以開通該功能
 * 如果連結無法訪問,商戶將無法接收到微信通知。
 * 通知url必須為直接可訪問的url,不能攜帶引數。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
 * <p>
 * 當商戶申請的退款有結果後,微信會把相關結果傳送給商戶,商戶需要接收處理,並返回應答。
 * 對後臺通知互動時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,儘可能提高通知的成功率,但微信不保證通知最終能成功。
 * (通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒)
 * 注意:同樣的通知可能會多次傳送給商戶系統。商戶系統必須能夠正確處理重複的通知。
 * 推薦的做法是,當收到通知進行處理時,首先檢查對應業務資料的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。在對業務資料進行狀態檢查和處理之前,要採用資料鎖進行併發控制,以避免函式重入造成的資料混亂。
 * 特別說明:退款結果對重要的資料進行了加密,商戶需要用商戶祕鑰進行解密後才能獲得結果通知的內容
 * @param request req
 * @param response resp
 * @return res xml
 *
 * @author yclimb
 * @date 2018/6/21
 */
@ApiOperation(value = "微信支付|微信退款回撥介面", httpMethod = "POST", notes = "該連結是通過【微信退款API】中提交的引數notify_url設定,如果引數中傳了notify_url,則商戶平臺上配置的回撥地址將不會生效。")
@RequestMapping("/refund")
public void refund(HttpServletRequest request, HttpServletResponse response) {

    String resXml = "";
    InputStream inStream;
    try {

        inStream = request.getInputStream();
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inStream.read(buffer)) != -1) {
            outSteam.write(buffer, 0, len);
        }
        WXPayUtil.getLogger().info("refund:微信退款----start----");

        // 獲取微信呼叫我們notify_url的返回資訊
        String result = new String(outSteam.toByteArray(), "utf-8");
        WXPayUtil.getLogger().info("refund:微信退款----result----=" + result);

        // 關閉流
        outSteam.close();
        inStream.close();

        // xml轉換為map
        Map<String, String> map = WXPayUtil.xmlToMap(result);
        if (WXPayConstants.SUCCESS.equalsIgnoreCase(map.get(WXPayConstants.RETURN_CODE))) {

            WXPayUtil.getLogger().info("refund:微信退款----返回成功");


            /** 以下欄位在return_code為SUCCESS的時候有返回: **/
            // 加密資訊:加密資訊請用商戶祕鑰進行解密,詳見解密方式
            String req_info = map.get("req_info");

            /**
             * 解密方式
             * 解密步驟如下:
             * (1)對加密串A做base64解碼,得到加密串B
             * (2)對商戶key做md5,得到32位小寫key* ( key設定路徑:微信商戶平臺(pay.weixin.qq.com)-->賬戶設定-->API安全-->金鑰設定 )
             * (3)用key*對加密串B做AES-256-ECB解密(PKCS7Padding)
             */
            String resultStr = AESUtil.decryptData(req_info);

            // WXPayUtil.getLogger().info("refund:解密後的字串:" + resultStr);
            Map<String, String> aesMap = WXPayUtil.xmlToMap(resultStr);


            /** 以下為返回的加密欄位: **/
            //	商戶退款單號	是	String(64)	1.21775E+27	商戶退款單號
            String out_refund_no = aesMap.get("out_refund_no");
            //	退款狀態	是	String(16)	SUCCESS	SUCCESS-退款成功、CHANGE-退款異常、REFUNDCLOSE—退款關閉
            String refund_status = aesMap.get("refund_status");
            //	商戶訂單號	是	String(32)	1.21775E+27	商戶系統內部的訂單號
            String out_trade_no = aesMap.get("out_trade_no");
            /*//	微信訂單號	是	String(32)	1.21775E+27	微信訂單號
            String transaction_id = null;
            //	微信退款單號	是	String(32)	1.21775E+27	微信退款單號
            String refund_id = null;
            //	訂單金額	是	Int	100	訂單總金額,單位為分,只能為整數,詳見支付金額
            String total_fee = null;
            //	應結訂單金額	否	Int	100	當該訂單有使用非充值券時,返回此欄位。應結訂單金額=訂單金額-非充值代金券金額,應結訂單金額<=訂單金額。
            String settlement_total_fee = null;
            //	申請退款金額	是	Int	100	退款總金額,單位為分
            String refund_fee = null;
            //	退款金額	是	Int	100	退款金額=申請退款金額-非充值代金券退款金額,退款金額<=申請退款金額
            String settlement_refund_fee = null;*/

            // 退款是否成功
            if (!WXPayConstants.SUCCESS.equals(refund_status)) {
                resXml = resFailXml;
            } else {
                // 通知微信.非同步確認成功.必寫.不然會一直通知後臺.八次之後就認為交易失敗了.
                resXml = resSuccessXml;
                isSuccess = true;
            }

            // 根據付款單號查詢付款記錄 out_refund_no

            // 付款記錄修改 & 記錄付款日誌
            if (payment != null) {
                WXPayUtil.getLogger().error("refund:微信支付回撥:修改支付單");
            } else {
                WXPayUtil.getLogger().error("refund:微信支付回撥:找不到對應的支付單");
            }


        } else {
            WXPayUtil.getLogger().error("refund:支付失敗,錯誤資訊:" + map.get(WXPayConstants.RETURN_MSG));
            resXml = resFailXml;
        }

    } catch (Exception e) {
        WXPayUtil.getLogger().error("refund:微信退款回撥發布異常:", e);
    } finally {
        try {
            // 處理業務完畢
            BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
            out.write(resXml.getBytes());
            out.flush();
            out.close();
        } catch (IOException e) {
            WXPayUtil.getLogger().error("refund:微信退款回撥發布異常:out:", e);
        }
    }
}

複製程式碼

以上程式碼詳細解釋瞭如何接收微信回撥資料和解碼資料,具體的AESUtil.decryptData(req_info)請參考作者原始碼,文末有地址,這裡就不細講了。

具體的退款接收引數請參考微信官方文件,需要注意的是商戶退款單號微信退款單號,此兩個引數是修改和記錄退款的必要憑證。

3、查詢退款

以下為微信官方的查詢退款文件:

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
複製程式碼

3.1. 應用場景

提交退款申請後,通過呼叫該介面查詢退款狀態。退款有一定延時,用零錢支付的退款20分鐘內到賬,銀行卡支付的退款3個工作日後重新查詢退款狀態。

注意:如果單個支付訂單部分退款次數超過20次請使用退款單號查詢

3.2. 介面連結

https://api.mch.weixin.qq.com/pay/refundquery
複製程式碼

3.3. 是否需要證照

不需要

3.4. 呼叫介面

注意:當一個訂單部分退款超過10筆後,商戶用微信訂單號或商戶訂單號調退款查詢API查詢退款時,預設返回前10筆和total_refund_count(訂單總退款次數)。商戶需要查詢同一訂單下超過10筆的退款單時,可傳入訂單號及offset來查詢,微信支付會返回offset及後面的10筆,以此類推。當商戶傳入的offset超過total_refund_count,則系統會返回報錯PARAM_ERROR

舉例:

一筆訂單下的退款單有36筆,當商戶想查詢第25筆時,可傳入訂單號及offset=24,微信支付平臺會返回第25筆到第35筆的退款單資訊,或商戶可直接傳入退款單號查詢退款
複製程式碼

以下為呼叫方式:

private void doRefundQuery() {
    // 四選一,微信訂單號查詢的優先順序是: refund_id > out_refund_no > transaction_id > out_trade_no
    HashMap<String, String> data = new HashMap<String, String>();
    // 商戶訂單號
    data.put("out_trade_no", out_trade_no);
    // 微信訂單號
    data.put("transaction_id", out_trade_no);
    // 商戶退款單號	
    data.put("out_refund_no", out_trade_no);
    // 微信退款單號	
    data.put("refund_id", out_trade_no);
    try {
        Map<String, String> r = wxpay.refundQuery(data);
        System.out.println(r);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
複製程式碼

PS:微信訂單號查詢的優先順序是: refund_id > out_refund_no > transaction_id > out_trade_no

需要注意的是,查詢退款時,需要注意退款返回的錯誤碼,如果出現錯誤,需要及時同步商戶系統中的退款資料。

結語

以上為申請退款、退款回撥介面、查詢退款相關的解釋和原始碼,特別需要注意的是接收退款時的解密方式和替換安全檔案,小夥伴們一定要注意哦,具體的原始碼可以看作者的github,裡面對每個方法有詳細的註釋。

預告:下一篇文章 下載對賬單和資金賬單,敬請期待!!!

​如果想要提前一覽原始碼的小夥伴,可以先看看我的 github,地址如下: ​​https://github.com/YClimb/wxpay-sdk/blob/master/README.md ​

加作者私人微信,作者微訊號如下 yclimb,標明 微信支付 可拉入微信支付討論群與小夥伴一起探討哦,一定要標明 微信支付 哦~

到此本文就結束了,關注公眾號檢視更多推送!!!


關注我的公眾號


相關文章