问题背景
话说有小程序支付就有小程序退款,退款和支付是对应的,不能凭空退。
解决方案
解决方案有点长,我们分两个部分,一个是业务参数拼接与Sign签名,一个是https请求/ssl请求与pkcs12证书,用到的包org.apache.httpcomponents/httpclient。
参数拼接
以下是官方规定的字段,有些可以不需要,根据业务情况来即可。
https://pay./wiki/doc/api/wxa/wxa_api.php?chapter=9_4
退款请求报文
以下是真实的业务场景所需要的参数,数据做了处理,可供参考。
=======================退款XML数据:
<xml><appid>wxe09a8f4******</appid><mch_id>150074*****</mch_id><nonce_str>aqw596hsfxs9f0kposs64pzw8xiwd692</nonce_str><out_trade_no>1210024229*****</out_trade_no><out_refund_no>121002422*****</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请求相关,引入即可。
<!-- /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./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 .ssl.SSLContext;/** * 微信支付工具类* @Author /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 certsSSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray())//这里也是写密码的.build();// Allow TLSv1 protocol onlySSLConnectionSocketFactory 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.");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>