版权声明:powered by 大狼狗郑锴/Moshow魔手 https://blog.csdn.net/moshowgame/article/details/84952585
问题背景
话说有小程序支付就有小程序退款,退款和支付是对应的,不能凭空退。
解决方案
解决方案有点长,我们分两个部分,一个是业务参数拼接与Sign签名,一个是https请求/ssl请求与pkcs12证书,用到的包org.apache.httpcomponents/httpclient。
参数拼接
以下是官方规定的字段,有些可以不需要,根据业务情况来即可。
https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
小程序ID | appid | 是 | String(32) | wx8888888888888888 | 微信分配的小程序ID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 微信支付分配的商户号 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位。推荐随机数生成算法 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 签名,详见签名生成算法 |
微信订单号 | transaction_id | 二选一 | String(32) | 1217752501201407033233368018 | 微信生成的订单号,在支付通知中有返回 |
商户订单号 | out_trade_no | String(32) | 1217752501201407033233368018 | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 | |
商户退款单号 | out_refund_no | 是 | String(64) | 1217752501201407033233368018 | 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 |
订单金额 | total_fee | 是 | Int | 100 | 订单总金额,单位为分,只能为整数,详见支付金额 |
退款金额 | refund_fee | 是 | Int | 100 | 退款总金额,订单总金额,单位为分,只能为整数,详见支付金额 |
退款请求报文
以下是真实的业务场景所需要的参数,数据做了处理,可供参考。
=======================退款XML数据:
<xml>
<appid>wxe09a8f4******</appid>
<mch_id>150074*****</mch_id>
<nonce_str>aqw596hsfxs9f0kposs64pzw8xiwd692</nonce_str>
<out_trade_no>20181210024229*****</out_trade_no>
<out_refund_no>2018121002422*****</out_refund_no>
<total_fee>1</total_fee>
<refund_fee>1</refund_fee>
<refund_desc>用户退票f906d8ae70434430ace5671651bde693</refund_desc>
<sign>6C2D267A54932D941C4F838D12D0C916</sign>
</xml>
PKCS12证书与SSl请求封装
用到的maven库是apache的httpclient,里面包含大量的SSL请求相关,引入即可。
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
外部Controller或者ServiceImpl调用方法
String result = "";
// 调用退款接口,并接受返回的结果
try{
result = PayUtil.doRefund(mch_id,refund_url,xml);
log.info("=======================退款RESPONSE数据:" + result);
}catch (Exception e){
e.printStackTrace();
}
核心业务请求,大部分基于httpclient,需要手工设置filepath,也可以自己修改成一个变量传进来。
- mchId=商户id用于解码秘钥
- refund_url=请求的url,官方是
https://api.mch.weixin.qq.com/secapi/pay/refund
基本不会变 - data是上文封装好的xml数据
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 javax.net.ssl.SSLContext;
/**
* 微信支付工具类
* @Author blog.csdn.com/moshowgame
*/
public class PayUtil {
public static String doRefund(String mchId,String url, String data) throws Exception{
/**
* 注意PKCS12证书 是从微信商户平台-》账户设置-》 API安全 中下载的
*/
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//P12文件目录 证书路径,这里需要你自己修改,linux下还是windows下的根路径
String filepath = "D:\\";
System.out.println("filepath->"+filepath);
FileInputStream instream = new FileInputStream(filepath+"apiclient_cert.p12");
try {
keyStore.load(instream, mchId.toCharArray());//这里写密码..默认是你的MCHID
} finally {
instream.close();
}
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStore, mchId.toCharArray())//这里也是写密码的
.build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
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();
}
}
退款返回
看到如果不是显示什么end file of server或者其他FAIL如签名错误的话,就证明成功了,剩下的可能是这些例如"基本账户余额不足,请充值后重新发起"的,账户里存些钱进去就可以退了,核心的业务逻辑已经搞定。
=======================退款RESPONSE数据:
<xml><return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wxe09a8f4******]]></appid>
<mch_id><![CDATA[15007******]]></mch_id>
<nonce_str><![CDATA[Lp9JL9qF1tvSxXBb]]></nonce_str>
<sign><![CDATA[B8075C857C9760023CB5A61D49F3138E]]></sign>
<result_code><![CDATA[FAIL]]></result_code>
<err_code><![CDATA[NOTENOUGH]]></err_code>
<err_code_des><![CDATA[基本账户余额不足,请充值后重新发起]]></err_code_des>
</xml>