遇到的坑要写下来。

企业微信接口定义:

企业微信AP地址:https://work.weixin.qq.com/api/doc/90000/90135/90275

请求方式:POST(HTTPS

请求地址:https://api.mch.weixin.qq.com/mmpaymkttransfers/sendworkwxredpack

是否需要证书:是(注:这里的是否需要证书指的是在post请求中需要携带证书,证书最好按照企业微信的建议放在一个有读取权限的位置)

数据格式:xml

证书使用详见:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=4_3

请求示例:

<xml>
    <nonce_str>5K8264ILTKCH16CQ2502SI8ZNMTM67VS</nonce_str>
    <sign>C380BEC2BFD727A4B6845133519F3AD6</sign>
    <mch_billno>123456</mch_billno>
    <mch_id>10000098</mch_id>
    <wxappid>wx8888888888888888</wxappid>
    <sender_name>XX活动</sender_name>
    <sender_header_media_id>1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0</sender_header_media_id>
    <re_openid>oxTWIuGaIt6gTKsQRLau2M0yL16E</re_openid>
    <total_amount>1000</total_amount>
    <wishing>感谢您参加猜灯谜活动,祝您元宵节快乐!</wishing>
    <act_name>猜灯谜抢红包活动</act_name>
    <remark>猜越多得越多,快来抢!</remark>
    <workwx_sign>99BCDAFF065A4B95628E3DB468A874A8</workwx_sign>
</xml>

参数说明:

字段名

字段

必填

示例值

类型

说明

随机字符串

nonce_str


5K8264ILTKCH16CQ2502SI8ZNMTM67VS

String(32)

随机字符串,不长于32位

微信支付签名

sign


C380BEC2BFD727A4B6845133519F3AD6

String(32)

参见“签名算法

商户订单号

mch_billno


123456

String(28)

商户订单号(每个订单号必须唯一。取值范围:0~9,a~z,A~Z).接口根据商户订单号支持重入,如出现超时可再调用。组成参考:mch_id+yyyymmdd+10位一天内不能重复的数字

商户号

mch_id


10000098

String(32)

微信支付分配的商户号

公众账号appid

wxappid


wx8888888888888888

String(32)

微信分配的公众账号ID(企业微信corpid即为此appId)。接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。

发送者名称

sender_name


XX活动

String(128)

以个人名义发红包,红包发送者名称(需要utf-8格式)。与agentid互斥,二者只能填一个。

发送红包的应用id

agentid


1

unsigned int

以企业应用的名义发红包,企业应用id,整型,可在企业微信管理端应用的设置页面查看。与sender_name互斥,二者只能填一个。

发送者头像

sender_header_media_id


1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0

String(128)

发送者头像素材id,通过企业微信开放上传素材接口获取

用户openid

re_openid


oxTWIuGaIt6gTKsQRLau2M0yL16E

String(32)

接受红包的用户.用户在wxappid下的openid。获取用户openid参见:http://work.weixin.qq.com/api/doc#11279

金额

total_amount


1000

int

金额,单位分,单笔最小金额默认为1元

红包祝福语

wishing


感谢您参加猜灯谜活动,祝您元宵节快乐!

String(128)

红包祝福语

项目名称

act_name


猜灯谜抢红包活动

String(32)

项目名称

备注

remark


猜越多得越多,快来抢!

String(256)

备注信息

场景

scene_id


PRODUCT_1

String(32)

发放红包使用场景,红包金额大于200或者小于1元时必传

PRODUCT_1:商品促销

PRODUCT_2:抽奖

PRODUCT_3:虚拟物品兑奖

PRODUCT_4:企业内部福利

PRODUCT_5:渠道分润

PRODUCT_6:保险回馈

PRODUCT_7:彩票派奖

PRODUCT_8:税务刮奖


企业微信签名

workwx_sign


企业微信签名

String(32)

参见“签名算法”

 代码实现:

   单元测试主类:

package com.cth.src;

import com.cth.src.work.request.EmployeeGetOpenIdRequest;
import com.cth.src.work.response.EmployeeGetOpenIdResponse;
import com.cth.src.work.response.MediaUploadResponse;
import com.cth.src.work.utils.IamMultipartFile;
import com.cth.src.work.utils.ScrmStrinUtils;
import com.cth.src.work.weComService.EmployeeService;
import com.cth.src.work.weComService.MediaService;
import com.cth.src.work.weComService.SendRedPacketService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author chengtonghua
 * @date 2020-11-17
 */
@SpringBootTest(classes = TopKApplication.class)
@Slf4j
public class PayTest {
    @Autowired
    private SendRedPacketService sendRedPacketService;

    @Test
    public void sendworkwxredpack() {
        //构建企业微信签名
        try {
            Map<String, String> paramsMap = new HashMap<>();
            //项目名称
            paramsMap.put("act_name", "猜灯谜抢红包活动");
            //商户订单号
            paramsMap.put("mch_billno", UUID.randomUUID().toString().replace("-", "").substring(0,28));
            //商户号微信商户平台获取
            paramsMap.put("mch_id", "");
            //随机字符串
            paramsMap.put("nonce_str", ScrmStrinUtils.generateNonceStr());
            //用户openid(根据应用类型,调用企业微信接口使用客户userId换取客户的openId)
            paramsMap.put("re_openid", "");
            //金额 单位分,单笔最小金额默认为1元
            paramsMap.put("total_amount", "1");
            //公众账号appid  天坑 此处一定要配置微信商户平台的appID
            paramsMap.put("wxappid", "你的微信商户平添appId");
            //放这的原因是以上字段为企业微信签名固定字段
            //此处的secret是 企业微信应用中的企业支付应用的Secret(文档并没有阐明,坑)
            String secret = "企业微信应用中的企业支付应用的Secret";
            String workwxSign = ScrmStrinUtils.generateSignature(paramsMap,secret, "qywx");
            //企业微信签名
            paramsMap.put("workwx_sign", workwxSign);
            //发送者名称
//           paramsMap.put("sender_name", "ss");
            //企业微信中发送红包的应用ID
            paramsMap.put("agentid", "应用ID");
            //发送者头像((需要调用素材库上传素材接口)上传用户头衔到临时素材库中换取头像的素材ID)
//            paramsMap.put("sender_header_media_id", "素材Id");
            //场景 --发放红包使用场景,红包金额大于200或者小于1元时必传
            paramsMap.put("scene_id", "PRODUCT_1");
            //红包祝福语
            paramsMap.put("wishing", "测试通过");
            //备注
            paramsMap.put("remark", "");
            //构建微信支付签名 
            String key = "此处的key为微信商户平台的api密钥";
            String sign = ScrmStrinUtils.generateSignature(paramsMap, key, "wx");
            //微信支付签名 需要将企业微信签名也包含进来
            paramsMap.put("sign", sign);
            String play = sendRedPacketService.sendworkwxredpack(ScrmStrinUtils.mapToXml(paramsMap));
            //注意 ,此处只是简单拿到企业微信返回值而已,如果要使用在生产环境一定要校验企业微信返回值内的sign进行验签操作
            System.out.println(play);
        } catch (Exception e) {
            log.info("报错信息" + e);
        }
    }

    @Test
    public void queryworkwxredpack() {
        //构建企业微信签名
        try {
            Map<String, String> paramsMap = new HashMap<>();
            //商户订单号
            paramsMap.put("mch_billno", "");
            //商户号
            paramsMap.put("mch_id", "");
            //随机字符串
            paramsMap.put("nonce_str", ScrmStrinUtils.generateNonceStr());
            //公众账号appid
            paramsMap.put("appid", "");
           //构建微信支付签名 
            String key = "此处的key为微信商户平台的api密钥";
            String sign = ScrmStrinUtils.generateSignature(paramsMap, key, "wx");
            //微信支付签名
            paramsMap.put("sign", sign);
            String play = sendRedPacketService.queryworkwxredpack(ScrmStrinUtils.mapToXml(paramsMap));
            System.out.println(play);
        } catch (Exception e) {
            log.info("报错信息" + e);
        }
    }
}

SendRedPacketService:

package com.cth.src.work.weComService;

import com.cth.src.work.config.WebbankConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * @author chengtonghua
 * @date 2020-11-18
 *此处是思想是将企业微信作为一个的单独微服务,使用Feign的方式进行调用。
 * WebbankConfiguration 作为发送红包接口的一个config配置,里面在请求中注入了企业微信请求需要的证书信息
 */
@FeignClient(name = "weComService", url = "https://api.mch.weixin.qq.com/mmpaymkttransfers", contextId = "sendService",configuration = WebbankConfiguration.class)
public interface SendRedPacketService {

    /**
     * 调用企业微信红包接口
     * @param play
     * @return
     */
    @PostMapping(value = "/sendworkwxredpack", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
    String sendworkwxredpack(@RequestBody String play);

    /**
     * 查询红包记录
     * @param play
     * @return
     */
    @PostMapping(value = "/queryworkwxredpack", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
    String queryworkwxredpack(@RequestBody String play);
}

ScrmStrinUtils:

package com.cth.src.work.utils;

import org.w3c.dom.Document;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 * @author chengtonghua
 * @date 2020-11-17
 */
public class ScrmStrinUtils {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();

    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        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);
    }


    /**
     * 生成签名
     * @param data 待签名数据
     * @param key API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key,String type) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals("sign")) {
                continue;
            }
            if (data.get(k).trim().length() > 0) {
                // 参数值为空,则不参与签名
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
            }
        }
        if("wx".equals(type)){
            sb.append("key=").append(key);
        }else{
            sb.append("secret=").append(key);
        }
        String sign;
        try {
            sign = MD5(sb.toString()).toUpperCase();
        }catch (Exception e){
            throw new Exception(String.format("Invalid sign_type: %s", e));
        }
        return sign;
    }

    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        Document document = newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        }
        catch (Exception ex) {
        }
        return output;
    }

    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);

        return documentBuilderFactory.newDocumentBuilder();
    }

    public static Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }
}

WebbankConfiguration:

package com.cth.src.work.config;

import feign.Client;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.ssl.SSLContexts;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.FileInputStream;
import java.security.*;


@Configuration
@Slf4j
public class WebbankConfiguration {

    @Bean
    public Client feignClient() throws Exception {
        Client trustSSLSockets = new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
        log.info("feignClient called");
        return trustSSLSockets;
    }


    //增加SSL
    public static SSLSocketFactory getSSLSocketFactory() throws Exception {
        //TODO Exception 需要抛出异常
        //商户id,证书的默认密码
        String mchId = "";
        KeyStore keyStore  = KeyStore.getInstance("PKCS12");
       //本地证书
        ClassPathResource resource = new ClassPathResource("微信支付需要的证书路径");
        FileInputStream instream = new FileInputStream(resource.getFile());
        keyStore.load(instream, mchId.toCharArray());
        SSLContext sslContext = null;
        try {
            sslContext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        }
        return sslContext.getSocketFactory();
    }
}

常见错误处理:

  1. 签名错误,这个错误最经常出现,但是呢情况很多所以不容易排查。这个时候就需要按照官方文档内的要求核对参数的顺序以及数量。
  1. 企业微信的签名一定要在微信支付签名之前执行,因为企业微信签名需要参与到微信支付的签名中去。(api地址:https://work.weixin.qq.com/api/doc/90000/90135/90281)
  2. wxappid是微信商户平台的appID
  3. 企业微信签名所使用的Secret是来源于企业微信应用中的企业支付应用的Secret
  4. 微信支付签名中使用的key为微信商户平台api设置中的key值
  5. 商户平台的证书和密码是否正确
  1. 签名的处理
  1. 千万不要删除或者增加字段,否则会报签名失败
企业微信签名字段说明:
发红包api固定如下几个字段参与签名:
act_name
mch_billno
mch_id
nonce_str
re_openid
total_amount
wxappid

付款api固定如下几个字段参与签名:
amount
appid
desc
mch_id
nonce_str
openid
partner_trade_no
ww_msg_type
  1. 关键参数来源截图
  1. 企业微信参与签名的应用secret和应用ID

 

         3. 微信商户平台的appid

              登录商户平台后->产品中心->AppID账号管理中即可看到。

        4. 微信支付key以及证书(需要注意的是:key一旦获取后需要妥善保存)

                      

java发送企业微信群 企业微信发送接口_java

补充一个比较狗的但是能在企业微信外部联系人群发送红包的方法:就是创建了一个外部联系人群之后,你把自己的微信账号也拉入群中,这样你自己就可以在微信客户端发送客户群红包了,产生的消费可以自己找公司财务报销。