注:本文代码适用于Java环境。可直接使用。
钉钉最近升级,发送群消息新增了安全设置,20191031已更新到最新版
背景
在实际的开发过程当中,有时候在一些特殊的地方,比如请求第三方接口,连接第三方中间件等过程中,总是会碰到一些莫名其妙的报错问题,要想让程序报错时及时通知到开发人员,最简单的实现方式之一,则可采用阿里的钉钉群进行自定义机器人进行通知。
成果展示
可以通过钉钉群自定义的群机器人,艾特相关的技术人员关注某些特定的业务执行过程或者及时处理某些异常报错接口。
接入过程
创建自定义机器人
将需要接收信息的人员拉入一个钉钉群,自己取一个高大上的群名哈,然后点击群机器人 -> 添加机器人 -> 选择自定义机器人 -> 根据提示完成机器人创建。如果需要的话,可以为机器人设置一个头像。点击“完成添加”,完成后会生成webhook地址,如下图:
点击“复制”按钮,即可获得这个机器人对应的Webhook地址,其格式如下:
https://oapi.dingtalk.com/robot/send?access_token=XXX
使用自定义机器人
自定义机器人发送消息时,可以通过手机号码指定“被@人列表”。在“被@人列表”里面的人员收到该消息时,会有@消息提醒(免打扰会话仍然通知提醒,首屏出现“有人@你”)。
消息发送频率限制:
每个机器人每分钟最多发送20条。消息发送太频繁会严重影响群成员的使用体验,大量发消息的场景 (譬如系统监控报警) 可以将这些信息进行整合,通过markdown消息以摘要的形式发送到群里。当然,也可以针对不同的业务创建多个不同的机器人。
以下是Java程序中发送自定义钉钉机器人的工具类(text类型&markdown类型):
效果:
发送消息(各种类型均可)工具类:(可直接使用)
package com.github.collection.common.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.collection.common.constant.enums.DingMsgPhoneEnum;
import com.github.collection.common.constant.enums.DingTokenEnum;
import com.xiaoleilu.hutool.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
/**
* 钉钉消息发送工具类
*
* 机器人发送消息频率限制:每个机器人每分钟最多发送20条。如果超过20条,会限流10分钟。
*/
@Slf4j
public class DingDingMsgSendUtils {
/**
* 调用钉钉官方接口发送钉钉消息(旧版本,不需要配置安全设置)
* @param accessToken
* @param textMsg
*/
private static void dealDingDingMsgSend(String accessToken, String textMsg) {
HttpClient httpclient = HttpClients.createDefault();
String WEBHOOK_TOKEN = "https://oapi.dingtalk.com/robot/send?access_token=" + accessToken;
HttpPost httppost = new HttpPost(WEBHOOK_TOKEN);
httppost.addHeader("Content-Type", "application/json; charset=utf-8");
StringEntity se = new StringEntity(textMsg, "utf-8");
httppost.setEntity(se);
try {
HttpResponse response = httpclient.execute(httppost);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
String result= EntityUtils.toString(response.getEntity(), "utf-8");
log.info("【发送钉钉群消息】消息响应结果:" + JSON.toJSONString(result));
}
} catch (Exception e) {
log.error("【发送钉钉群消息】error:" + e.getMessage(), e);
}
}
/**
* 调用钉钉官方接口发送钉钉消息(新版本,需要配置安全设置)
* @param accessToken
* @param secret
* @param textMsg
*/
private static void dealDingDingMsgSendNew(String accessToken, String secret, String textMsg) {
Long timestamp = System.currentTimeMillis();
String sign = getSign(secret, timestamp);
String url = "https://oapi.dingtalk.com/robot/send?access_token=" + accessToken + "×tamp=" + timestamp + "&sign=" + sign;
try {
log.info("【发送钉钉群消息】请求参数:url = {}, textMsg = {}", url, textMsg);
String res = HttpUtil.post(url, textMsg);
log.info("【发送钉钉群消息】消息响应结果:" + res);
} catch (Exception e) {
log.warn("【发送钉钉群消息】请求钉钉接口异常,errMsg = {}", e);
}
}
/**
* 计算签名
* @param secret 密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符
* @param timestamp
* @return
*/
private static String getSign(String secret, Long timestamp){
try {
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)),"UTF-8");
log.info("【发送钉钉群消息】获取到签名sign = {}", sign);
return sign;
} catch (Exception e) {
log.error("【发送钉钉群消息】计算签名异常,errMsg = {}", e);
return null;
}
}
/**
* 发送钉钉群消息
* @param accessToken
* @param content
*/
public static void sendDingDingGroupMsg(String accessToken, String content) {
String textMsg = "{ \"msgtype\": \"text\", \"text\": {\"content\": \"" + content + "\"}}";
dealDingDingMsgSend(accessToken, textMsg);
}
/**
* 发送钉钉群消息(可以艾特人)
* @param accessToken 群机器人accessToken
* @param content 发送内容
* @param atPhone 艾特人电话,如:13867400741,15608457257,15072328011
*/
public static void sendDingDingGroupMsg(String accessToken, String content, String atPhone) {
String textMsg = "{\n" +
" \"msgtype\": \"text\", \n" +
" \"text\": {\n" +
" \"content\": \""+ content +"\"\n" +
" }, \n" +
" \"at\": {\n" +
" \"atMobiles\": [\n" +
" " + atPhone +
" ], \n" +
" \"isAtAll\": false\n" +
" }\n" +
"}";
dealDingDingMsgSend(accessToken, textMsg);
}
/**
* 发送钉钉群消息(link类型)
* @param accessToken
* @param title 消息标题
* @param text 消息内容。如果太长只会部分展示
* @param picUrl 图片URL
* @param messageUrl 点击消息跳转的URL
*/
public static void sendDingDingLinkGroupMsg(String accessToken, String title, String text, String picUrl, String messageUrl) {
String textMsg = "{\n" +
" \"msgtype\": \"link\", \n" +
" \"link\": {\n" +
" \"text\": \""+text+"\", \n" +
" \"title\": \""+title+"\", \n" +
" \"picUrl\": \""+picUrl+"\", \n" +
" \"messageUrl\": \""+messageUrl+"\"\n" +
" }\n" +
"}";
dealDingDingMsgSend(accessToken, textMsg);
}
/**
* markdown类型
* @param accessToken
* @param title
* @param text
* @param atMobiles
*/
public static void sendDingDingMarkdownGroupMsg(String accessToken, String secret, String title, String text, String atMobiles) {
log.info("【发送钉钉群消息】正在发送markdown类型的钉钉消息...");
JSONObject markdown = new JSONObject();
markdown.put("title", title);
markdown.put("text", text);
JSONObject at = new JSONObject();
at.put("atMobiles", atMobiles);
at.put("isAtAll", false);
JSONObject textMsg = new JSONObject();
textMsg.put("msgtype", "markdown");
textMsg.put("markdown", markdown);
textMsg.put("at", at);
dealDingDingMsgSendNew(accessToken, secret, textMsg.toJSONString());
}
/**
* 整体跳转ActionCard类型
* @param accessToken
* @param title
* @param text
* @param singleTitle
* @param singleURL
*/
public static void sendDingDingActionCardGroupMsg(String accessToken, String title, String text, String singleTitle, String singleURL) {
String textMsg = "{\n" +
" \"actionCard\": {\n" +
" \"title\": \""+title+"\", \n" +
" \"text\": \""+text+"\", \n" +
" \"hideAvatar\": \"0\", \n" +
" \"btnOrientation\": \"0\", \n" +
" \"singleTitle\" : \""+singleTitle+"\",\n" +
" \"singleURL\" : \""+singleURL+"\"\n" +
" }, \n" +
" \"msgtype\": \"actionCard\"\n" +
"}";
dealDingDingMsgSend(accessToken, textMsg);
}
/**
* 独立跳转ActionCard类型
* @param accessToken
* @param title
* @param text
* @param singleTitle
* @param singleURL
*/
public static void sendDingDingActionCardGroupMsg2(String accessToken, String title, String text, String singleTitle, String singleURL) {
String textMsg = "{\n" +
" \"actionCard\": {\n" +
" \"title\": \"乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身\", \n" +
" \"text\": \" \n" +
" ### 乔布斯 20 年前想打造的苹果咖啡厅 \n" +
" Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划\", \n" +
" \"hideAvatar\": \"0\", \n" +
" \"btnOrientation\": \"0\", \n" +
" \"btns\": [\n" +
" {\n" +
" \"title\": \"内容不错\", \n" +
" \"actionURL\": \"https://www.dingtalk.com/\"\n" +
" }, \n" +
" {\n" +
" \"title\": \"不感兴趣\", \n" +
" \"actionURL\": \"https://www.dingtalk.com/\"\n" +
" }\n" +
" ]\n" +
" }, \n" +
" \"msgtype\": \"actionCard\"\n" +
"}\n";
dealDingDingMsgSend(accessToken, textMsg);
}
/**
* FeedCard类型
* @param accessToken
* @param title
* @param text
* @param singleTitle
* @param singleURL
*/
public static void sendDingDingFeedCardGroupMsg(String accessToken, String title, String text, String singleTitle, String singleURL) {
String textMsg = "{\n" +
" \"feedCard\": {\n" +
" \"links\": [\n" +
" {\n" +
" \"title\": \"时代的火车向前开\", \n" +
" \"messageURL\": \"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\", \n" +
" \"picURL\": \"https://www.dingtalk.com/\"\n" +
" },\n" +
" {\n" +
" \"title\": \"时代的火车向前开2\", \n" +
" \"messageURL\": \"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\", \n" +
" \"picURL\": \"https://www.dingtalk.com/\"\n" +
" }\n" +
" ]\n" +
" }, \n" +
" \"msgtype\": \"feedCard\"\n" +
"}";
dealDingDingMsgSend(accessToken, textMsg);
}
/**
* 发送钉钉群消息(可以艾特人)- 技术专用
* @param content 发送内容
*/
public static void sendDingDingGroupMsg(String content){
sendDingDingGroupMsg(DingTokenEnum.SEND_SMS_BY_DEVELOPER_TOKEN.getToken(), content, DingMsgPhoneEnum.DEVELOPER_PHONE.getPhone());
}
public static void main(String[] args) {
// sendDingDingGroupMsg(DingTokenEnum.SEND_SMS_BY_DEVELOPER_TOKEN.getToken(), "【系统消息】钉钉消息推送测试,by:songfayuan...", DingMsgPhoneEnum.DEVELOPER_PHONE.getPhone());
// sendDingDingLinkGroupMsg(DingTokenEnum.SEND_SMS_BY_DEVELOPER_TOKEN.getToken(), "【系统消息】", "我喜欢,驾驭着代码在风驰电掣中创造完美!我喜欢,操纵着代码在随心所欲中体验生活!我喜欢,书写着代码在时代浪潮中完成经典!每一段新的代码在我手中诞生对我来说就象观看刹那花开的感动!", "https://blog.csdn.net/u011019141/static/img/homepage_icon_welcome.b870f20.jpg", "https://blog.csdn.net/u011019141");
String title = "杭州天气";
String text = "#### 杭州天气 @138*****41\n" +
"> 9度,西北风1级,空气良89,相对温度73%\n\n" +
"> \n" +
"> ###### 10点20分发布 [天气](http://www.weather.com.cn/weather/101210101.shtml) \n";
String atMobiles = "[138*****41,156*****57,150*****011]";
sendDingDingMarkdownGroupMsg(DingTokenEnum.SEND_SMS_BY_MARKDOWN_TOKEN.getToken(), DingTokenEnum.SEND_SMS_BY_MARKDOWN_TOKEN.getSecret(),title, text, atMobiles);
// sendDingDingActionCardGroupMsg(DingTokenEnum.SEND_SMS_BY_DEVELOPER_TOKEN.getToken(), "【系统消息】", "2323", "阅读全文", "https://blog.csdn.net/u011019141");
// sendDingDingActionCardGroupMsg2(DingTokenEnum.SEND_SMS_BY_DEVELOPER_TOKEN.getToken(), "【系统消息】", "2323", "阅读全文", "https://blog.csdn.net/u011019141");
// sendDingDingFeedCardGroupMsg(DingTokenEnum.SEND_SMS_BY_DEVELOPER_TOKEN.getToken(), "【系统消息】", "2323", "阅读全文", "https://blog.csdn.net/u011019141");
}
}
自定义群机器人webhook地址中access_token值的枚举类:
package com.github.collection.common.constant.enums;
import lombok.Getter;
/**
* 钉钉消息群机器人access_token、secret
*/
@Getter
public enum DingTokenEnum {
SEND_SMS_GROUP("替换成你自己机器人的access_token", "替换成你自己机器人的secret", "短信发送异常通知"),
SEND_SMS_BY_MARKET_IMPORT_TOKEN("替换成你自己机器人的access_token", "替换成你自己机器人的secret", "导入通知消息"),
SEND_SMS_BY_PRE_CASE_WARN_TOKEN("替换成你自己机器人的access_token","替换成你自己机器人的secret", "预警消息通知"),
SEND_SMS_BY_MARKDOWN_TOKEN("替换成你自己机器人的access_token", "替换成你自己机器人的secret", "系统消息通知,markdown类型"),
SEND_SMS_BY_DEVELOPER_TOKEN("替换成你自己机器人的access_token", "替换成你自己机器人的secret", "系统消息通知,技术专用");
private String token; //机器人 Webhook 地址中的 access_token
private String secret; //密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
private String name;
DingTokenEnum(String token, String secret, String name) {
this.token = token;
this.secret = secret;
this.name = name;
}
}
需要艾特人的手机号枚举类
注:手机号需是钉钉注册手机号
package com.github.****.common.constant.enums;
import lombok.Getter;
/**
* 钉钉消息接收用户,配置钉钉绑定的电话即可
*/
@Getter
public enum DingMsgPhoneEnum {
GENERAL_PURPOSE("138****0741,156****7257,150****8011,188****535", "通用(包含技术,产品,领导等)"),
DEVELOPER_PHONE("138****0741,156****7257,150****8011,188****3535", "技术人员"),
PRODUCT_PERSONNEL_PHONE("", "产品人员"),
DATA_ANALYST_PHONE("", "数据分析人员");
private String phone;
private String name;
DingMsgPhoneEnum(String phone, String name) {
this.phone = phone;
this.name = name;
}
}
最后,在你需要发送消息的地方,直接通过调用DingDingMsgSendUtils工具即可发送钉钉消息。
DingDingMsgSendUtils.sendDingDingGroupMsg(DingTokenEnum.SEND_SMS_BY_DEVELOPER_TOKEN.getToken(), "【系统消息】"+profile+"环境,XXX任务开始执行...", DingMsgPhoneEnum.DEVELOPER_PHONE.getPhone());
上面事例中的profile为环境参数,在Springboot和SpringCloud项目中,可以直接通过如下代码进行获取:
@Value("${spring.profiles.active}")
private String profile;
文中使用到的HttpUtil工具需引入资源:
<dependency>
<groupId>com.xiaoleilu</groupId>
<artifactId>hutool-all</artifactId>
<version>3.3.2</version>
</dependency>
延伸
以上采用的是***text类型***进行消息发送,想支持更多的发送格式,请移步到钉钉开发文档:https://open-doc.dingtalk.com/microapp/serverapi2/qf2nxq