微信小程序后端java服务商分账实现

微信小程序后端java服务商分账实现

最近公司申请微信服务商,需要给第三方提供支付、分账功能。

商户调用服务商统一支付

首先,服务商小程序支付,基本与普通商户小程序支付一致

支付使用服务商统一下单接口:微信官方文档地址https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_sl_api.php?chapter=9_1
微信小程序后端java服务商分账实现_第1张图片
这里的商户号是服务商的商户号,小程序appid是商家的appid,子商户号是商家的商户号且商家是服务商的特约商户需要授权。
微信小程序后端java服务商分账实现_第2张图片
这两个openid建议使用sub_openid,商户小程序的唯一openid。

在这里插入图片描述最重要的参数,传Y才能进行分账。

服务商分账

服务商分账有两种接口:单次分账和多次分账
微信官方文档地址: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: 

微信小程序后端java服务商分账实现_第3张图片

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();
    }
}

最后,可以通过微信工具检查是否签名正确。

你可能感兴趣的:(java,小程序)