微信和支付宝请求退款

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

评论区

JFinal

2019-01-08 15:01

目前为止第一个微信和支付宝请求退款的分享,十分有用的资源,收藏 + 点赞是必须的,感谢分享

jiaxiang

2019-01-11 10:04

有没有做过转账(提现)到个人微信支付宝账号的业务呢,分享下呗。

路遥_美好人生

2019-01-11 14:02

@jiaxiang 暂时没有哦~

Jss

2019-09-10 17:23

有没有微信退款的全部代码

路遥_美好人生

2019-09-11 10:16

@Jss 本文档已更新,你要的应该都在这儿

热门分享

扫码入社