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>
至此,微信退款已完成。官方文档。