更新和优化了微信及支付宝退款的相关代码,去除了支付宝SDK的依赖,新分享了重要的微信和支付宝支付及退款需要用到的方法,例如微信和支付宝对请求参数的签名,xml和map的互转等
统一退款工具类
/** * 退款工具类 * * @author Luyao * */ public class RefundKit { /** * 请求微信退款 * * @param orderNo * @param totalAmount * @param refundAmount * @return */ @SuppressWarnings("unchecked") public static boolean requestWechatRefund(String orderNo, BigDecimal totalAmount, BigDecimal refundAmount) { // 总金额 String totalFee = String.valueOf(totalAmount.multiply(new BigDecimal("100")).intValue()); // 退款金额 String refundFee = String.valueOf(refundAmount.multiply(new BigDecimal("100")).intValue()); // 获取配置文件 Prop prop = PropKit.use(ConfigFile.WECHAT_CONFIG); // 封装请求参数 Kv params = Kv.by("appid", prop.get("appid")); params.set("mch_id", prop.get("mchid")); params.set("nonce_str", StrKit.getRandomUUID()); params.set("out_trade_no", orderNo); params.set("out_refund_no", orderNo); params.set("total_fee", totalFee); params.set("refund_fee", refundFee); params.set("sign", WechatKit.genMd5Sign(params, prop.get("key"))); try { // 实例化密码库并设置证书格式 KeyStore keyStore = KeyStore.getInstance("PKCS12"); // 将证书文件转为文件输入流 String certPath = PathKit.getRootClassPath() + prop.get("refund.cert.path"); FileInputStream inputStream = new FileInputStream(new File(certPath)); // 加载证书文件流和密码(默认为商户id)到密钥库 keyStore.load(inputStream, prop.get("mchid").toCharArray()); SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, prop.get("mchid").toCharArray()) .build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext); // 构建ssl套接字的证书内容和密码 CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build(); // 创建post请求 HttpPost httpPost = new HttpPost(prop.get("refund.url")); // 填充数据实体 httpPost.setEntity(new StringEntity(WechatKit.mapToXml(params), Constant.CHARSET)); // 发送退款请求 HttpResponse response = httpClient.execute(httpPost); // 获取返回数据实体 HttpEntity entity = response.getEntity(); // 将该实体转可读的字符串类型,微信支付返回的数据为xml字符串 String result = EntityUtils.toString(entity, Constant.CHARSET); // 将请求结果的数据类型由xml转为map Map<String, String> resultMap = WechatKit.xmlToMap(result); // 成功的状态码 String successCode = "SUCCESS"; if (successCode.equals(resultMap.get("return_code")) && successCode.equals(resultMap.get("result_code"))) { return true; } // 失败原因 String failReason = String.format("订单号%s微信请求退款失败,原因:%s,%s", orderNo, resultMap.get("return_msg"), resultMap.get("err_code_des")); LogKit.warn(failReason); return false; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 请求支付宝退款 * * @param orderNo * @param refundAmount * @return */ @SuppressWarnings("unchecked") public static boolean requestAlipayRefund(String orderNo, BigDecimal refundAmount) { // 获取配置文件 Prop prop = PropKit.use(ConfigFile.ALIPAY_CONFIG); // 封装请求参数 Kv params = Kv.by("app_id", prop.get("appid")); params.set("method", prop.get("method.refund")); params.set("charset", Constant.CHARSET); params.set("version", prop.get("method.version")); params.set("timestamp", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); params.set("sign_type", prop.get("sign.type")); Kv bizContent = Kv.by("out_trade_no", orderNo).set("refund_amount", refundAmount.toString()); params.set("biz_content", bizContent.toJson()); try { // 将签名结果加入请求参数 params.set("sign", AlipayKit.getSign(params)); // 发送退款请求 String result = HttpKit.get(prop.get("http.url"), params); // 将JSON字符串转map对象 Kv resultMap = JSON.parseObject(result, Kv.class); resultMap = JSON.parseObject(resultMap.getStr("alipay_trade_refund_response"), Kv.class); // 成功的状态码 String successCode = "10000"; // 获取退款结果 if (successCode.equals(resultMap.getStr("code"))) { return true; } // 失败原因 String failReason = String.format("订单号%s支付宝请求退款失败,原因:%s,%s", orderNo, resultMap.get("sub_code"), resultMap.get("sub_msg")); LogKit.warn(failReason); return false; } catch (Exception e) { e.printStackTrace(); return false; } } }
微信相关工具类
/** * 微信相关工具类 * * @author Luyao * */ public class WechatKit { /** * xml转map * * @param xml * @return */ public static Map<String, String> xmlToMap(String xml) { Map<String, String> data = new HashMap<>(); try (InputStream stream = new ByteArrayInputStream(xml.getBytes("UTF-8"));) { DocumentBuilder documentBuilder = newDocumentBuilder(); Document doc = documentBuilder.parse(stream); doc.getDocumentElement().normalize(); NodeList nodeList = doc.getDocumentElement().getChildNodes(); for (int idx = 0; idx < nodeList.getLength(); ++idx) { Node node = nodeList.item(idx); if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) node; data.put(element.getNodeName(), element.getTextContent()); } } return data; } catch (Exception e) { e.printStackTrace(); return data; } } /** * map转xml * * @param map * @return * @throws Exception */ public static String mapToXml(Map<String, String> map) throws Exception { Document document = newDocument(); Element root = document.createElement("xml"); document.appendChild(root); Set<String> keySet = map.keySet(); for (String key : keySet) { String value = map.get(key); if (value == null) { value = ""; } Element filed = document.createElement(key); filed.appendChild(document.createTextNode(value.trim())); root.appendChild(filed); } TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); DOMSource source = new DOMSource(document); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); try (StringWriter writer = new StringWriter();) { StreamResult result = new StreamResult(writer); transformer.transform(source, result); String output = writer.getBuffer().toString(); return output; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 构建xml文档 * * @return * @throws ParserConfigurationException */ public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); documentBuilderFactory.setXIncludeAware(false); documentBuilderFactory.setExpandEntityReferences(false); return documentBuilderFactory.newDocumentBuilder(); } public static Document newDocument() throws ParserConfigurationException { return newDocumentBuilder().newDocument(); } /** * MD5签名 * * @param params * @param signKey * @return */ public static String genMd5Sign(Map<String, String> params, String signKey) { // 签名要求保持顺序,需对Map进行排序 Set<String> keySet = params.keySet(); String[] keyArray = keySet.toArray(new String[keySet.size()]); Arrays.sort(keyArray); StringBuilder sb = new StringBuilder(); for (String k : keyArray) { if (k.equals("sign")) { continue; } else if (StrKit.notBlank(params.get(k))) { sb.append(k.trim()).append("=").append(params.get(k).trim()).append("&"); } } sb.append("key=").append(signKey); return HashKit.md5(sb.toString()).toUpperCase(); } /** * 校验MD5签名 * * @param data * @param key */ public static boolean verifyMd5Sign(Map<String, String> data, String key) { if (!data.containsKey("sign") || StrKit.isBlank(data.get("sign"))) return false; return genMd5Sign(data, key).equals(data.get("sign")); } }
支付宝相关工具类
/** * 支付宝相关工具类 * * @author Luyao * */ public class AlipayKit { /** * 获取参数签名 * * @param params * @return * @throws Exception */ public static String getSign(Map<String, String> params) throws Exception { // 参数排序 TreeMap<String, String> sortParams = new TreeMap<>(); sortParams.putAll(params); // 参数拼接 StringBuilder sortedParamsSb = new StringBuilder(); for (Map.Entry<String, String> param : sortParams.entrySet()) { if (param.getKey().equals("sign")) { continue; } else if (StrKit.notBlank(param.getValue())) { sortedParamsSb.append(param.getKey().trim()).append("=").append(param.getValue().trim()).append("&"); } } // 去掉最后一个& String sortedParamStr = sortedParamsSb.substring(0, sortedParamsSb.length() - 1); // rsa2签名 return rsa256Sign(sortedParamStr); } /** * rsa2签名 * * @param content * @return * @throws Exception */ private static String rsa256Sign(String content) throws Exception { String privateKey = PropKit.use(ConfigFile.ALIPAY_CONFIG).get("key.private"); // 获取私钥 PrivateKey priKey = getPrivateKeyFromPKCS8("RSA", privateKey.getBytes()); // 获取指定算法的实例 Signature signature = Signature.getInstance("SHA256WithRSA"); // 初始化 signature.initSign(priKey); // 更新签名内容 signature.update(content.getBytes(Constant.CHARSET)); // 执行签名并base64编码 return Base64Kit.encode(signature.sign()); } /** * 获取私钥 * * @param algorithm * @param encodedKey * @return * @throws Exception */ private static PrivateKey getPrivateKeyFromPKCS8(String algorithm, byte[] encodedKey) throws Exception { KeyFactory keyFactory = KeyFactory.getInstance(algorithm); encodedKey = Base64.getDecoder().decode(encodedKey); return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey)); } }
微信退款需要的http客户端
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.9</version> </dependency>
注意:微信退款需要登录微信后台下载双向证书
2019.09.11