开发企业微信群机器人,实现定时提醒

1,249 阅读10分钟

大家好,我是鱼皮,今天分享一个用程序解决生活工作问题的真实案例。

说来惭愧,事情是这样的,在我们公司,每天都要轮流安排一名员工(当然也包括我)去楼层中间一个很牛的饮水机那里接水。但由于大家每天都有自己的工作,经常出现忘记接水的情况,导致大家口渴难耐。

怎么解决这个问题呢?

我想到了几种方法:

1)每天大家轮流提醒。但是别说提醒别人了,自己都不记得什么时候轮到自己接水。

2)由一个员工负责提醒大家接水,必要时招募一个 “接水提醒员”。

3)在企业微信的日历功能给员工安排接水日程,就像下面这样:

但问题是我们的人数和天数不是完全对应的、反复安排日程也很麻烦。

你觉得上面哪种方案好呢?其实我觉得第二个方案是最好的 —— 招募一个 “接水提醒员”。

别笑,我认真的!

只不过这个 “接水提醒员” 何必是人?

没错,作为一名程序员,我们可以搞一个机器人,让它在企业微信群聊中每天提醒不同的员工去接水即可。

其实这个功能和员工排班打卡系统是很类似的,只不过更轻量一些。我也调研了很多排班系统,但是都要收费,索性自己开发一个好了。

在企业微信中接入机器人其实非常简单,因为企业微信官方就支持群聊机器人功能,所以这次的任务我就安排给了实习生,他很快就完成了,所以我相信大家应该也都能学会~

企微群聊机器人开发

学习开发第三方应用时,一定要先完整阅读官方文档,比如企业微信群机器人配置文档。

指路:developer.work.weixin.qq.com/document/pa…

设计 SDK 结构

虽然我们的目标是做一个提醒接水机器人,但是企业微信群聊机器人其实是一个通用的功能,所以我们决定开发一个企微机器人 SDK,以后公司其他业务需要时都能够快速复用。(比如开发一个定时喝水提醒机器人)

设计好 SDK 是需要一定技巧的,之前给大家分享过:如何设计一个优秀的 SDK ,可以阅读参考。

在查阅企微机器人文档后,了解到企业微信机器人支持发送多种类型的消息,包括文本、 Markdown 、图片、图文、文件、语音和模块卡片等,文档中对每一种类型的请求参数和字段含义都做了详尽的解释。

吐槽一下,跟微信开发者文档比起来,企微机器人的文档写得清晰多了!

企微文本消息格式

企微文本消息格式

由于每种消息最终都是要转换成 JSON 格式作为 HTTP 请求的参数的,所以我们可以设计一个基础的消息类(Message)来存放公共参数,然后定义各种不同的具体消息类来集成它(比如文本消息 TextMessage、Markdown 消息 MarkdownMessage 等)。

为了简化开发者使用 SDK 来发送消息,定义统一的 MessageSender 类,在类中提供发送消息的方法(比如发送文本消息 sendText),可以接受 Message 并发送到企业微信服务器。

最终,客户端只需调用统一的消息发送方法即可。SDK 的整体结构如下图所示:

值得一提的是,如果要制作更通用的消息发送 SDK。可以将 MessageSender 定义成接口,编写不同的子类比如飞书 MessageSender、短信 MessageSender 等。

开发 SDK

做好设计之后,接下来就可以开始开发 SDK 了。

步骤如下:

  1. 获取 webhook
  2. 创建 SDK 项目
  3. 编写代码
  4. SDK 打包
  5. 调用 SDK

1、获取 webhook

首先,必须在企业微信群聊中创建一个企业微信机器人,并获取机器人的 webhook。

webhook 是一个 url 地址,用于接受我们开发者自己服务器的请求,从而控制企业微信机器人。后续所有的开发过程,都需要通过 webhook 才可以实现。

复制并保存好这个 Webhook 地址,注意不要泄露该地址!

2、创建 SDK 项目

SDK 通常是一个很干净的项目,此处我们使用 Maven 来构建一个空的项目,并在 pom.xml 文件中配置项目信息。

需要特别注意的是,既然我们正在创建一个 SDK,这意味着它将被更多的开发者使用。因此,在配置 groupId 和 artifactId 时,我们应当遵循以下规范:

  • groupId:它是项目组织或项目开发者的唯一标识符,其实际对应的是 main 目录下的 Java 目录结构。
  • artifactId:它是项目的唯一标识符,对应的是项目名称,即项目的根目录名称。通常,它应当为纯小写,并且多个词之间使用中划线(-)隔开。
  • version:它指定了项目的当前版本。其中,SNAPSHOT 表示该项目仍在开发中,是一个不稳定的版本。

以下是我们配置好的项目信息:

<groupId>com.yupi</groupId>
<artifactId>rtx-robot</artifactId>
<version>1.0-SNAPSHOT</version>

为了让我们的项目更加易用,我们还要能做到让开发者通过配置文件来传入配置(比如 webhook),而不是通过硬编码重复配置各种信息。

所以此处我们把项目只作为 Spring Boot 的 starter,需要在 pom.xml 文件中引入依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

最后,我们还需要添加一个配置,配置项 <skip>true</skip> 表示跳过执行该插件的默认行为:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <skip>true</skip>
            </configuration>
        </plugin>
    </plugins>
</build>

这样,一个 SDK 项目的初始依赖就配置好了。

3、编写配置类

现在我们就可以按照之前设计的结构开发了。

首先,我们要写一个配置类,用来接受开发者在配置文件中写入的 webhook。

同时,我们可以在配置类中,将需要被调用的 MessageSender 对象 Bean 自动注入到 IOC 容器中,不用让开发者自己 new 对象了。

示例代码如下:

@Configuration
@ConfigurationProperties(prefix = "wechatwork-bot")
@ComponentScan
@Data
public class WebhookConfig {

    private String webhook;

    @Bean
    public RtxRobotMessageSender rtxRobotMessageSender() {
        return new RtxRobotMessageSender(webhook);
    }
}

接下来,为了让 Spring Boot 项目在启动时能自动识别并应用配置类,需要把配置类写入到 resources/META-INF/spring.factories 文件中,示例代码如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yupi.rtxrobot.config.WebhookConfig

4、编写消息类

接下来,我们要按照官方文档的请求参数把几种类型的消息对象编写好。

由于每个消息类都有一个固定的字段 msgtype,所以我们定义一个基类 Message,方便后续将不同类型的消息传入统一的方法:

public class Message {

    /**
     * 消息类型
     **/
    String msgtype;
}

接下来编写具体的消息类,比如纯文本类型消息 TextMessage,示例代码如下:

@Data
public class TextMessage extends Message {

    /**
     * 消息内容
     */
    private String content;

    /**
     * 被提及者userId列表
     */
    private List<String> mentionedList;

    /**
     * 被提及者电话号码列表
     */
    private List<String> mentionedMobileList;
  
    /**
     * 提及全体
     */
    private Boolean mentionAll = false;

    public TextMessage(String content, List<String> mentionedList, List<String> mentionedMobileList, Boolean mentionAll) {
        this.content = content;
        this.mentionedList = mentionedList;
        this.mentionedMobileList = mentionedMobileList;
        this.mentionAll = mentionAll;

        if (mentionAll) {
            if (CollUtil.isNotEmpty(this.mentionedList) || CollUtil.isNotEmpty(this.mentionedMobileList)) {
                if (CollUtil.isNotEmpty(mentionedList)) {
                    this.mentionedList.add("@all");
                } else {
                    this.mentionedList = CollUtil.newArrayList("@all");
                }
            } else {
                this.mentionedList = CollUtil.newArrayList("@all");
            }
        }
    }

    public TextMessage(String content) {
        this(content, nullnullfalse);
    }
}

上面的代码中,有个代码优化小细节,官方文档是使用 “@all” 字符串来表示 @全体成员的,但 “@all” 是一个魔法值,为了简化调用,我们将其封装为 mentionAll 布尔类型字段,并且在构造函数中自动转换为实际请求需要的字段。

5、编写消息发送类

接下来,我们将编写一个消息发送类。在这个类中,定义了用于发送各种类型消息的方法,并且所有的方法都会依赖调用底层的 send 方法。send 方法的作用是通过向企微机器人的 webhook 地址发送请求,从而驱动企微机器人发送消息。

以下是示例代码,有很多编码细节:

/**
 * 微信机器人消息发送器
 * @author yuyuanweb
 */
@Slf4j
@Data
public class RtxRobotMessageSender {

    private final String webhook;
  
    public WebhookConfig webhookConfig;

    public RtxRobotMessageSender(String webhook) {
        this.webhook = webhook;
    }

    /**
     * 支持自定义消息发送
     */
    public void sendMessage(Message message) throws Exception {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            send(textMessage);
        } else if (message instanceof MarkdownMessage) {
            MarkdownMessage markdownMessage = (MarkdownMessage) message;
            send(markdownMessage);
        } else {
            throw new RuntimeException("Unsupported message type");
        }
    }

    /**
     * 发送文本(简化调用)
     */ 
    public void sendText(String content) throws Exception {
        sendText(content, nullnullfalse);
    }
  
    public void sendText(String content, List<String> mentionedList, List<String> mentionedMobileList) throws Exception {
        TextMessage textMessage = new TextMessage(content, mentionedList, mentionedMobileList, false);
        send(textMessage);
    }
    
    /**
     * 发送消息的公共依赖底层代码
     */
    private void send(Message message) throws Exception {
        String webhook = this.webhook;
        String messageJsonObject = JSONUtil.toJsonStr(message);
       // 未传入配置,降级为从配置文件中寻找
        if (StrUtil.isBlank(this.webhook)) {
            try {
                webhook = webhookConfig.getWebhook();
            } catch (Exception e) {
                log.error("没有找到配置项中的webhook,请检查:1.是否在application.yml中填写webhook 2.是否在spring环境下运行");
                throw new RuntimeException(e);
            }
        }
        OkHttpClient client = new OkHttpClient();
        RequestBody body = RequestBody.create(
                MediaType.get("application/json; charset=utf-8"),
                messageJsonObject);
        Request request = new Request.Builder()
                .url(webhook)
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                log.info("消息发送成功");
            } else {
                log.error("消息发送失败,响应码:{}", response.code());
                throw new Exception("消息发送失败,响应码:" + response.code());
            }
        } catch (IOException e) {
            log.error("发送消息时发生错误:" + e);
            throw new Exception("发送消息时发生错误", e);
        }
    }
}

代码部分就到这里,是不是也没有很复杂?

6、SDK 打包

接下来就可以对 SDK 进行打包,然后本地使用或者上传到远程仓库了。

SDK 的打包非常简单,通过 Maven 的 install 命令即可,SDK 的 jar 包就会被导入到你的本地仓库中。

在打包前建议先执行 clean 来清理垃圾文件。

7、调用 SDK

最后我们来调用自己写的 SDK,首先将你的 SDK 作为依赖引入到项目中,比如我们的接水提醒应用。

引入代码如下:

<dependency>
  <groupId>com.yupi</groupId>
  <artifactId>rtx-robot</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

然后将之前复制的 webhook 写入到 Spring Boot 的配置文件中:

wechatwork-bot:
  webhook: 你的webhook地址

随后你就可以用依赖注入的方式得到一个消息发送者对象了:

@Resource
public RtxRobotMessageSender rtxRobotMessageSender;

当然你也可以选择在一个非 Spring 环境中手动创建对象,自己传入 webhook:

String webhook = "你的webhook地址";
RtxRobotMessageSender rtxRobotMessageSender = new RtxRobotMessageSender(webhook);

现在,就可以轻松实现我们之前提到的提醒接水工具了。

这里我们就用最简单的方式,定义一个员工数组,分别对应到每周 X,然后用定时任务每日执行消息发送。

示例代码如下:

@Component
public class WaterReminderTask {

    @Resource
    public RtxRobotMessageSender rtxRobotMessageSender;

    private String[] names = {"员工a""员工b""员工c""员工d""员工e"};

    @Scheduled(cron = "0 55 9 * * MON-FRI")
    public void remindToGetWater() {
        LocalDate today = LocalDate.now();
        DayOfWeek dayOfWeek = today.getDayOfWeek();
        String nameToRemind;
        switch (dayOfWeek) {
            case MONDAY:
                nameToRemind = names[0];
                break;
            case TUESDAY:
                nameToRemind = names[1];
                break;
            case WEDNESDAY:
                nameToRemind = names[2];
                break;
            case THURSDAY:
                nameToRemind = names[3];
                break;
            case FRIDAY:
                nameToRemind = names[4];
                break;
            default:
                return;
        }
      
        String message = "提醒:" + nameToRemind + ",是你接水的时间了!";
        rtxRobotMessageSender.sendText(message);
    }
}

好了,现在大家每天都有水喝了,真不错 👍🏻

最后

虽然开发企微机器人 SDK 并不难,但想做一个完善的、易用的 SDK 还是需要两把刷子的,而且沉淀 SDK 对自己未来做项目帮助会非常大。

希望本文对大家有帮助,学会的话 点个赞在看 吧,谢谢大家~