微信退款

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

需要准备如下东东:

微信公appid、微信秘钥、商户号id、商户号秘钥、微信支付证书

 

方式一V2

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

 

2.业务层

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,商户秘钥等等,这里就不搬出来了.

 

3.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));
}

 

4.退款成功回调

/**
 * 退款通知,退款成功业务处理
 *
 * @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("系统繁忙,请稍后重试");
}

 

5.附加

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

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

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;

 

查询退款订单

controller

 /**
     * 获取微信退款详情
     * <p>
     * 微信订单号查询的优先级是: refund_id > out_refund_no > transaction_id > out_trade_no
     *
     * @param wxRefundParam 微信退款参数类
     * @return 微信参数数据
     */
    @PostMapping("weChatQueryRefund")
    public AjaxResult weChatQueryRefund(@RequestBody WxRefundParam wxRefundParam) {
        Map<String, String> map = payService.weChatQueryRefund(wxRefundParam);
        String errCode = map.get("err_code");
        if (errCode != null) {
            String errCodeDes = map.get("err_code_des");
            return AjaxResult.error(errCodeDes);
        }
        return AjaxResult.success(map);
    }

 

service实现层

  /**
     * 获取微信退款详情
     *
     * @param wxRefundParam 微信退款参数类
     * @return
     */
    @Override
    public Map<String, String> weChatQueryRefund(WxRefundParam wxRefundParam) {
        Map<String, String> params = RefundQueryModel.builder()
                .appid(WxPayConfig.APP_ID)
                .mch_id(WxPayConfig.MCH_ID)
                .nonce_str(WeChatPayKit.generateStr())
                .transaction_id(wxRefundParam.getTransactionId())
                .out_trade_no(wxRefundParam.getOutTradeNo())
                .out_refund_no(wxRefundParam.getOutRefundNo())
                .refund_id(wxRefundParam.getRefundId())
                .build()
                .createSign(WxPayConfig.API_SECRET, WxSignType.MD5);
        String resultStr = WeChatPayApi.orderRefundQuery(false, params);
        Map<String, String> map = null;
        try {
            map = WXPayUtil.xmlToMap(resultStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

 

 

方式二V2

1.导入依赖

        <!--IJPay-WxPay-->
        <dependency>
            <groupId>com.github.javen205</groupId>
            <artifactId>IJPay-WxPay</artifactId>
            <version>2.7.4</version>
        </dependency>

 

2.业务层

/**
     * 微信退款处理V2 方式二
     *
     * @param wxRefundParam 微信退款参数类
     * @return
     */
    @Override
    public Map<String, String> weChatRefundDealWith(WxRefundParam wxRefundParam) {
        try {
            //订单总金额
            String totalFee = String.valueOf(wxRefundParam.getTotalFee().multiply(new BigDecimal(100)).intValue());
            //退款金额
            String refundFee = String.valueOf(wxRefundParam.getRefundFee().multiply(new BigDecimal(100)).intValue());
            Map<String, String> params = com.ijpay.wxpay.model.RefundModel.builder()
                    .appid(WeChatPayConfig.APP_ID)
                    .mch_id(WeChatPayConfig.MCH_ID)
                    .nonce_str(WxPayKit.generateStr())
                    .transaction_id(wxRefundParam.getTransactionId())
                    .out_trade_no(wxRefundParam.getOutTradeNo())
                    .out_refund_no(WxPayKit.generateStr())
                    .total_fee(totalFee)
                    .refund_fee(refundFee)
                    .notify_url(WeChatPayConfig.DomainName + WeChatPayConfig.Refund_Callback)
                    .build()
                    .createSign(WeChatPayConfig.API_SECRET, SignType.MD5);
            File file = ResourceUtils.getFile(WeChatPayConfig.CERT_PATH_P12);
            FileInputStream certPath = new FileInputStream(file);
            String refundStr = WxPayApi.orderRefund(false, params, certPath, WeChatPayConfig.MCH_ID);
            log.info("refundStr: {}", refundStr);
            //XML格式字符串转换为Map
            return WXPayUtil.xmlToMap(refundStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        throw new CustomException("系统繁忙,请稍后重试");
    }

 

3.异步通知

 /**
     * 微信退款成功回调通知 方式二
     *
     * @param xmlData 回调信息
     * @return
     */
    @Override
    public String weChatRefundNotify(String xmlData) {
        log.info("退款通知: {}" + xmlData);
        Map<String, String> params = WxPayKit.xmlToMap(xmlData);
        Map<String, String> xml = new HashMap<String, String>(2);
        String returnCode = params.get("return_code");
        // 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态
        if (WxPayKit.codeIsOk(returnCode)) {
            String reqInfo = params.get("req_info");
            //解密数据
            String decryptData = WxPayKit.decryptData(reqInfo, WxPayApiConfigKit.getWxPayApiConfig().getPartnerKey());
            log.info("退款通知解密后的数据: {}" + decryptData);
            //xml解析map
            Map<String, String> aesMap = WxPayKit.xmlToMap(decryptData);
            //商户退款单号
            String outRefundNo = aesMap.get("out_refund_no");
            //商户单号
            String outTradeNo = aesMap.get("out_trade_no");
            //交易单号
            String transactionId = aesMap.get("transaction_id");
            //微信内部退款单号
            String refundId = aesMap.get("refund_id");
            //退款时间
            String refundTime = aesMap.get("success_time");
            //退款状态
            String refundStatus = aesMap.get("refund_status");
            if (WxPayKit.codeIsOk(refundStatus)) {
                // 更新订单信息
                // 发送通知等
            }
            xml.put("return_code", "SUCCESS");
            xml.put("return_msg", "OK");
        } else {
            xml.put("return_code", "fail");
        }
        return WxPayKit.toXml(xml);
    }

方式二的这种方法代码比较清晰,就是添加退款原因的时候判断比较麻烦,因为微信的退款金额需大于1和退款金额等于总金额的情况下(非部分退款)才能添加退款原因

 

 

微信支付篇: 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

 

 

posted @ 2021-08-13 21:15  安详的苦丁茶  阅读(653)  评论(1编辑  收藏  举报