開發(fā)企業(yè)微信群機(jī)器人,實(shí)現(xiàn)定時(shí)提醒

大家好,我是魚皮,今天分享一個(gè)用程序解決生活工作問題的真實(shí)案例。
說來慚愧,事情是這樣的,在我們公司,每天都要輪流安排一名員工(當(dāng)然也包括我)去樓層中間一個(gè)很牛的飲水機(jī)那里接水。但由于大家每天都有自己的工作,經(jīng)常出現(xiàn)忘記接水的情況,導(dǎo)致大家口渴難耐。
怎么解決這個(gè)問題呢?
我想到了幾種方法:
1)每天大家輪流提醒。但是別說提醒別人了,自己都不記得什么時(shí)候輪到自己接水。
2)由一個(gè)員工負(fù)責(zé)提醒大家接水,必要時(shí)招募一個(gè) “接水提醒員”。
3)在企業(yè)微信的日歷功能給員工安排接水日程,就像下面這樣:

但問題是我們的人數(shù)和天數(shù)不是完全對應(yīng)的、反復(fù)安排日程也很麻煩。
你覺得上面哪種方案好呢?其實(shí)我覺得第二個(gè)方案是最好的 —— 招募一個(gè) “接水提醒員”。
別笑,我認(rèn)真的!
只不過這個(gè) “接水提醒員” 何必是人?
沒錯(cuò),作為一名程序員,我們可以搞一個(gè)機(jī)器人,讓它在企業(yè)微信群聊中每天提醒不同的員工去接水即可。
其實(shí)這個(gè)功能和員工排班打卡系統(tǒng)是很類似的,只不過更輕量一些。我也調(diào)研了很多排班系統(tǒng),但是都要收費(fèi),索性自己開發(fā)一個(gè)好了。
在企業(yè)微信中接入機(jī)器人其實(shí)非常簡單,因?yàn)槠髽I(yè)微信官方就支持群聊機(jī)器人功能,所以這次的任務(wù)我就安排給了實(shí)習(xí)生,他很快就完成了,所以我相信大家應(yīng)該也都能學(xué)會~
企微群聊機(jī)器人開發(fā)
學(xué)習(xí)開發(fā)第三方應(yīng)用時(shí),一定要先完整閱讀官方文檔,比如企業(yè)微信群機(jī)器人配置文檔。
指路:https://developer.work.weixin.qq.com/document/path/99110

設(shè)計(jì) SDK 結(jié)構(gòu)
雖然我們的目標(biāo)是做一個(gè)提醒接水機(jī)器人,但是企業(yè)微信群聊機(jī)器人其實(shí)是一個(gè)通用的功能,所以我們決定開發(fā)一個(gè)企微機(jī)器人 SDK,以后公司其他業(yè)務(wù)需要時(shí)都能夠快速復(fù)用。(比如開發(fā)一個(gè)定時(shí)喝水提醒機(jī)器人)
設(shè)計(jì)好 SDK 是需要一定技巧的,之前給大家分享過:如何設(shè)計(jì)一個(gè)優(yōu)秀的 SDK ,可以閱讀參考。
在查閱企微機(jī)器人文檔后,了解到企業(yè)微信機(jī)器人支持發(fā)送多種類型的消息,包括文本、 Markdown 、圖片、圖文、文件、語音和模塊卡片等,文檔中對每一種類型的請求參數(shù)和字段含義都做了詳盡的解釋。
吐槽一下,跟微信開發(fā)者文檔比起來,企微機(jī)器人的文檔寫得清晰多了!

由于每種消息最終都是要轉(zhuǎn)換成 JSON 格式作為 HTTP 請求的參數(shù)的,所以我們可以設(shè)計(jì)一個(gè)基礎(chǔ)的消息類(Message)來存放公共參數(shù),然后定義各種不同的具體消息類來集成它(比如文本消息 TextMessage、Markdown 消息 MarkdownMessage 等)。
為了簡化開發(fā)者使用 SDK 來發(fā)送消息,定義統(tǒng)一的 MessageSender 類,在類中提供發(fā)送消息的方法(比如發(fā)送文本消息 sendText),可以接受 Message 并發(fā)送到企業(yè)微信服務(wù)器。
最終,客戶端只需調(diào)用統(tǒng)一的消息發(fā)送方法即可。SDK 的整體結(jié)構(gòu)如下圖所示:

值得一提的是,如果要制作更通用的消息發(fā)送 SDK??梢詫?MessageSender 定義成接口,編寫不同的子類比如飛書 MessageSender、短信 MessageSender 等。
開發(fā) SDK
做好設(shè)計(jì)之后,接下來就可以開始開發(fā) SDK 了。
步驟如下:
獲取 webhook
創(chuàng)建 SDK 項(xiàng)目
編寫代碼
SDK 打包
調(diào)用 SDK
1、獲取 webhook
首先,必須在企業(yè)微信群聊中創(chuàng)建一個(gè)企業(yè)微信機(jī)器人,并獲取機(jī)器人的 webhook。
webhook 是一個(gè) url 地址,用于接受我們開發(fā)者自己服務(wù)器的請求,從而控制企業(yè)微信機(jī)器人。后續(xù)所有的開發(fā)過程,都需要通過 webhook 才可以實(shí)現(xiàn)。

復(fù)制并保存好這個(gè) Webhook 地址,注意不要泄露該地址!

2、創(chuàng)建 SDK 項(xiàng)目
SDK 通常是一個(gè)很干凈的項(xiàng)目,此處我們使用 Maven 來構(gòu)建一個(gè)空的項(xiàng)目,并在 pom.xml
?文件中配置項(xiàng)目信息。
需要特別注意的是,既然我們正在創(chuàng)建一個(gè) SDK,這意味著它將被更多的開發(fā)者使用。因此,在配置 groupId 和 artifactId 時(shí),我們應(yīng)當(dāng)遵循以下規(guī)范:
groupId:它是項(xiàng)目組織或項(xiàng)目開發(fā)者的唯一標(biāo)識符,其實(shí)際對應(yīng)的是 main 目錄下的 Java 目錄結(jié)構(gòu)。
artifactId:它是項(xiàng)目的唯一標(biāo)識符,對應(yīng)的是項(xiàng)目名稱,即項(xiàng)目的根目錄名稱。通常,它應(yīng)當(dāng)為純小寫,并且多個(gè)詞之間使用中劃線(-)隔開。
version:它指定了項(xiàng)目的當(dāng)前版本。其中,SNAPSHOT 表示該項(xiàng)目仍在開發(fā)中,是一個(gè)不穩(wěn)定的版本。
以下是我們配置好的項(xiàng)目信息:
<groupId>com.yupi</groupId>
<artifactId>rtx-robot</artifactId>
<version>1.0-SNAPSHOT</version>
為了讓我們的項(xiàng)目更加易用,我們還要能做到讓開發(fā)者通過配置文件來傳入配置(比如 webhook),而不是通過硬編碼重復(fù)配置各種信息。
所以此處我們把項(xiàng)目只作為 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>
最后,我們還需要添加一個(gè)配置,配置項(xiàng) ?<skip>true</skip>
?表示跳過執(zhí)行該插件的默認(rèn)行為:
<build>
????<plugins>
????????<plugin>
????????????<groupId>org.springframework.boot</groupId>
????????????<artifactId>spring-boot-maven-plugin</artifactId>
????????????<configuration>
????????????????<skip>true</skip>
????????????</configuration>
????????</plugin>
????</plugins>
</build>
這樣,一個(gè) SDK 項(xiàng)目的初始依賴就配置好了。
3、編寫配置類
現(xiàn)在我們就可以按照之前設(shè)計(jì)的結(jié)構(gòu)開發(fā)了。
首先,我們要寫一個(gè)配置類,用來接受開發(fā)者在配置文件中寫入的 webhook。
同時(shí),我們可以在配置類中,將需要被調(diào)用的 MessageSender 對象 Bean 自動注入到 IOC 容器中,不用讓開發(fā)者自己 new 對象了。
示例代碼如下:
(prefix?=?"wechatwork-bot")
public?class?WebhookConfig?{
????private?String?webhook;
????
????public?RtxRobotMessageSender?rtxRobotMessageSender()?{
????????return?new?RtxRobotMessageSender(webhook);
????}
}
接下來,為了讓 Spring Boot 項(xiàng)目在啟動時(shí)能自動識別并應(yīng)用配置類,需要把配置類寫入到 resources/META-INF/spring.factories
文件中,示例代碼如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yupi.rtxrobot.config.WebhookConfig
4、編寫消息類
接下來,我們要按照官方文檔的請求參數(shù)把幾種類型的消息對象編寫好。
由于每個(gè)消息類都有一個(gè)固定的字段 msgtype,所以我們定義一個(gè)基類 Message,方便后續(xù)將不同類型的消息傳入統(tǒng)一的方法:
public?class?Message?{
????/**
?????*?消息類型
?????**/
????String?msgtype;
}
接下來編寫具體的消息類,比如純文本類型消息 TextMessage,示例代碼如下:
public?class?TextMessage?extends?Message?{
????/**
?????*?消息內(nèi)容
?????*/
????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,?null,?null,?false);
????}
}
上面的代碼中,有個(gè)代碼優(yōu)化小細(xì)節(jié),官方文檔是使用 “@all” 字符串來表示 @全體成員的,但 “@all” 是一個(gè)魔法值,為了簡化調(diào)用,我們將其封裝為 mentionAll 布爾類型字段,并且在構(gòu)造函數(shù)中自動轉(zhuǎn)換為實(shí)際請求需要的字段。
5、編寫消息發(fā)送類
接下來,我們將編寫一個(gè)消息發(fā)送類。在這個(gè)類中,定義了用于發(fā)送各種類型消息的方法,并且所有的方法都會依賴調(diào)用底層的 send 方法。send 方法的作用是通過向企微機(jī)器人的 webhook 地址發(fā)送請求,從而驅(qū)動企微機(jī)器人發(fā)送消息。
以下是示例代碼,有很多編碼細(xì)節(jié):
/**
?*?微信機(jī)器人消息發(fā)送器
?*?@author?yuyuanweb
?*/
4j
public?class?RtxRobotMessageSender?{
????private?final?String?webhook;
??
????public?WebhookConfig?webhookConfig;
????public?RtxRobotMessageSender(String?webhook)?{
????????this.webhook?=?webhook;
????}
????/**
?????*?支持自定義消息發(fā)送
?????*/
????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");
????????}
????}
????/**
?????*?發(fā)送文本(簡化調(diào)用)
?????*/?
????public?void?sendText(String?content)?throws?Exception?{
????????sendText(content,?null,?null,?false);
????}
??
????public?void?sendText(String?content,?List<String>?mentionedList,?List<String>?mentionedMobileList)?throws?Exception?{
????????TextMessage?textMessage?=?new?TextMessage(content,?mentionedList,?mentionedMobileList,?false);
????????send(textMessage);
????}
????
????/**
?????*?發(fā)送消息的公共依賴底層代碼
?????*/
????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("沒有找到配置項(xiàng)中的webhook,請檢查:1.是否在application.yml中填寫webhook?2.是否在spring環(huán)境下運(yùn)行");
????????????????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("消息發(fā)送成功");
????????????}?else?{
????????????????log.error("消息發(fā)送失敗,響應(yīng)碼:{}",?response.code());
????????????????throw?new?Exception("消息發(fā)送失敗,響應(yīng)碼:"?+?response.code());
????????????}
????????}?catch?(IOException?e)?{
????????????log.error("發(fā)送消息時(shí)發(fā)生錯(cuò)誤:"?+?e);
????????????throw?new?Exception("發(fā)送消息時(shí)發(fā)生錯(cuò)誤",?e);
????????}
????}
}
代碼部分就到這里,是不是也沒有很復(fù)雜?
6、SDK 打包
接下來就可以對 SDK 進(jìn)行打包,然后本地使用或者上傳到遠(yuǎn)程倉庫了。
SDK 的打包非常簡單,通過 Maven 的 install 命令即可,SDK 的 jar 包就會被導(dǎo)入到你的本地倉庫中。
在打包前建議先執(zhí)行 clean 來清理垃圾文件。

7、調(diào)用 SDK
最后我們來調(diào)用自己寫的 SDK,首先將你的 SDK 作為依賴引入到項(xiàng)目中,比如我們的接水提醒應(yīng)用。
引入代碼如下:
<dependency>
??<groupId>com.yupi</groupId>
??<artifactId>rtx-robot</artifactId>
??<version>1.0-SNAPSHOT</version>
</dependency>
然后將之前復(fù)制的 webhook 寫入到 Spring Boot 的配置文件中:
wechatwork-bot:
??webhook:?你的webhook地址
隨后你就可以用依賴注入的方式得到一個(gè)消息發(fā)送者對象了:
public?RtxRobotMessageSender?rtxRobotMessageSender;
當(dāng)然你也可以選擇在一個(gè)非 Spring 環(huán)境中手動創(chuàng)建對象,自己傳入 webhook:
String?webhook?=?"你的webhook地址";
RtxRobotMessageSender?rtxRobotMessageSender?=?new?RtxRobotMessageSender(webhook);
現(xiàn)在,就可以輕松實(shí)現(xiàn)我們之前提到的提醒接水工具了。
這里我們就用最簡單的方式,定義一個(gè)員工數(shù)組,分別對應(yīng)到每周 X,然后用定時(shí)任務(wù)每日執(zhí)行消息發(fā)送。
示例代碼如下:
public?class?WaterReminderTask?{
????
????public?RtxRobotMessageSender?rtxRobotMessageSender;
????private?String[]?names?=?{"員工a",?"員工b",?"員工c",?"員工d",?"員工e"};
???? (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?+?",是你接水的時(shí)間了!";
????????rtxRobotMessageSender.sendText(message);
????}
}
好了,現(xiàn)在大家每天都有水喝了,真不錯(cuò) ????

最后
雖然開發(fā)企微機(jī)器人 SDK 并不難,但想做一個(gè)完善的、易用的 SDK 還是需要兩把刷子的,而且沉淀 SDK 對自己未來做項(xiàng)目幫助會非常大。
希望本文對大家有幫助,學(xué)會的話 點(diǎn)個(gè)贊
?或 在看
吧,謝謝大家~