记录一次微信提现接口实现(官方名称是企业付款到个人),头一次接触,中间遇到了好多坑,下面开始介绍:
首先是必要的一些商户信息:商户号、商户账号appid、秘钥、商户证书以及用户的openid
我是中途接手别人的项目,由于操作失误导致将原本申请好的证书文件弄丢了,在申请证书这里踩了个坑,证书丢失以后我去微信支付平台申请证书,申请后没有将原证书作废,导致测试的时候一直提示我证书错误,请重新下载证书。由于没怎么接触过,我以为上次的证书申请的有问题,所以再次申请证书,提示我证书一年只能申请三次,瞬间就慌了,这个功能要是实现到明年,客户都要疯掉了,还好联系微信客服帮我重置了申请证书的次数。随后再次申请证书,申请后我就点击立即作废(作废的是原来的证书),这里如果不作废的话还是原来的证书在生效,会导致我无法测试。
作废之后,在本地调试,提示我签名错误,这里也是个坑,不过是粗心导致的,我设置的秘钥是32位的,但是由于疏忽,写到系统中的秘钥只有31位,少了一位,然后本地测试一直提示我签名错误,我拿着签名去微信校验工具测了好几次,都提示校验成功,为什么能校成功呢,因为我从代码里把错误的秘钥拿去微信校验工具,所以校验出来也是对的。还好及时发现了这个问题,不然一直以为自己的签名是正确的。
以上是需要传入的参数,其实提现的接口并不是很复杂,就是把这些参数封装成一个xml格式,通过https的形式发送给微信,然后再接收返回的xml进行解析,麻烦就是各种工具类生成签名什么的,让人感觉有点复杂。考虑尽快实现,所以上面必填项是否的参数我都没有传,只填了必传的参数。
package com.zs.common.util;
import com.zs.common.util.WXPayConstants.SignType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;
/**
* 微信支付工具类
*
* @author ljp
* @date 2020年6月5日19:22:38
*/
public class WXPayUtil {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.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) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
}
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key : data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
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");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
} catch (Exception ex) {
}
return output;
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
return generateSignedXml(data, key, SignType.MD5);
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名类型
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
String sign = generateSignature(data, key, signType);
data.put(WXPayConstants.FIELD_SIGN, sign);
return mapToXml(data);
}
/**
* 判断签名是否正确
*
* @param xmlStr XML格式数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
Map<String, String> data = xmlToMap(xmlStr);
if (!data.containsKey(WXPayConstants.FIELD_SIGN)) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key).equals(sign);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
*
* @param data Map类型数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
return isSignatureValid(data, key, SignType.MD5);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
if (!data.containsKey(WXPayConstants.FIELD_SIGN)) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key, signType).equals(sign);
}
/**
* 生成签名
*
* @param data 待签名数据
* @param key API密钥
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key) throws Exception {
return generateSignature(data, key, SignType.MD5);
}
//
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
// 参数值为空,则不参与签名
if (data.get(k).trim().length() > 0) {
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
sb.append("key=").append(key);
if (SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
} else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
} else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
java.security.MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 生成 HMACSHA256
*
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 日志
*
* @return
*/
public static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
}
/**
* 获取当前时间戳,单位秒
*
* @return
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis() / 1000;
}
/**
* 获取当前时间戳,单位毫秒
*
* @return
*/
public static long getCurrentTimestampMs() {
return System.currentTimeMillis();
}
}
以上是一个工具类,主要有map转xml的方法、生成签名的方法、获取随机字符串的方法等等
package com.zs.common.util;
import javax.servlet.http.HttpServletRequest;
public class AuthUtil {
public static final String CERT_PATH = "classpath:certificate/apiclient_cert.p12";
/**
* 秘钥
*/
public static final String PATERNERKEY = "*********";
/**
* @Title: getRequestIp
* @Description: 获取用户的ip地址
* @param:
* @return:
*/
public static String getRequestIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip.indexOf(",") != -1) {
String[] ips = ip.split(",");
ip = ips[0].trim();
}
return ip;
}
}
以上也是一个工具类,我的秘钥放到了这里,这里的秘钥应该是32位的,然后里面还有一个获取ip的方法。
以上是证书的存放路径,对应的代码路径为public static final String CERT_PATH = “classpath:certificate/apiclient_cert.p12”;
package com.zs.modules.mobile.wx.controller;
import com.zs.common.util.AuthUtil;
import com.zs.common.util.CertHttpUtil;
import com.zs.common.util.VerificationUtil;
import com.zs.common.util.WXPayUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 微信提现控制器类
*
* @author ljp
* @date 2020年6月8日13:43:40
*/
@Controller
@RequestMapping("/mobile/wx")
public class PayController {
private static Logger log = LoggerFactory.getLogger(PayController.class);
/**
* 商户appId
*/
@Value("${merchant.apiID}")
private String apiId;
/**
* 商户号
*/
@Value("${merchant.mchId}")
private String mchId;
/**
* @Title: transfer
* @Description: 企业转账到零钱
* @param: 用户的openId 提现金额
* @return:
*/
@RequestMapping("/pay")
@ResponseBody
public Object transfer(@RequestBody Map<String, Object> paramMap, HttpServletRequest request) {
Map<String, Object> resultMap = new HashMap<>(16);
//验证必要的参数
if (!VerificationUtil.isNull((String) paramMap.get("openId"))
|| !VerificationUtil.isNull((String) paramMap.get("amount"))) {
resultMap.put("status", 0);
resultMap.put("message", "缺少参数");
return resultMap;
}
Map<String, String> map = new HashMap<>(16);
// 1.0 拼凑企业支付需要的参数
// 微信公众号的appid
// String appid = AuthUtil.APPID;
// 商户号
// String mch_id = AuthUtil.MCHID;
// 生成随机数
String nonce_str = WXPayUtil.generateNonceStr();
// 生成商户订单号
String partner_trade_no = WXPayUtil.generateNonceStr();
// 用户的openid
String openid = paramMap.get("openId").toString();
// 是否验证真实姓名呢 这里填NO_CHECK:不校验真实姓名 FORCE_CHECK:强校验真实姓名
// 这里填写NO_CHECK 就不用传递用户真实姓名
String checkName = "NO_CHECK";
// 企业付款金额,最少为100,单位为分 就是提现最低一元
String amount = paramMap.get("amount").toString();
// 企业付款操作说明信息。必填。
String desc = "**系统提现";
// 工具类生成的ip地址
String spbill_create_ip = AuthUtil.getRequestIp(request);
// 这里对参数进行封装
SortedMap<String, String> packageParams = new TreeMap<>();
// 商户号appid
packageParams.put("mch_appid", apiId);
// 商户号
packageParams.put("mchid", mchId);
// 随机生成后数字,保证安全性
packageParams.put("nonce_str", nonce_str);
// 商户订单号
packageParams.put("partner_trade_no", partner_trade_no);
// 用户的openid
packageParams.put("openid", openid);
// 不校验用户姓名
packageParams.put("check_name", checkName);
// 收款用户姓名 上面填写的是NO_CHECK 就不需要传递改参数
//packageParams.put("re_user_name", re_user_name);
// 企业付款金额,单位为分
packageParams.put("amount", amount);
// 企业付款操作说明信息。必填。
packageParams.put("desc", desc);
// 调用接口的机器Ip地址 此参数为非必传 所以不添加改参数
// packageParams.put("spbill_create_ip", spbill_create_ip);
try {
// 生成签名 第二个参数是商户的秘钥
String sign = WXPayUtil.generateSignature(packageParams, AuthUtil.PATERNERKEY);
// 将签名也放入参数中
packageParams.put("sign", sign);
// 最终转换为xml格式的数据
String xml = WXPayUtil.mapToXml(packageParams);
// 退款的api接口
String wxUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";
System.out.println("发送前的xml为:" + xml);
// 发起提现请求 前三个参数就不说了,都在上面有体现 最后一个参数是申请的证书 这里是一个路径,这个路径在上面有提到
String returnXml = CertHttpUtil.postData(wxUrl, xml, mchId, AuthUtil.CERT_PATH);
System.out.println("返回的returnXml为:" + returnXml);
// 返回的xml结果转成map格式
map = WXPayUtil.xmlToMap(returnXml);
if (map.get("return_code").equals("SUCCESS") && map.get("result_code").equals("SUCCESS")) {
//提现成功
resultMap.put("status", 1);
resultMap.put("message", "提现成功");
} else {
//提现失败
resultMap.put("status", 0);
resultMap.put("message", map.get("err_code_des"));
}
return resultMap;
} catch (Exception e) {
log.error("微信提现接口出错,出错信息为" + e.getMessage());
//提现失败
resultMap.put("status", 0);
resultMap.put("message", e.getMessage());
return resultMap;
}
}
}
以上是提现的接口实现,也是第一次接触支付相关的接口,如果有哪里不对,可以评论出来,我会再对文章进行补充。