一、简介
背景
- 客户需要通过钉钉接收消息通知
名词解释
- 群聊机器人:钉钉群里可以创建一个机器人,平台通过机器人把告警/通知推送到群里
- 私聊机器人:钉钉后台开启机器人配置,平台绑定此机器人后,可以通过私聊的方式将告警/通知推送到个人
- 系统消息:平台可以通过钉钉系统消息将告警/通知推送到个人
- corpId:企业ID
- agentId:应用ID
- appKey:应用Key
- appSecret:应用Secret
- Webhook:钉钉群机器人Webhook
- robotCode:私聊机器人编码(非必填,且不是只能作为私聊,目前我们平台只需要私聊功能)
二、设计思路
思路:
- 方案一:群聊机器人,告警/通知会直接推送到钉钉群;此时还需要配置Webhook,群消息支持@指定人。经讨论此方案会导致群消息增多,且每个人都可以看到告警/通知,不采纳
- 方案二:私聊机器人,告警/通知会以私聊的方式推送给指定人;此时还需要额外配置robotCode,采纳(当使用机器人推送失败时,会继续尝试用系统消息推送)
- 方案三:系统消息,告警/通知会以钉钉系统消息的方式推送给指定人,比较友好,故采纳
钉钉消息推送
- 当配置了robotCode时:使用单聊机器人推送消息
- 当未配置robotCode时:使用系统通知推送消息
获取钉钉用户ID
- 背景:发送钉钉系统消息,需要指定钉钉的用户ID,钉钉提供了根据手机号查询用户ID的API
- 实现方式:根据告警/通知推送的平台用户信息,获取到手机号;再根据手机号获取钉钉用户ID
钉钉测试
- 配置 corpId、agentId、appKey、appSecret
- 填写钉钉账户关联的手机号
- 钉钉测试文案:测试钉钉消息发送
钉钉token缓存维护
- 钉钉默认access_token有效期2小时,无需频繁获取token(钉钉存在访问限制),维护token内存级缓存
三、概要设计
3.1.钉钉消息通知配置
- 入参:
|
3.2.钉钉测试
- 接口:POST /base/sys/config/ding-talk/test
- 入参:
|
3.3.是否可以开启钉钉
- 接口:GET /base/message/sysmessageevent/open-dingtalk
- 入参:无
3.4.钉钉token缓存维护
- url:GET https://oapi.dingtalk.com/gettoken?appkey=xxx&appsecret=xxx
- 维护内存缓存,有效期2小时
3.5.获取钉钉用户ID
- url:GET https://oapi.dingtalk.com/topapi/v2/user/getbymobile
- 入参:mobile、access_token
-
添加接口调用权限(根据手机号查询用户):添加接口调用权限 - 钉钉开放平台 (dingtalk.com)
3.6.钉钉消息通知
3.6.1.私聊机器人
- url POST /v1.0/robot/oToMessages/batchSend
- 入参:
|
3.6.2.系统通知
- url:POST https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token=xxx
- 入参:
|
- 限制:给同一员工一天只能发送一条内容相同的消息通知。为了兼容这一点,钉钉的消息在末尾追加时间 yyyy-MM-dd HH:mm:ss
- eg:**通知-提交数据授权申请 2023-12-20 17:49:53**
尊敬的【密态数据流通系统】用户您好:万曾辉提交数据授权申请,[去审批](http://192.168.9.25/#/dta/data/authorization?uuid=4&type=2)
- eg:**通知-提交数据授权申请 2023-12-20 17:49:53**
四、实现效果
4.1.私聊机器人
- **通知-提交权限申请**
尊敬的【xxxx系统】用户您好:xxx提交权限申请,去审批
4.2.系统通知
- **通知-提交权限申请 2023-12-20 17:49:53**
尊敬的【xxxx系统】用户您好:xxx提交权限申请,[去审批](http://192.168.9.25/#/dta/data/authorization?uuid=4&type=2)
五、maven依赖
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.dingtalk.open</groupId>
<artifactId>app-stream-client</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.0.71</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
</dependencies>
六、代码demo
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.aliyun.dingtalkrobot_1_0.Client;
import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTOHeaders;
import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTORequest;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.request.OapiV2UserGetbymobileRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.dingtalk.api.response.OapiV2UserGetbymobileResponse;
import com.taobao.api.ApiException;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
public class SendMessageToDing {
public static void main(String[] args) throws Exception {
// 获取token
String accessToken = getAccessToken();
// 获取用户ID
String userId = getUserId("手机号", accessToken);
// 发送工作提醒
sendWorkNotification(accessToken, "agentId", userId, "这里是消息");
// 通过机器人发送私聊消息
sendMsgByRobot(
"robotCode",
accessToken,
Collections.singletonList(userId),
"**通知-提交权限申请 2023-12-20 17:48:58**",
"**通知-提交权限申请 2023-12-20 17:48:58**尊敬的【xxx系统】用户您好:xxx提交权限申请,[去审批](http://192.168.9.25/#/test/list?uuid=3&type=2)");
// 通过机器人发送群聊
sendMsg();
}
public static void sendMsgByRobot(String robotCode, String accessToken, List<String> userIds, String title, String text) throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
Client client = new Client(config);
BatchSendOTOHeaders batchSendOTOHeaders = new BatchSendOTOHeaders();
batchSendOTOHeaders.xAcsDingtalkAccessToken = accessToken;
BatchSendOTORequest batchSendOTORequest = new BatchSendOTORequest()
.setRobotCode(robotCode)
.setUserIds(userIds)
.setMsgKey("sampleMarkdown")
.setMsgParam(String.format("{\"text\": \"%s\",\"title\": \"%s\"}", text, title));
try {
client.batchSendOTOWithOptions(batchSendOTORequest, batchSendOTOHeaders, new RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
public static String getUserId(String mobile, String accessToken) {
try {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getbymobile");
OapiV2UserGetbymobileRequest req = new OapiV2UserGetbymobileRequest();
req.setMobile(mobile);
OapiV2UserGetbymobileResponse rsp = client.execute(req, accessToken);
System.out.println(rsp.getBody());
JSONObject result = (JSONObject) JSON.parseObject(rsp.getBody()).get("result");
System.out.println((String) result.get("userid"));
return (String) result.get("userid");
} catch (ApiException e) {
e.printStackTrace();
}
return "";
}
public static String getAccessToken() throws IOException {
String appKey = "xxx";
String appSecret = "xxx";
String requestUrl = "https://oapi.dingtalk.com/gettoken?appkey=" + appKey + "&appsecret=" + appSecret;
URL url = new URL(requestUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
Message message = JSON.parseObject(response.toString(), Message.class);
System.out.println(response.toString());
return message.getAccess_token();
} else {
System.out.println("GET请求失败,状态码:" + responseCode);
}
return "";
}
public static void sendWorkNotification(String accessToken, String agentId, String userId, String message) {
String requestUrl = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token=" + accessToken;
try {
URL url = new URL(requestUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
String jsonPayload = String.format(
"{\"agent_id\": \"%s\", \"userid_list\": \"%s\", \"msg\": {\"msgtype\": \"text\", \"text\": {\"content\": \"%s\"}}}",
agentId, userId, message
);
OutputStream outputStream = connection.getOutputStream();
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
outputStream.write(input, 0, input.length);
System.out.println("Response Code: " + connection.getResponseCode());
System.out.println("Response Message: " + connection.getResponseMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void sendMsg() throws ApiException {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send?access_token=群聊机器人token");
OapiRobotSendRequest request = new OapiRobotSendRequest();
// request.setMsgtype("text");
// OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
// text.setContent("【消息推送】测试文本消息xxxxxx");
// request.setText(text);
// OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
// at.setAtMobiles(Arrays.asList("132xxxxxxxx"));
// isAtAll类型如果不为Boolean,请升级至最新SDK
// at.setIsAtAll(true);
// at.setAtUserIds(Arrays.asList("109929", "32099"));
// request.setAt(at);
// request.setMsgtype("link");
// OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link();
// link.setMessageUrl("https://www.dingtalk.com/");
// link.setPicUrl("");
// link.setTitle("时代的火车向前开");
// link.setText("消息推送这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林");
// request.setLink(link);
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("杭州天气");
markdown.setText("#### 消息推送杭州天气 @13520280136\n" +
"> 9度,西北风1级,空气良89,相对温度73%\n\n" +
"> ![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png)\n" +
"> ###### 10点20分发布 [天气](http://www.thinkpage.cn/) \n");
request.setMarkdown(markdown);
OapiRobotSendResponse response = client.execute(request);
}
}