更新和优化了微信及支付宝退款的相关代码,去除了支付宝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