Java 微信企业付款到个人钱包
文章目录
- **Java 微信企业付款到个人钱包**
- 前言
- 一、需要准备的配置
- 二、开发
- 总结
前言
微信企业付款到个人钱包,此功能模块需要提前在微信商户平台开通。因为开通需要先决条件,并不是所有商户都满足。商户需满足三个条件,可以在商户平台的产品中心,找到入口,申请开通。
1)入驻满90天
2)截至今日,回推30天有连续不断交易
3)交易需为健康交易
一、需要准备的配置
1. appid 可以是您的小程序或者公众号的appid(需要和微信的商户绑定)
2. mchId 商户号,微信商户开通时分配的商户id
3. paternerKey 密钥,在后台配置,在向微信发送请求时需要用来加密签名
4. certfile 支付证书,在商户后台下载
5. openid 用户的唯一标识,需要是你绑定的appid下的openid,因为企业付款需要通过 appid + openid 指定一个收款用户
比较重要的一点是,企业账户里得有钱。。。
二、开发
思路:
企业付款到零钱的入参:
- 微信入参需要是xml形式的入参,所以你需要先将你的入参转为xml格式
- 在此之前,你需要获取签名,这很重要
- 但是签名的生成,需要你其他的参数的拼接,加密获取
- 最后,你可以加载证书,并向微信发送付款的请求
所以我们现在的思路就已经很明确了,在我们接收前端发送的请求时,我们可以将入参直接设定成一个付款对象。里面可以包含一些,姓名,提现金额这些个信息。后台可以做一些配置处理,比如将我们配置好的商户信息set进去,通过这些现有的信息,获取到签名。然后加载证书,发送请求。
支付请求的对象:
public class TransfersDto {
/**
* 与商户号关联应用(如微信公众号/小程序)的APPID
*/
private String mch_appid;
/**
* 微信支付分配的商户号
*/
private String mchid;
/**
* 微信支付分配的终端设备号
*/
private String device_info;
/**
* 随机字符串,不长于32位
*/
private String nonce_str;
/**
* 签名
*/
private String sign;
/**
* 商户订单号,需保持唯一性(只能是字母或者数字,不能包含有其他字符)
*/
private String partner_trade_no;
/**
* 商户appid下,某用户的openid
*/
private String openid;
/**
* NO_CHECK:不校验真实姓名 FORCE_CHECK:强校验真实姓名
*/
private String check_name;
/**
* 收款用户真实姓名。
* 强校验必填项
*/
private String re_user_name;
/**
* 企业付款金额,单位为分
*/
private Integer amount;
/**
* 企业付款备注
*/
private String desc;
/**
* 发起者IP地址+该IP可传用户端或者服务端的IP。
*/
private String spbill_create_ip;
此处忽略get set 方法
/**
* 获取随机字符串 Nonce Str
* 此处借助 hutool的工具类,当然你也可以另写方法
* @return String 随机字符串
*/
public static String generateNonceStr() {
return IdUtil.fastSimpleUUID();
}
/**
* 获取商户订单号
* @param mchId 商户号
* @desc 这里通过商户号生成商户订单号
* @return
*/
public static String getPaterNo(String mchId){
return mchId
+ new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
+ (int)((Math.random() * 9 + 1) * 1000);
}
然后你再把自己维护的商户配置一顿set,我们的必传入参就剩一个签名sign了,这也是很重要的一个步骤。
transfersDto.setSign(WeChatPayKit.createSign(BeanUtil.beanToMap(transfersDto), merChantDto.getPaternerKey()));
此处的BeanUtil同样是hutool提供的工具类,我们将bean转map作为创建签名的入参
private static final String FIELD_SIGN = "sign";
/**
* 创建签名
* @param map 方法
* @param paterNerkey api密钥
* @return
*/
public static String createSign(Map<String, Object> map, String paterNerkey) {
return createSign(map, paterNerkey, SignType.MD5);
}
public static String createSign(Map<String, Object> map, String partnerKey, SignType signType) {
map.remove(FIELD_SIGN);
String sign = createLinkString(map, "&");
String SignTemp = sign += "&key=" + partnerKey;
if (signType == SignType.MD5) {
return md5(SignTemp).toUpperCase();
} else {
return hmacSha256(SignTemp, partnerKey).toUpperCase();
}
}
/**
* 生成MD5字符串
* SecureUtil 来自 hutool
* @param data 数据
* @return MD5字符串
*/
public static String md5(String data) {
return SecureUtil.md5(data);
}
/**
* 生成16进制的 sha256 字符串
* SecureUtil 来自 hutool
* @param data 数据
* @param key 密钥
* @return sha256 字符串
*/
public static String hmacSha256(String data, String key) {
return SecureUtil.hmac(HmacAlgorithm.HmacSHA256, key).digestHex(data, CharsetUtil.UTF_8);
}
/**
* 排序并拼接
* @param map 需要排序并拼接的map
* @param delimiter 拼接符
* @return 拼接字符串
*/
public static String createLinkString(Map<String, Object> map, String delimiter) {
List<String> keys = new ArrayList<>(map.keySet());
Collections.sort(keys);
String sign = keys.stream()
.filter(k -> !Objects.isNull(map.get(k)))
.map(key -> {
return key + "=" + map.get(key).toString();
})
.collect(Collectors.joining(delimiter));
return sign;
}
public enum SignType {
/**
* HMAC-SHA256 加密
*/
HMACSHA256("HMAC-SHA256"),
/**
* MD5 加密
*/
MD5("MD5"),
/**
* RSA
*/
RSA("RSA");
SignType(String type) {
this.type = type;
}
private final String type;
public String getType() {
return type;
}
}
附 微信签名算法:
第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
特别注意以下重要规则:
◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。
最后的我们,开始加载证书,发送请求给微信,进行打款。
/**
* 微信 企业付款给个人钱包
*/
private static final String WX_COM_DO_TRANS_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";
//商户配置 包含商户id,证书等字段
WechatMchTransInfo merChantDto = getMerChantConfig();
String xmlParam = XmlUtil.beanToXml(transfersDto,TransfersDto.class);
String resultXml = postWxTransfers(WX_COM_DO_TRANS_URL, merChantDto, xmlParam);
public class XmlUtil {
// xml转java对象
public static <T> T xmlToBean(String xml, Class<T> clazz) {
XStream xstream = new XStream() {
@Override
protected MapperWrapper wrapMapper(MapperWrapper next) {
return new MapperWrapper(next) {
@Override
public boolean shouldSerializeMember(Class definedIn, String fieldName) {
if (definedIn == Object.class) {
try {
return this.realClass(fieldName) != null;
} catch (Exception e) {
return false;
}
} else {
return super.shouldSerializeMember(definedIn, fieldName);
}
}
};
}
};
XStream.setupDefaultSecurity(xstream);
xstream.autodetectAnnotations(true);
xstream.alias("xml", clazz);
xstream.allowTypes(new Class[] { clazz });
return (T) xstream.fromXML(xml);
}
// java对象转xml
public static <T> String beanToXml(T t, Class<T> clazz) {
XStream xstream = new XStream(new DomDriver("UTF-8",new XmlFriendlyReplacer("_-", "_")));
XStream.setupDefaultSecurity(xstream);
xstream.autodetectAnnotations(true);
xstream.alias("xml", clazz);
xstream.allowTypes(new Class[] { clazz });
return xstream.toXML(t);
}
}
WeChatPayRequest weChatPayRequest = new WeChatPayRequest(merChantDto);
String returnXml = null;
try {
returnXml = weChatPayRequest.request(url,xmlStr,true);
} catch (Exception e) {
logger.error("postWxTransfers 微信处理异常 ==>{}",ExceptionUtils.getStackTrace(e));
}
public class WeChatPayRequest {
//此配置你可以换成自己维护的商户配置
private WechatMchTransInfo config;
public WeChatPayRequest(WechatMchTransInfo config) {
this.config = config;
}
private static int socketTimeout = 10000;// 连接超时时间,默认10秒
private static int connectTimeout = 30000;// 传输超时时间,默认30秒
/**
* 向微信发送请求
* @param data
* @param useCert 是否使用证书
* @return
* @throws Exception
*/
public String request(final String url, String data, boolean useCert) throws Exception {
BasicHttpClientConnectionManager connManager;
if (useCert) {
// 证书
char[] password = config.getMchId().toCharArray();
//因为我司多租户模式,会存在多个商户,所以做了商户的维护,证书通过byte[]存储
InputStream certStream = new ByteArrayInputStream(config.getCertfile());
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certStream, password);
// 实例化密钥库 & 初始化密钥工厂
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);
// 创建 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
sslContext,
new String[]{"TLSv1"},
null,
new DefaultHostnameVerifier());
connManager = new BasicHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslConnectionSocketFactory)
.build(),
null,
null,
null
);
}
else {
connManager = new BasicHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build(),
null,
null,
null
);
}
HttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(connManager)
.build();
HttpPost httpPost = new HttpPost(url);
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
httpPost.setConfig(requestConfig);
StringEntity postEntity = new StringEntity(data, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(postEntity);
HttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
return EntityUtils.toString(httpEntity, "UTF-8");
}
}
总结