SpringBoot 微信退款申请

1、证书下载

  微信退款申请和微信支付大体都一样,只是退款需要证书,下载教程地址:https://kf.qq.com/faq/161222NneAJf161222U7fARv.html,加压得到三个文件,如下:
在这里插入图片描述
  其中,apiclient_cert.pem 和 apiclent_key.pem 两份文件是 PHP 的,而 apiclient_cert.p12 则是 Java 等语言需要的。这些文件包含了私钥信息的证书文件,由微信支付签发给您用来标识和界定您的身份,请妥善保管不要泄漏和被他人复制。

2、读取证书

  证书可以放在服务器的固定文件路径上,也可以放于项目中,我是直接放在项目中的 resource 文件下,然后需要在 pom.xml 中添加相关插件。
在这里插入图片描述

	<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>

  微信支付的相关信息:

	/**
	 * 微信配置类
	 * @author PkyShare
	 * @date 2020/8/21 0021 14:43
	 */
	public class WechatConfig {
	    public static final String APP_ID = "wx*********"; // 小程序 appid
	    public static final String MCH_ID = "**********"; // 微信支付的商户id
	    public static final String KEY = "****************"; // 商户平台设置的密钥key
	    public static final String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund"; // 申请退款接口
	}

  读取并装载证书:

	/**
	* 微信申请退款
	* @param url 申请地址
	* @param data 退款xml
	* @return
	* @throws Exception
	*/
	public static String doWechatRefund(String url, String data) throws Exception {
	        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
	        InputStream inputStream = classLoader.getResourceAsStream("apiclient_cert.p12");
	        KeyStore keyStore = KeyStore.getInstance("PKCS12");
	        try {
	            //装载证书资源文件
	            keyStore.load(inputStream, WechatConfig.MCH_ID.toCharArray());  
	        } finally {
	            inputStream.close();
	        }
	        return "";
	}

3、微信退款申请

  设置请求xml

    /**
     * 设置退款申请xml
     * @param outRefundNo 退单号
     * @param outTradeNo 订单号
     * @param refundFee 退款金额
     * @param totalFee 订单总额
     * @return
     */
    private String setXml(String outRefundNo, String outTradeNo, String refundFee, String totalFee) {
        // 获取随机 UUID
        String nonceStr = UUIDUtils.getUUID();
        // 拼接签名
        String signStr = "appid=" + WechatConfig.APP_ID + "&mch_id=" + WechatConfig.MCH_ID + "&nonce_str=" + nonceStr +
                "&out_refund_no=" + outRefundNo + "&out_trade_no=" + outTradeNo + "&refund_fee=" + refundFee +
                "&total_fee=" + totalFee;
        signStr = signStr + "&key=" + WechatConfig.KEY;
        String sign = MD5Util.md5Encode(signStr, "utf-8").toUpperCase();
        // 拼接请求 xml
        String xml = "<xml>" +
                "<appid>" + WechatConfig.APP_ID + "</appid>" +  // 微信分配的公众账号ID(企业号corpid即为此appId)
                "<mch_id>" + WechatConfig.MCH_ID + "</mch_id>" + // 微信支付分配的商户号
                "<nonce_str>" + nonceStr + "</nonce_str>" + // 随机字符串,不长于32位。
                "<out_refund_no>" + outRefundNo + "</out_refund_no>" + // 商户系统内部的退款单号,商户系统内部唯一
                "<out_trade_no>" + outTradeNo + "</out_trade_no>" +  // 商户系统内部订单号
                "<refund_fee>" + refundFee + "</refund_fee>" +  // 退款总金额,订单总金额,单位为分,只能为整数
                "<total_fee>" + totalFee + "</total_fee>" +  // 订单总金额,单位为分,只能为整数
                "<sign>" + sign + "</sign>" +  // 签名
                "</xml>";
        return xml;
    }

:这里需要注意, 签名的拼接是按照参数名 ASCII 码从小到大排序(字典序),具体可以参考 微信支付签名算法开发文档;金额这些参数都是 整数

  UUIDUtils

import java.util.UUID;

public class UUIDUtils {

    /**
     * 获取随机UUID
     * @return
     */
    public static String getUUID(){
        return UUID.randomUUID().toString().replace("-", "");
    }
}

  MD5Util

import java.security.MessageDigest;

public class MD5Util {
    /***
     * MD5加密 生成32位md5码
     *
     * @param inStr 待加密字符串
     * @param charsetName 编码格式名字 如 UTF-8
     * @return 返回32位md5码
     */
    public static String md5Encode(String inStr, String charsetName) {
        char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9','A', 'B', 'C', 'D', 'E', 'F' };
        try {
            byte[] btInput = inStr.getBytes(charsetName);
            // 获得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;
        }
    }
}

  申请退款:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.huanda.app.manage.commons.config.WechatConfig;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

import javax.net.ssl.SSLContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.util.*;

/**
 * @author PkyShare
 * @date 2020/8/19 0019 18:30
 */
public class PayUtil {

    /**
     * 微信申请退款
     * @param url 申请地址
     * @param data 退款xml
     * @return
     * @throws Exception
     */
    public static String doWechatRefund(String url, String data) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream("apiclient_cert.p12");
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try {
            //装载证书资源文件
            keyStore.load(inputStream, WechatConfig.MCH_ID.toCharArray());
        } finally {
            inputStream.close();
        }
        SSLContext sslcontext = SSLContexts.custom()
                .loadKeyMaterial(keyStore, WechatConfig.MCH_ID.toCharArray())// 这里也是写密码的
                .build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext, new String[] { "TLSv1" }, null,
                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        CloseableHttpClient httpclient = HttpClients.custom()
                .setSSLSocketFactory(sslsf).build();
        try {
            HttpPost httpost = new HttpPost(url); // 设置响应头信息
            httpost.addHeader("Connection", "keep-alive");
            httpost.addHeader("Accept", "*/*");
            httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
            httpost.addHeader("Host", "api.mch.weixin.qq.com");
            httpost.addHeader("X-Requested-With", "XMLHttpRequest");
            httpost.addHeader("Cache-Control", "max-age=0");
            httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
            httpost.setEntity(new StringEntity(data, "UTF-8"));
            CloseableHttpResponse response = httpclient.execute(httpost);
            try {
                HttpEntity entity = response.getEntity();
                String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
                EntityUtils.consume(entity);
                return jsonStr;
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
    }

    /**
     * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
     * @param strxml
     * @return
     * @throws IOException
     */
    public static Map<String, String> doXMLParse(String strxml) throws Exception {
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
        if(null == strxml || "".equals(strxml)) {
            return null;
        }
        Map<String, String> m = new HashMap<>();
        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
        SAXBuilder builder = new SAXBuilder();
        Document doc = builder.build(in);
        Element root = doc.getRootElement();
        List list = root.getChildren();
        Iterator it = list.iterator();
        while(it.hasNext()) {
            Element e = (Element) it.next();
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
            if(children.isEmpty()) {
                v = e.getTextNormalize();
            } else {
                v = getChildrenText(children);
            }
            m.put(k, v);
        }
        //关闭流
        in.close();
        return m;
    }
    /**
     * 获取子结点的xml
     * @param children
     * @return String
     */
    public static String getChildrenText(List children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        return sb.toString();
    }
}

4、测试

  测试类:

/**
 * 微信接口测试
 * @author PkyShare
 * @date 2020/12/8 0008 13:47
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppManageAdminApplication.class)
public class WeChatTest {

    /**
     * 微信支付退款测试
     */
    @Test
    public void resund() throws Exception {
        String outRefundNo = "541692001679048704"; // 退单号
        String outTradeNo = "541692001679048704"; // 订单号
        String refundFee = "1"; // 退款金额(单位/分)
        String totalFee = "1"; // 订单总额(单位/分)
        // 设置xml
        String xml = setXml(outRefundNo, outTradeNo, refundFee, totalFee);
        // 微信退款申请
        String resultXml = PayUtil.doWechatRefund(WechatConfig.REFUND_URL, xml);
        System.out.println(resultXml);
        // 将返回的xml转换为map
        Map map = PayUtil.doXMLParse(resultXml);
        System.out.println(map);
    }
  • 成功结果:
	<xml>
		<return_code><![CDATA[SUCCESS]]></return_code>
		<return_msg><![CDATA[OK]]></return_msg>
		<appid><![CDATA[wx***********]]></appid>
		<mch_id><![CDATA[*******]]></mch_id>
		<nonce_str><![CDATA[0FZhZLKRdY8ayIpW]]></nonce_str>
		<sign><![CDATA[******************]]></sign>
		<result_code><![CDATA[SUCCESS]]></result_code>
		<transaction_id><![CDATA[4200000829202011285005821704]]></transaction_id>
		<out_trade_no><![CDATA[539499818427351040]]></out_trade_no>
		<out_refund_no><![CDATA[539499818427351040]]></out_refund_no>
		<refund_id><![CDATA[50300006662020120804545811285]]></refund_id>
		<refund_channel><![CDATA[]]></refund_channel>
		<refund_fee>1</refund_fee>
		<coupon_refund_fee>0</coupon_refund_fee>
		<total_fee>1</total_fee>
		<cash_fee>1</cash_fee>
		<coupon_refund_count>0</coupon_refund_count>
		<cash_refund_fee>1</cash_refund_fee>
	</xml>

在这里插入图片描述

  • 失败结果:
	<xml>
		<return_code><![CDATA[SUCCESS]]></return_code>
		<return_msg><![CDATA[OK]]></return_msg>
		<appid><![CDATA[wx********]]></appid>
		<mch_id><![CDATA[**********]]></mch_id>
		<nonce_str><![CDATA[DHl0zHPKF3nNYDpg]]></nonce_str>
		<sign><![CDATA[**************]]></sign>
		<result_code><![CDATA[FAIL]]></result_code>
		<err_code><![CDATA[ERROR]]></err_code>
		<err_code_des><![CDATA[订单已全额退款]]></err_code_des>
	</xml>

  至此,微信退款已完成。官方文档

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值