一、前言
这次的项目主要是关于微信公众号的一个开发,本人这次分配的模块是后台微信公众号的支付和退款,第一次接触微信公众的项目刚开始一脸懵逼,开发过程中遇到各种坑,所以想自己写一篇详细的关于微信公众号的开发,希望能对小伙伴们有所帮助!
二、微信申请退款接口
微信退款接口文档:微信公众号退款申请接口开发文档
退款申请流程:前端调用微信退款申请接口,退款申请需要双向的证书验证,登录微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->证书下载。具体安装请参考微信证书安装文档:商户证书安装指导。在微信退款申请接口调用时需要读取服务器安装的证书,然后才能想微信发送请求,否则请求回发送失败或者返回数据为空。如果退款接口中设置了退款结果通知的URL,那么在退款申请成功后会给设置的通知接口返回数据,当放回的结果为SUCCESS时,会携带一部分加密的数据,数据解密方式:
解密步骤如下:
(1)对加密串A做base64解码,得到加密串B
(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
解密成功后可进行相应的业务处理,代码如下:
申请退款接口:
/**
*
* @Title: refund
* @Description: 微信退款
* @param @param request
* @param @param response
* @param @return
* @param @throws Exception
* @return Map<String,String>
* @throws
*/
@ResponseBody
@RequestMapping("/refund")
public JsPayResult refund(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 公众账号ID
String appid = Constants.APPID;
// 商户号
String mch_id = Constants.MCHID;
// 随机字符串
String nonce_str = CommonUtil.getRandomStr();
// 商户订单号
String out_trade_no = request.getParameter("out_trade_no");
// 商户退款单号,订单号是唯一的,加上订单号防止在高并发下退款单号不唯一
String out_refund_no = CommonUtil.getOrderIdByTime()+out_trade_no;
// 订单金额
String total_fee1 = CommonUtil.getMoney(request.getParameter("total_fee"));
String total_fee = CommonUtil.getMoney("0.01");
// 退款金额
String refund_fee1 = request.getParameter("refund_fee");
String refund_fee = CommonUtil.getMoney("0.01");
//退款结果通知url
String notify_url = Constants.REFOUND_NOTIFY_URL;
// 将请求参数封装至Map集合中
SortedMap<String, String> paramMap = new TreeMap<String, String>();
paramMap.put("appid", appid);
paramMap.put("mch_id", mch_id);
paramMap.put("nonce_str", nonce_str);
paramMap.put("out_trade_no", out_trade_no);
paramMap.put("out_refund_no", out_refund_no);
paramMap.put("total_fee", total_fee);
paramMap.put("refund_fee", refund_fee);
paramMap.put("notify_url", notify_url);
logger.info(paramMap);
// 签名
String sign = SignUtil.createSign(paramMap, Constants.PARTNER_KEY);
paramMap.put("sign", sign);
// 请求的xml数据
String requestXml = XMLUtil.map2Xml(paramMap, "xml");
//1.指定读取证书格式为PKCS12
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//2.读取本机存放的PKCS12证书文件
FileInputStream instream = new FileInputStream(new File(Constants.CERTIFICATE_PATH));
try {
//指定PKCS12的密码(商户ID)
keyStore.load(instream, Constants.MCHID.toCharArray());
} finally {
instream.close();
}
//3.ssl双向验证发送http请求报文
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, Constants.MCHID.toCharArray()).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,new String[] { "TLSv1" },null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
//4.发送数据到微信的退款接口
HttpPost httpost= HttpClientConnectionManager.getPostMethod(Constants.REFUND_URL);
httpost.setEntity(new StringEntity(requestXml, "UTF-8"));
HttpResponse weixinResponse = httpClient.execute(httpost);
String resposeXmL = EntityUtils.toString(weixinResponse.getEntity(), "UTF-8");
//5.将返回的xml转换为map
Map<String, String> responseMap = XMLUtil.xml2Map(resposeXmL);
JsPayResult result = new JsPayResult();
if (Constants.RETURN_CODE.equals(responseMap.get("return_code"))) {
result.setAppId(responseMap.get("appid"));
result.setMchId(responseMap.get("mch_id"));
result.setNonceStr(responseMap.get("nonce_str"));
// 微信订单号
result.setTransactionId(responseMap.get("transaction_id"));
// 商户订单号
result.setOutRradeNo(responseMap.get("out_trade_no"));
// 商户退款单号
result.setOutRefundNo(responseMap.get("out_refund_no"));
// 微信退款单号
result.setRefundId(responseMap.get("refund_id"));
// 退款金额
result.setSettlementRefundRee(responseMap.get("settlement_refund_fee"));
// 订单金额
result.setTotalFee(responseMap.get("total_fee"));
//申请退款金额
result.setRefundFee(responseMap.get("refund_fee"));
// 现金支付金额
result.setCashFee(responseMap.get("cash_fee"));
//退款状态
result.setRefundStatus(responseMap.get("refund_status"));
//退款成功时间
result.setSuccessTime(responseMap.get("success_time"));
//退款入账账户
result.setRefundRecvAccout(responseMap.get("refund_recv_accout"));
//退款资金来源
result.setRefundAccount(responseMap.get("refund_account"));
//退款发起来源
result.setRefundRequestSource(responseMap.get("refund_request_source"));
result.setResultCode(Constants.RESULT_CODE_SUCCESS);
result.setMessage("退款成功!");
logger.info("*******退款申请**********"+"退款成功!");
}
else {
result.setResultCode(Constants.RESULT_CODE_FAIL);
result.setMessage("退款失败!");
logger.info("*******退款申请**********"+"退款失败!");
}
return result;
}
退款接口中涉及到的双向证书验证:
//1.指定读取证书格式为PKCS12
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//2.读取本机存放的PKCS12证书文件
FileInputStream instream = new FileInputStream(new File(Constants.CERTIFICATE_PATH));//读取证书的安装路径
try {
//指定PKCS12的密码(商户ID)
keyStore.load(instream, Constants.MCHID.toCharArray());
} finally {
instream.close();
}
//3.ssl双向验证发送http请求报文
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, Constants.MCHID.toCharArray()).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,new String[] { "TLSv1" },null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
//4.发送数据到微信的退款接口
HttpPost httpost= HttpClientConnectionManager.getPostMethod(Constants.REFUND_URL);
httpost.setEntity(new StringEntity(requestXml, "UTF-8"));
HttpResponse weixinResponse = httpClient.execute(httpost);
String resposeXmL = EntityUtils.toString(weixinResponse.getEntity(), "UTF-8");
三、退款结果通知接口
/**
*
* @Title: refundNotify
* @Description: 退款结果通知
* @param @param request
* @param @param response
* @param @return
* @param @throws Exception
* @return Map<String,String>
* @throws
*/
@ResponseBody
@RequestMapping("/refundNotify")
public JsPayResult refundNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 将request请求中的数据转换为字符串
String reqpXml = CommonUtil.readRequestStr(request);
// 将返回串转换成 Map
Map<String, String> xmlToMap = XMLUtil.xml2Map(reqpXml);
// 返回给微信的结果
String respXml = "";
JsPayResult result = new JsPayResult();
// 在return_code为SUCCESS的时候有返回 req_info
if (Constants.RETURN_CODE.equals(xmlToMap.get("return_code"))) {
// 退款返回加密信息
String reqInfo = xmlToMap.get("req_info");
// 解密后的信息
String decodeReqInfo = AESUtil.decryptData(reqInfo);
// 将解密后的信息换成 Map
Map<String, String> reqInfoMap = XMLUtil.xml2Map(decodeReqInfo);
ResponseResult responseResult = refundRegistration(Constants.BRANCHCODE, reqInfoMap.get("out_trade_no"),
reqInfoMap.get("transaction_id"), "3300", DateUtil.getTradeTime(reqInfoMap.get("success_time")),
Constants.YYSOURCE);
if (Constants.RESULTCODE.equals(responseResult.getResultCode())) {
respXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
result.setMessage("退款成功!");
} else {
respXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[ERROR]]></return_msg>" + "</xml> ";
result.setMessage("退款失败!");
}
logger.info("*******退款通知**********" + "退款成功!");
} else {
respXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA["
+ xmlToMap.get("return_code") + "]]></return_msg>" + "</xml> ";
result.setMessage("退款失败!");
logger.info("*******退款通知**********" + "退款失败!");
}
result.setResultCode(xmlToMap.get("return_code"));
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(respXml.getBytes());
out.flush();
out.close();
return result;
}
解密方式:
public class AESUtil {
/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
/**
* 生成key(商户密钥)
*/
private static final String key = "qp2tMA7jIDLyRRhz83Ut2eVQh8qaI5PD";
/**
* 对商户key做md5
*/
private static SecretKeySpec secretKey = new SecretKeySpec(MD5Util.MD5Encode(key, "UTF-8").toLowerCase().getBytes(),
ALGORITHM);
/**
* AES加密
*
* @param data
* @return
* @throws Exception
*/
public static String encryptData(String data) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
// 创建密码器
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
// 初始化
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return Base64Util.encode(cipher.doFinal(data.getBytes()));
}
/**
* AES解密
*
* @param base64Data
* @return
* @throws Exception
*/
public static String decryptData(String base64Data) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
// 创建密码器
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
// 初始化
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return new String(cipher.doFinal(Base64Util.decode(base64Data)));
}
public static void main(String[] args) throws Exception {
// String A 为测试字符串,是微信返回过来的退款通知字符串
String A = "N0lJ0LWjHQlVOTEYdlRoVDbqWq+tRo9ZzvcwvIGHjFN6pYmHEX44W9l/jlyw8cwHueWk3m4hpldha73MgmCZJUu5LBZv27y/Vx2RkvHKCkiI5mV9pqQxiJmZTB1PN6s2wT3EVN1BFciGNzhozqQNIOyn/B9VOVKXkTeh1to8nI/UFVDexDE4ZyMBoB9oCQVcnkAuPaWqibHMU7i0iapB1UEMYJCgRKza9OGtvs0WbqIRgVVhtFxpxMhHmIaxzvH0JrdD/iOAYEV/NxUkye2HNJahatcYFBFQlbrTTBJ67MXZ6NzwFaOqqQYxZAKKEDrU++zu7hhX0lC5rZ5Uoyavn/sYTK3ZoCgAg/6O+S/f9sg+FoD8BrZmxC6tZwTfkDGsEO09m/JSTDxgU9ToCypyQt+bCxIFhLkGt8wKAtIEo0VOrfT9yMvHyBNLHtvNXj9gTQP+bMtpWAr0iMNgLwyXC2KY2FVxLmBEAnIIcXw7W15QItPcNVpQZceZooZwXn3QT6D4QDoHyg7ymHiAbtax0xHeYVGuGDB95E22q5C1Hh1a+7nyqkkJm1tzgJwyU+hhCw3Kw0Sj4JJVoLn6hFIBmVrDHf9x7j6VBULZ+39zk2upEudu/2TU1QVx96RCMW2O8EKXthPjzoOzZh1KeBsdkodrSn6gpBRNhIdbeimyAANVTyN+eeHThdx4tgEhodr9nVawFCSnD7jajowwaABFv/5AeWXSohfbbxAVrghNCjsfR4Grybr9fb6wB7hJ2yPZIKgdf8nGa9B7joKfZl2N7xIRawhGAVR3RRC2ajBEiabqaNhBCvhHPzR75oXViUL5OzVSyYznvrE1JEIgtGSN0rI/hUBIxhnTEv/X9C3NWiYRWoLMt29vJbQlk9hgQHVnTpH00khjXe8tdtMIkY7FUJmIsZH8D0jDMAvDj00Zl6r5z7FsyzRR+0xNsiyj8BPAxmSLqyrvXtgYx91N8I16TsgEBPJACL7tHkUr+kjQNXNzRp32mJFkB7/ZNQaXH8cm5aUAFk9eCuQAD4GqQxoYHOs/L7q2WMdajxPPxQSO6JU=";
String B = AESUtil.decryptData(A);
System.out.println(B);
}
}
有兴趣的朋友可以关注下本人的微信公众号:“JAVA菜鸟程序猿”