微信公眾平台開發(6) 微信退款接口


接口鏈接: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>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM