微信退款(支持部分退款)

经过一天颓废的战斗,终于跑通奇经八脉了;现在的我(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,粘贴完毕,如有哪里不明白的地方请下方留言哦!

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

原文地址:https://www.cnblogs.com/ckfeng/p/15139142.html