本文是【淺析微信支付】系列文章的第八篇,主要講解商戶如何處理微信申請退款、退款回調、查詢退款接口,其中有一些坑的地方,會着重強調。
淺析微信支付系列已經更新七篇了喲~,沒有看過的朋友們能夠看一下哦。php
淺析微信支付:查詢訂單和關閉訂單html
淺析微信支付:支付結果通知java
在實際場景中,申請退款和退款回調接口是比較經常使用到的微信支付接口,這裏咱們會講原路返回
方式的退款,還有的是使用直接爲用戶付款到零錢
、現金紅包
等方式來退款,此種狀況主要會出如今客服退款時,不是所有退款的狀況,也有的會出如今使用了微信代金券-單品券
的時候,由於單品券不能部分退款,因此只能走企業付款用戶的方式,如下咱們主要講原路返回
退款。github
PS:原路返回的意思就是,從你支付時的關聯支付單中扣款,微信會記錄相關數據,能夠在客戶端通知中展現。算法
如下爲微信官方的申請退款
文檔:小程序
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
當交易發生以後一段時間內,因爲買家或者賣家的緣由須要退款時,賣家能夠經過退款接口將支付款退還給買家,微信支付將在收到退款請求而且驗證成功以後,按照退款規則將支付款按原路退到買家賬號上。api
注意: 一、交易時間超過一年的訂單沒法提交退款 二、微信支付退款支持單筆交易分屢次退款,屢次退款須要提交原支付訂單的商戶訂單號和設置不一樣的退款單號。申請退款總金額不能超過訂單金額。 一筆退款失敗後從新提交,請不要更換退款單號,請使用原商戶退款單號 三、請求頻率限制:150qps,即每秒鐘正常的申請退款請求次數不超過150次 錯誤或無效請求頻率限制:6qps,即每秒鐘異常或錯誤的退款申請請求不超過6次 四、每一個支付訂單的部分退款次數不能超過50次
PS:以上限制通常狀況下不會出現,但咱們也必須寫入系統異常場景處理中,請求頻率可使用隊列或增長延遲等方式來處理,部分退款此時不要超過微信的限制。安全
https://api.mch.weixin.qq.com/secapi/pay/refund
請求須要雙向證書。微信
PS:關於微信證書,能夠在 [商戶平臺-帳戶中心-API安全] 去下載,此證書不少支付接口均須要使用,請將證書地址配置爲常量,具體實現能夠參考做者github源碼。
先看源碼,以下:
/** * [微信退款接口] - 保存調用的相關記錄 * @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,咱們能夠在系統中對其進行處理,這裏就不細說了。
如下爲微信官方的退款結果通知
文檔:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10
當商戶申請的退款有結果後,微信會把相關結果發送給商戶,商戶須要接收處理,並返回應答。
對後臺通知交互時,若是微信收到商戶的應答不是成功或超時,微信認爲通知失敗,微信會經過必定的策略按期從新發起通知,儘量提升通知的成功率,但微信不保證通知最終能成功。
(通知頻率爲15/15/30/180/1800/1800/1800/1800/3600,單位:秒)
注意:一樣的通知可能會屢次發送給商戶系統。商戶系統必須可以正確處理重複的通知。
推薦的作法是,當收到通知進行處理時,首先檢查對應業務數據的狀態,判斷該通知是否已經處理過,若是沒有處理過再進行處理,若是處理過直接返回結果成功。在對業務數據進行狀態檢查和處理以前,要採用數據鎖進行併發控制,以免函數重入形成的數據混亂。
特別說明:退款結果對重要的數據進行了加密,商戶須要用商戶祕鑰進行解密後才能得到結果通知的內容
在申請退款接口中上傳參數「notify_url」以開通該功能
若是連接沒法訪問,商戶將沒法接收到微信通知。
通知url必須爲直接可訪問的url,不能攜帶參數。
示例:notify_url:「https://pay.weixin.qq.com/wxpay/pay.action」
解密步驟以下: (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%libsecurity和%JDK_HOME%jrelibsecurity下原來的文件,注意先備份原文件。
若是是jdk8,可能會遇到安全目錄下有policy
文件夾的狀況,拿做者的電腦舉例,jdk路徑爲/opt/jdk1.8.0_152/jre/lib/security/policy
,此目錄下有兩個子文件夾limited
、limited
,須要替換limited
文件夾下的文件 local_policy.jar
、US_export_policy.jar
這兩個,最好先備份哦~!!!替換後重啓項目便可。
由於退款回調接口是咋們系統被動接收微信的消息,因此此處和支付回調接口一致,也是使用了流的方式,格式爲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)
請參考做者源碼,文末有地址,這裏就不細講了。
具體的退款接收參數請參考微信官方文檔,須要注意的是商戶退款單號
和微信退款單號
,此兩個參數是修改和記錄退款的必要憑證。
如下爲微信官方的查詢退款
文檔:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
提交退款申請後,經過調用該接口查詢退款狀態。退款有必定延時,用零錢支付的退款20分鐘內到帳,銀行卡支付的退款3個工做往後從新查詢退款狀態。
注意:若是單個支付訂單部分退款次數超過20次請使用退款單號查詢
https://api.mch.weixin.qq.com/pay/refundquery
不須要
注意:當一個訂單部分退款超過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/wxp...
`
加做者私人微信,做者微信號以下 yclimb
,標明 微信支付
可拉入微信支付討論羣與小夥伴一塊兒探討哦,必定要標明 微信支付
哦~
到此本文就結束了,關注公衆號查看更多推送!!!