工具类-企业微信自助QA机器人接入

  1. 需求背景

在企业当中,经常会被多次问到相同的问题,而我们都有自己的其他需求需要做,于是为了提高办公效率,缩短沟通成本,我们可以在企业微信里面拉一个群,创建一个机器人,通过@企业机器人自助问答的方式获取想要的结果;
2. 相关文档整理

添加机器人
https://developer.work.weixin.qq.com/document/path/91770
如何接受群里面艾特机器人后输入的聊天内容https://developer.work.weixin.qq.com/document/path/90930
如何发送信息到群群
https://developer.work.weixin.qq.com/document/path/91770
加解密相关
https://developer.work.weixin.qq.com/document/path/90968
https://developer.work.weixin.qq.com/document/path/90468

获取用户相关(企微机器人只能通过聊天信息拿到userid,如果需要通过userid获取用户信息,需要申请企微应用然后调用企微通讯录相关api来获取,企微应用可以在流程中心进行企微应用申请,然后联系it的同学获取更多信息)

申请企业微信应用以及应用的一些基本概念
https://developer.work.weixin.qq.com/document/path/90665

获取用户信息(注意因为保密原因企微通讯录拿不到用户email,你还需要和sso团队去拿到相关api来进行用户信息获取)
https://developer.work.weixin.qq.com/document/path/90196

提示
    接受企业微信的聊天内容需要一个公网可达的接收点
    加解密中有一个corpid参数是企业微信中识别企业的id,这个可以联系it的同学获取
    企业微信文档那是非常差,需要多阅读和动手尝试,有问题可以在企业微信的社区提交问题咨询
    多阅读企业微信的api文档
  1. 接入流程演示
    3.1 拉群,添加机器人

在这里插入图片描述
3.2 给机器人取个名字

在这里插入图片描述
3.3 点击配置说明

在这里插入图片描述
3.4 配置 接收消息配置 信息

在这里插入图片描述

第一栏URL 必须是外网可以访问的,且该接口已经在线上可以直接访问,所以配置该信息之前,你的线上环境已经有相关接口了
第二栏的 Token 随机生成即可,后续需要配置在代码里面
第三栏 和 第二栏一样,随机生成即可,后续需要配置在代码里面

讲一下流程:如上图所示,当点击保存的时候,机器人后台会去check一下输入的url,这个url会承担两部分功能,第一个是check一下url是否可用,第二个是当有人在群里@机器人时,会回调该接口,因此下面会出现两个相同url接口但是不同的接收参数形式:
4. 代码演示

Controller层:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import static org.springframework.util.MimeTypeUtils.APPLICATION_XML_VALUE;


@RestController
@RequestMapping(value = "/chat")
@Slf4j
public class ChatRobotController {

    @Autowired
    private ChatRobotService chatRobotService;


    /**
     * 支持Http Get请求验证URL有效性
     *
     * @param msgSignature 企业微信加密签名,msg_signature计算结合了企业填写的token、请求中的timestamp、nonce、加密的消息体
     * @param timestamp    时间戳。与nonce结合使用,用于防止请求重放攻击
     * @param nonce        随机数。与timestamp结合使用,用于防止请求重放攻击
     * @param echoStr      加密的字符串。需要解密得到消息内容明文,解密后有random、msg_len、msg、receiveid四个字段,其中msg即为消息内容明文
     * @return 回调服务需要作出正确的响应
     */
    @GetMapping(value = "/notify")
    public String verifyUrl(@RequestParam("msg_signature") String msgSignature,
                            @RequestParam("timestamp") Integer timestamp,
                            @RequestParam("nonce") String nonce,
                            @RequestParam("echostr") String echoStr) {
        log.info("verifyUrl--> msgSignature:{},timestamp:{},nonce:{},echoStr:{}", msgSignature, timestamp, nonce, echoStr);
        return chatRobotService.verifyUrl(msgSignature, timestamp, nonce, echoStr);
    }


    /**
     * 支持Http Post请求接收业务数据
     * 当用户触发回调行为时,企业微信会发送回调消息到填写的URL,请求内容
     *
     * @param msgSignature 企业微信加密签名,msg_signature结合了企业填写的token、请求中的timestamp、nonce参数、加密的消息体
     * @param timestamp    时间戳。与nonce结合使用,用于防止请求重放攻击
     * @param nonce        随机数。与timestamp结合使用,用于防止请求重放攻击。
     * @param dto          报文数据
     */
    @PostMapping(value = "/notify", consumes = APPLICATION_XML_VALUE)
    public void callBack(@RequestParam("msg_signature") String msgSignature,
                         @RequestParam("timestamp") Integer timestamp,
                         @RequestParam("nonce") String nonce,
                         @RequestBody ChatRobotCallBackDTO dto) {
        log.info("callBack-->msgSignature:{},timestamp:{},nonce:{},dto:{}", msgSignature, timestamp, nonce, JsonUtil.toJSON(dto));
        chatRobotService.callBack(msgSignature, timestamp, nonce, dto);
    }
}


Service层:

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import static cn.hutool.core.util.XmlUtil.readXML;

/**
 * 企微机器人
 *
 * @author wql
 * @date 2023/4/23 10:19
 */
@Service
@Slf4j
public class ChatRobotServiceImpl implements ChatRobotService {

    private final static String TOKEN = "iLOUyBRIN6CWScZtPqHF15";


    @Override
    public String verifyUrl(String msgSignature, Integer timestamp, String nonce, String echoStr) {
        String signature = getSignature(TOKEN, String.valueOf(timestamp), nonce, echoStr);
        if (!Objects.equals(signature, msgSignature)) {
            throw new VerifyException(VerifyException.GET_SIGNATURE_ERROR);
        }
        return getVerifyUrlContent(getOriginByte(echoStr));
    }


    /**
     * <xml>
     *     <From>
     *         <UserId>
     *             <![CDATA[wo-ApVEAAANKrX-iuxI9zy34Enju8YeQ]]>
     *         </UserId>
     *     </From>
     *     <WebhookUrl>
     *         <![CDATA[https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=6132293c-dfcd-4b0e-a79c-b0009362e51e]]>
     *     </WebhookUrl>
     *     <ChatId>
     *         <![CDATA[wr-ApVEAAAXa_Vwj9LjYp18C1jB_SPbA]]>
     *     </ChatId>
     *     <MsgId>
     *         <![CDATA[CIGABBDxnJOiBhjl1aHskICAAyCQAQ==]]>
     *     </MsgId>
     *     <ChatType>
     *         <![CDATA[group]]>
     *     </ChatType>
     *     <MsgType>
     *         <![CDATA[text]]>
     *     </MsgType>
     *     <Text>
     *         <Content>
     *             <![CDATA[@TestRobot 1234567]]>
     *         </Content>
     *     </Text>
     * </xml>
     */
    @Override
    public void callBack(String msgSignature, Integer timestamp, String nonce, ChatRobotCallBackDTO dto) {
        //content 报文如上所示
        String content = this.verifyUrl(msgSignature, timestamp, nonce, dto.getEncrypt());
        if (StrUtil.isNotBlank(content)) {
            String text = readXML(content).getElementsByTagName("Text").item(0).getFirstChild().getTextContent();
            if (StrUtil.isNotBlank(text)) {
                List<String> textItems = Arrays.asList(text.split(" "));
                if (CollUtil.isNotEmpty(textItems)) {
                    //0 是机器人, 1是输入的文本
                    String queryText = textItems.get(1);
                    //TODO 查询数据源并回答
                    List<String> resultList = Lists.newArrayList();
                    resultList.add("我叫李四");
                    resultList.add("[这是一个链接](http://work.weixin.qq.com/api/doc)");
                    WeChatBotUtils.sendMarkDown(queryText, resultList);
                }
            }
        }
    }


    public static void main(String[] args) {
        List<String> resultList = Lists.newArrayList();
        resultList.add("我叫李四");
        resultList.add("[这是一个链接](http://work.weixin.qq.com/api/doc)");
        WeChatBotUtils.sendMarkDown("我是张三", resultList);
    }
}

工具类:

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;


@Slf4j
public class WeChatBotUtils {
    private final static String ROBOT_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=6132293c-dfcd-4b0e" +
            "-a79c-b00e";

    /**
     * 发送文字消息
     * <p>
     * {
     * "msgtype": "text",
     * "text": {
     * "content": "广州今日天气:29度,大部分多云,降雨概率:60%",
     * "mentioned_list":["wangng","@all"],
     * "mentioned_mobile_list":["13800001111","@all"]
     * }
     * }
     *
     * @param msg 需要发送的消息
     */
    public static String sendTextMsg(String msg) {
        JSONObject text = new JSONObject();
        text.put("content", msg);
//        ArrayList<String> users = Lists.newArrayList();
//        users.add("@all");
//        text.put("mentioned_list", users);
        JSONObject reqBody = new JSONObject();
        //本内容,最长不超过2048个字节,必须是utf8编码
        reqBody.put("text", text);
        //消息类型,此时固定为text
        reqBody.put("msgtype", "text");
        reqBody.put("safe", 0);

        return callWeChatBot(reqBody.toString());
    }

    /**
     * 发送图片消息,需要对图片进行base64编码并计算图片的md5值
     *
     * @param path 需要发送的图片路径
     */
    public static String sendImgMsg(String path) throws Exception {

        String base64 = "";
        String md5 = "";
        // 获取Base64编码
        try {
            FileInputStream inputStream = new FileInputStream(path);
            byte[] bs = new byte[inputStream.available()];
            inputStream.read(bs);
            base64 = Base64.getEncoder().encodeToString(bs);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 获取md5值
        try {
            FileInputStream inputStream = new FileInputStream(path);
            byte[] buf = new byte[inputStream.available()];
            inputStream.read(buf);
            md5 = DigestUtils.md5Hex(buf);
        } catch (IOException e) {
            e.printStackTrace();
        }

        JSONObject image = new JSONObject();
        image.put("base64", base64);
        image.put("md5", md5);
        JSONObject reqBody = new JSONObject();
        reqBody.put("msgtype", "image");
        reqBody.put("image", image);
        reqBody.put("safe", 0);

        return callWeChatBot(reqBody.toString());
    }

    /**
     * 发送MarKDown消息
     *
     * @param question 需要发送的消息
     */
    public static String sendMarkDown(String question, List<String> resultList) {
         JSONObject markdown = new JSONObject();
        String res = "您输入的关键字:\t<font color =\"warning\">" + question + "</font>\t机器人从文档库匹配出如下文档";
        if (CollUtil.isEmpty(resultList)) {
            res = res + "\n\t无法通过输入的关键词匹配到相关文档";
        }else {
            for (int i = 1; i <= resultList.size(); i++) {
                res = res + "\n\t" + i + ".\t" + resultList.get(i - 1);
            }
        }
        markdown.put("content", res);
        JSONObject reqBody = new JSONObject();
        reqBody.put("msgtype", "markdown");
        reqBody.put("markdown", markdown);
        reqBody.put("safe", 0);
        return callWeChatBot(reqBody.toString());
    }


    /**
     * 调用群机器人
     */
    public static String callWeChatBot(String reqBody) {
        try {
            // 构造RequestBody对象,用来携带要提交的数据;需要指定MediaType,用于描述请求/响应 body 的内容类型
            MediaType contentType = MediaType.parse("application/json; charset=utf-8");
            RequestBody body = RequestBody.create(contentType, reqBody);
            // 调用群机器人
            String respMsg = okHttp(body, ROBOT_URL);
            if (StrUtil.isNotBlank(respMsg)) {
                int errCode = (Integer) getParamsFromJson(respMsg, "errcode");
                if (errCode == 0) {
                    log.info("向群发送消息成功!");
                } else {
                    log.info("向群发送消息失败!,错误信息为:{}", JSON.toJSONString(respMsg));
                }
            }
            return respMsg;
        } catch (Exception e) {
            log.error("群机器人推送消息失败:{}", ErrorUtil.getErrorStackTraceMsg(e));
        }
        return null;
    }


    private static Object getParamsFromJson(String json, String sourceKeyName) {
        Object object = null;
        try {
            object = JsonPath.read(json, "$." + sourceKeyName);
        } catch (PathNotFoundException e) {
            log.error(ErrorUtil.getErrorStackTraceMsg(e));
        }
        return object;
    }


    public static String okHttp(RequestBody body, String url) throws Exception {
        // 构造和配置OkHttpClient
        OkHttpClient client;
        client = new OkHttpClient.Builder()
                // 设置连接超时时间
                .connectTimeout(10, TimeUnit.SECONDS)
                // 设置读取超时时间
                .readTimeout(20, TimeUnit.SECONDS)
                .build();
        // 构造Request对象
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                // 响应消息不缓存
                .addHeader("cache-control", "no-cache")
                .build();

        // 构建Call对象,通过Call对象的execute()方法提交异步请求
        Response response = null;
        try {
            response = client.newCall(request).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 请求结果处理
        assert response != null;
        assert response.body() != null;
        byte[] datas = response.body().bytes();
        return new String(datas);
    }

}


ChatRobotUtil


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;

/**
 * 签名工具类
 *
 */
@Slf4j
public class ChatRobotUtil {

    private final static Charset CHARSET = StandardCharsets.UTF_8;


    /**
     * 用SHA1算法生成安全签名
     *
     * @param token     票据
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @param encrypt   密文
     * @return 安全签名
     */
    public static String getSignature(String token, String timestamp, String nonce, String encrypt) {
        try {
            String[] array = new String[]{token, timestamp, nonce, encrypt};
            StringBuilder sb = new StringBuilder();
            // 字符串排序
            Arrays.sort(array);
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuilder hexstr = new StringBuilder();
            String shaHex = "";
            for (byte b : digest) {
                shaHex = Integer.toHexString(b & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            log.error(ErrorUtil.getErrorStackTraceMsg(e));
            throw new VerifyException(VerifyException.GET_SIGNATURE_ERROR);
        }
    }


    public static byte[] getOriginByte(String echoStr) {
        String encodingAesKey = ApolloUtil.getKey("robot.encodingAesKey", "4pGyYEdb0qfvLN1yyNhNvLzDcLaSLMlBCGo9Q5ixW8w");
        byte[] aesKey = Base64.decodeBase64(encodingAesKey + "=");
        byte[] original;
        try {
            // 设置解密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            // 使用BASE64对密文进行解码
            byte[] encrypted = Base64.decodeBase64(echoStr);
            // 解密
            original = cipher.doFinal(encrypted);
        } catch (Exception e) {
            log.error(ErrorUtil.getErrorStackTraceMsg(e));
            throw new VerifyException(VerifyException.PARSER_ECHO_STR_ERROR);
        }
        return original;
    }


    public static String getVerifyUrlContent(byte[] original) {
        String xmlContent;
        try {
            byte[] bytes = decode(original);
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
            int xmlLength = recoverNetworkBytesOrder(networkOrder);
            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
        } catch (Exception e) {
            log.error(ErrorUtil.getErrorStackTraceMsg(e));
            throw new VerifyException(VerifyException.XML_CONTENT_ERROR);
        }

        return xmlContent;
    }

    private static int recoverNetworkBytesOrder(byte[] orderBytes) {
        int sourceNumber = 0;
        for (int i = 0; i < 4; i++) {
            sourceNumber <<= 8;
            sourceNumber |= orderBytes[i] & 0xff;
        }
        return sourceNumber;
    }


    private static byte[] decode(byte[] decrypted) {
        int pad = (int) decrypted[decrypted.length - 1];
        if (pad < 1 || pad > 32) {
            pad = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
    }
}


errorUtil:

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

public class ErrorUtil {
    public ErrorUtil() {
    }

    public static String getErrorStackTraceMsg(Throwable error) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        error.printStackTrace(new PrintStream(baos));
        return baos.toString();
    }
}


DTO:

import lombok.Data;

import java.io.Serializable;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;


/**
 * 当用户触发回调行为时,企业微信会发送回调消息到填写的URL,请求内容
 *
 * @author wangqinglong01
 */
@Data
public class ChatRobotCallBackDTO implements Serializable {
    private static final long serialVersionUID = -2513785018658444032L;

    /**
     * 企业微信的CorpID,当为第三方应用回调事件时,CorpID的内容为suiteid
     */
    @JacksonXmlProperty(localName = "ToUserName")
    private String toUserName;

    /**
     * 接收的应用id,可在应用的设置页面获取。仅应用相关的回调会带该字段。
     */
    @JacksonXmlProperty(localName = "AgentID")
    private String agentId;

    /**
     * 消息结构体加密后的字符串
     */
    @JacksonXmlProperty(localName = "Encrypt")
    private String encrypt;

}

需要引入的依赖

compile(‘com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8’)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值