准备工作:
获取商户证书(java开发使用的证书文件apiclient_cert.p12)
微信支付接口中,涉及资金回滚的接口会使用到商户证书,包括退款、撤销接口。商家在申请微信支付成功后,收到的相应邮件后,可以按照指引下载API证书,也可以按照以下路径下载:微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->证书下载 。证书文件说明如下:
证书附件 | 描述 | 使用场景 | 备注 |
---|---|---|---|
pkcs12格式 (apiclient_cert.p12、 |
包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份 | 撤销、退款申请API中调用 | windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户ID(如:10010000) |
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --> <link rel="stylesheet" href="../../../resources/css/example.css"/> <link rel="stylesheet" href="../../../resources/css/weui.min.css"/> <script src="../../../resources/js/jquery.min.js"></script> <title>退还押金</title> </head> <body> <div class="container"> <div class="page__bd"> <div class="weui-flex body_item"> <div class="weui-flex__item"> <h2>押金退还</h2> </div> <div class="weui-flex__item"> <h2 style="text-align: right;">${deposit}元</h2> </div> </div> <div class="weui-flex body_item"> <div class="weui-flex__item"> <div class=""><span style="color:#cfcfcf;">退还押金后,需再次充值才能借阅</span></div> </div> </div> <div class="weui-btn-area" style="padding-top: 40px;"> <a class="weui-btn weui-btn_primary" href="javascript:toRefund();">退还押金</a> </div> </div> </div> <script type="text/javascript"> function toRefund() { window.location.href="${basePath}/wechat/refundPay?totalFee=${deposit}&openId=${openId}"; } </script> </body> </html>
请求代码(申请退款用到的一些方法在上一遍文章微信公众号开发之调起微信支付中有):
@RequestMapping(value = "/refundPay", method = RequestMethod.GET, produces = MEDIATYPE_CHARSET_JSON_UTF8) public String refundPay(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response, String refundFee, String openId, Model model) throws Exception { Map<String, String> map = new HashMap<String, String>(); //商户平台id map.put("mch_id", mchId); //微信分配的公众账号ID(企业号corpid即为此appId) map.put("appid", appId); //随机字符串 map.put("nonce_str", CheckUtil.generateNonceStr()); //商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 // TODO 获取用户押金订单号(用户在微信支付时的订单号) String outTradeNo = ""; //订单总金额,单位为分,只能为整数(用户在微信支付时的总金额) String totalFee = ""; map.put("out_trade_no", outTradeNo);//微信订单号 内部订单号和微信订单号二选一 //商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 //todo 用户退款单号 先暂用时间戳 String outRefundNo = CheckUtil.create_timestamp(); map.put("out_refund_no", outRefundNo); map.put("total_fee", totalFee); //退款总金额,单位为分,只能为整数(不能大于订单总金额) map.put("refund_fee", refundFee); map.put("refund_desc", "小豆借阅押金退还"); //异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数 //如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效 map.put("notify_url", WXConstants.BASE_SERVER + "wechat/refundNotice"); String sign = CheckUtil.generateSignature(map, key, "MD5"); //签名 map.put("sign", sign); String xml = CheckUtil.getXMLFromMap(map); try { //获取apiclient_cert.p12证书,以下内容是直接在微信提供的demo中摘取出来的 InputStream certStream = request.getServletContext().getResourceAsStream("/WEB-INF/apiclient_cert.p12"); char[] password = mchId.toCharArray(); KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(certStream, password); // 实例化密钥库 & 初始化密钥工厂 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, password); // 创建 SSLContext SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory( sslContext, new String[]{"TLSv1"}, null, new DefaultHostnameVerifier()); BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager( RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslConnectionSocketFactory) .build(), null, null, null ); HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .build(); String url = "https://api.mch.weixin.qq.com/secapi/pay/refund"; HttpPost httpPost = new HttpPost(url); RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build(); httpPost.setConfig(requestConfig); StringEntity postEntity = new StringEntity(xml, "UTF-8"); httpPost.addHeader("Content-Type", "text/xml"); httpPost.addHeader("User-Agent", "wxpay sdk java v1.0 " + mchId); httpPost.setEntity(postEntity); HttpResponse httpResponse = httpClient.execute(httpPost); HttpEntity httpEntity = httpResponse.getEntity(); String data = EntityUtils.toString(httpEntity, "UTF-8"); Map<String, String> refounResult = CheckUtil.xmlToMap(data); logger.info("退款返回的信息" + refounResult); if ("SUCCESS".equals(refounResult.get("return_code")) && "SUCCESS".equals(refounResult.get("result_code"))) { //TODO 获得退款详细信息,封装到数据库中 outTradeNo = refounResult.get("out_trade_no");//商户订单号 } else { String msg = refounResult.get("return_msg"); model.addAttribute("msg", msg); return "wechat/return/refundFail"; } } catch (Exception e) { logger.info("退款失败" + e.getMessage()); model.addAttribute("msg", "退款发生异常"); return "wechat/return/refundFail"; } return "wechat/return/refundSuccess"; }
退款回调:
@RequestMapping(value = "/refundNotice") public void refund(HttpServletResponse response, HttpServletRequest request) { try { String retStr = null; try { retStr = new String(readInput(request.getInputStream()), "utf-8"); } catch (IOException e) { e.printStackTrace(); } logger.info("退款回调信息:" + retStr); Map<String, String> map = CheckUtil.xmlToMap(retStr); if (map.get("return_code").toString().equalsIgnoreCase("SUCCESS")) { //对加密串req_info进行解密,这部分根据项目需求来,若不需要,可不获取解密数据 String req_info = map.get("req_info"); String data = AESUtil.decryptData(req_info); logger.info("返回的加密字段", data); //判断退款结果是否已经处理,处理过后直接返回通知 Map<String, String> map1 = CheckUtil.xmlToMap(data); String refund_status = map1.get("refund_status");//退款状态 String outTradeNo = map1.get("out_trade_no"); if (refund_status.equals("SUCCESS")) { //TODO 退款状态为成功后,对数据进行处理 } //告诉微信已收到通知 response.setContentType("text/xml"); String xml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>"; response.getWriter().print(xml); response.getWriter().flush(); response.getWriter().close(); } } catch (Exception e) { e.printStackTrace(); } }
解密算法:AESUtil.java
package com.xiaodou.park.util; import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.security.Security; import java.util.Map; 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 = "";//正式环境的商户密钥 private static SecretKeySpec secretKey = new SecretKeySpec(MD5Utils.MD5Encode(key, "UTF-8").toLowerCase().getBytes(), ALGORITHM); //在解密类中需要添加一个静态代码块来提供支持,静态代码块如下 static { try { Security.addProvider(new BouncyCastleProvider()); } catch (Exception e) { e.printStackTrace(); } } /** * AES加密 * * @param data * @return * @throws Exception */ public static String encryptData(String data) throws Exception { // 创建密码器 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 { // 创建密码器 Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); // 初始化 cipher.init(Cipher.DECRYPT_MODE, secretKey); return new String(cipher.doFinal(Base64Util.decode(base64Data))); } }