zoukankan      html  css  js  c++  java
  • 微信公众平台开发(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>
  • 相关阅读:
    算法刷题训练(2020.10.6)
    使用Gitee作为图片仓库,博客园引入图片【2020.10.3】
    阿里云DevOps助理工程师认证学习笔记:敏捷项目管理+需求分析【2020.10.2 10.3】
    算法刷题训练(2020.9.30)
    爬取百度贴吧数据(练习Python爬虫)
    算法刷题训练(2020.9.29)
    算法刷题训练(2020.9.28)
    软件测试:软件开发模型和软件测试模型
    计算机组成原理练习题(有助于理解概念)
    wamp环境下Wordpress的安装
  • 原文地址:https://www.cnblogs.com/zhangxianming/p/8659790.html
Copyright © 2011-2022 走看看