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,粘贴完毕,如有哪里不明白的地方请下方留言哦!

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

  • 相关阅读:
    stenciljs 学习四 组件装饰器
    stenciljs 学习三 组件生命周期
    stenciljs 学习二 pwa 简单应用开发
    stenciljs ionic 团队开发的方便web 组件框架
    stenciljs 学习一 web 组件开发
    使用npm init快速创建web 应用
    adnanh webhook 框架 hook rule
    adnanh webhook 框架 hook 定义
    adnanh webhook 框架request values 说明
    adnanh webhook 框架execute-command 以及参数传递处理
  • 原文地址:https://www.cnblogs.com/ckfeng/p/15139142.html
Copyright © 2011-2022 走看看