企业付款到零钱「微信小程序别样发放红包」

目录

一、开发前的准备工作

二、接入微信企业付款到零钱API

1)接入「企业付款到零钱」API

2)接入「查询企业付款」API

三、开发过程的参数封装以及工具类封装

四、调试注意事项汇总


 

一、开发前的准备工作

  1. 前往商户平台开通「企业付款到零钱」。
  2. 配置好API密钥和生成API证书。

「企业付款到零钱」介绍:

企业付款到零钱「微信小程序别样发放红包」_第1张图片

开通注意事项见下图。详情请戳:企业付款场景介绍&操作指导

企业付款到零钱「微信小程序别样发放红包」_第2张图片

二、接入微信企业付款到零钱API

先附上微信官方文档:企业付款到零钱  & 查询企业付款

1)接入「企业付款到零钱」API

API 接入注意事项参见下方截图:
 

企业付款到零钱「微信小程序别样发放红包」_第3张图片


嗯,浏览官网文档后,相信应该已经找到感觉,哪怕一丁点都好!
接下来,总体梳理一下编码思路:

  • 读取对接必需配置项:appid & 商户号mch_id & 商户API密钥mchKey
  • 读取微信「企业付款到零钱」接口URL配置项
  • SSL加载API证书
  • 组装「企业付款到零钱」接口所需的请求参数
  • 按照既定规则生成商户订单号
  • 生成签名
  • 正式请求「企业付款到零钱」API
  • 接收API响应结果,处理相关业务逻辑

 

2)接入「查询企业付款」API

嗯,还是梳理编码步骤:

  • 读取对接必需配置项:appid & 商户号mch_id & 商户API密钥mchKey
  • 读取微信「查询企业付款」接口URL配置项
  • SSL加载API证书
  • 组装「查询企业付款」接口所需的请求参数(此处商户订单号跟付款API使用的商户订单号保持一致)
  • 生成签名
  • 正式请求「查询企业付款」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("");
        }
        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("");
            }
        }
        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;
}

四、调试注意事项汇总

  • 想保证调试过程顺利的话,强烈建议将项目部署至linux服务器,且保证外网可访问
  • 调试金额amount必须保证:1 <= amount <= 5000,单位:元

 

你可能感兴趣的:(Java,Spring,Boot,wxpay)