|
@@ -0,0 +1,325 @@
|
|
|
|
+package com.backendsys.modules.dingtalk.utils;
|
|
|
|
+
|
|
|
|
+import cn.hutool.http.HttpUtil;
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
|
+import com.backendsys.modules.dingtalk.config.DingTalkConfig;
|
|
|
|
+import com.google.common.collect.Lists;
|
|
|
|
+import com.google.common.collect.Maps;
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
+import org.apache.commons.codec.binary.Base64;
|
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
|
+
|
|
|
|
+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.HashMap;
|
|
|
|
+import java.util.List;
|
|
|
|
+import java.util.Map;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 钉钉群机器人
|
|
|
|
+ * https://open.dingtalk.com/document/orgapp/robot-application-overview
|
|
|
|
+ * https://blog.csdn.net/weixin_44996457/article/details/128565224
|
|
|
|
+ *
|
|
|
|
+ * 使用示例:
|
|
|
|
+ * 单条消息:DingTalkUtil.testSendTextMsg();
|
|
|
|
+ * 带链接的消息:DingTalkUtil.testSendLinkMsg();
|
|
|
|
+ * 卡片式的消息 (单按钮:阅读全文):DingTalkUtil.testWholeSendActionCardMsg();
|
|
|
|
+ * 卡片式的消息 (多按钮:内容不错/不感兴趣):DingTalkUtil.testIndependentSendActionCardMsg();
|
|
|
|
+ */
|
|
|
|
+@Slf4j
|
|
|
|
+public class DingTalkUtil {
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 文本消息
|
|
|
|
+ */
|
|
|
|
+ public static void testSendTextMsg() {
|
|
|
|
+ try {
|
|
|
|
+ String message = "【系统环境】:正式服\n" +
|
|
|
|
+ "【异常接口】:/app/demo/demo\n" +
|
|
|
|
+ "【异常代码】:500\n" +
|
|
|
|
+ "【异常描述】:内部服务器错误\n" +
|
|
|
|
+ "【异常时间】:2024-10-31 13:45:00";
|
|
|
|
+ sendTextMsg(message, Lists.newArrayList(), Lists.newLinkedList(), false);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ System.out.println("钉钉群消息发送异常,异常原因:");
|
|
|
|
+ System.out.println(e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * link类型
|
|
|
|
+ */
|
|
|
|
+ public static void testSendLinkMsg() {
|
|
|
|
+ try {
|
|
|
|
+ sendLinkMsg("时代的火车向前开",
|
|
|
|
+ "这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林",
|
|
|
|
+ "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",
|
|
|
|
+ "");
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ System.out.println("钉钉群消息发送异常,异常原因:");
|
|
|
|
+ System.out.println(e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 整体跳转ActionCard类型
|
|
|
|
+ */
|
|
|
|
+ public static void testWholeSendActionCardMsg() {
|
|
|
|
+ try {
|
|
|
|
+ sendActionCardMsg("打造一间咖啡厅", " 乔布斯 20 年前想打造的苹果咖啡厅,Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划",
|
|
|
|
+ "0", "阅读全文", "https://www.dingtalk.com/");
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ System.out.println("钉钉群消息发送异常,异常原因:");
|
|
|
|
+ System.out.println(e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 独立跳转ActionCard类型
|
|
|
|
+ */
|
|
|
|
+ public static void testIndependentSendActionCardMsg() {
|
|
|
|
+ try {
|
|
|
|
+ List<Map<String, String>> btnsList = new ArrayList<>();
|
|
|
|
+ Map<String, String> map1 = new HashMap<>();
|
|
|
|
+ Map<String, String> map2 = new HashMap<>();
|
|
|
|
+ map1.put("title", "内容不错");
|
|
|
|
+ map1.put("messageURL", "https://www.dingtalk.com/");
|
|
|
|
+ map2.put("title", "不感兴趣");
|
|
|
|
+ map2.put("actionURL", "https://www.dingtalk.com/");
|
|
|
|
+ btnsList.add(map1);
|
|
|
|
+ btnsList.add(map2);
|
|
|
|
+ sendActionCardMsg("打造一间咖啡厅", " 乔布斯 20 年前想打造的苹果咖啡厅,Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划",
|
|
|
|
+ "0", btnsList);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ System.out.println("钉钉群消息发送异常,异常原因:{}");
|
|
|
|
+ System.out.println(e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 群里面发送消息
|
|
|
|
+ *
|
|
|
|
+ * @param content 消息内容
|
|
|
|
+ * @param isAtAll 是否@所有人
|
|
|
|
+ * @param mobileList 被@人的手机号
|
|
|
|
+ * @param userIdList 被@人的用户userid
|
|
|
|
+ * @throws Exception
|
|
|
|
+ */
|
|
|
|
+ public static void sendTextMsg(String content, List<String> mobileList, List<String> userIdList, boolean isAtAll) throws Exception {
|
|
|
|
+ String dingUrl = getDingUrl();
|
|
|
|
+ // 组装请求内容
|
|
|
|
+
|
|
|
|
+// // 被@人的手机号
|
|
|
|
+// mobileList.add("13537082474");
|
|
|
|
+
|
|
|
|
+ String reqStr = buildReqTextStr(content, isAtAll, mobileList, userIdList);
|
|
|
|
+ // 推送消息(http请求)
|
|
|
|
+ String result = HttpUtil.post(dingUrl, reqStr);
|
|
|
|
+ handleErrorCode(result);
|
|
|
|
+ System.out.println();
|
|
|
|
+ System.out.println("钉钉请求发送成功,返回结果:" + result);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 群里面发送消息
|
|
|
|
+ *
|
|
|
|
+ * @param title 消息标题
|
|
|
|
+ * @param messageUrl 点击消息跳转的URL
|
|
|
|
+ * @param picUrl 图片URL
|
|
|
|
+ * @param text 消息内容
|
|
|
|
+ * @throws Exception
|
|
|
|
+ */
|
|
|
|
+ public static void sendLinkMsg(String title, String text, String messageUrl, String picUrl) throws Exception {
|
|
|
|
+ String dingUrl = getDingUrl();
|
|
|
|
+ // 组装请求内容
|
|
|
|
+ String reqStr = buildReqLinkStr(title, text, messageUrl, picUrl);
|
|
|
|
+ // 推送消息(http请求)
|
|
|
|
+ String result = HttpUtil.post(dingUrl, reqStr);
|
|
|
|
+ handleErrorCode(result);
|
|
|
|
+ System.out.println("钉钉请求发送成功,返回结果:" + result);
|
|
|
|
+ }
|
|
|
|
+ /**
|
|
|
|
+ * 群里面发送消息
|
|
|
|
+ *
|
|
|
|
+ * @param title 首屏会话透出的展示内容
|
|
|
|
+ * @param text markdown格式的消息
|
|
|
|
+ * @param btnOrientation 0:按钮竖直排列 1:按钮横向排列
|
|
|
|
+ * @param singleTitle 单个按钮的标题
|
|
|
|
+ * @param singleURL 点击消息跳转的URL
|
|
|
|
+ * @throws Exception
|
|
|
|
+ */
|
|
|
|
+ public static void sendActionCardMsg(String title, String text, String btnOrientation, String singleTitle, String singleURL) throws Exception {
|
|
|
|
+ String dingUrl = getDingUrl();
|
|
|
|
+ // 组装请求内容
|
|
|
|
+ String reqStr = buildReqActionCard(title, text, btnOrientation, singleTitle, singleURL);
|
|
|
|
+ // 推送消息(http请求)
|
|
|
|
+ String result = HttpUtil.post(dingUrl, reqStr);
|
|
|
|
+ handleErrorCode(result);
|
|
|
|
+ System.out.println("钉钉请求发送成功,返回结果:" + result);
|
|
|
|
+ }
|
|
|
|
+ /**
|
|
|
|
+ * 群里面发送消息
|
|
|
|
+ *
|
|
|
|
+ * @param title 首屏会话透出的展示内容
|
|
|
|
+ * @param text markdown格式的消息
|
|
|
|
+ * @param btnOrientation 0:按钮竖直排列 1:按钮横向排列
|
|
|
|
+ * @param btnsList 按钮
|
|
|
|
+ * @throws Exception
|
|
|
|
+ */
|
|
|
|
+ public static void sendActionCardMsg(String title, String text, String btnOrientation, List<Map<String, String>> btnsList) throws Exception {
|
|
|
|
+ String dingUrl = getDingUrl();
|
|
|
|
+ // 组装请求内容
|
|
|
|
+ String reqStr = buildReqActionCard(title, text, btnOrientation, btnsList);
|
|
|
|
+ // 推送消息(http请求)
|
|
|
|
+ String result = HttpUtil.post(dingUrl, reqStr);
|
|
|
|
+ handleErrorCode(result);
|
|
|
|
+ System.out.println("钉钉请求发送成功,返回结果:" + result);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 组装请求报文-整体跳转ActionCard类型
|
|
|
|
+ *
|
|
|
|
+ * @param title 首屏会话透出的展示内容
|
|
|
|
+ * @param text markdown格式的消息
|
|
|
|
+ * @param btnOrientation 0:按钮竖直排列 1:按钮横向排列
|
|
|
|
+ * @param singleTitle 单个按钮的标题
|
|
|
|
+ * @param singleURL 点击消息跳转的URL
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private static String buildReqActionCard(String title, String text, String btnOrientation, String singleTitle, String singleURL) {
|
|
|
|
+ Map<String, String> actionCardMap = Maps.newHashMap();
|
|
|
|
+ actionCardMap.put("title", title);
|
|
|
|
+ actionCardMap.put("text", text);
|
|
|
|
+ actionCardMap.put("btnOrientation", btnOrientation);
|
|
|
|
+ actionCardMap.put("singleTitle", singleTitle);
|
|
|
|
+ actionCardMap.put("singleURL", singleURL);
|
|
|
|
+
|
|
|
|
+ Map<String, Object> reqMap = Maps.newHashMap();
|
|
|
|
+ reqMap.put("msgtype", "actionCard");
|
|
|
|
+ reqMap.put("actionCard", actionCardMap);
|
|
|
|
+
|
|
|
|
+ return JSONObject.toJSONString(reqMap);
|
|
|
|
+ }
|
|
|
|
+ /**
|
|
|
|
+ * 组装请求报文-独立跳转ActionCard类型
|
|
|
|
+ *
|
|
|
|
+ * @param title 首屏会话透出的展示内容
|
|
|
|
+ * @param text markdown格式的消息
|
|
|
|
+ * @param btnOrientation 0:按钮竖直排列 1:按钮横向排列
|
|
|
|
+ * @param btnsList 按钮
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private static String buildReqActionCard(String title, String text, String btnOrientation, List<Map<String, String>> btnsList) {
|
|
|
|
+ Map<String, Object> actionCardMap = Maps.newHashMap();
|
|
|
|
+ actionCardMap.put("title", title);
|
|
|
|
+ actionCardMap.put("text", text);
|
|
|
|
+ actionCardMap.put("btnOrientation", btnOrientation);
|
|
|
|
+ actionCardMap.put("btns", btnsList);
|
|
|
|
+
|
|
|
|
+ Map<String, Object> reqMap = Maps.newHashMap();
|
|
|
|
+ reqMap.put("msgtype", "actionCard");
|
|
|
|
+ reqMap.put("actionCard", actionCardMap);
|
|
|
|
+
|
|
|
|
+ return JSONObject.toJSONString(reqMap);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 组装请求报文-link类型
|
|
|
|
+ *
|
|
|
|
+ * @param title 消息标题
|
|
|
|
+ * @param text 消息内容
|
|
|
|
+ * @param messageUrl 点击消息跳转的URL
|
|
|
|
+ * @param picUrl 图片URL
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private static String buildReqLinkStr(String title, String text, String messageUrl, String picUrl) {
|
|
|
|
+ Map<String, String> linkMap = Maps.newHashMap();
|
|
|
|
+
|
|
|
|
+ linkMap.put("text", text);
|
|
|
|
+ linkMap.put("title", title);
|
|
|
|
+ linkMap.put("picUrl", picUrl);
|
|
|
|
+ linkMap.put("messageUrl", messageUrl);
|
|
|
|
+
|
|
|
|
+ Map<String, Object> reqMap = Maps.newHashMap();
|
|
|
|
+ reqMap.put("msgtype", "link");
|
|
|
|
+ reqMap.put("link", linkMap);
|
|
|
|
+
|
|
|
|
+ return JSONObject.toJSONString(reqMap);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取请求url
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private static String getDingUrl() throws Exception {
|
|
|
|
+ // 获取系统时间戳
|
|
|
|
+ Long timestamp = System.currentTimeMillis();
|
|
|
|
+ // 拼接
|
|
|
|
+ String stringToSign = timestamp + "\n" + DingTalkConfig.secret;
|
|
|
|
+ // 使用HmacSHA256算法计算签名
|
|
|
|
+ Mac mac = Mac.getInstance("HmacSHA256");
|
|
|
|
+ mac.init(new SecretKeySpec(DingTalkConfig.secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
|
|
|
+ byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
|
|
|
|
+ // 进行Base64 encode 得到最后的sign,可以拼接进url里
|
|
|
|
+ String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");//Base64.encodeBase64(signData)
|
|
|
|
+ // 钉钉机器人地址(配置机器人的webhook),为了让每次请求不同,避免钉钉拦截,加上时间戳
|
|
|
|
+ String dingUrl = DingTalkConfig.webhook + "×tamp=" + timestamp + "&sign=" + sign;
|
|
|
|
+ return dingUrl;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 组装请求报文-text类型
|
|
|
|
+ *
|
|
|
|
+ * @param content 消息内容
|
|
|
|
+ * @param isAtAll 是否@所有人
|
|
|
|
+ * @param mobileList 被@人的手机号
|
|
|
|
+ * @param atUserIds 被@人的用户userid
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ private static String buildReqTextStr(String content, boolean isAtAll, List<String> mobileList, List<String> atUserIds) {
|
|
|
|
+ Map<String, String> contentMap = Maps.newHashMap();
|
|
|
|
+ contentMap.put("content", content);
|
|
|
|
+
|
|
|
|
+ Map<String, Object> atMap = Maps.newHashMap();
|
|
|
|
+ atMap.put("isAtAll", isAtAll);
|
|
|
|
+ atMap.put("atMobiles", mobileList);
|
|
|
|
+ atMap.put("atUserIds", atUserIds);
|
|
|
|
+
|
|
|
|
+ Map<String, Object> reqMap = Maps.newHashMap();
|
|
|
|
+ reqMap.put("msgtype", "text");
|
|
|
|
+ reqMap.put("text", contentMap);
|
|
|
|
+ reqMap.put("at", atMap);
|
|
|
|
+
|
|
|
|
+ return JSONObject.toJSONString(reqMap);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * errcode处理
|
|
|
|
+ * @param resultStr
|
|
|
|
+ */
|
|
|
|
+ private static void handleErrorCode(String resultStr) {
|
|
|
|
+ if (StringUtils.isEmpty(resultStr)) {
|
|
|
|
+ throw new RuntimeException("返回结果为空");
|
|
|
|
+ }
|
|
|
|
+ JSONObject jsonObject = JSONObject.parseObject(resultStr);
|
|
|
|
+ if (310000 == jsonObject.getLong("errcode")) {
|
|
|
|
+ throw new RuntimeException("keywords not in content");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}
|