最近公司申请微信服务商,需要给第三方提供支付、分账功能。
首先,服务商小程序支付,基本与普通商户小程序支付一致
支付使用服务商统一下单接口:微信官方文档地址https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_sl_api.php?chapter=9_1
这里的商户号是服务商的商户号,小程序appid是商家的appid,子商户号是商家的商户号且商家是服务商的特约商户需要授权。
这两个openid建议使用sub_openid,商户小程序的唯一openid。
服务商分账有两种接口:单次分账和多次分账
微信官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/allocation_sl.php?chapter=25_1&index=1
多的不说直接上代码:
引入maven 依赖
<dependency>
<groupId>org.joddgroupId>
<artifactId>jodd-coreartifactId>
<version>5.1.5version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.55version>
dependency>
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-payartifactId>
<version>3.8.0version>
dependency>
分账请求类
package com.linli.pay.service.pay.req;
import lombok.Data;
@Data
public class WxSharingOrderRequest {
/**
* 服务商商户号
*/
private String mch_id;
/**
* 子商户号
*/
private String sub_mch_id;
/**
* 服务商appid
*/
private String appid;
/**
* 子商户appid
*/
private String sub_appid;
/**
* 随机字符串
*/
private String nonce_str;
/**
* 签名
*/
private String sign;
/**
* 签名类型(只支持HMAC-SHA256)
*/
private String sign_type;
/**
* 微信订单号
*/
private String transaction_id;
/**
* 商家订单号
*/
private String out_trade_no;
/**
* 商户分账单号(同一个单号多次提交只算一次)
*/
private String out_order_no;
/**
* 商户分账金额(小于等于订单金额*(1-手续费)*最大分账比例)
*/
private Integer amount;
/**
* 分账接收方列表(单次分账不能即是支付商户又是接收商户,多次分账没有限制)
*/
private String receivers;
}
分账返回类
package com.linli.pay.service.pay.resp;
import lombok.Data;
@Data
public class WxSharingOrderResp {
//返回状态码,通信标识,SUCCESS/FAIL
private String return_code;
//返回信息,通信标识OK
private String return_msg;
//业务结果,交易标识,SUCCESS/FAIL
private String result_code;
//错误代码
private String err_code;
//错误代码描述
private String err_code_des;
//商户号
private String mch_id;
//子商户号
private String sub_mch_id;
//公众账号id
private String appid;
private String sub_appid;
//随机字符串
private String nonce_str;
//签名
private String sign;
//微信支付订单号
private String transaction_id;
//商户分账单号(商户订单号)
private String out_order_no;
//商户分账单号
private String order_id;
}
分账接收方
package com.linli.pay.service.pay.req;
import lombok.Data;
@Data
public class WxSharingReceiversVO {
/**
* 分账接收方类型
*/
private String type;
/**
* 分账接收方帐号
*/
private String account;
/**
* 分账金额
*/
private Integer amount;
/**
* 分账描述
*/
private String description;
}
application.yml配置类
wx:
mini:
keyPath: classpath:cert/apiclient_cert.p12 # 商户p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
busAppId:
busMchId:
busMchKey:
busSubAppId:
busSubMchId:
package com.linli.pay.config.wx;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "wx.mini")
public class WxMiniPayProperties {
/**
* apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
*/
private String keyPath;
/**
* 服务商appid
*/
private String busAppId;
/**
*服务商商户号
*/
private String busMchId;
/**
* 服务商商户密钥
*/
private String busMchKey;
/**
* 调起支付的小程序APPID
*/
private String busSubAppId;
/**
* 微信支付子商户号
*/
private String busSubMchId;
@Bean(name = "wxBusMiniPayService")
public WxPayService wxBusMiniPayService() {
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(StringUtils.trimToNull(this.busAppId));
payConfig.setMchId(StringUtils.trimToNull(this.busMchId));
payConfig.setMchKey(StringUtils.trimToNull(this.busMchKey));
payConfig.setSubAppId(StringUtils.trimToNull(this.busSubAppId));
payConfig.setSubMchId(StringUtils.trimToNull(this.busSubMchId));
payConfig.setKeyPath(StringUtils.trimToNull(this.keyPath));
// 可以指定是否使用沙箱环境
payConfig.setUseSandboxEnv(false);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}
service实现类
/**
* 单次分账
* @param data
* @return
*/
public WxSharingOrderResp oncePaySharing(WxSharingOrderRequest data)throws Exception{
WxSharingOrderRequest wxSharingOrderRequest = new WxSharingOrderRequest();
wxSharingOrderRequest.setAppid(wxBusMiniPayService.getConfig().getAppId());
wxSharingOrderRequest.setMch_id(wxBusMiniPayService.getConfig().getMchId());
wxSharingOrderRequest.setSub_mch_id(wxBusMiniPayService.getConfig().getSubMchId());
wxSharingOrderRequest.setSub_appid(wxBusMiniPayService.getConfig().getSubAppId());
wxSharingOrderRequest.setTransaction_id(data.getTransaction_id());
wxSharingOrderRequest.setNonce_str(WxUtils.makeNonStr());
wxSharingOrderRequest.setOut_order_no(data.getOut_order_no());
List<WxSharingReceiversVO> list = Lists.newArrayList();
WxSharingReceiversVO receiversVO = new WxSharingReceiversVO();
receiversVO.setAccount(wxBusMiniPayService.getConfig().getSubMchId());
receiversVO.setType("MERCHANT_ID");
receiversVO.setAmount(1);
receiversVO.setDescription("分到商户");
list.add(receiversVO);
wxSharingOrderRequest.setReceivers(FastJsonUtils.listToString(list));
BeanMap beanMap = BeanMap.create(wxSharingOrderRequest);
wxSharingOrderRequest.setSign(WxUtils.makeSign(beanMap,wxBusMiniPayService.getConfig().getMchKey(),"SHA256"));
String xmlStr = WxUtils.truncateDataToXML(WxSharingOrderRequest.class, wxSharingOrderRequest).replace(""","\"");
String url = "https://api.mch.weixin.qq.com/secapi/pay/profitsharing";
String result = WxCertHttpUtil.postData(url,xmlStr,wxBusMiniPayService.getConfig().getMchId(),wxBusMiniPayService.getConfig().getKeyPath());
Object obj = WxUtils.truncateDataFromXML(WxSharingOrderResp.class, result);
WxSharingOrderResp resp = new WxSharingOrderResp();
BeanUtils.copyProperties(obj,resp);
return resp;
}
/**
* 多次分账
* @param data
* @return
*/
public WxSharingOrderResp multiPaySharing(WxSharingOrderRequest data)throws Exception{
WxSharingOrderRequest wxSharingOrderRequest = new WxSharingOrderRequest();
wxSharingOrderRequest.setAppid(wxBusMiniPayService.getConfig().getAppId());
wxSharingOrderRequest.setMch_id(wxBusMiniPayService.getConfig().getMchId());
wxSharingOrderRequest.setSub_mch_id(wxBusMiniPayService.getConfig().getSubMchId());
wxSharingOrderRequest.setSub_appid(wxBusMiniPayService.getConfig().getSubAppId());
wxSharingOrderRequest.setTransaction_id(data.getTransaction_id());
wxSharingOrderRequest.setNonce_str(WxUtils.makeNonStr());
wxSharingOrderRequest.setOut_order_no(data.getOut_order_no());
List<WxSharingReceiversVO> list = Lists.newArrayList();
WxSharingReceiversVO receiversVO = new WxSharingReceiversVO();
receiversVO.setAccount(wxBusMiniPayService.getConfig().getSubMchId());
receiversVO.setType("MERCHANT_ID");
receiversVO.setAmount(data.getAmount());
receiversVO.setDescription("给商家分账");
list.add(receiversVO);
wxSharingOrderRequest.setReceivers(FastJsonUtils.listToString(list));
BeanMap beanMap = BeanMap.create(wxSharingOrderRequest);
wxSharingOrderRequest.setSign(WxUtils.makeSign(beanMap,wxBusMiniPayService.getConfig().getMchKey(),"SHA256"));
String xmlStr = WxUtils.truncateDataToXML(WxSharingOrderRequest.class, wxSharingOrderRequest).replace(""","\"");
String url = "https://api.mch.weixin.qq.com/secapi/pay/multiprofitsharing";
String result = WxCertHttpUtil.postData(url,xmlStr,wxBusMiniPayService.getConfig().getMchId(),wxBusMiniPayService.getConfig().getKeyPath());
Object obj = WxUtils.truncateDataFromXML(WxSharingOrderResp.class, result);
WxSharingOrderResp resp = new WxSharingOrderResp();
BeanUtils.copyProperties(obj,resp);
return resp;
}
微信工具类
/**
* 微信工具类
*/
public class WxUtils {
private static Logger logger = LoggerFactory.getLogger(WxUtils.class);
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* 数据转换为xml格式
*
* @param object
* @param obj
* @return
*/
public static String truncateDataToXML(Class<?> object, Object obj) {
XStream xStream = new XStream(new XppDriver(new NoNameCoder()));
xStream.alias("xml", object);
return xStream.toXML(obj);
}
/**
* 数据转换为对象
*
* @param object
* @param str
* @return
*/
public static Object truncateDataFromXML(Class<?> object, String str) {
XStream xstream = new XStream(new StaxDriver());
xstream.alias("xml", object);
return xstream.fromXML(str);
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String makeNonStr() {
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);
}
public static String map2XmlString(Map<String, String> map) {
String xmlResult = "";
StringBuffer sb = new StringBuffer();
sb.append("" );
for (String key : map.keySet()) {
String value = " + map.get(key) + "]]>";
sb.append("<" + key + ">" + value + "" + key + ">");
System.out.println();
}
sb.append("");
xmlResult = sb.toString();
return xmlResult;
}
/**
* 拼接签名数据
*
* @return
*/
public static String makeSign(BeanMap beanMap,String mchKey,String signType)throws Exception {
SortedMap<String, String> signMaps = Maps.newTreeMap();
for (Object key : beanMap.keySet()) {
Object value = beanMap.get(key);
// 排除空数据
if (value == null) {
continue;
}
signMaps.put(key + "", String.valueOf(value));
}
if(signType.equals("MD5")) {
// 生成签名
return generateSign(signMaps, mchKey);
}else if(signType.equals("SHA256")){
return generateSignSHA256(signMaps, mchKey);
}else{
return null;
}
}
/**
* 生成签名
*
* @param signMaps
* @return
* @throws Exception
*/
public static String generateSign(SortedMap<String, String> signMaps,String mchKey) {
StringBuffer sb = new StringBuffer();
// 字典序
for (Map.Entry signMap : signMaps.entrySet()) {
String key = (String) signMap.getKey();
String value = (String) signMap.getValue();
// 为空不参与签名、参数名区分大小写
if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
sb.append(key).append("=").append(value).append("&");
}
}
// 拼接key
sb.append("key=").append(mchKey);
// MD5加密
String sign = MD5Encode(sb.toString(), "UTF-8").toUpperCase();
return sign;
}
public static String generateSignSHA256(SortedMap<String, String> signMaps,String mchKey)throws Exception{
StringBuffer sb = new StringBuffer();
// 字典序
for (Map.Entry signMap : signMaps.entrySet()) {
String key = (String) signMap.getKey();
String value = (String) signMap.getValue();
// 为空不参与签名、参数名区分大小写
if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
sb.append(key).append("=").append(value).append("&");
}
}
// 拼接key
sb.append("key=").append(mchKey);
// MD5加密
String sign = HMACSHA256(sb.toString(), mchKey).toUpperCase();
return sign;
}
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = {
"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
private static HashMap<String, String> sortAsc(Map<String, String> map) {
HashMap<String, String> tempMap = new LinkedHashMap<String, String>();
List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(map.entrySet());
//排序
Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() {
@Override
public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
for (int i = 0; i < infoIds.size(); i++) {
Map.Entry<String, String> item = infoIds.get(i);
tempMap.put(item.getKey(), item.getValue());
}
return tempMap;
}
private static String SHA1(String str) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1"); //如果是SHA加密只需要将"SHA-1"改成"SHA"即可
digest.update(str.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
StringBuffer hexStr = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexStr.append(0);
}
hexStr.append(shaHex);
}
return hexStr.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
/**
* 生成 HMACSHA256
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
String hash = "";
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] bytes = sha256_HMAC.doFinal(data.getBytes());
hash = byteArrayToHexString(bytes);
} catch (Exception e) {
System.out.println("Error HmacSHA256 ===========" + e.getMessage());
}
return hash.toUpperCase();
}
}
带双向证书的post请求工具类
import jodd.util.ResourcesUtil;
import org.apache.commons.lang3.RegExUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
public class WxCertHttpUtil {
private static int socketTimeout = 10000;// 连接超时时间,默认10秒
private static int connectTimeout = 30000;// 传输超时时间,默认30秒
private static RequestConfig requestConfig;// 请求配置
private static CloseableHttpClient httpClient;// HTTP请求
/**
*
* @param url API地址
* @param xmlObj 要提交的XML
* @param mchId 服务商商户ID
* @param certPath证书路径
* @return
*/
public static String postData(String url, String xmlObj, String mchId, String certPath) {
// 加载证书
try {
loadCert(mchId, certPath);
} catch (Exception e) {
e.printStackTrace();
}
String result = null;
HttpPost httpPost = new HttpPost(url);
StringEntity postEntity = new StringEntity(xmlObj, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(postEntity);
requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
httpPost.setConfig(requestConfig);
try {
HttpResponse response = null;
try {
response = httpClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
}
HttpEntity entity = response.getEntity();
try {
result = EntityUtils.toString(entity, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
} finally {
httpPost.abort();
}
return result;
}
/**
*加载证书
*
* @param mchId 服务商商户ID
* @param certPath 证书路径
* @throws Exception
*/
private static void loadCert(String mchId, String certPath) throws Exception {
// 证书密码,默认为服务商商戶ID
String key = mchId;
// 证书路径
String path = RegExUtils.removeFirst(certPath, "classpath:");
if (!path.startsWith("/")) {
path = "/" + path;
}
// 指定证书格式为PKCS12
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// 读取PKCS12证书文件
InputStream instream = ResourcesUtil.getResourceAsStream(path);
try {
// 指定PKCS12的密碼(商戶ID)
keyStore.load(instream, key.toCharArray());
} finally {
instream.close();
}
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, key.toCharArray()).build();
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE);
httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
}
}
最后,可以通过微信工具检查是否签名正确。