场景介绍:
企业付款为企业提供付款至用户零钱的能力,支持通过API接口付款,或通过微信支付商户平台(pay.weixin.qq.com)网页操作付款。
1、商户号(或同主体其他非服务商商户号)已入驻90日
2、截止今日回推30天,商户号(或同主体其他非服务商商户号)连续不间断保持有交易
3、 登录微信支付商户平台-产品中心,开通企业付款。
以上来自于官方文档的简单说明,具体可参照官方文档说明
数据准备:
首先,我们需要证书 apiclient_cert.p12证书,在微信商户平台(pay.weixin.qq.com)–>账户设置–>API安全–>证书中下载 。 把下载好的证书文件放到resources目录下。后面会引用
接下来就进入编码了
老规矩,官方文档和我这个教程一起来:
根据文档,我们知道了一些调用接口必要的字段:
Map map = Maps.newHashMap();
map.put("mch_appid",wxappId);//appid(微信公众号)
map.put("mchid",wxMchId);//商户号
map.put("nonce_str", DateUtil.getAllTime() + RandomUtil.getRandomNumber(4));//随机码
map.put("partner_trade_no",transfer.getTransferSn());//商户订单号(唯一)
map.put("openid",openid);//接受企业转账的用户的openid(关联上面微信公众号的)
map.put("amount",transfer.getApplyMonery().toString());
map.put("desc","推广佣金");//企业转账描述
//是否强制校验姓名,如果选了FORCE_CHECK,那就得传一个re_user_name字段,value为被转账用户真实姓名(非实名用户转账会失败)
map.put("check_name","FORCE_CHECK");//FORCE_CHECK强制校验姓名 NO_CHECK不校验姓名
map.put("re_user_name",agent.getAgentName());
String sign = getSign(map);//根据微信签名规则签名
map.put("sign",sign);//签名,必传。根据官方文档规则生成数据签名。
上面就是我们调用接口必要的字段,值得注意点是,我们还需对着整个数据段进行sign签名。签名规则如下:对所有字段的key进行自然排序或者按ASCII码从小到大排序(字典序),然后再将数据拼接成url的格式,比如:www.xc123.com?mch_appid=?&mchid=?……
拼接完后,将我们商户号的密钥也加在这个url最后面。最后再用MD5对数据进行加密后转大写。
public String getSign(Map<String,String> map){
StringBuffer sb = new StringBuffer();
Set<String> strings = map.keySet();
Set<String> set = new TreeSet<String>();
set.addAll(strings);
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()){
String key = iterator.next();
sb.append(key+"=");
String value = map.get(key);
sb.append(value+"&");
}
sb.append("key="+wxMchKey);
String md5String = MD5.getMD5String(sb.toString()).toUpperCase();
logger.info("MD5加密且转为大写后的签名信息:"+md5String);
return md5String;
}
MD5加密代码:
public class MD5 {
public static final Logger LOG = LoggerFactory.getLogger(MD5.class);
/**
* 16进制字符集
*/
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
/**
* 循环次数
*/
public final static int HASH_ITERATIONS = 1024;
/**
* 加盐参数
*/
public final static String HASH_ALGORITHM_NAME = "MD5";
/**
* 指定算法为MD5的MessageDigest
*/
private static MessageDigest MESSAGE_DIGEST = null;
/** 初始化messageDigest的加密算法为MD5 */
static {
try {
MESSAGE_DIGEST = MessageDigest.getInstance("MD5");
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
/**
* * MD5加密字符串
*
* @param str 目标字符串
* @return MD5加密后的字符串
*/
public static String getMD5String(String str) {
if (Strings.isNullOrEmpty(str)) {
return null;
}
return getMD5String(str.getBytes());
}
/**
* * MD5加密以byte数组表示的字符串
*
* @param bytes 目标byte数组
* @return MD5加密后的字符串
*/
public static String getMD5String(byte[] bytes) {
MESSAGE_DIGEST.update(bytes);
return bytesToHex(MESSAGE_DIGEST.digest());
}
/**
* 获取文件的MD5值
*
* @param file 目标文件
* @return MD5字符串
*/
public static String getFileMD5String(File file) {
String ret = "";
FileInputStream in = null;
FileChannel ch = null;
try {
in = new FileInputStream(file);
ch = in.getChannel();
ByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
MESSAGE_DIGEST.update(byteBuffer);
ret = bytesToHex(MESSAGE_DIGEST.digest());
} catch (Exception e) {
LOG.error(e.getMessage(), e);
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(ch);
}
return ret;
}
/**
* * 获取文件的MD5值
*
* @param fileName 目标文件的完整名称
* @return MD5字符串
*/
public static String getFileMD5String(String fileName) {
return getFileMD5String(new File(fileName));
}
/**
* * 将字节数组转换成16进制字符串
*
* @param bytes 目标字节数组
* @return 转换结果
*/
public static String bytesToHex(byte[] bytes) {
return bytesToHex(bytes, 0, bytes.length);
}
/**
* * 将字节数组中指定区间的子数组转换成16进制字符串
*
* @param bytes 目标字节数组
* @param start 起始位置(包括该位置)
* @param end 结束位置(不包括该位置)
* @return 转换结果
*/
public static String bytesToHex(byte[] bytes, int start, int end) {
StringBuilder sb = new StringBuilder();
for (int i = start; i < start + end; i++) {
sb.append(byteToHex(bytes[i]));
}
return sb.toString();
}
/**
* * 将单个字节码转换成16进制字符串
*
* @param bt 目标字节
* @return 转换结果
*/
public static String byteToHex(byte bt) {
return HEX_DIGITS[(bt & 0xf0) >> 4] + "" + HEX_DIGITS[bt & 0xf];
}
/**
* shiro密码加密工具类
*
* @param credentials 密码
* @param salt 密码盐
* @return
*/
public static String md5(String credentials, String salt) {
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
//先加盐
messageDigest.update(salt.getBytes("UTF-8"));
//再放需要被加密的数据
messageDigest.update(credentials.getBytes("UTF-8"));
} catch (NoSuchAlgorithmException e) {
System.out.println("NoSuchAlgorithmException caught!");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
byte[] byteArray = messageDigest.digest();
StringBuffer md5StrBuff = new StringBuffer();
for (int i = 0; i < byteArray.length; i++) {
if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) {
md5StrBuff.append("0").append(Integer.toHexString(0xFF & byteArray[i]));
} else {
md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i]));
}
}
return md5StrBuff.toString();
}
public static void main(String[] args) {
System.out.println(MD5.md5("admin", "8pgby"));
}
}
到这里,调接口前的数据准备就已经完成了。接下来就是接口调用了
接口调用
根据文档,我们知道了企业付款到零钱的接口是基于xml格式交互的。所以我们需要有一段代码将你的map数据转成xml格式。
public String getXml(Map<String,String> map){
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set<String> strings = map.keySet();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
String key = iterator.next();
sb.append("<"+key+">");
String value = map.get(key);
sb.append(value);
sb.append("</"+key+">");
}
sb.append("</xml>");
logger.info("生成后的xml格式:"+sb.toString());
return sb.toString();
}
由于企业到零钱转账只是单层嵌套,所以这里就随便写了个循环转换格式。
然后这边拿到xml格式的数据后,就可以直接调接口请求转账了。
继续看代码
String xml = getXml(map);//map转xml
CloseableHttpClient httpClient = null;
HttpPost httpPost = new HttpPost(TRANSFER_PAY);
String body = null;
CloseableHttpResponse response = null;
try{
httpClient = HttpClients.custom().setDefaultRequestConfig(HttpUtils.REQUEST_CONFIG).setSslcontext(HttpUtils.wx_ssl_context).build();
httpPost.setEntity(new StringEntity(xml, "UTF-8"));
response = httpClient.execute(httpPost);
body = EntityUtils.toString(response.getEntity(), "UTF-8");
logger.info("微信付款返回的消息:"+body);
if (StringUtil.isNotEmpty(body)){
Map<String,String> resultMap = getMap(body.trim());//xml转map
if (resultMap.get("return_code").equals("SUCCESS") ){
if (resultMap.get("result_code").equals("SUCCESS")){
//处理自己的业务逻辑
}else if (resultMap.get("result_code").equals("FAIL")) {
logger.error("业务结果未明确,具体信息如下:");
logger.error("错误代码{},错误描述{}",resultMap.get("err_code"),resultMap.get("err_code_des"));
return Rets.failure("转账失败,失败原因:"+resultMap.get("err_code_des"));
}
}else {
logger.error("连接微信转账接口通信失败,信息:"+map.get("return_msg"));
}
}else {
return Rets.failure("转账失败,微信返回的消息为空");
}
}catch (Exception e){
logger.error("转账异常,异常消息如下:"+e.getMessage());
return Rets.failure("转账异常,信息如下:"+e.getMessage());
}finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面发送请求的代码中,还有一个要注意的点是 httpClient = HttpClients.custom().setDefaultRequestConfig(HttpUtils.REQUEST_CONFIG).setSslcontext(HttpUtils.wx_ssl_context).build();
因为微信支付接口中,涉及资金回滚的接口会使用到API证书,所以这里我们发送企业付款到零钱接口请求的时候需要将这个api证书带上。
之前我们有从微信商户号后台下载好了专门的证书文件apiclient_cert.p12 并将它放到了resources文件下。这里要用到它。
public class HttpUtils {
private static final String DEFAULT_CHARSET = "UTF-8";
private static final int CONNECT_TIME_OUT = 5000; //链接超时时间3秒
public static final RequestConfig REQUEST_CONFIG = RequestConfig.custom().setConnectTimeout(CONNECT_TIME_OUT).build();
public static SSLContext wx_ssl_context = null; //微信支付ssl证书
static{
Resource resource = new ClassPathResource("cert/apiclient_cert.p12");//证书存放的路径
try {
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] keyPassword = "********".toCharArray(); //证书密码,默认为你的商户号id
keystore.load(resource.getInputStream(), keyPassword);
wx_ssl_context = SSLContexts.custom().loadKeyMaterial(keystore, keyPassword).build();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里初始化一个SSLcontext 对象,配置好你的证书以及RequestConfig 对象,见上文 加粗 代码处。
接口发送成功后,我们接收微信的返回也是xml格式,所以我们同样得把xml格式转成map格式后再去判断处理结果。
public Map<String,String> getMap(String text) throws DocumentException {
Map<String,String> resultMap = Maps.newHashMap();
//适用于单层嵌套的xml格式
Document document = DocumentHelper.parseText(text);//将xml字符串转为document对象
Element rootElement = document.getRootElement();//获取到document的根节点元素
List<Element> elements = rootElement.elements();//获取根节点下面所有子节点的元素。
for (Element e:elements
) {
resultMap.put(e.getName(),e.getText());//遍历转为map
}
return resultMap;
}
至此,企业付款到零钱的接口已经完成了,该项接口功能适用于很多业务场景。比如最基本的分销、红包等等,具体可根据业务需求来。