钉钉自定义机器人简单使用

960 阅读14分钟

钉钉自定义机器人简单使用

前言:

年前公司的需求里面有用到钉钉机器人,使用之后发现真的非常简单,不得不感叹阿里的牛逼,这篇文章总结了一下个人使用钉钉机器人的经验,同时介绍个人据此构建一个工具类来方便后续直接“开箱即用”,希望对于读者有所启发。

文章目的:

  1. 简单的说明一下钉钉自定义机器人使用,注意是自定义机器人
  2. 说明一下个人针对钉钉机器人设计了一个工具类,说明一下设计的思路。(重点)
  3. 汇总一些个人使用钉钉机器人的小坑,同时提供解决办法希望读者参考可以解决问题

钉钉文档:

机器人的使用还是非常简单的,直接参考文档就可以进行构建,如果了解过这一部分可以直接跳到编写工具类的部分进行文章的后续阅读。

developers.dingtalk.com/document/ap…

由于钉钉的官方文档更新较为频繁,这里的连接可能在以后会失效

如何创建一个机器人

文档里面介绍的比较详细了,我们根据文档的内容进行实战一下即可。这里使用了 新手体验群 创建的机器人进行实验。下面的内容包括创建自定义机器人以及测试机器人如何使用。

创建一个自定义机器人

随意点击一个机器人,右击菜单,出现“更多机器人”,进入到界面

点击“更多机器人”

选择钉钉的自定义机器人进行使用:

这里还有很多其他的机器人,如果感兴趣可以查看钉钉的文档进行更多的了解

在下面的界面选择添加:

到达下一个界面,根据指示需要填写如下的内容:

  • 机器人的名称:自己取一个合适的名字,自己喜欢就行
  • 添加到群组:关键的一步,意味着你的机器人要添加到哪一个具体的群组里面进行使用。也意味着只有在这个群组里面的人才可以收到对应的通知。

下面说明一下安全设置的内容:

  • 自定义关键词:关键配置,这里自定义关键词可以按照自己的喜好进行设置。但是一旦设置在发送请求的时候必须要携带关键词,请求才会生效,否则会返回对应的错误码31000和对应的错误信息。
  • 加签:建议勾上,这里加签可以在请求中更好的保护接口,同时注意一下加上签名之后要复制一下内容
  • IP地址(段):这里个人没有进行过测试,所以没有进行勾选,正式的生产环境建议使用IP限制,保证万无一失

这里建议保存一下前面和关键字,当然忘记了也可以在构建完成之后从设置里面查看:

签名:SECf075e3890b7d79ca645e51b42644fc57c2402577d5a955bce51cb980cec0a3b6

关键词:新人

至此,我们成功创建了一个钉钉的自定义机器人,整个过程十分简单,这里记得保存一下对应的信息:

https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050

上面为个人的配置。发文的时候此机器人已经删除,所以读者自己实验即可。

测试机器人是否可以正常使用

通过上面的步骤,我们已经构建了一个基本的机器人为我们使用,再进行下一步之前,我们需要验证一下钉钉机器人是否可以正常使用。这里针对不同的平台说下比较简单快捷的验证方法。

windows 验证方式:

windows 推荐使用git的一个shell命令框进行测试,因为windows 本身是没有curl这个命令的,当然也有其他的办法,但是为了图省事直接使用git给我们开发的一个小工具即可。

如下图所示,我们选择Git Bash Here,打开命令行的界面

我们根据上一步的机器人配置,构建一个CURL请求进行测试:

curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613211530113&secret=SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text","text": {"content": "新人内容测试"}}'

不出所料,这里按照官方文档给的方式验证失败了,这是为什么呢?原因有几个:

  • 加签密文:我们设置了加签,所以在请求参数里面要加入对应的签名密文,也就是在添加这一步勾选了签名这一步。

  • 时间戳:请求需要传递时间戳,但是我们没有在请求参数里面附带时间戳,同时时间戳必须在系统时间的一小时之内,超过这个时间即使请求参数正确也无法通过

timestamp = 1613212103494 sign = MO79EJ58O9lmuQJo1dB1KGMhkZI%2BM5KkyD0NYuNe8%2B8%3D

我们根据上面的说明修复一下,注意在URL增加了两个参数:

curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text","text": {"content": "新人为什么你这么牛逼"}}'

关于这一部分内容,已经汇总到“问题汇总”这一部分,如果还是感到迷惑可以参考。

我们再次验证一下,发现依然失败,比较奇怪,个人设置的关键字在请求content里面却失败了:

zhaoxudong@LAPTOP-MEUFMP1M MINGW64 /d/Users/zhaoxudong/Desktop
$ curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \
>    -H 'Content-Type: application/json' \
>    -d '{"msgtype": "text","text": {"content": "新人为什么你这么牛逼"}}'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   178  100   115  100    63    991    543 --:--:-- --:--:-- --:--:--  1534
{"errcode":310000,"errmsg":"keywords not in content, more: [https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq]"}

排查问题之后发现由于windows系统默认使用了gb2312的编码,所以我们此时需要切换一下系统的编码,为了证明是系统编码的问题,我们先验证一下编码:

打开window的cmd窗口,我们输入chcp命令进入到具体的页面,可以看到下面936,百度一下发现就是GB2312,在请求发送的过程中被转码导致乱码。

C:\Users\zhaoxudong>chcp
活动代码页: 936

解决办法也比较简单,改一下整改系统的编码即可,关于设置的方法:blog.csdn.net/robinhunan/…

插曲:个人在设置过后,因为编码的问题导致编辑器无法编译,经过核实发现是由于文件夹的编码乱码找不到类的问题,所以这里建议放置Java项目的时候放置到全英文的目录。所以更推荐linux的方式,可以省去很多麻烦

linux 验证方式:

linux 验证比较简单,而且出问题的概率比较小,根据window内容得知最后需要三个参数才能请求成功,这里直接给出一个相似的CURL请求作为案例说明:

curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text","text": {"content": "新人为什么你这么牛逼"}}'

我们把这个请求放到linux命令行里面进行运行,如果errorcode返回0,说明请求成功:

{"errcode":0,"errmsg":"ok"}

请求成功之后,我们可以看到对应的结果:

注意一下钉钉机器人不能请求过于频繁。建议限制一下每分钟的请求QPS

编写工具类

从上一节可以看到,整个钉钉机器人的构建还是十分简单的。但是使用起来不是特别的方便,个人之前有使用钉钉做过一个预警的小需求,为了后续可以直接开箱即用,自己构建了工具类,下面的部分主要说个人的工具类的设计以及个人的构建思路

个人水平有限,工具类还有很大的改进空间,但是对于我来说暂时没有遇到使用的瓶颈。

工具类的代码地址

这里个人的小工具类整合到了个人小项目里面,想要参考的可以直接进行下载,下面的文章代码也是来源于这个项目里面。

具体请查看:com.zxd.interview.dingrobot这个包

具体的代码地址:gitee.com/lazyTimes/i…

构建工具类的思路

把整个请求的流程需要的组件分为了以下的几个部分:

构建基本的请求环境:也就是需要的请求地址,请求签名或者关键字等参数,这些参数都是必须的,否则请求无法正常运行,所以我们提出来作为环境使用。

构建请求参数:由于钉钉支持非常多的msgtype也就是文本类型,个人参考了一下SDK,对应构建了一个请求的参数类,为了方便扩展,设计了一个接口进行后续的扩展和兼容。

使用JAVA代码发送请求:本着最小依赖的原则,使用最常见的HttpClient进行模拟JAVA的请求发送。但是在这个基础上做了一点点的封装,方便后续扩展

  1. HttpClient的封装,将请求所需要的一些请求参数封装到一个配置对象进行管理
  2. 请求方法的封装,这里用了一个对象进行封装,也可以直接使用Spring封装的org.springframework.web.bind.annotation.RequestMethod或者直接使用枚举构建常量即可。
  3. 构建钉钉请求工具类:最后我们整合上面所有步骤构建一个核心请求工具类,通过环境参数构建请求URL和一些Header设置,以及构建不同的请求方法发送请求,调用HttpClient工具类进行请求发送,以及发送之后转化为结果对象等一系列操作均由该工具类完成,是本次工具类最核心的类。
  4. 构建钉钉的请求Msg:该对象包含了请求所支持的所有JSON参数格式对应的实体对象,根据参数格式构建对应的对象,个人利用内部类全部封装到一个对象里面,方便客户端理解调用。

返回请求结果:包含了错误码,错误信息,以及其他的参数等,也可以修改为直接返回字符串,由客户端决定如何处理

请求之后返回结果:将上面的错误码或者错误信息等封装为一个简单对象进行返回,同样如果不喜欢也可以改为返回字符串的结果。

单元测试

在介绍正式的结果之前,我们看下结果,下面是效果截图,包含了钉钉文档里面的所有类型,包含了目前钉钉文档支持的几种主要的类型:

测试结果1

测试截图2

下面为单元测试的代码,整个单元测试测试各种不同请求类型,调用工具包发送请求:

注意下面的请求text里面包含了之前请求示例里面设置的关键字,没有关键字是无法请求成功的

import com.alibaba.fastjson.JSON;
import org.apache.commons.codec.binary.Base64;
import org.junit.jupiter.api.Test;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.zxd.interview.dingrobot
 * @Description : 钉钉机器人测试类
 * @Create on : 2021/2/7 11:06
 **/
public class DingRobotUtilsTest {

    /**
   	运行下面五个单元测试的结果
    */ 
    @Test
    public void testAll() {
        testText();
        testLink();
        testMarkdown();
        testActionCard();
        testFeedCard();
    }

    /**
     * 构建当前的系统时间戳
     */
    @Test
    public void generateSystemCurrentTime() throws Exception {
        long currentTimeMillis = System.currentTimeMillis();
        String secret = "SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc";
        String sign = generateSign(currentTimeMillis, secret);
        System.out.println("timestamp = " + currentTimeMillis);
        System.out.println("sign = " + sign);
    }

    /**
     * 测试link类型的请求
     */
    @Test
    public void testLink() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateLink()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试text类型
     */
    @Test
    public void testText() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateText()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**测试markdown 类型 */
    @Test
    public void testMarkdown() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateMarkdown()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

     /**测试ActionCard 类型 */
    @Test
    public void testActionCard() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateActionCard()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**测试FeedCard 类型 */
    @Test
    public void testFeedCard() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateFeed()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private DingRobotRequestBody generateFeed() {
        List<DingRobotRequestBody.FeedCard.FeedItem> list = new ArrayList<>();
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.FeedCard feedCard = new DingRobotRequestBody.FeedCard();
        DingRobotRequestBody.FeedCard.FeedItem feedItem = new DingRobotRequestBody.FeedCard.FeedItem();
        feedItem.setMessageURL("https://www.dingtalk.com/");
        feedItem.setTitle("新人时代的火车向前开");
        feedItem.setPicURL("https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png");
        list.add(feedItem);
        feedCard.setLinks(list);
        dingRobotRequestBody.setFeedCard(feedCard);
        dingRobotRequestBody.setMsgType("feedCard");
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateActionCard() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.ActionCard actionCard = new DingRobotRequestBody.ActionCard();
        actionCard.setBtnOrientation("0");
        actionCard.setSingleTitle("阅读全文");
        actionCard.setSingleURL("https://www.dingtalk.com/");
        actionCard.setText("新人![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png) \n" +
                " ### 乔布斯 20 年前想打造的苹果咖啡厅 \n" +
                " Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划");
        actionCard.setTitle("乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身");
        dingRobotRequestBody.setMsgType("actionCard");
        dingRobotRequestBody.setActionCard(actionCard);
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateMarkdown() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.MarkDown markDown = new DingRobotRequestBody.MarkDown();
        dingRobotRequestBody.setMsgType("markdown");
        markDown.setTitle("杭州天气");
        markDown.setText("新人测试 标题\n" +
                "# 一级标题\n" +
                "## 二级标题\n" +
                "### 三级标题\n" +
                "#### 四级标题\n" +
                "##### 五级标题\n" +
                "###### 六级标题\n" +
                "\n" +
                "引用\n" +
                "> A man who stands for nothing will fall for anything.\n" +
                "\n" +
                "文字加粗、斜体\n" +
                "**bold**\n" +
                "*italic*\n" +
                "\n" +
                "链接\n" +
                "[this is a link](http://name.com)\n" +
                "\n" +
                "图片\n" +
                "![](http://name.com/pic.jpg)\n" +
                "\n" +
                "无序列表\n" +
                "- item1\n" +
                "- item2\n" +
                "\n" +
                "有序列表\n" +
                "1. item1\n" +
                "2. item2");
        dingRobotRequestBody.setMarkDown(markDown);
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateText() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.Text text = new DingRobotRequestBody.Text();
        text.setContent("新人为什么这么牛逼");
        DingRobotRequestBody.At at = getnerateAt();
        dingRobotRequestBody.setMsgType("text");
        dingRobotRequestBody.setAt(at);
        dingRobotRequestBody.setText(text);
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateLink() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.Link link = new DingRobotRequestBody.Link();
        link.setMessageUrl("https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI");
        link.setPicUrl("");
        link.setTitle("时代的火车向前开");
        link.setText("新人:这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林");
        DingRobotRequestBody.At at = getnerateAt();
        dingRobotRequestBody.setMsgType("link");
        dingRobotRequestBody.setAt(at);
        dingRobotRequestBody.setLink(link);
        return dingRobotRequestBody;
    }

    /**
     * 构建at请求
     *
     * @return
     */
    private DingRobotRequestBody.At getnerateAt() {
        DingRobotRequestBody.At at = new DingRobotRequestBody.At();
        at.setAtAll(true);
        at.setAtMobiles(Arrays.asList("xxxxx", "123456789"));
        return at;
    }

    /**
     * 构建签名方法
     *
     * @param timestamp 时间戳
     * @param secret    秘钥
     * @return
     * @throws Exception
     */
    private String generateSign(Long timestamp, String secret) throws Exception {
        String stringToSign = timestamp + "\n" + secret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
    }

}

构建工具类:

下面就上面的单元测试,说明一下个人的基本设计。我们根据思路构建一个支持拿来即用的钉钉工具类。

类结构介绍:

Maven依赖:

在进行具体的代码编写之前,需要引入对应的依赖,个人秉持最小依赖的原则,使用的三方jar包仅仅为一些测试工具包和Httpclient请求工具包还有最熟悉的fastjson的工具包。

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

类结构:

类结构包含了之前设计思路里面说明的情况,包含请求类,工具类,参数封装和请求对象结构封装等。

+ DingRobotRequest.java			钉钉请求对象
+ DingRobotRequestAble.java		请求接口,允许发送钉钉请求的接口
+ DingRobotRequestBody.java 	允许发送钉钉请求的接口具体的实现类,比较重要,对接文档的钉钉对象
+ DingRobotRequestMsg.java		废弃对象,但是依然保留s
+ DingRobotResponseMsg.java		请求返回对象
+ DingRobotUtils.java			钉钉请求工具类,非常重要的一个类
+ HttpClientUtil.java			httpclient请求工具类
+ HttpConfig.java				请求参数构建类
+ HttpMethods.java				请求方法类

构建基本的请求环境

构建基本的请求环境,我们使用对象来封装所有的环境参数,并且使用建造模式构建一个建造器,使用建造来构建我们需要的环境参数,它的使用方式如下:

  • 构建请求URL
  • 构建请求accessToken
  • 构建请求msg,重点,可以通过构建对应的请求来实现发送不同的信息
DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateActionCard()).build();

具体的源代码如下,包含了几个简单的必要参数,以及一个建造器,注意对于构造器的私有化,对外只允许使用构建器进行初始化:

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.field
 * @Description : 钉钉机器人请求实体类
 * @Create on : 2021/2/5 15:40
 **/
public class DingRobotRequest {

    /**
     * 请求URL
     */
    private String url;

    /**
     * token
     */
    private String accessToken;

    /**
     * 秘钥
     */
    private String secret;

    /**
     * 请求msg
     */
    private DingRobotRequestBody msg;

    private DingRobotRequest(){

    }

    private DingRobotRequest(Builder builder) {
        this.url = builder.url;
        this.accessToken = builder.accessToken;
        this.secret = builder.secret;
        this.msg = builder.msg;
    }

    public static class Builder {

        private String url;
        private String accessToken;
        private String secret;
        private DingRobotRequestBody msg;

        public DingRobotRequest.Builder url(String url){
            this.url = url;
            return this;
        }
        public DingRobotRequest.Builder accessToken(String accessToken){
            this.accessToken = accessToken;
            return this;
        }
        public DingRobotRequest.Builder secret(String secret){
            this.secret = secret;
            return this;
        }
        public DingRobotRequest.Builder msg(DingRobotRequestBody msg){
            this.msg = msg;
            return this;
        }

        public DingRobotRequest build(){
            return new DingRobotRequest(this);
        }
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public DingRobotRequestBody getMsg() {
        return msg;
    }

    public void setMsg(DingRobotRequestBody msg) {
        this.msg = msg;
    }

    @Override
    public String toString() {
        return "DingRobotRequest{" +
                "url='" + url + '\'' +
                ", accessToken='" + accessToken + '\'' +
                ", secret='" + secret + '\'' +
                ", msg='" + msg + '\'' +
                '}';
    }
}

构建请求参数

下面是请求参数的构建案例,我们可以使用链式调用的方式构建不同的request请求:

 /**
     * 钉钉机器人的默认配置
     *
     * @param dingRobotRequest    钉钉机器人请求对象
     * @param dingRobotRequestMsg 钉钉机器人请求实体
     * @return
     */
    private static HttpConfig buildDefaultHttpConfig(DingRobotRequest dingRobotRequest, DingRobotRequestAble dingRobotRequestMsg) {
        return HttpConfig.custom().headers(defaultBasicHeader())
                .url(dingRobotRequest.getUrl())
                .encoding("UTF-8")
                .method(HttpMethods.POST)
                .json(JSON.toJSONString(dingRobotRequestMsg));
    }

从上面的案例可以看到下面对于请求配置类,构建HttpConfig请求,同样类似构建器进行对象的参数构建,我们定义了基本的请求encoding、请求header,请求方法参数,请求的context等对应的参数配置。

/**
 * 请求配置类
 *
 */
public class HttpConfig {

    private HttpConfig() {
    }

    // 传入参数特定类型
    public static final String ENTITY_STRING = "$ENTITY_STRING$";
    public static final String ENTITY_MULTIPART = "$ENTITY_MULTIPART$";

    /**
     * 获取实例
     *
     * @return
     */
    public static HttpConfig custom() {
        return new HttpConfig();
    }

    /**
     * HttpClient对象
     */
    private HttpClient client;

    /**
     * Header头信息
     */
    private Header[] headers;

    /**
     * 是否返回response的headers
     */
    private boolean isReturnRespHeaders;

    /**
     * 请求方法
     */
    private HttpMethods method = HttpMethods.GET;

    /**
     * 请求方法名称
     */
    private String methodName;

    /**
     * 用于cookie操作
     */
    private HttpContext context;

    /**
     * 传递参数
     */
    private Map<String, Object> map;

    /**
     * 以json格式作为输入参数
     */
    private String json;

    /**
     * 输入输出编码
     */
    private String encoding = Charset.defaultCharset().displayName();

    /**
     * 输入编码
     */
    private String inenc;

    /**
     * 输出编码
     */
    private String outenc;

    /**
     * 解决多线程下载时,strean被close的问题
     */
    private static final ThreadLocal<OutputStream> outs = new ThreadLocal<OutputStream>();

    /**
     * 解决多线程处理时,url被覆盖问题
     */
    private static final ThreadLocal<String> urls = new ThreadLocal<String>();

    /**
     * HttpClient对象
     */
    public HttpConfig client(HttpClient client) {
        this.client = client;
        return this;
    }

    /**
     * 资源url
     */
    public HttpConfig url(String url) {
        urls.set(url);
        return this;
    }

    /**
     * Header头信息
     */
    public HttpConfig headers(Header[] headers) {
        this.headers = headers;
        return this;
    }

    /**
     * Header头信息(是否返回response中的headers)
     */
    public HttpConfig headers(Header[] headers, boolean isReturnRespHeaders) {
        this.headers = headers;
        this.isReturnRespHeaders = isReturnRespHeaders;
        return this;
    }

    /**
     * 请求方法
     */
    public HttpConfig method(HttpMethods method) {
        this.method = method;
        return this;
    }

    /**
     * 请求方法
     */
    public HttpConfig methodName(String methodName) {
        this.methodName = methodName;
        return this;
    }

    /**
     * cookie操作相关
     */
    public HttpConfig context(HttpContext context) {
        this.context = context;
        return this;
    }

    /**
     * 传递参数
     */
    public HttpConfig map(Map<String, Object> map) {
        synchronized (getClass()) {
            if (this.map == null || map == null) {
                this.map = map;
            } else {
                this.map.putAll(map);
                ;
            }
        }
        return this;
    }

    /**
     * 以json格式字符串作为参数
     */
    public HttpConfig json(String json) {
        this.json = json;
        map = new HashMap<String, Object>();
        map.put(ENTITY_STRING, json);
        return this;
    }

    /**
     * 上传文件时用到
     */
    public HttpConfig files(String[] filePaths) {
        return files(filePaths, "file");
    }

    /**
     * 上传文件时用到
     *
     * @param filePaths 待上传文件所在路径
     */
    public HttpConfig files(String[] filePaths, String inputName) {
        return files(filePaths, inputName, false);
    }

    /**
     * 上传文件时用到
     *
     * @param filePaths                     待上传文件所在路径
     * @param inputName                     即file input 标签的name值,默认为file
     * @param forceRemoveContentTypeChraset
     * @return
     */
    public HttpConfig files(String[] filePaths, String inputName, boolean forceRemoveContentTypeChraset) {
        synchronized (getClass()) {
            if (this.map == null) {
                this.map = new HashMap<String, Object>();
            }
        }
        map.put(ENTITY_MULTIPART, filePaths);
        map.put(ENTITY_MULTIPART + ".name", inputName);
        map.put(ENTITY_MULTIPART + ".rmCharset", forceRemoveContentTypeChraset);
        return this;
    }

    /**
     * 输入输出编码
     */
    public HttpConfig encoding(String encoding) {
        //设置输入输出
        inenc(encoding);
        outenc(encoding);
        this.encoding = encoding;
        return this;
    }

    /**
     * 输入编码
     */
    public HttpConfig inenc(String inenc) {
        this.inenc = inenc;
        return this;
    }

    /**
     * 输出编码
     */
    public HttpConfig outenc(String outenc) {
        this.outenc = outenc;
        return this;
    }

    /**
     * 输出流对象
     */
    public HttpConfig out(OutputStream out) {
        outs.set(out);
        return this;
    }

    public HttpClient client() {
        return client;
    }

    public Header[] headers() {
        return headers;
    }

    public boolean isReturnRespHeaders() {
        return isReturnRespHeaders;
    }

    public String url() {
        return urls.get();
    }

    public HttpMethods method() {
        return method;
    }

    public String methodName() {
        return methodName;
    }

    public HttpContext context() {
        return context;
    }

    public Map<String, Object> map() {
        return map;
    }

    public String json() {
        return json;
    }

    public String encoding() {
        return encoding;
    }

    public String inenc() {
        return inenc == null ? encoding : inenc;
    }

    public String outenc() {
        return outenc == null ? encoding : outenc;
    }

    public OutputStream out() {
        return outs.get();
    }

}

使用JAVA代码发送请求

之前说明,我们使用最常用的Httpclient进行设计请求,根据Httpclient请求工具包构建一个基本的工具类:

这个类是一个很难复用和扩展的高耦合类,并且设计不是非常良好。

/**
 * httpclient 请求工具封装类
 */
public class HttpClientUtil {

	public static String doGet(String url, Map<String, String> param) {

		// 创建Httpclient对象
		CloseableHttpClient httpclient = HttpClients.createDefault();

		String resultString = "";
		CloseableHttpResponse response = null;
		try {
			// 创建uri
			URIBuilder builder = new URIBuilder(url);
			if (param != null) {
				for (String key : param.keySet()) {
					builder.addParameter(key, param.get(key));
				}
			}
			URI uri = builder.build();

			// 创建http GET请求
			HttpGet httpGet = new HttpGet(uri);

			// 执行请求
			response = httpclient.execute(httpGet);
			// 判断返回状态是否为200
			if (response.getStatusLine().getStatusCode() == 200) {
				resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (response != null) {
					response.close();
				}
				httpclient.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return resultString;
	}

	public static String doGet(String url) {
		return doGet(url, null);
	}

	public static String doPost(String url, Map<String, String> param) {
		// 创建Httpclient对象
		CloseableHttpClient httpClient = HttpClients.createDefault();
		CloseableHttpResponse response = null;
		String resultString = "";
		try {
			// 创建Http Post请求
			HttpPost httpPost = new HttpPost(url);
			// 创建参数列表
			if (param != null) {
				List<NameValuePair> paramList = new ArrayList<>();
				for (String key : param.keySet()) {
					paramList.add(new BasicNameValuePair(key, param.get(key)));
				}
				// 模拟表单
				UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
				httpPost.setEntity(entity);
			}
			// 执行http请求
			response = httpClient.execute(httpPost);
			resultString = EntityUtils.toString(response.getEntity(), "utf-8");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				response.close();
			} catch (IOException e) {

				e.printStackTrace();
			}
		}

		return resultString;
	}

	public static String doPost(String url) {
		return doPost(url, null);
	}
	
	public static String doPostJson(String url, String json) {
		// 创建Httpclient对象
		CloseableHttpClient httpClient = HttpClients.createDefault();
		CloseableHttpResponse response = null;
		String resultString = "";
		try {
			// 创建Http Post请求
			HttpPost httpPost = new HttpPost(url);
			// 创建请求内容
			StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
			httpPost.setEntity(entity);
			// 执行http请求
			response = httpClient.execute(httpPost);
			resultString = EntityUtils.toString(response.getEntity(), "utf-8");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if(response != null){
					response.close();
				}
			} catch (IOException e) {

				e.printStackTrace();
			}
		}

		return resultString;
	}

	/**
	 * 根据请求Config 进行请求发送
	 * @param httpConfig
	 * @return
	 */
	public static String send(HttpConfig httpConfig) {
		return doPostJson(httpConfig.url(), httpConfig.json());
	}
}

接着根据请求的结果设计一个钉钉机器人的返回对象,返回对象的设计也比较的简单。

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.field
 * @Description : 钉钉机器人返回对象
 * @Create on : 2021/2/5 18:26
 **/
public class DingRobotResponseMsg {

    /**
     * 错误码
     */
    private String errcode;

    /**
     * 错误信息
     */
    private String errmsg;
    /**
     * 更多链接
     */
    private String more;

    public DingRobotResponseMsg(String errcode, String errmsg, String more) {
        this.errcode = errcode;
        this.errmsg = errmsg;
        this.more = more;
    }

    public DingRobotResponseMsg() {

    }

    public String getErrcode() {
        return errcode;
    }

    public String getErrmsg() {
        return errmsg;
    }

    public String getMore() {
        return more;
    }

    public void setErrcode(String errcode) {
        this.errcode = errcode;
    }

    public void setErrmsg(String errmsg) {
        this.errmsg = errmsg;
    }

    public void setMore(String more) {
        this.more = more;
    }
}

最后,也是最重要的,我们要根据钉钉的文档,构建一个所有类型的请求对象类,这个类包含了钉钉文档目前支持的所有类型。内部使用了大量的内部类,客户端需要了解一定的细节才可以具体的调用。下面简要说明一下内容类的基本使用结构。

  • At 艾特对象内部类
  • Text 文本类型
  • Link 请求链接类型
  • MarkDown markdown类型
  • ActionCard 整体跳转类型
  • FeedCard 分享卡片类型
/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.field
 * @Description : 钉钉机器人请求实体对象
 * 请求案例:{"msgtype": "text","text": {"content": "自定义具体内容"}}
 * @link {https://developers.dingtalk.com/document/app/custom-robot-access}
 *
 * @Create on : 2021/2/5 11:55
 **/
public class DingRobotRequestBody implements DingRobotRequestAble {

    /**
     * 艾特对象内容
     */
    private At at;

    /**
     * 类型
     */
    private String msgtype;

    /**
     * 文本类型
     */
    private Text text;

    /**
     * 连接类型
     */
    private Link link;

    /**
     * markdown 类型
     */
    private MarkDown markdown;

    /**
     * 整体跳转ActionCard类型
     */
    private ActionCard actionCard;

    /**
     * FeedCard类型
     */
    private FeedCard feedCard;


    /**
     * FeedCard类型
     *
     * msgtype	    String	是	此消息类型为固定feedCard。
     * title	    String	是	单条信息文本。
     * messageURL	String	是	点击单条信息到跳转链接。
     * picURL	    String	是	单条信息后面图片的URL。
     */
    public static class FeedCard{

        private List<FeedItem> links;

        /**
         * 代表 FeedCard类型 子类型
         */
        public static class FeedItem{

            private String title;

            private String messageURL;

            private String picURL;

            public String getTitle() {
                return title;
            }

            public void setTitle(String title) {
                this.title = title;
            }

            public String getMessageURL() {
                return messageURL;
            }

            public void setMessageURL(String messageURL) {
                this.messageURL = messageURL;
            }

            public String getPicURL() {
                return picURL;
            }

            public void setPicURL(String picURL) {
                this.picURL = picURL;
            }
        }

        public List<FeedItem> getLinks() {
            return links;
        }

        public void setLinks(List<FeedItem> links) {
            this.links = links;
        }
    }


    /**
     * 整体跳转ActionCard类型
     * msgtype	        String	是	消息类型,此时固定为:actionCard。
     * title	        String	是	首屏会话透出的展示内容。
     * text	            String	是	markdown格式的消息。
     * singleTitle	    String	是	单个按钮的标题。
     *
     * 注意 设置此项和singleURL后,btns无效。
     *
     * singleURL	    String	是	点击singleTitle按钮触发的URL。
     * btnOrientation	String	否	0:按钮竖直排列1:按钮横向排列
     */
    public static class ActionCard{

        private String title;

        private String text;

        private String btnOrientation;

        private String singleTitle;

        private String singleURL;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public String getBtnOrientation() {
            return btnOrientation;
        }

        public void setBtnOrientation(String btnOrientation) {
            this.btnOrientation = btnOrientation;
        }

        public String getSingleTitle() {
            return singleTitle;
        }

        public void setSingleTitle(String singleTitle) {
            this.singleTitle = singleTitle;
        }

        public String getSingleURL() {
            return singleURL;
        }

        public void setSingleURL(String singleURL) {
            this.singleURL = singleURL;
        }
    }

    /**
     * 艾特类
     */
    public static class At{

        /**
         * 是否通知全部人
         */
        private boolean atAll;

        /**
         * 需要@的手机号数组
         */
        private List<String> atMobiles;

        public boolean isAtAll() {
            return atAll;
        }

        public void setAtAll(boolean atAll) {
            this.atAll = atAll;
        }

        public List<String> getAtMobiles() {
            return atMobiles;
        }

        public void setAtMobiles(List<String> atMobiles) {
            this.atMobiles = atMobiles;
        }
    }

    /**
     *
     * markdown 类型, 可以发送markdown 的语法格式
     * msgtype	    String	是	消息类型,此时固定为:markdown。
     * title	    String	是	首屏会话透出的展示内容。
     * text	        String	是	markdown格式的消息。
     * atMobiles	Array	否	被@人的手机号。 注意 在text内容里要有@人的手机号。
     * isAtAll	Boolean	否	是否@所有人。
     */
    public static class MarkDown{

        private String title;

        private String text;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }
    }

    /**
     * 钉钉请求:链接类型
     *
         msgtype	    String	是	消息类型,此时固定为:link。
         title	        String	是	消息标题。
         text	        String	是	消息内容。如果太长只会部分展示。
         messageUrl	    String	是	点击消息跳转的URL。
         picUrl	        String	否	图片URL。
     */
    public static class Link{

        private String text;

        private String messageUrl;

        private String picUrl;

        private String title;

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public String getMessageUrl() {
            return messageUrl;
        }

        public void setMessageUrl(String messageUrl) {
            this.messageUrl = messageUrl;
        }

        public String getPicUrl() {
            return picUrl;
        }

        public void setPicUrl(String picUrl) {
            this.picUrl = picUrl;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }
    }

    /**
     * 钉钉请求:纯文本类型
     */
    public static class Text{

        /**
         * text请求内容
         */
        private String content;

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }
    }

    @Override
    public void setMsgType(String msgtype) {
        this.msgtype = msgtype;
    }

    @Override
    public void setText(Text text) {
        this.text = text;
    }

    @Override
    public void setLink(Link link) {
        this.link = link;
    }

    @Override
    public void setMarkDown(MarkDown markDown) {
        this.markdown = markDown;
    }

    @Override
    public void setActionCard(ActionCard actionCard) {
        this.actionCard = actionCard;
    }

    @Override
    public void setFeedCard(FeedCard feedCard) {
        this.feedCard = feedCard;
    }

    public At getAt() {
        return at;
    }

    public void setAt(At at) {
        this.at = at;
    }

    public String getMsgtype() {
        return msgtype;
    }

    public Text getText() {
        return text;
    }

    public Link getLink() {
        return link;
    }

    public MarkDown getMarkdown() {
        return markdown;
    }

    public ActionCard getActionCard() {
        return actionCard;
    }

    public FeedCard getFeedCard() {
        return feedCard;
    }
}

插曲:在生成具体的钉钉对应请求对象时候,我们构建了一个对应的接口

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.zxd.interview.dingrobot
 * @Description : 允许发送钉钉请求的接口
 * @Create on : 2021/2/7 11:45
 **/
public interface DingRobotRequestAble {

    /**
     * 所有的子类需要集成该接口
     * @return
     */
    void setMsgType(String msgType);

    /**
     * 普通文本类型
     * @param text
     */
    void setText(DingRobotRequestBody.Text text);

    /**
     * link类型
     * @param link
     */
    void setLink(DingRobotRequestBody.Link link);

    /**
     * markdown 类型
     * @param markDown
     */
    void setMarkDown(DingRobotRequestBody.MarkDown markDown);

    /**
     * 整体跳转ActionCard类型
     * @param actionCard
     */
    void setActionCard(DingRobotRequestBody.ActionCard actionCard);

    /**
     * feedcard 类型
     * @param feedCard
     */
    void setFeedCard(DingRobotRequestBody.FeedCard feedCard);

}

构建钉钉请求工具类

介绍完上面所有的辅助对象之后,我们着手构建核心的钉钉请求工具类,钉钉的请求工具类包含了基本的请求步骤,提供对外的请求方法,调用者根据请求对象构建对应的请求参数即可,从下面的代码可以看到最核心的方法是notifyRobot这个方法,这个方法非常简单,内部的逻辑分为如下的几步:

  • 构建请求环境参数
  • 构建请求的URL和对应的携带参数
  • 构建具体的请求参数
  • 将请求返回的JSON字符串进行转化
/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.utils
 * @Description : 钉钉机器人工具类
 * @Create on : 2021/2/4 00:11
 **/
public class DingRobotUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(DingRobotUtils.class);

    public static DingRobotResponseMsg notifyRobot(DingRobotRequest dingRobotRequest, long currentTimeMillis) throws Exception {
        Map<String, Object> param = buildParam(dingRobotRequest, currentTimeMillis);
        String s = buildParamUrl(param);
        // 钉钉的请求参数需要拼接到URL链接
        dingRobotRequest.setUrl(String.format("%s?%s", dingRobotRequest.getUrl(), s));
        HttpConfig httpConfig = buildDefaultHttpConfig(dingRobotRequest, dingRobotRequest.getMsg());
        return parseResponse(notifyRobot(httpConfig));
    }

    /**
     * 转化为对应对象
     *
     * @param notifyRobot 转化JSON
     * @return
     */
    private static DingRobotResponseMsg parseResponse(String notifyRobot) {
        try {
            return JSON.parseObject(notifyRobot, DingRobotResponseMsg.class);
        } catch (Exception e) {
            LOGGER.error("类型转化失败,失败原因为:{}", e.getMessage());
            throw e;
        }
    }

    /**
     * 按照自定时间戳进行通知
     *
     * @param dingRobotRequest 钉钉机器人请求
     * @throws Exception
     */
    public static DingRobotResponseMsg notifyRobot(DingRobotRequest dingRobotRequest) throws Exception {
        long currentTimeMillis = System.currentTimeMillis();
        return notifyRobot(dingRobotRequest, currentTimeMillis);
    }


    /**
     * 构建请求环境参数
     *
     * @param dingRobotRequest  请求request
     * @param currentTimeMillis 当前时间戳
     * @return
     * @throws Exception
     */
    private static Map<String, Object> buildParam(DingRobotRequest dingRobotRequest, long currentTimeMillis) throws Exception {
        Map<String, Object> param = new HashMap<>(3);
        param.put("access_token", dingRobotRequest.getAccessToken());
        param.put("timestamp", currentTimeMillis);
        param.put("sign", generateSign(currentTimeMillis, dingRobotRequest.getSecret()));
        return param;
    }

    /**
     * 钉钉机器人的默认配置
     *
     * @param dingRobotRequest    钉钉机器人请求对象
     * @param dingRobotRequestMsg 钉钉机器人请求实体
     * @return
     */
    private static HttpConfig buildDefaultHttpConfig(DingRobotRequest dingRobotRequest, DingRobotRequestAble dingRobotRequestMsg) {
        return HttpConfig.custom().headers(defaultBasicHeader())
                .url(dingRobotRequest.getUrl())
                .encoding("UTF-8")
                .method(HttpMethods.POST)
                .json(JSON.toJSONString(dingRobotRequestMsg));
    }

    /**
     * 默认headers配置
     *
     * @return
     */
    private static Header[] defaultBasicHeader() {
        Header[] headers = new Header[1];
        headers[0] = new BasicHeader("Content-Type", "application/json");
        return headers;
    }

    private static String notifyRobot(HttpConfig httpConfig) throws Exception {
        String send = "";
        try {
            send = HttpClientUtil.send(httpConfig);
        } catch (Exception e) {
            LOGGER.error("HTTPClient请求发送失败, 失败原因为:{}", e.getMessage());
            throw e;
        }
        return send;
    }

    /**
     * 根据时间戳和秘钥生成一份签名
     *
     * @param timestamp 时间戳
     * @param secret    秘钥
     * @return
     * @throws Exception
     */
    private static String generateSign(Long timestamp, String secret) throws Exception {
        String stringToSign = timestamp + "\n" + secret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
    }

    /**
     * 构建URL参数
     *
     * @param param 请求MAP参数
     * @return
     */
    private static String buildParamUrl(Map<String, Object> param) {
        if (null == param || param.size() == 0) {
            return "";
        }
        StringBuilder stringBuilder = new StringBuilder();
        param.forEach((key, value) -> {
            stringBuilder.append(key).append("=").append(value);
            stringBuilder.append("&");
        });
        stringBuilder.deleteCharAt(stringBuilder.length() - 1);
        return stringBuilder.toString();
    }

}

下面是本工具类的使用方式,只需要传入环境参数并且传入必须的请求msg,就可以直接发送请求并且返回对应的结果。

 DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
    .url("https://oapi.dingtalk.com/robot/send")
    .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
    .msg(generateActionCard()).build();
try {
    DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
    System.err.println(JSON.toJSONString(dingRobotResponseMsg));
} catch (Exception e) {
    e.printStackTrace();
}

至此,一个工具类构建就完成了,整个构建的过程还是十分简单的。这次的工具代码也是不断进行小改动的成果。个人的代码水平功底有限,如果有什么意见欢迎点评。

问题汇总:

下面汇总了一些个人使用钉钉花的时间比较多的点。

吐槽:其实个人感觉钉钉的机器人在错误码这一块并不是特别的直观,下面说下个人踩到的一些小坑。

关于加签测试机器人出现31000的问题

如果在添加机器人的时候进行加签是需要加入对应的signtimestamp参数才可以测试成功,这里个人卡了一会儿才明白设计者的意图,虽然很好理解,但是对于第一次使用的人不是十分友好,同时在文档里面明显对于这一块的描述比较少,这里提供一下个人的小坑说明:

首先,我们需要根据请求的时间戳和秘钥生成签名

.....
/**
 * 构建当前的系统时间戳
 */
@Test
public void generateSystemCurrentTime() throws Exception {
    long l = System.currentTimeMillis();
    String secret = "SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc";
    String sign = generateSign(l, secret);
    System.out.println("timestamp = "+ l);
    System.out.println("sign = " + sign);
}

private String generateSign(Long timestamp, String secret) throws Exception {
    String stringToSign = timestamp + "\n" + secret;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
    return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
}

....

生成签名之后,我们需要把时间戳签名放入到请求的URL参数里面,测试方可通过:

https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D

提示:还是注意一下,在设置里面增加了加签

结尾

本文主要为记录个人使用钉钉的一些心得体会,以及以此编写了一个工具包方便以后有需要的时候可以直接拿来使用。

钉钉机器人的使用就告一段落了,目前工具类已经应用到公司项目正常的发送请求通知。后续看心情对于HttpClient请求工具类重构,但是目前个人还在参考和学习设计记录,发现可以拆分的对象还是不少的。包含请求方法,请求Header,请求编码等各种形式的转化。

最后,个人最近从**《代码简洁之道》**里面学习了很多有用的编程技巧和编写代码的细节问题,推荐读者看一看这本书,对于写出一个好代码和好注释或者想要学习改良自己的代码都是很有好处的,后续个人也会写一篇学习笔记,感兴趣的可以关注一波。