微信商家红包发放接入

发布时间 2023-05-26 10:07:26作者: 天葬

微信红包

准备工作

微信商家开通支付功能、微信公众号、开通红包功能

相关网站

需要资料

  • API证书(apiclient_cert.p12):微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全
  • API支付秘钥:商户平台设置的密钥key 32 位
  • openId:用户微信公众授权获取到得 openId

注意事项

  1. 现金红包接口目前微信还没升级,使用的v2版本,所以提交数据和返回数据采用的是xml格式
  2. 付款金额,单位分
  3. 红包发放成功,会发放到公众号中,需要用户主动点击领取
  4. 测试过程中保证商户运营账号有余额
  5. 微信红包-产品设置-添加测试地址ip白名单,不知道自己外网ip地址 百度搜索 ip 即可

代码示例

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
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.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContexts;
import org.jeecg.common.util.Md5Util;
import org.jeecg.modules.video.utitls.Constants;
import org.springframework.core.io.ClassPathResource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.net.ssl.SSLContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.util.*;

/**
 * @description:
 * @author: Mr.Fang
 * @create: 2023-05-26 
 **/
@Slf4j
public class WxTest {
    public static void main(String[] args) throws Exception {
        // 1、参数封装
        TreeMap<String, Object> map = new TreeMap<>();
        map.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); // 随机字符串
        map.put("mch_billno", "202302260000001");// "商家订单编号"
        map.put("mch_id", Constants.WX_MERCHANT_ID); // 商户号
        map.put("wxappid", Constants.WX_APP_ID); // 公众号 APPID
        map.put("send_name", "我发的");
        map.put("re_openid", "123456"); // openid
        map.put("total_amount", "100"); // 金额
        map.put("total_num", 1); // 数量
        map.put("wishing", "恭喜发财");
        map.put("client_ip", "127.0.0.1");
        map.put("act_name", "任务分佣");
        map.put("remark", "无");

        // 2、参数拼接 k=v 格式
        List<String> list = new ArrayList<>();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            list.add(String.format("%s=%s", key, value.toString()));
        }
        String join = String.join("&", list);
        log.debug("拼接参数结果:{}", join);
        // 3、参数+&key=API支付秘钥 md5 签名
        String sign = Md5Util.md5Encode(join + "&key=" + Constants.WX_MERCHANT_API_SECRET, "UTF-8");
        log.debug("拼接参数md5:{}", sign.toUpperCase(Locale.ROOT));
        map.put("sign", sign.toUpperCase(Locale.ROOT));
        log.debug("请求参数Map:{}", map);
        // 4、获取 API 证书,当前证书是放在 resource 下的
        ClassPathResource resource = new ClassPathResource("apiclient_cert.p12");
        InputStream inputStream = null;
        try {
            inputStream = resource.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // FIXME: 2023/5/26 也可以这样
//        inputStream = new FileInputStream("证书路径");
        // 5、加载 SSL 证书
        String pass =Constants.WX_MERCHANT_ID; // 密码商户号
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(inputStream, pass.toCharArray()); // 证书默认密码是商户号码
        SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial((chain, authType) -> true).loadKeyMaterial(keyStore, pass.toCharArray()).build();
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        // 6、创建 httpClient 客户端 并设置 ssl 对象
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
        CloseableHttpClient build = httpClientBuilder.build();

        // 7、创建 post 请求
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack");
        String xml = parseMapToXml(map);
        log.debug("请求体xml:{}", xml);
        StringEntity stringEntity = new StringEntity(xml, "UTF-8");
        stringEntity.setContentEncoding("UTF-8");
        httpPost.setEntity(stringEntity);

        // 8、发起请求
        CloseableHttpResponse response = build.execute(httpPost);
        Map<String, Object> toMap = parseXMLToMap(response.getEntity().getContent());
        log.info("mchRedEnvelope:{}", toMap);
    }

    /**
     * 解析 xml
     *
     * @param map
     * @return
     */
    public static String parseMapToXml(Map<String, ? extends Object> map) {
        StringBuilder sb = new StringBuilder("<xml>");
        if (null != map && map.size() > 0) {
            Set<? extends Map.Entry<String, ?>> entries = map.entrySet();

            String key;
            for (Iterator iterator = entries.iterator(); iterator.hasNext(); sb.append("</" + key + ">")) {
                Map.Entry next = (Map.Entry) iterator.next();
                key = (String) next.getKey();
                Object value = next.getValue();
                sb.append("<" + key + ">");
                if (Objects.nonNull(value)) {
                    sb.append("<![CDATA[" + value + "]]>");
                }
            }
        }

        sb.append("</xml>");
        return sb.toString();
    }

    /**
     * xml 转 map
     * @param inputStream
     * @return
     */
    public static Map<String, Object> parseXMLToMap(InputStream inputStream) {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        try {
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(inputStream);
            Element documentElement = document.getDocumentElement();
            Map<String, Object> map = recursiveFindXmlElement(documentElement);
            return map;
        } catch (ParserConfigurationException | SAXException | IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    private static Map<String, Object> recursiveFindXmlElement(Element element) {
        if (Objects.nonNull(element)) {
            HashMap<String, Object> map = new HashMap();
            NodeList childNodes = element.getChildNodes();

            for(int i = 0; i < childNodes.getLength(); ++i) {
                Node item = childNodes.item(i);
                short nodeType = item.getNodeType();
                if (Objects.equals(nodeType, (short) 1)) {
                    String nodeName = item.getNodeName();
                    Element childElement = (Element)item;
                    Map<String, Object> childMap = recursiveFindXmlElement(childElement);
                    if (null != childMap && childMap.size() > 0) {
                        map.put(nodeName, childMap);
                    } else {
                        String nodeValue = StringUtils.isBlank(item.getTextContent()) ? null : item.getTextContent().trim();
                        map.put(nodeName, nodeValue);
                    }
                }
            }
            if (map.size() > 0) {
                return map;
            }
        }

        return null;
    }
}

响应结果

展示参数进行处理,不代表真实发送数据内容

拼接参数结果:

act_name=任务分佣&client_ip=127.0.0.1&mch_billno=202302260000001&mch_id=1299535101&nonce_str=c3dad56d4bc74f9d910a6210edaddd0a&re_openid=1111111&remark=无&send_name=我发的&total_amount=100&total_num=1&wishing=恭喜发财&wxappid=1111111

拼接参数md5:

426a84083ea5adc67398a7918490adeb

请求参数Map:

{act_name=任务分佣, client_ip=127.0.0.1, mch_billno=202302260000001, mch_id=1111111, nonce_str=c3dad56d4bc74f9d910a6210edaddd0a, re_openid=1111111, remark=无, send_name=恭喜发财, sign=426a84083eadadc67398a7918490adeb, total_amount=100, total_num=1, wishing=恭喜发财, wxappid=1111111}

请求体xml

<xml>
    <act_name><![CDATA[任务分佣]]></act_name>
    <client_ip><![CDATA[127.0.0.1]]></client_ip>
    <mch_billno><![CDATA[202302260000001]]></mch_billno>
    <mch_id><![CDATA[1111111]]></mch_id>
    <nonce_str><![CDATA[c3dad56d4bc74f9d910a6210edaddd0a]]></nonce_str>
    <re_openid><![CDATA[1111111]]></re_openid>
    <remark><![CDATA[无]]></remark>
    <send_name><![CDATA[33333]]></send_name>
    <sign><![CDATA[426a84083eadadc67398a7918490adeb]]></sign>
    <total_amount><![CDATA[100]]></total_amount>
    <total_num><![CDATA[1]]></total_num>
    <wishing><![CDATA[恭喜发财]]></wishing>
    <wxappid><![CDATA[1111111]]></wxappid>
</xml>