目录
一、开发前的准备工作
二、接入微信企业付款到零钱API
1)接入「企业付款到零钱」API
2)接入「查询企业付款」API
三、开发过程的参数封装以及工具类封装
四、调试注意事项汇总
「企业付款到零钱」介绍:
开通注意事项见下图。详情请戳:企业付款场景介绍&操作指导
先附上微信官方文档:企业付款到零钱 & 查询企业付款
API 接入注意事项参见下方截图:
嗯,浏览官网文档后,相信应该已经找到感觉,哪怕一丁点都好!
接下来,总体梳理一下编码思路:
嗯,还是梳理编码步骤:
/**
* @return java.util.Map
* @throws
* @description 查询企业付款
* @params [partnerTradeNo]
*/
@Override
public Map getTransferInfo(String partnerTradeNo) throws IOException {
// 加载API证书
SSLContext sslContext = initSSLContext();
SSLConnectionSocketFactory sslSkF = new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1"},
null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
SortedMap parameters = new TreeMap<>();
// 组装请求参数
parameters.put("partner_trade_no", partnerTradeNo);
parameters.put("nonce_str", weChatUtils.gen32RandomString());
parameters.put("appid", wxEpProperties.getAppid());
parameters.put("mch_id", wxEpProperties.getMchId());
// 生成签名
String sign = weChatUtils.createSign(parameters, wxEpProperties.getMchKey());
parameters.put("sign", sign);
try {
// 查询企业付款 响应结果=> Xml格式
String entPaymentQueryRes = weChatUtils.executeHttpPost(wxEpProperties.getGetTransferInfoUrl(), parameters, sslSkF);
log.info("WeChatEntPaymentServiceImpl.getTransferInfo ======== 查询企业付款响应结果:[{}] ======== ", entPaymentQueryRes);
// XML => Map
Map resInfoMap = weChatUtils.transferXmlToMap(entPaymentQueryRes);
log.info("WeChatEntPaymentServiceImpl.getTransferInfo ======== 查询企业付款响应结果Map结构:[{}] ======== ", resInfoMap.toString());
return resInfoMap;
} catch (Exception e) {
log.error("WeChatEntPaymentServiceImpl.getTransferInfo ======== 查询企业付款发生异常 ======== ");
throw new CommonBusinessException("查询企业付款失败!");
}
}
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @Description 微信生态常用工具方法
* @Author blake
* @Date 2018/12/11 下午4:15
* @Version 1.0
*/
@Component
@Slf4j
public class WeChatUtils {
public String getRemoteHost(javax.servlet.http.HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("WeChatUtils.getRemoteHost ======= 第一处Value:[{}] ======== ",ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WeChatUtils.getRemoteHost ======= 第二处Value:[{}] ======== ",ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("WeChatUtils.getRemoteHost ======= 第三处Value:[{}] ======== ",ip);
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
/**
* 执行 POST 方法的 HTTP 请求
*
* @param url
* @param parameters
* @param sslsf
* @return
* @throws IOException
*/
public String executeHttpPost(String url, SortedMap parameters, SSLConnectionSocketFactory sslsf)
throws IOException {
HttpClient client = null;
if (Objects.isNull(sslsf)) {
client = HttpClients.createDefault();
} else {
client = HttpClients.custom().setSSLSocketFactory(sslsf).build();
}
HttpPost request = new HttpPost(url);
request.setHeader("Content-type", "application/xml");
request.setHeader("Accept", "application/xml");
request.setEntity(new StringEntity(transferMapToXml(parameters), "UTF-8"));
HttpResponse response = client.execute(request);
return readResponse(response);
}
/**
* 执行 GET 方法的 HTTP 请求
*
* @param url
* @return
* @throws IOException
*/
public String executeHttpGet(String url) throws IOException {
HttpClient client = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
request.setHeader("Content-type", "application/xml");
request.setHeader("Accept", "application/xml");
HttpResponse response = client.execute(request);
return readResponse(response);
}
/**
* 第一次签名
*
* @param parameters 数据为服务器生成,下单时必须的字段排序签名
* @param key
* @return
*/
public String createSign(SortedMap parameters, String key) {
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + key);
return encodeMD5(sb.toString());
}
/**
* 第二次签名
*
* @param result 数据为微信返回给服务器的数据(XML 的 String),再次签名后传回给客户端(APP)使用
* @param key 密钥
* @return
* @throws IOException
*/
public Map createSign2(String result, String key) throws IOException {
SortedMap map = new TreeMap<>(transferXmlToMap(result));
Map app = new HashMap<>();
app.put("signType", "MD5");
app.put("appId", map.get("appid"));
app.put("nonceStr", map.get("nonce_str"));
// 统一下单接口返回的prepay_id参数值
String packageStr = "prepay_id=" + map.get("prepay_id");
app.put("package", packageStr);
// 当前时间戳
app.put("timeStamp", Long.toString(new Date().getTime() / 1000));
// 微信支付正式签名
app.put("paySign", createSign(new TreeMap<>(app), key));
return app;
}
/**
* 验证签名是否正确
*
* @return boolean
* @throws Exception
*/
public boolean checkSign(SortedMap parameters, String key) throws Exception {
String signWx = parameters.get("sign").toString();
if (signWx == null) return false;
parameters.remove("sign"); // 需要去掉原 map 中包含的 sign 字段再进行签名
String signMe = createSign(parameters, key);
return signWx.equals(signMe);
}
/**
* 读取 request body 内容作为字符串
*
* @param request
* @return
* @throws IOException
*/
public String readRequest(HttpServletRequest request) throws IOException {
InputStream inputStream;
StringBuffer sb = new StringBuffer();
inputStream = request.getInputStream();
String str;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((str = in.readLine()) != null) {
sb.append(str);
}
in.close();
inputStream.close();
return sb.toString();
}
/**
* 读取 response body 内容为字符串
*/
public String readResponse(HttpResponse response) throws IOException {
BufferedReader in = new BufferedReader(
new InputStreamReader(response.getEntity().getContent()));
String result = new String();
String line;
while ((line = in.readLine()) != null) {
result += line;
}
return result;
}
/**
* 将 Map 转化为 XML
*
* @param map
* @return
*/
public String transferMapToXml(SortedMap map) {
StringBuffer sb = new StringBuffer();
sb.append("");
for (String key : map.keySet()) {
sb.append("<").append(key).append(">")
.append(map.get(key))
.append("").append(key).append(">");
}
return sb.append(" ").toString();
}
/**
* 将 XML 转化为 map
*
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public Map transferXmlToMap(String strxml) throws IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if (null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = null;
try {
doc = builder.build(in);
} catch (JDOMException e) {
throw new IOException(e.getMessage()); // 统一转化为 IO 异常输出
}
// 解析 DOM
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if (children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}
// 辅助 transferXmlToMap 方法递归提取子节点数据
private String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if (!children.isEmpty()) {
Iterator it = children.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if (!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("" + name + ">");
}
}
return sb.toString();
}
/**
* 生成 32 位随机字符串,包含:数字、字母大小写
*
* @return
*/
public String gen32RandomString() {
char[] dict = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 32; i++) {
sb.append(String.valueOf(dict[(int) (Math.random() * 36)]));
}
return sb.toString();
}
/**
* MD5 签名
*
* @param str
* @return 签名后的字符串信息
*/
public String encodeMD5(String str) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] inputByteArray = (str).getBytes();
messageDigest.update(inputByteArray);
byte[] resultByteArray = messageDigest.digest();
return byteArrayToHex(resultByteArray);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
// 辅助 encodeMD5 方法实现
private String byteArrayToHex(byte[] byteArray) {
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] resultCharArray = new char[byteArray.length * 2];
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b & 0xf];
}
// 字符数组组合成字符串返回
return new String(resultCharArray);
}
public static void main(String[] args) {
}
}
/**
* 初始化ssl.
*
* @return the ssl context
* @throws CommonBusinessException the wx pay exception
*/
public SSLContext initSSLContext() throws CommonBusinessException, IOException {
if (StringUtils.isBlank(wxEpProperties.getMchId())) {
throw new CommonBusinessException("请确保商户号mchId已设置");
}
// SpringBoot项目,API证书可直接放至类路径resources下
InputStream resourceAsStream = new ClassPathResource(wxEpProperties.getCertPath()).getInputStream();
byte[] bytes = IOUtils.toByteArray(resourceAsStream);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try {
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] partnerId2charArray = wxEpProperties.getMchId().toCharArray();
keystore.load(byteArrayInputStream, partnerId2charArray);
return SSLContexts.custom().loadKeyMaterial(keystore, partnerId2charArray).build();
} catch (Exception e) {
throw new CommonBusinessException("商户密钥不匹配或证书文件有问题,请核实!");
} finally {
byteArrayInputStream.close();
resourceAsStream.close();
}
}
/**
* @Description 微信企业付款到零钱 基础配置数据项
* @Author blake
* @Date 2019-05-16 14:48
* @Version 1.0
*/
@Configuration // SpringBoot读取配置项其中一种用法
public class WxEntPaymentProperties {
/**
* 设置商户号关联的appid
*/
@Value("${wechat.ent.payment.appid}")
private String appid;
/**
* 商户号
*/
@Value("${wechat.ent.payment.mchid}")
private String mchId;
/**
* 商户密钥
*/
@Value("${wechat.ent.payment.mchKey}")
private String mchKey;
/**
* API证书存放路径
*/
@Value("${wechat.ent.payment.certPath}")
private String certPath;
/**
* 企业付款到零钱接口Url
*/
@Value("${wechat.ent.payment.transfers}")
private String transfersUrl;
/**
* 查询企业付款接口Url
*/
@Value("${wechat.ent.payment.gettransferinfo}")
private String getTransferInfoUrl;
}