接口鏈接:https://api.mch.weixin.qq.com/secapi/pay/refund
當交易發生之后一段時間內,由於買家或者賣家的原因需要退款時,賣家可以通過退款接口將支付款退還給買家,微信支付將在收到退款請求並且驗證成功之后,按照退款規則將支付款按原路退到買家帳號上。需要下載數
字證書,Java只需要商戶證書文件apiclient_cert.p12。
注意:
1、交易時間超過一年的訂單無法提交退款
2、微信支付退款支持單筆交易分多次退款,多次退款需要提交原支付訂單的商戶訂單號和設置不同的退款單號。申請退款總金額不能超過訂單金額。 一筆退款失敗后重新提交,請不要更換退款單號,請使用原商戶退款單號
3、請求頻率限制:150qps,即每秒鍾正常的申請退款請求次數不超過150次
錯誤或無效請求頻率限制:6qps,即每秒鍾異常或錯誤的退款申請請求不超過6次
4、每個支付訂單的部分退款次數不能超過50次
1、將微信退款所需參數封裝成RefundInfo實體
public class RefundInfo implements Serializable{ /** * */ private static final long serialVersionUID = 1L; /** * 公眾賬號ID */ private String appid; /** * 商戶號 */ private String mch_id; /** * 隨機字符串 */ private String nonce_str; /** * 簽名 */ private String sign; /** * 微信訂單號 */ private String transaction_id; /** * 商戶退款單號,同一退款單號多次請求 只退款一次 */ private String out_refund_no; /** * 訂單金額 */ private int total_fee; /** * 退款金額 */ private int refund_fee; /** * 退款結果通知url */ private String notify_url; /** * 退款原因:可不填 */ private String refund_desc; //省略setter、getter方法 }
創建RefundInfo
/** * 微信退款的xml的java對象 * @param params UniformOrderParams * @return */ public static RefundInfo createRefundInfo(RefundParams refundParams) { WeixinConfig wxConfig = WeixinConfig.getInstance(); String nonce_str = new StringWidthWeightRandom().getNextString(32); RefundInfo refundInfo = new RefundInfo(); refundInfo.setAppid(wxConfig.getAppid()); refundInfo.setMch_id(wxConfig.getMch_id()); refundInfo.setNonce_str(nonce_str); refundInfo.setNotify_url(wxConfig.getWx_refund_notify_url()); refundInfo.setRefund_desc(refundParams.getRefund_desc()); refundInfo.setRefund_fee(refundParams.getRefund_fee()); refundInfo.setTotal_fee(refundParams.getTotal_fee()); refundInfo.setTransaction_id(refundParams.getTransaction_id()); refundInfo.setOut_refund_no(refundParams.getOut_refund_no()); return refundInfo; }
2、調前面寫的統一的微信調用接口申請退款,將微信的返回結果轉換成map
@Override public Map<String, String> refund(RefundParams refundParams) { RefundInfo refundInfo = CommonUtil.createRefundInfo(refundParams); //將bean轉換為map SortedMap<Object,Object> paras = CommonUtil.convertBean(refundInfo); String sgin = SginUtil.createSgin(paras); refundInfo.setSign(sgin); String xml = CommonUtil.beanToXML(refundInfo).replace("__", "_"). replace("<![CDATA[", "").replace("]]>", ""); WeixinConfig wxConfig = WeixinConfig.getInstance(); Map<String, String> map = CommonUtil.httpsRequestToXML( wxConfig.getWx_refund_url(),"POST",xml,true); return map; }
3、微信將退款結果通過notify_url通知商戶處理退款結果
在微信返回的退款結果中有一個加密字段:req_info,這個加密字段需要進行三步解密才能獲得完整的退款結果。官方給出的解密步驟如下:
3.1 通過微信退款通知獲取到req_info
//從request中獲取通知信息,並轉化成map Map<Object, Object> map = CommonUtil.parseXml(request); //微信退款信息 String return_code = (String) map.get("return_code"); String return_msg = (String) map.get("return_msg"); String req_info = (String) map.get("req_info");//返回的加密信息,需要解密
3.2 對加密串req_info做base64解碼,得到加密串B
byte[] B = Base64.decodeBase64(base64Data)
3.3對商戶key做md5,得到32位小寫key*
MD5Util.MD5Encode(password, "UTF-8").toLowerCase().getBytes()
3.4用key*對加密串B做AES-256-ECB解密(PKCS7Padding)
/** * AES解密 * * @param base64Data 解密內容 * @param password 解密密碼 * @return * @throws Exception */ public static String decryptData(String base64Data,String password) throws Exception { // 創建密碼器 Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); //使用密鑰初始化,設置為解密模式 cipher.init(Cipher.DECRYPT_MODE, getSecretKey(password)); //執行操作 byte[] result = cipher.doFinal(Base64.decodeBase64(base64Data)); return new String(result, "utf-8"); }
3.5 將解密后的字符串轉化為map,解析出退款結果
String decryptResult = AESUtil.decryptData(req_info,WeixinConfig.getInstance().getWxKey()); Map<String,String> reqMap = CommonUtil.parseXml(decryptResult); log.info(reqMap); //微信退款單號 String refundId = reqMap.get("refund_id"); //微信付款訂單號 String outTradeNo = reqMap.get("out_trade_no"); ...
4、解密過程可能出現的問題
4.1微信官網指定解密的填充方式為:PKCS7Padding,解密時出現bug:
在java中用aes256進行加密,但是發現java里面不能使用PKCS7Padding,而java中自帶的是PKCS5Padding填充,那解決辦法是,通過BouncyCastle組件來讓java里面支持PKCS7Padding填充。
Security.addProvider(new BouncyCastleProvider());
4.2 因為美國的出口限制,Sun通過權限文件(local_policy.jar、US_export_policy.jar)做了相應限制。可能出現bug:
Oracle在其官方網站上提供了無政策限制權限文件(Unlimited Strength Jurisdiction Policy Files),我們只需要將其部署在JRE環境中,就可以解決限制問題。把無政策限制權限文件的local_policy.jar文件和US_export_policy.jar替換掉原來jdk安裝目錄的安全目錄下,如:%jre%/lib/security。
JDK8 jar包下載地址:
http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
JDK7 jar包下載地址:
http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
DK6 jar包下載地址:
http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
附完整AES加解密代碼:
AESUtil:
package com.sanwn.framework.core.util; import java.security.Security; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; public class AESUtil { /** * 密鑰算法 */ private static final String ALGORITHM = "AES"; /** * 加解密算法/工作模式/填充方式 */ private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding"; /** * AES加密 * * @param data 加密內容 * @param password 加密密碼 * @return * @throws Exception */ public static String encryptData(String data,String password) throws Exception { //Security.addProvider(new BouncyCastleProvider()); // 創建密碼器 Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); // 初始化為加密模式的密碼 cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(password)); // 加密 byte[] result = cipher.doFinal(data.getBytes()); return Base64.encodeBase64String(result); } /** * AES解密 * * @param base64Data 解密內容 * @param password 解密密碼 * @return * @throws Exception */ public static String decryptData(String base64Data,String password) throws Exception { Security.addProvider(new BouncyCastleProvider()); // 創建密碼器 Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); //使用密鑰初始化,設置為解密模式 cipher.init(Cipher.DECRYPT_MODE, getSecretKey(password)); //執行操作 byte[] result = cipher.doFinal(Base64.decodeBase64(base64Data)); return new String(result, "utf-8"); } /** * 生成加密秘鑰 * * @return */ private static SecretKeySpec getSecretKey(String password) { SecretKeySpec key = new SecretKeySpec(MD5Util.MD5Encode(password, "UTF-8").toLowerCase().getBytes(), ALGORITHM); return key; } public static void main(String[] args){ String A = "QMp6bLccUtxAhoK6KxevK0yA0hMESKUbnz1paA2dU4nIw5tPbUjr3UiRdGzNxfRve91MZgHuUSMcOqfvQcRWoxrEoWGLEeqabGsPgZe538vbAaLVGBhV49BEFP8MfGu3ux/q/+Clz5tmtgG7JdZzEsV3S9z1ki2JlG0usNmsWbSS8VIhKBRbAsCejzGs7YLD4FNA89YZ0fEpAMLhAhmRJmw5ymjPTSUHZ4RkPWDqOrN58AkDuKkM3eL/JzFK6coimp9YJhkeY8rCEmKcLgDM3G6tfPBQ3z2hS3yyhJWLoYkpuRk7qcWMyuls0t8ix/2vuWmilQCyraC6uSLdfK4d7wr6H+t7cTELoNOyrKSIIrTvy5IGqGQuS4+fUjrC5G3jVDa9Ol7SHDJlYzWTvtN3/WS+MjMPsjyrkEudjZNen6kMuiQcTNyCtynAshSpmLQa9CQx4u1pqkthtisRKvMjizefZEPSjW0bezM1aZOkkw4syDy/4PB18QnMjRbJJqZ0S5EfRJ9gN3fgxb6+GXQy3M9BIP/Pvx7FRMorKapq/6ACJJHesG2Rq4sWdAMBoYiFz5OKpIlLAHhz3KpFXbulYimm3zSJChpxXiymOqEt4ozrStiK6jet5Df/jGkjXJAiUG1xEkmDXoG9+WbHr054V4qr2NFCotjOoNCxN1XM18OtFbU4ZBEX9sCtsx5AmEAbyexu8M7/7NpBtXZhSB8VwcoYGhg7VgiEMpAqZaG/94RkjLGjQ9vRn3yQCwaUyAkkgvlqOqV5KcoQvq0UKN/6adNGuoEfiF5daPh2y3JfYTiY2fTMTS9iLfWK0vgZ9doLys8UJvEwwxl5ohnLXYTi7I6tA4dNkRihFMnuNqJblg1VtX4fTfYfQTMyYj2SbiP8MuNLjJxE60gDjC2fZnv6evbqp7ARSsSH/O0EGcYcgfLCTrfODWkJVkUZrxTMl4muuekafqA15wmGMpl4BwjC3rTepdd2YpY8Psilst8q7kbmZCtQ4ezykoFuanzvVmz+T0Ku72hmXd7VCRaU+Q3ORA=="; try { String B = decryptData(A,"your password"); System.out.println(B); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
MD5Util
package com.sanwn.framework.core.util; import java.security.MessageDigest; public class MD5Util { public final static String MD5(String s) { char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; try { byte[] btInput = s.getBytes(); // 獲得MD5摘要算法的 MessageDigest 對象 MessageDigest mdInst = MessageDigest.getInstance("MD5"); // 使用指定的字節更新摘要 mdInst.update(btInput); // 獲得密文 byte[] md = mdInst.digest(); // 把密文轉換成十六進制的字符串形式 int j = md.length; char str[] = new char[j * 2]; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md[i]; str[k++] = hexDigits[byte0 >>> 4 & 0xf]; str[k++] = hexDigits[byte0 & 0xf]; } return new String(str); } catch (Exception e) { e.printStackTrace(); return null; } } private static String byteArrayToHexString(byte b[]) { StringBuffer resultSb = new StringBuffer(); for (int i = 0; i < b.length; i++) resultSb.append(byteToHexString(b[i])); return resultSb.toString(); } private static String byteToHexString(byte b) { int n = b; if (n < 0) n += 256; int d1 = n / 16; int d2 = n % 16; return hexDigits[d1] + hexDigits[d2]; } public static String MD5Encode(String origin, String charsetname) { String resultString = null; try { resultString = new String(origin); MessageDigest md = MessageDigest.getInstance("MD5"); if (charsetname == null || "".equals(charsetname)) resultString = byteArrayToHexString(md.digest(resultString.getBytes())); else resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname))); } catch (Exception exception) { } return resultString; } private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" }; public static void main(String[] asd) { String con = "your password"; String str = MD5Encode(con, "UTF-8"); System.out.println(str.toUpperCase()); } }
測試結果:
<root> <out_refund_no><![CDATA[R18032701140]]></out_refund_no> <out_trade_no><![CDATA[OT18032701139]]></out_trade_no> <refund_account><![CDATA[REFUND_SOURCE_RECHARGE_FUNDS]]></refund_account> <refund_fee><![CDATA[1]]></refund_fee> <refund_id><![CDATA[50000106222018032703920525020]]></refund_id> <refund_recv_accout><![CDATA[支付用戶零錢]]></refund_recv_accout> <refund_request_source><![CDATA[API]]></refund_request_source> <refund_status><![CDATA[SUCCESS]]></refund_status> <settlement_refund_fee><![CDATA[1]]></settlement_refund_fee> <settlement_total_fee><![CDATA[1]]></settlement_total_fee> <success_time><![CDATA[2018-03-27 12:16:50]]></success_time> <total_fee><![CDATA[1]]></total_fee> <transaction_id><![CDATA[4200000063201803276508012305]]></transaction_id> </root>