zoukankan      html  css  js  c++  java
  • 微信退款(支持部分退款)

    经过一天颓废的战斗,终于跑通奇经八脉了;现在的我(body)比跑十圈操场还舒服......

    微信退款实质上是根据商户单号和交易单号来原路返回退款的。

    需要准备什么,这里就不多介绍了哈,在微信支付的基础上加上证书就好了。

    微信支付篇: https://www.cnblogs.com/ckfeng/p/14953135.html

    微信退款官方文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4

    证书获取方法: https://kf.qq.com/faq/161222NneAJf161222U7fARv.html

    微信自带的sdk代码demo: pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1

    废话少说,直接上代码

    依赖 

            <!--WXPay api-->
    <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>${weixin.version}</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>${weixin.version}</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-mp</artifactId> <version>${weixin.version}</version> </dependency>


    <!--微信小程序 解密依赖--> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> </dependency>


    <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> <!-- 过滤后缀为pem、pfx的证书文件 --> <nonFilteredFileExtensions> <nonFilteredFileExtension>pem</nonFilteredFileExtension> <nonFilteredFileExtension>pfx</nonFilteredFileExtension> <nonFilteredFileExtension>p12</nonFilteredFileExtension> </nonFilteredFileExtensions> </configuration> </plugin>

    版本统一为:3.5.0

    service层

     /**
         * 微信退款
         *
         * @param wxRefundParam 微信退款参数类
         * @return
         */
        public Map weChatRefund(WxRefundParam wxRefundParam);

    service实现类

    注意: 若订单退款金额≤1元,且属于部分退款,则不会在退款消息中体现退款原因,这里也可以在逻辑中做判断就好了。

    /**
         * 微信退款
         *
         * @param wxRefundParam 微信退款参数类
         * @return
         */
        @Override
        public Map weChatRefund(WxRefundParam wxRefundParam) {
            System.out.println("----------进入微信退款业务层--------");
            //随机字符串
            String nonce_str = PayUtil.getRandomStringByLength(32);
            SortedMap<String, String> params = new TreeMap<>();
            params.put("appid", WxPayConfig.appID);
            params.put("mch_id", WxPayConfig.MCH_ID);
            params.put("nonce_str", nonce_str);
            params.put("out_trade_no", wxRefundParam.getOutTradeNo());
            params.put("transaction_id", wxRefundParam.getTransactionId());
            //生成退款单号
            String returnNo = String.valueOf(snowflake.nextId());
            params.put("out_refund_no", returnNo);
            params.put("refund_desc", wxRefundParam.getRefundDesc());
            //把元转化成分, 金额*100, 注意:要将金额保留整数,否则参数无法转换
            params.put("total_fee", String.valueOf(df.format(wxRefundParam.getTotalFee().doubleValue() * 100)));
            String refundFee = String.valueOf(df.format(wxRefundParam.getRefundFee().doubleValue() * 100));
            params.put("refund_fee", refundFee);
            //退款回调地址
            params.put("notify_url", apiConfig.getDomainName() + "/paySuccess/refundSuccess");
    
            //签名算法
            String stringA = PayUtil.createLinkString(params);
            //第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。(签名)
            String sign = PayUtil.sign(stringA, WxPayConfig.mchKey, "utf-8").toUpperCase();
            params.put("sign", sign);
            try {
                String xml = PayUtil.GetMapToXML(params);
                String xmlStr = doRefund("https://api.mch.weixin.qq.com/secapi/pay/refund", xml);
                Map map = PayUtil.doXMLParse(xmlStr);
                log.info("返回的前端数据-->{}", map);
                if (map == null || !"SUCCESS".equals(map.get("return_code"))) {
                    //消息通知
                    log.info("退款发起失败-->{}", map);
                    throw new CustomException("退款发起失败,请稍后重试");
                }
    
                //成功的话就在下面写自己的逻辑吧
                log.info("退款成功,退款金额为:{}", refundFee + "分");
                return map;
            } catch (Exception e) {
                //微信退款接口异常
                log.info("微信退款接口异常");
            }
    
            throw new CustomException("系统繁忙,请稍后重试");
        }
    
        /**
         * 处理退款
         *
         * @param url  微信商户退款url
         * @param data xml数据
         * @return
         * @throws Exception
         */
        public static String doRefund(String url, String data){
            StringBuilder sb = new StringBuilder();
            try {
                KeyStore keyStore = KeyStore.getInstance("PKCS12");
                //证书放好哦,我这个是linux的路径,相信乖巧的你也肯定知道windows该怎么写
                // /usr/local/tomcat/webapps/cert/apiclient_cert.p12
                //FileInputStream instream = new FileInputStream(new File("classpath:apiclient_cert.p12"));
                File file = ResourceUtils.getFile("classpath:apiclient_cert.p12");
                FileInputStream certStream = new FileInputStream(file);
                String mchid = WxPayConfig.MCH_ID;
                try {
                    keyStore.load(certStream, mchid.toCharArray());
                } finally {
                    certStream.close();
                }
                // 证书
                SSLContext sslcontext = SSLContexts.custom()
                        .loadKeyMaterial(keyStore, mchid.toCharArray())
                        .build();
                // 只允许TLSv1协议
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                        sslcontext,
                        new String[]{"TLSv1"},
                        null,
                        SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
                //创建基于证书的httpClient,后面要用到
                CloseableHttpClient client = HttpClients.custom()
                        .setSSLSocketFactory(sslsf)
                        .build();
    
                HttpPost httpPost = new HttpPost(url);
                //这里加入utf-8编码解决退款原因为中文的错误
                StringEntity reqEntity = new StringEntity(data, "UTF-8");
                // 设置类型
                reqEntity.setContentType("application/x-www-form-urlencoded");
                httpPost.setEntity(reqEntity);
                CloseableHttpResponse response = client.execute(httpPost);
                try {
                    HttpEntity entity = response.getEntity();
                    System.out.println(response.getStatusLine());
                    if (entity != null) {
                        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
                        String text = "";
                        while ((text = bufferedReader.readLine()) != null) {
                            sb.append(text);
                        }
                    }
                    EntityUtils.consume(entity);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        response.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return sb.toString();
        }

    WxPayConfig为位置配置常量,主要装appid,商户id,商户秘钥等等,这里就不搬出来了.

    微信退款controller层

    /**
     * 申请退款
     *
     * @param refundParam 微信退款参数类
     */
    @PostMapping("refundMargin")
    public AjaxResult refundMargin(@RequestBody WxRefundParam refundParam) {
        if (refundParam.getTransactionId() == null || refundParam.getTransactionId() == "") {
            return AjaxResult.error("微信支付单号不能为空");
        }
        if (refundParam.getOutTradeNo() == null || refundParam.getOutTradeNo() == "") {
            return AjaxResult.error("商户号不能为空");
        }
        if (refundParam.getTotalFee() == null || refundParam.getRefundFee() == null) {
            return AjaxResult.error("总金额或者退款金额不能为空");
        }
        return AjaxResult.success(payService.weChatRefund(refundParam));
    }

    退款成功回调

    /**
     * 退款通知,退款成功业务处理
     *
     * @param xmlData 回调信息
     * @return
     */
    @RequestMapping("refundSuccess")
    public String refundSuccessfully(@RequestBody String xmlData) {
        log.info("微信退款通知-->{}:" + xmlData);
        try {
            Map<String, String> params = WXPayUtil.xmlToMap(xmlData);
            String returnCode = params.get("return_code");
            if (WxPayKit.codeIsOk(returnCode)) {
                String reqInfo = params.get("req_info");
                if (returnCode != null || "SUCCESS".equals(returnCode)) {
                    log.info("退款成功");
                }
                //reqInfo解析
                String decryptData = ParseReqInfo.reqInfoDecryption(reqInfo);
                log.info("退款通知解密后的数据-->{}" + decryptData);
                // 更新订单信息
                // 发送通知等
                Map<String, String> xml = new HashMap<String, String>(2);
                xml.put("return_code", returnCode);
                xml.put("return_msg", "OK");
                return WxPayKit.toXml(xml);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        throw new CustomException("系统繁忙,请稍后重试");
    }

    微信退款成功回调请求解析类

    注意: 退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容

    import com.cainaer.common.core.exception.CustomException;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    
    import javax.crypto.Cipher;
    import javax.crypto.NoSuchPaddingException;
    import javax.crypto.spec.SecretKeySpec;
    import java.security.InvalidKeyException;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.security.Security;
    import java.util.Base64;
    
    /**
     * 微信退款请求解析类
     *
     * @author serence
     * @date 2021/8/13 17:49
     */
    public class ParseReqInfo {
    
        //解码器
        private static Cipher cipher = null;
        //商户秘钥
        private static String mchkey = "微信商户秘钥";
    
    
        /**
         * reqInfo解析
         *
         * @param reqInfo 请求信息
         * @return
         */
        public static String reqInfoDecryption(String reqInfo) {
            init();
            try {
                return parseReqInfo(reqInfo);
            } catch (Exception e) {
                e.printStackTrace();
            }
            throw new CustomException("系统繁忙,请稍后重试");
        }
    
        /**
         * 解析请求信息
         *
         * @param reqInfo 请求信息
         * @return
         * @throws Exception
         */
        public static String parseReqInfo(String reqInfo) throws Exception {
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] base64ByteArr = decoder.decode(reqInfo);
            return new String(cipher.doFinal(base64ByteArr));
        }
    
        public static void init() {
            String key = getMD5(mchkey);
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
            Security.addProvider(new BouncyCastleProvider());
            try {
                cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
                cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            }
        }
    
        public static String getMD5(String str) {
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                String result = MD5(str, md);
                return result;
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
                return "";
            }
        }
    
        public static String MD5(String strSrc, MessageDigest md) {
            byte[] bt = strSrc.getBytes();
            md.update(bt);
            String strDes = bytes2Hex(md.digest());
            return strDes;
        }
    
        public static String bytes2Hex(byte[] bts) {
            StringBuffer des = new StringBuffer();
            String tmp = null;
            for (int i = 0; i < bts.length; i++) {
                tmp = (Integer.toHexString(bts[i] & 0xFF));
                if (tmp.length() == 1) {
                    des.append("0");
                }
                des.append(tmp);
            }
            return des.toString();
        }

    微信传参类

     /**
         * 商户订单号 支付时的订单号
         */
        private String outTradeNo;
    
        /**
         * 微信支付订单号
         */
        private String transactionId;
    
        /**
         * 商户退款单号 新生成
         */
        private String outRefundNo;
    
        /**
         * 订单总金额 单位为分
         */
        private BigDecimal totalFee;
    
        /**
         * 退款金额 单位为分
         */
        private BigDecimal refundFee;
    
        /**
         * 退款原因
         */
        private String refundDesc;

    ok,粘贴完毕,如有哪里不明白的地方请下方留言哦!

    我要去过七七了......

  • 相关阅读:
    通知:逆天异常库 V1.0版本支持下载了~~
    【源码】Word转PDF V1.0.1 小软件,供新手参考
    GitHub实战系列汇总篇
    GitHub实战系列~4.把github里面的库克隆到指定目录+日常使用 2015-12-11
    GitHub实战系列~3.提交github的时候过滤某些文件 2015-12-10
    Windows无法安装到这个磁盘。请确保在计算机的BIOS菜单中启用了磁盘控制器
    GitHub实战系列~2.把本地项目提交到github中 2015-12-10
    Git异常:fatal: could not create work tree dir 'XXX': No such file or directory
    GitHub实战系列~1.环境部署+创建第一个文件 2015-12-9
    肉肉好走,愿你在异界依旧快乐活泼
  • 原文地址:https://www.cnblogs.com/ckfeng/p/15139142.html
Copyright © 2011-2022 走看看