18、Spring Boot 3.2.5 + JDK17 对接钉钉机器人发送消息完整实现
Spring Boot 3.2.5 + JDK17 对接钉钉机器人发送消息完整实现
1. 项目环境准备
1.1 创建 Spring Boot 项目
pom.xml 依赖配置:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.5</version><relativePath/></parent><groupId>com.example</groupId><artifactId>dingtalk-robot</artifactId><version>1.0.0</version><properties><java.version>17</java.version><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Boot Validation --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- Hutool工具包 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.27</version></dependency><!-- Jackson --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency><!-- Spring Boot Test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- 配置文件处理器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
2. 钉钉机器人配置
2.1 配置文件
application.yml:
server:port: 8080servlet:context-path: /spring:application:name: dingtalk-robot-demojackson:time-zone: Asia/Shanghaidate-format: yyyy-MM-dd HH:mm:ss# 钉钉机器人配置dingtalk:robot:# 默认机器人配置default:webhook: ${DINGTALK_WEBHOOK:}secret: ${DINGTALK_SECRET:}enabled: true# 多机器人配置robots:monitor:webhook: ${DINGTALK_MONITOR_WEBHOOK:}secret: ${DINGTALK_MONITOR_SECRET:}name: 监控机器人notify:webhook: ${DINGTALK_NOTIFY_WEBHOOK:}secret: ${DINGTALK_NOTIFY_SECRET:}name: 通知机器人alert:webhook: ${DINGTALK_ALERT_WEBHOOK:}secret: ${DINGTALK_ALERT_SECRET:}name: 告警机器人# 消息模板配置message:at:at-all: falseat-mobiles: []at-user-ids: []markdown:title: 系统通知text: ''template-path: classpath:templates/dingtalk/# 应用配置app:env: devname: DingTalk机器人服务version: 1.0.0# 日志配置logging:level:com.example: debugfile:name: logs/dingtalk-robot.logpattern:console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
2.2 配置类
DingTalkProperties.java:
package com.example.dingtalk.config;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;import java.util.List;import java.util.Map;@Data@Component@ConfigurationProperties(prefix = "dingtalk.robot")public class DingTalkProperties {/*** 默认机器人配置*/private RobotConfig defaultConfig = new RobotConfig();/*** 多机器人配置*/private Map<String, RobotConfig> robots;/*** 消息配置*/private MessageConfig message = new MessageConfig();@Datapublic static class RobotConfig {/*** Webhook地址*/private String webhook;/*** 加签密钥*/private String secret;/*** 机器人名称*/private String name = "默认机器人";/*** 是否启用*/private Boolean enabled = true;/*** 连接超时时间(毫秒)*/private Integer connectTimeout = 5000;/*** 读取超时时间(毫秒)*/private Integer readTimeout = 10000;}@Datapublic static class MessageConfig {/*** @配置*/private AtConfig at = new AtConfig();/*** Markdown配置*/private MarkdownConfig markdown = new MarkdownConfig();/*** 模板路径*/private String templatePath = "classpath:templates/dingtalk/";/*** 默认模板*/private String defaultTemplate = "default.json";}@Datapublic static class AtConfig {/*** 是否@所有人*/private Boolean atAll = false;/*** 被@人的手机号*/private List<String> atMobiles = List.of();/*** 被@人的用户ID*/private List<String> atUserIds = List.of();}@Datapublic static class MarkdownConfig {/*** 默认标题*/private String title = "系统通知";/*** 默认文本*/private String text = "";}}
3. 钉钉机器人核心工具类
3.1 签名工具
SignUtils.java:
package com.example.dingtalk.util;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.net.URLEncoder;import java.nio.charset.StandardCharsets;import java.util.Base64;/*** 钉钉机器人签名工具类*/public class SignUtils {private SignUtils() {throw new IllegalStateException("工具类不允许实例化");}/*** 生成签名* @param timestamp 时间戳* @param secret 密钥* @return 签名*/public static String generateSign(Long timestamp, String secret) {if (timestamp == null || secret == null || secret.isEmpty()) {return "";}try {String stringToSign = timestamp + "\n" + secret;Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));String sign = Base64.getEncoder().encodeToString(signData);return URLEncoder.encode(sign, StandardCharsets.UTF_8);} catch (Exception e) {throw new RuntimeException("生成签名失败", e);}}/*** 验证签名* @param timestamp 时间戳* @param secret 密钥* @param sign 待验证签名* @return 是否验证通过*/public static boolean verifySign(Long timestamp, String secret, String sign) {if (timestamp == null || secret == null || sign == null) {return false;}try {String generatedSign = generateSign(timestamp, secret);return generatedSign.equals(sign);} catch (Exception e) {return false;}}/*** 获取带签名参数的完整URL* @param webhook Webhook地址* @param secret 密钥* @return 带签名的URL*/public static String getSignedUrl(String webhook, String secret) {if (secret == null || secret.isEmpty()) {return webhook;}long timestamp = System.currentTimeMillis();String sign = generateSign(timestamp, secret);if (webhook.contains("?")) {return webhook + "×tamp=" + timestamp + "&sign=" + sign;} else {return webhook + "?timestamp=" + timestamp + "&sign=" + sign;}}}
3.2 HTTP客户端工具
HttpClientUtils.java:
package com.example.dingtalk.util;import lombok.extern.slf4j.Slf4j;import org.springframework.http.*;import org.springframework.stereotype.Component;import org.springframework.web.client.RestTemplate;import java.util.Map;import java.util.concurrent.TimeUnit;@Slf4j@Componentpublic class HttpClientUtils {private final RestTemplate restTemplate;public HttpClientUtils() {this.restTemplate = new RestTemplate();// 设置超时时间var requestFactory = new org.springframework.http.client.SimpleClientHttpRequestFactory();requestFactory.setConnectTimeout(5000);requestFactory.setReadTimeout(10000);restTemplate.setRequestFactory(requestFactory);}/*** 发送POST请求* @param url 请求地址* @param body 请求体* @return 响应体*/public String post(String url, Object body) {return post(url, body, String.class);}/*** 发送POST请求* @param url 请求地址* @param body 请求体* @param responseType 响应类型* @return 响应结果*/public <T> T post(String url, Object body, Class<T> responseType) {try {HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);headers.set("User-Agent", "Mozilla/5.0 DingTalk-Robot");HttpEntity<Object> requestEntity = new HttpEntity<>(body, headers);long startTime = System.nanoTime();ResponseEntity<T> response = restTemplate.exchange(url,HttpMethod.POST,requestEntity,responseType);long endTime = System.nanoTime();if (log.isDebugEnabled()) {long duration = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);log.debug("请求钉钉机器人完成,URL: {}, 耗时: {}ms", url, duration);}if (response.getStatusCode().is2xxSuccessful()) {return response.getBody();} else {log.error("钉钉机器人请求失败,状态码: {}", response.getStatusCode());throw new RuntimeException("请求失败,状态码: " + response.getStatusCode());}} catch (Exception e) {log.error("发送钉钉消息失败,URL: {}, 错误: {}", url, e.getMessage(), e);throw new RuntimeException("发送钉钉消息失败: " + e.getMessage(), e);}}/*** 发送POST请求(带重试)* @param url 请求地址* @param body 请求体* @param retryTimes 重试次数* @param retryDelay 重试延迟(毫秒)* @return 响应结果*/public String postWithRetry(String url, Object body, int retryTimes, long retryDelay) {Exception lastException = null;for (int i = 0; i <= retryTimes; i++) {try {if (i > 0) {log.warn("第{}次重试发送钉钉消息,URL: {}", i, url);Thread.sleep(retryDelay);}return post(url, body);} catch (Exception e) {lastException = e;if (i == retryTimes) {break;}}}throw new RuntimeException("发送钉钉消息失败,重试" + retryTimes + "次后仍然失败", lastException);}}
4. 消息模型定义
4.1 基础消息模型
DingTalkMessage.java:
package com.example.dingtalk.model;import com.fasterxml.jackson.annotation.JsonInclude;import com.fasterxml.jackson.annotation.JsonProperty;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import java.util.List;import java.util.Map;/*** 钉钉机器人消息基础模型*/@Data@Builder@NoArgsConstructor@AllArgsConstructor@JsonInclude(JsonInclude.Include.NON_NULL)public class DingTalkMessage {/*** 消息类型*/@JsonProperty("msgtype")private String msgType;/*** 文本消息*/private Text text;/*** Markdown消息*/private Markdown markdown;/*** Link消息*/private Link link;/*** ActionCard消息*/@JsonProperty("actionCard")private ActionCard actionCard;/*** FeedCard消息*/@JsonProperty("feedCard")private FeedCard feedCard;/*** @配置*/private At at;@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class Text {private String content;}@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class Markdown {private String title;private String text;}@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class Link {private String title;private String text;private String messageUrl;private String picUrl;}@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class ActionCard {private String title;private String text;private String singleTitle;private String singleURL;private String btnOrientation;private String hideAvatar;private List<Button> btns;@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class Button {private String title;private String actionURL;}}@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class FeedCard {private List<LinkItem> links;@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class LinkItem {private String title;private String messageURL;private String picURL;}}@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class At {@JsonProperty("atMobiles")private List<String> atMobiles;@JsonProperty("atUserIds")private List<String> atUserIds;@JsonProperty("isAtAll")private Boolean isAtAll = false;}/*** 快速创建文本消息*/public static DingTalkMessage text(String content) {return DingTalkMessage.builder().msgType("text").text(Text.builder().content(content).build()).build();}/*** 快速创建带@的文本消息*/public static DingTalkMessage text(String content, List<String> atMobiles, List<String> atUserIds, Boolean isAtAll) {return DingTalkMessage.builder().msgType("text").text(Text.builder().content(content).build()).at(At.builder().atMobiles(atMobiles).atUserIds(atUserIds).isAtAll(isAtAll).build()).build();}/*** 快速创建Markdown消息*/public static DingTalkMessage markdown(String title, String text) {return DingTalkMessage.builder().msgType("markdown").markdown(Markdown.builder().title(title).text(text).build()).build();}/*** 快速创建Link消息*/public static DingTalkMessage link(String title, String text, String messageUrl, String picUrl) {return DingTalkMessage.builder().msgType("link").link(Link.builder().title(title).text(text).messageUrl(messageUrl).picUrl(picUrl).build()).build();}}
4.2 钉钉响应模型
DingTalkResponse.java:
package com.example.dingtalk.model;import com.fasterxml.jackson.annotation.JsonProperty;import lombok.Data;import java.util.List;/*** 钉钉机器人响应模型*/@Datapublic class DingTalkResponse {/*** 错误码* 0: 成功* 其他: 失败*/@JsonProperty("errcode")private Integer errCode;/*** 错误信息*/@JsonProperty("errmsg")private String errMsg;/*** 是否成功*/public boolean isSuccess() {return errCode != null && errCode == 0;}}/*** 发送消息请求参数*/@Dataclass DingTalkRequest {/*** 消息类型*/@JsonProperty("msgtype")private String msgType;/*** 消息内容*/private Object content;/*** @配置*/private At at;}
5. 钉钉机器人服务
5.1 服务接口
DingTalkRobotService.java:
package com.example.dingtalk.service;import com.example.dingtalk.model.DingTalkMessage;import com.example.dingtalk.model.DingTalkResponse;import java.util.List;import java.util.Map;/*** 钉钉机器人服务接口*/public interface DingTalkRobotService {/*** 发送文本消息* @param content 消息内容* @return 发送结果*/DingTalkResponse sendText(String content);/*** 发送文本消息(带@)* @param content 消息内容* @param atMobiles 被@的手机号* @param atUserIds 被@的用户ID* @param isAtAll 是否@所有人* @return 发送结果*/DingTalkResponse sendText(String content, List<String> atMobiles, List<String> atUserIds, Boolean isAtAll);/*** 发送Markdown消息* @param title 标题* @param text 内容* @return 发送结果*/DingTalkResponse sendMarkdown(String title, String text);/*** 发送Markdown消息(带@)*/DingTalkResponse sendMarkdown(String title, String text, List<String> atMobiles,List<String> atUserIds, Boolean isAtAll);/*** 发送Link消息* @param title 标题* @param text 内容* @param messageUrl 消息链接* @param picUrl 图片链接* @return 发送结果*/DingTalkResponse sendLink(String title, String text, String messageUrl, String picUrl);/*** 发送ActionCard消息* @param title 标题* @param text 内容* @param singleTitle 按钮标题* @param singleURL 按钮链接* @return 发送结果*/DingTalkResponse sendActionCard(String title, String text, String singleTitle, String singleURL);/*** 发送自定义消息* @param message 消息对象* @return 发送结果*/DingTalkResponse sendMessage(DingTalkMessage message);/*** 发送模板消息* @param templateName 模板名称* @param params 模板参数* @return 发送结果*/DingTalkResponse sendTemplate(String templateName, Map<String, Object> params);/*** 发送消息到指定机器人* @param robotKey 机器人key* @param message 消息对象* @return 发送结果*/DingTalkResponse sendToRobot(String robotKey, DingTalkMessage message);}
5.2 服务实现
DingTalkRobotServiceImpl.java:
package com.example.dingtalk.service.impl;import com.example.dingtalk.config.DingTalkProperties;import com.example.dingtalk.model.DingTalkMessage;import com.example.dingtalk.model.DingTalkResponse;import com.example.dingtalk.service.DingTalkRobotService;import com.example.dingtalk.util.HttpClientUtils;import com.example.dingtalk.util.SignUtils;import com.fasterxml.jackson.core.type.TypeReference;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.core.io.ClassPathResource;import org.springframework.stereotype.Service;import org.springframework.util.StringUtils;import java.io.InputStream;import java.nio.charset.StandardCharsets;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.List;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;@Slf4j@Servicepublic class DingTalkRobotServiceImpl implements DingTalkRobotService {private final DingTalkProperties dingTalkProperties;private final HttpClientUtils httpClientUtils;private final ObjectMapper objectMapper;// 机器人配置缓存private final Map<String, DingTalkProperties.RobotConfig> robotConfigCache = new ConcurrentHashMap<>();public DingTalkRobotServiceImpl(DingTalkProperties dingTalkProperties,HttpClientUtils httpClientUtils,ObjectMapper objectMapper) {this.dingTalkProperties = dingTalkProperties;this.httpClientUtils = httpClientUtils;this.objectMapper = objectMapper;initRobotConfigs();}/*** 初始化机器人配置*/private void initRobotConfigs() {// 缓存默认机器人if (dingTalkProperties.getDefaultConfig() != null &&dingTalkProperties.getDefaultConfig().getEnabled()) {robotConfigCache.put("default", dingTalkProperties.getDefaultConfig());}// 缓存其他机器人if (dingTalkProperties.getRobots() != null) {dingTalkProperties.getRobots().forEach((key, config) -> {if (config.getEnabled()) {robotConfigCache.put(key, config);}});}log.info("已加载钉钉机器人配置: {}", robotConfigCache.keySet());}/*** 获取机器人配置* @param robotKey 机器人key* @return 机器人配置*/private DingTalkProperties.RobotConfig getRobotConfig(String robotKey) {DingTalkProperties.RobotConfig config = robotConfigCache.get(robotKey);if (config == null) {throw new IllegalArgumentException("未找到机器人配置: " + robotKey);}if (!StringUtils.hasText(config.getWebhook())) {throw new IllegalArgumentException("机器人webhook未配置: " + robotKey);}return config;}/*** 获取签名的Webhook URL* @param webhook 原始webhook* @param secret 密钥* @return 带签名的URL*/private String getSignedWebhook(String webhook, String secret) {if (StringUtils.hasText(secret)) {return SignUtils.getSignedUrl(webhook, secret);}return webhook;}@Overridepublic DingTalkResponse sendText(String content) {return sendText(content, null, null, false);}@Overridepublic DingTalkResponse sendText(String content, List<String> atMobiles,List<String> atUserIds, Boolean isAtAll) {DingTalkMessage message = DingTalkMessage.text(content, atMobiles, atUserIds, isAtAll);return sendMessage(message);}@Overridepublic DingTalkResponse sendMarkdown(String title, String text) {return sendMarkdown(title, text, null, null, false);}@Overridepublic DingTalkResponse sendMarkdown(String title, String text, List<String> atMobiles,List<String> atUserIds, Boolean isAtAll) {DingTalkMessage message = DingTalkMessage.builder().msgType("markdown").markdown(DingTalkMessage.Markdown.builder().title(title).text(text).build()).at(DingTalkMessage.At.builder().atMobiles(atMobiles).atUserIds(atUserIds).isAtAll(isAtAll).build()).build();return sendMessage(message);}@Overridepublic DingTalkResponse sendLink(String title, String text, String messageUrl, String picUrl) {DingTalkMessage message = DingTalkMessage.link(title, text, messageUrl, picUrl);return sendMessage(message);}@Overridepublic DingTalkResponse sendActionCard(String title, String text, String singleTitle, String singleURL) {DingTalkMessage message = DingTalkMessage.builder().msgType("actionCard").actionCard(DingTalkMessage.ActionCard.builder().title(title).text(text).singleTitle(singleTitle).singleURL(singleURL).build()).build();return sendMessage(message);}@Overridepublic DingTalkResponse sendMessage(DingTalkMessage message) {return sendToRobot("default", message);}@Overridepublic DingTalkResponse sendTemplate(String templateName, Map<String, Object> params) {try {String templateContent = loadTemplate(templateName);String messageJson = replaceTemplateVariables(templateContent, params);DingTalkMessage message = objectMapper.readValue(messageJson, DingTalkMessage.class);return sendMessage(message);} catch (Exception e) {log.error("发送模板消息失败,模板: {}", templateName, e);throw new RuntimeException("发送模板消息失败: " + e.getMessage(), e);}}@Overridepublic DingTalkResponse sendToRobot(String robotKey, DingTalkMessage message) {DingTalkProperties.RobotConfig config = getRobotConfig(robotKey);// 设置默认@配置if (message.getAt() == null) {message.setAt(DingTalkMessage.At.builder().atMobiles(dingTalkProperties.getMessage().getAt().getAtMobiles()).atUserIds(dingTalkProperties.getMessage().getAt().getAtUserIds()).isAtAll(dingTalkProperties.getMessage().getAt().getAtAll()).build());}try {// 转换为JSON字符串String jsonMessage = objectMapper.writeValueAsString(message);// 获取签名的webhookString signedWebhook = getSignedWebhook(config.getWebhook(), config.getSecret());// 发送消息String response = httpClientUtils.post(signedWebhook, message);// 解析响应DingTalkResponse dingTalkResponse = objectMapper.readValue(response, DingTalkResponse.class);if (dingTalkResponse.isSuccess()) {log.info("钉钉消息发送成功,机器人: {},消息类型: {}", config.getName(), message.getMsgType());} else {log.error("钉钉消息发送失败,机器人: {},错误码: {},错误信息: {}",config.getName(), dingTalkResponse.getErrCode(), dingTalkResponse.getErrMsg());}return dingTalkResponse;} catch (Exception e) {log.error("发送钉钉消息异常,机器人: {}", config.getName(), e);DingTalkResponse response = new DingTalkResponse();response.setErrCode(-1);response.setErrMsg("发送消息异常: " + e.getMessage());return response;}}/*** 加载模板文件* @param templateName 模板名称* @return 模板内容*/private String loadTemplate(String templateName) {try {String templatePath = dingTalkProperties.getMessage().getTemplatePath() + templateName;ClassPathResource resource = new ClassPathResource(templatePath);if (!resource.exists()) {// 尝试使用默认模板resource = new ClassPathResource(dingTalkProperties.getMessage().getTemplatePath() +dingTalkProperties.getMessage().getDefaultTemplate());}try (InputStream inputStream = resource.getInputStream()) {return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);}} catch (Exception e) {throw new RuntimeException("加载模板失败: " + templateName, e);}}/*** 替换模板变量* @param template 模板内容* @param params 参数* @return 替换后的内容*/private String replaceTemplateVariables(String template, Map<String, Object> params) {String result = template;if (params != null) {for (Map.Entry<String, Object> entry : params.entrySet()) {String key = "\\$\\{" + entry.getKey() + "\\}";String value = entry.getValue() != null ? entry.getValue().toString() : "";result = result.replaceAll(key, value);}}// 添加系统变量result = result.replaceAll("\\$\\{timestamp\\}",String.valueOf(System.currentTimeMillis()));result = result.replaceAll("\\$\\{datetime\\}",LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));return result;}/*** 发送消息到所有机器人* @param message 消息* @return 发送结果列表*/public Map<String, DingTalkResponse> broadcast(DingTalkMessage message) {Map<String, DingTalkResponse> results = new ConcurrentHashMap<>();robotConfigCache.forEach((key, config) -> {if (config.getEnabled()) {DingTalkResponse response = sendToRobot(key, message);results.put(key, response);}});return results;}/*** 发送带重试的消息* @param robotKey 机器人key* @param message 消息* @param retryTimes 重试次数* @param retryDelay 重试延迟(毫秒)* @return 发送结果*/public DingTalkResponse sendWithRetry(String robotKey, DingTalkMessage message,int retryTimes, long retryDelay) {Exception lastException = null;for (int i = 0; i <= retryTimes; i++) {try {if (i > 0) {log.warn("第{}次重试发送钉钉消息,机器人: {}", i, robotKey);Thread.sleep(retryDelay);}return sendToRobot(robotKey, message);} catch (Exception e) {lastException = e;if (i == retryTimes) {break;}}}DingTalkResponse response = new DingTalkResponse();response.setErrCode(-1);response.setErrMsg("发送消息失败,重试" + retryTimes + "次后仍然失败: " +(lastException != null ? lastException.getMessage() : "未知错误"));return response;}}
6. 模板消息示例
6.1 创建模板目录
在 src/main/resources/templates/dingtalk/目录下创建模板文件:
default.json(默认模板):
{"msgtype": "markdown","markdown": {"title": "系统通知","text": "**系统通知**\n\n- 时间: ${datetime}\n- 内容: ${content}\n- 状态: ${status}\n\n> 请及时处理!"},"at": {"isAtAll": false}}
alert.json(告警模板):
{"msgtype": "markdown","markdown": {"title": "🚨 系统告警","text": "### 🚨 ${level} 告警\n\n**告警信息**\n\n- 🔍 **服务**: ${service}\n- 📊 **指标**: ${metric}\n- 📈 **当前值**: ${value}\n- 📅 **时间**: ${datetime}\n- 📍 **主机**: ${host}\n- 📋 **详情**: ${detail}\n\n**建议操作**\n\n${suggestion}\n\n> 请立即处理!"},"at": {"atMobiles": ["${mobile}"],"atUserIds": ["${userId}"],"isAtAll": ${atAll}}}
system-status.json(系统状态模板):
{"msgtype": "markdown","markdown": {"title": "📊 系统状态报告","text": "### 📊 系统状态报告\n\n**基本信息**\n- 🏷️ **系统**: ${systemName}\n- 🏷️ **环境**: ${env}\n- 🏷️ **版本**: ${version}\n- 🏷️ **时间**: ${datetime}\n\n**系统资源**\n- 💾 **CPU使用率**: ${cpu}%\n- 🎯 **内存使用率**: ${memory}%\n- 💿 **磁盘使用率**: ${disk}%\n\n**应用状态**\n- ✅ **运行状态**: ${status}\n- ⏱️ **运行时长**: ${uptime}\n- 📦 **JVM内存**: ${jvmMemory}\n- 🎲 **活跃连接**: ${connections}\n\n**性能指标**\n- 🚀 **QPS**: ${qps}\n- ⏳ **平均响应时间**: ${avgResponseTime}ms\n- ❌ **错误率**: ${errorRate}%\n\n> 报告生成时间: ${datetime}"},"at": {"isAtAll": false}}
7. 控制器层
7.1 消息发送控制器
DingTalkController.java:
package com.example.dingtalk.controller;import com.example.dingtalk.model.DingTalkMessage;import com.example.dingtalk.model.DingTalkResponse;import com.example.dingtalk.service.DingTalkRobotService;import jakarta.validation.Valid;import jakarta.validation.constraints.NotBlank;import lombok.Data;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.ResponseEntity;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import java.util.List;import java.util.Map;@RestController@RequestMapping("/api/dingtalk")@Validatedpublic class DingTalkController {@Autowiredprivate DingTalkRobotService dingTalkRobotService;/*** 发送文本消息*/@PostMapping("/send/text")public ResponseEntity<DingTalkResponse> sendText(@RequestBody @Valid TextMessageRequest request) {DingTalkResponse response = dingTalkRobotService.sendText(request.getContent(),request.getAtMobiles(),request.getAtUserIds(),request.getIsAtAll());return ResponseEntity.ok(response);}/*** 发送Markdown消息*/@PostMapping("/send/markdown")public ResponseEntity<DingTalkResponse> sendMarkdown(@RequestBody @Valid MarkdownMessageRequest request) {DingTalkResponse response = dingTalkRobotService.sendMarkdown(request.getTitle(),request.getText(),request.getAtMobiles(),request.getAtUserIds(),request.getIsAtAll());return ResponseEntity.ok(response);}/*** 发送Link消息*/@PostMapping("/send/link")public ResponseEntity<DingTalkResponse> sendLink(@RequestBody @Valid LinkMessageRequest request) {DingTalkResponse response = dingTalkRobotService.sendLink(request.getTitle(),request.getText(),request.getMessageUrl(),request.getPicUrl());return ResponseEntity.ok(response);}/*** 发送ActionCard消息*/@PostMapping("/send/actioncard")public ResponseEntity<DingTalkResponse> sendActionCard(@RequestBody @Valid ActionCardMessageRequest request) {DingTalkResponse response = dingTalkRobotService.sendActionCard(request.getTitle(),request.getText(),request.getSingleTitle(),request.getSingleURL());return ResponseEntity.ok(response);}/*** 发送自定义消息*/@PostMapping("/send/message")public ResponseEntity<DingTalkResponse> sendMessage(@RequestBody DingTalkMessage message) {DingTalkResponse response = dingTalkRobotService.sendMessage(message);return ResponseEntity.ok(response);}/*** 发送模板消息*/@PostMapping("/send/template/{template}")public ResponseEntity<DingTalkResponse> sendTemplate(@PathVariable String template,@RequestBody Map<String, Object> params) {DingTalkResponse response = dingTalkRobotService.sendTemplate(template, params);return ResponseEntity.ok(response);}/*** 发送消息到指定机器人*/@PostMapping("/send/{robotKey}")public ResponseEntity<DingTalkResponse> sendToRobot(@PathVariable String robotKey,@RequestBody DingTalkMessage message) {DingTalkResponse response = dingTalkRobotService.sendToRobot(robotKey, message);return ResponseEntity.ok(response);}/*** 发送消息到指定机器人(带模板)*/@PostMapping("/send/{robotKey}/{template}")public ResponseEntity<DingTalkResponse> sendTemplateToRobot(@PathVariable String robotKey,@PathVariable String template,@RequestBody Map<String, Object> params) {DingTalkResponse response = dingTalkRobotService.sendToRobot(robotKey,DingTalkMessage.builder().build() // 这里需要根据模板创建消息);return ResponseEntity.ok(response);}/*** 快速发送简单文本消息*/@PostMapping("/quick")public ResponseEntity<DingTalkResponse> quickSend(@RequestParam @NotBlank String content) {DingTalkResponse response = dingTalkRobotService.sendText(content);return ResponseEntity.ok(response);}// 请求参数类@Datapublic static class TextMessageRequest {@NotBlank(message = "消息内容不能为空")private String content;private List<String> atMobiles;private List<String> atUserIds;private Boolean isAtAll = false;}@Datapublic static class MarkdownMessageRequest {@NotBlank(message = "标题不能为空")private String title;@NotBlank(message = "内容不能为空")private String text;private List<String> atMobiles;private List<String> atUserIds;private Boolean isAtAll = false;}@Datapublic static class LinkMessageRequest {@NotBlank(message = "标题不能为空")private String title;@NotBlank(message = "内容不能为空")private String text;@NotBlank(message = "消息链接不能为空")private String messageUrl;private String picUrl;}@Datapublic static class ActionCardMessageRequest {@NotBlank(message = "标题不能为空")private String title;@NotBlank(message = "内容不能为空")private String text;@NotBlank(message = "按钮标题不能为空")private String singleTitle;@NotBlank(message = "按钮链接不能为空")private String singleURL;}}
8. 系统监控集成示例
8.1 系统监控服务
SystemMonitorService.java:
package com.example.dingtalk.service;import com.example.dingtalk.model.DingTalkMessage;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Service;import java.lang.management.ManagementFactory;import java.lang.management.MemoryMXBean;import java.lang.management.MemoryUsage;import java.lang.management.OperatingSystemMXBean;import java.net.InetAddress;import java.net.NetworkInterface;import java.text.DecimalFormat;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;import java.util.concurrent.atomic.AtomicLong;@Slf4j@Servicepublic class SystemMonitorService {@Autowiredprivate DingTalkRobotService dingTalkRobotService;@Value("${app.name:未命名应用}")private String appName;@Value("${app.env:dev}")private String appEnv;@Value("${app.version:1.0.0}")private String appVersion;private final AtomicLong requestCount = new AtomicLong(0);private final AtomicLong errorCount = new AtomicLong(0);private long startTime = System.currentTimeMillis();/*** 获取系统信息*/public Map<String, Object> getSystemInfo() {Map<String, Object> info = new HashMap<>();// 系统信息OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();info.put("systemName", osBean.getName());info.put("systemVersion", osBean.getVersion());info.put("systemArch", osBean.getArch());info.put("availableProcessors", osBean.getAvailableProcessors());info.put("systemLoadAverage", formatDouble(osBean.getSystemLoadAverage()));// 内存信息MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();info.put("heapUsed", formatBytes(heapUsage.getUsed()));info.put("heapCommitted", formatBytes(heapUsage.getCommitted()));info.put("heapMax", formatBytes(heapUsage.getMax()));info.put("heapUsage", formatPercent((double) heapUsage.getUsed() / heapUsage.getCommitted()));info.put("nonHeapUsed", formatBytes(nonHeapUsage.getUsed()));info.put("nonHeapCommitted", formatBytes(nonHeapUsage.getCommitted()));info.put("nonHeapUsage", formatPercent((double) nonHeapUsage.getUsed() / nonHeapUsage.getCommitted()));// 运行时信息Runtime runtime = Runtime.getRuntime();info.put("totalMemory", formatBytes(runtime.totalMemory()));info.put("freeMemory", formatBytes(runtime.freeMemory()));info.put("maxMemory", formatBytes(runtime.maxMemory()));info.put("usedMemory", formatBytes(runtime.totalMemory() - runtime.freeMemory()));info.put("memoryUsage", formatPercent((double) (runtime.totalMemory() - runtime.freeMemory()) / runtime.maxMemory()));// 应用信息info.put("appName", appName);info.put("appEnv", appEnv);info.put("appVersion", appVersion);info.put("startTime", formatTime(startTime));info.put("uptime", formatDuration(System.currentTimeMillis() - startTime));info.put("requestCount", requestCount.get());info.put("errorCount", errorCount.get());info.put("errorRate", formatPercent(errorCount.get() * 1.0 / Math.max(requestCount.get(), 1)));// IP信息try {info.put("localIp", getLocalIp());info.put("hostname", InetAddress.getLocalHost().getHostName());} catch (Exception e) {info.put("localIp", "获取失败");info.put("hostname", "获取失败");}info.put("currentTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));return info;}/*** 记录请求*/public void recordRequest() {requestCount.incrementAndGet();}/*** 记录错误*/public void recordError() {errorCount.incrementAndGet();}/*** 发送系统状态报告*/public void sendSystemStatusReport() {try {Map<String, Object> systemInfo = getSystemInfo();String markdown = buildSystemStatusMarkdown(systemInfo);dingTalkRobotService.sendMarkdown("📊 系统状态报告", markdown);log.info("系统状态报告发送成功");} catch (Exception e) {log.error("发送系统状态报告失败", e);}}/*** 发送CPU告警*/public void sendCpuAlert(double cpuUsage, double threshold) {String title = "🚨 CPU告警";String markdown = String.format("""### 🚨 CPU使用率过高告警**告警信息**- 🔍 **服务**: %s- 📊 **指标**: CPU使用率- 📈 **当前值**: %.2f%%- ⚠️ **阈值**: %.2f%%- 📅 **时间**: %s- 📍 **主机**: %s**可能原因**1. 应用处理压力过大2. 存在死循环或资源泄漏3. 系统资源不足**建议操作**1. 检查应用日志2. 分析线程堆栈3. 考虑扩容4. 优化代码逻辑> 请立即处理!""",appName, cpuUsage, threshold,LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),getLocalIp());dingTalkRobotService.sendMarkdown(title, markdown);}/*** 构建系统状态Markdown*/private String buildSystemStatusMarkdown(Map<String, Object> info) {return String.format("""### 📊 系统状态报告**基本信息**- 🏷️ **系统**: %s- 🏷️ **环境**: %s- 🏷️ **版本**: %s- 🏷️ **主机**: %s- 🏷️ **时间**: %s**系统资源**- ⚙️ **CPU核心数**: %s- 📈 **系统负载**: %s- 🎯 **内存使用率**: %s**JVM内存**- 💾 **堆内存使用**: %s / %s (%s)- 💾 **非堆内存使用**: %s / %s (%s)- 💿 **总内存**: %s- 🆓 **空闲内存**: %s- 📊 **最大内存**: %s**应用状态**- ✅ **运行时长**: %s- 📦 **总请求数**: %s- ❌ **错误数**: %s- 📉 **错误率**: %s**网络信息**- 🌐 **本地IP**: %s- 💻 **主机名**: %s> 报告生成时间: %s""",info.get("appName"),info.get("appEnv"),info.get("appVersion"),info.get("hostname"),info.get("currentTime"),info.get("availableProcessors"),info.get("systemLoadAverage"),info.get("memoryUsage"),info.get("heapUsed"), info.get("heapCommitted"), info.get("heapUsage"),info.get("nonHeapUsed"), info.get("nonHeapCommitted"), info.get("nonHeapUsage"),info.get("totalMemory"),info.get("freeMemory"),info.get("maxMemory"),info.get("uptime"),info.get("requestCount"),info.get("errorCount"),info.get("errorRate"),info.get("localIp"),info.get("hostname"),info.get("currentTime"));}/*** 获取本地IP*/private String getLocalIp() {try {Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();while (interfaces.hasMoreElements()) {NetworkInterface networkInterface = interfaces.nextElement();if (networkInterface.isLoopback() || !networkInterface.isUp()) {continue;}Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();while (addresses.hasMoreElements()) {InetAddress addr = addresses.nextElement();if (!addr.isLoopbackAddress() && addr.getHostAddress().indexOf(':') == -1) {return addr.getHostAddress();}}}} catch (Exception e) {log.error("获取本地IP失败", e);}return "127.0.0.1";}/*** 格式化字节*/private String formatBytes(long bytes) {if (bytes < 1024) return bytes + " B";int exp = (int) (Math.log(bytes) / Math.log(1024));char pre = "KMGTPE".charAt(exp - 1);return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);}/*** 格式化百分比*/private String formatPercent(double value) {DecimalFormat df = new DecimalFormat("#0.00");return df.format(value * 100) + "%";}/*** 格式化时间*/private String formatTime(long timestamp) {return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}/*** 格式化持续时间*/private String formatDuration(long duration) {long seconds = duration / 1000;long days = seconds / (24 * 3600);long hours = (seconds % (24 * 3600)) / 3600;long minutes = (seconds % 3600) / 60;long secs = seconds % 60;if (days > 0) {return String.format("%d天%02d小时%02d分%02d秒", days, hours, minutes, secs);} else if (hours > 0) {return String.format("%02d小时%02d分%02d秒", hours, minutes, secs);} else if (minutes > 0) {return String.format("%02d分%02d秒", minutes, secs);} else {return String.format("%02d秒", secs);}}/*** 格式化小数*/private String formatDouble(double value) {if (Double.isNaN(value)) {return "N/A";}DecimalFormat df = new DecimalFormat("#0.00");return df.format(value);}/*** 定时发送系统状态报告(每小时一次)*/@Scheduled(cron = "0 0 * * * ?")public void scheduleSystemReport() {sendSystemStatusReport();}/*** 定时检查系统状态(每分钟一次)*/@Scheduled(cron = "0 * * * * ?")public void scheduleSystemCheck() {OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();double systemLoad = osBean.getSystemLoadAverage();int availableProcessors = osBean.getAvailableProcessors();// 如果系统负载超过CPU核心数的80%,发送告警double loadThreshold = availableProcessors * 0.8;if (systemLoad > 0 && systemLoad > loadThreshold) {sendCpuAlert(systemLoad, loadThreshold);}}}
9. 全局异常处理器
GlobalExceptionHandler.java:
package com.example.dingtalk.handler;import com.example.dingtalk.model.DingTalkMessage;import com.example.dingtalk.model.DingTalkResponse;import com.example.dingtalk.service.DingTalkRobotService;import com.example.dingtalk.service.SystemMonitorService;import jakarta.validation.ConstraintViolationException;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.validation.BindException;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.HashMap;import java.util.Map;import java.util.stream.Collectors;@Slf4j@RestControllerAdvicepublic class GlobalExceptionHandler {@Autowiredprivate DingTalkRobotService dingTalkRobotService;@Autowiredprivate SystemMonitorService systemMonitorService;/*** 处理验证异常*/@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class, ConstraintViolationException.class})public ResponseEntity<Map<String, Object>> handleValidationException(Exception e) {systemMonitorService.recordError();String message = "参数验证失败";if (e instanceof MethodArgumentNotValidException ex) {message = ex.getBindingResult().getFieldErrors().stream().map(error -> error.getField() + ": " + error.getDefaultMessage()).collect(Collectors.joining("; "));} else if (e instanceof BindException ex) {message = ex.getFieldErrors().stream().map(error -> error.getField() + ": " + error.getDefaultMessage()).collect(Collectors.joining("; "));} else if (e instanceof ConstraintViolationException ex) {message = ex.getConstraintViolations().stream().map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()).collect(Collectors.joining("; "));}Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.BAD_REQUEST.value());response.put("message", message);response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));return ResponseEntity.badRequest().body(response);}/*** 处理业务异常*/@ExceptionHandler(RuntimeException.class)public ResponseEntity<Map<String, Object>> handleBusinessException(RuntimeException e) {systemMonitorService.recordError();// 发送异常告警到钉钉sendExceptionAlert(e);Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());response.put("message", e.getMessage());response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);}/*** 处理其他异常*/@ExceptionHandler(Exception.class)public ResponseEntity<Map<String, Object>> handleOtherException(Exception e) {systemMonitorService.recordError();log.error("系统异常", e);// 发送异常告警到钉钉sendExceptionAlert(e);Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());response.put("message", "系统内部错误");response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);}/*** 发送异常告警*/private void sendExceptionAlert(Exception e) {try {String title = "🚨 系统异常告警";String markdown = String.format("""### 🚨 系统发生异常**异常信息**- 📅 **时间**: %s- 📍 **异常类型**: %s- 📋 **异常消息**: %s- 🔍 **异常堆栈**:%s**建议操作**1. 查看应用日志2. 检查相关服务3. 联系开发人员> 请立即处理!""",LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),e.getClass().getSimpleName(),e.getMessage(),getStackTrace(e));DingTalkResponse response = dingTalkRobotService.sendMarkdown(title, markdown);if (!response.isSuccess()) {log.error("发送异常告警失败: {}", response.getErrMsg());}} catch (Exception alertException) {log.error("发送异常告警时发生错误", alertException);}}/*** 获取堆栈跟踪*/private String getStackTrace(Exception e) {StackTraceElement[] stackTrace = e.getStackTrace();StringBuilder sb = new StringBuilder();for (int i = 0; i < Math.min(stackTrace.length, 10); i++) {sb.append(stackTrace[i].toString()).append("\n");}if (stackTrace.length > 10) {sb.append("... 更多\n");}return sb.toString();}}
10. 启动类
DingTalkRobotApplication.java:
package com.example.dingtalk;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication@EnableSchedulingpublic class DingTalkRobotApplication {public static void main(String[] args) {SpringApplication.run(DingTalkRobotApplication.class, args);System.out.println("""=========================================钉钉机器人服务启动成功!访问地址: http://localhost:8080API文档: http://localhost:8080/swagger-ui.html支持的接口:POST /api/dingtalk/send/text 发送文本消息POST /api/dingtalk/send/markdown 发送Markdown消息POST /api/dingtalk/send/link 发送Link消息POST /api/dingtalk/send/actioncard 发送ActionCard消息POST /api/dingtalk/send/message 发送自定义消息POST /api/dingtalk/send/template/{template} 发送模板消息POST /api/dingtalk/send/{robotKey} 发送消息到指定机器人POST /api/dingtalk/quick 快速发送文本消息环境变量配置:DINGTALK_WEBHOOK: 钉钉机器人Webhook地址DINGTALK_SECRET: 钉钉机器人加签密钥=========================================""");}}
11. 配置文件示例
.env.example(环境变量示例):
# 钉钉机器人配置DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=your_tokenDINGTALK_SECRET=your_secret# 多个机器人配置DINGTALK_MONITOR_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=monitor_tokenDINGTALK_MONITOR_SECRET=monitor_secretDINGTALK_NOTIFY_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=notify_tokenDINGTALK_NOTIFY_SECRET=notify_secretDINGTALK_ALERT_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=alert_tokenDINGTALK_ALERT_SECRET=alert_secret
12. 测试用例
DingTalkRobotServiceTest.java:
package com.example.dingtalk;import com.example.dingtalk.model.DingTalkMessage;import com.example.dingtalk.model.DingTalkResponse;import com.example.dingtalk.service.DingTalkRobotService;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.Arrays;import java.util.HashMap;import java.util.Map;@SpringBootTestclass DingTalkRobotServiceTest {@Autowiredprivate DingTalkRobotService dingTalkRobotService;@Testvoid testSendText() {DingTalkResponse response = dingTalkRobotService.sendText("测试文本消息\n当前时间: " + java.time.LocalDateTime.now());System.out.println("文本消息发送结果: " + response.isSuccess());}@Testvoid testSendMarkdown() {String markdown = """### 📊 测试Markdown消息- **项目**: 钉钉机器人测试- **时间**: %s- **状态**: ✅ 正常javapublic class Test {public static void main(String[] args) {System.out.println("Hello DingTalk!");}}> 这是一条测试消息""".formatted(java.time.LocalDateTime.now());DingTalkResponse response = dingTalkRobotService.sendMarkdown("测试Markdown消息",markdown);System.out.println("Markdown消息发送结果: " + response.isSuccess());}@Testvoid testSendLink() {DingTalkResponse response = dingTalkRobotService.sendLink("GitHub开源项目","Spring Boot钉钉机器人集成项目","https://github.com/your-repo/dingtalk-robot","https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png");System.out.println("Link消息发送结果: " + response.isSuccess());}@Testvoid testSendWithAt() {DingTalkResponse response = dingTalkRobotService.sendText("这是一条@某人的消息",Arrays.asList("13800138000"), // 被@的手机号Arrays.asList("user123"), // 被@的用户IDfalse // 是否@所有人);System.out.println("@消息发送结果: " + response.isSuccess());}@Testvoid testSendTemplate() {Map<String, Object> params = new HashMap<>();params.put("content", "这是一条模板消息测试");params.put("status", "测试中");params.put("mobile", "13800138000");DingTalkResponse response = dingTalkRobotService.sendTemplate("default.json", params);System.out.println("模板消息发送结果: " + response.isSuccess());}@Testvoid testSendToSpecificRobot() {DingTalkMessage message = DingTalkMessage.markdown("监控告警","### 🚨 监控告警\n\n- 服务: 订单服务\n- 时间: " + java.time.LocalDateTime.now() + "\n- 状态: ⚠️ 异常");DingTalkResponse response = dingTalkRobotService.sendToRobot("monitor", message);System.out.println("指定机器人发送结果: " + response.isSuccess());}}
13. 使用示例
13.1 通过HTTP接口调用
# 发送文本消息curl -X POST http://localhost:8080/api/dingtalk/send/text \-H "Content-Type: application/json" \-d '{"content": "这是一条测试消息","atMobiles": ["13800138000"],"isAtAll": false}'# 发送Markdown消息curl -X POST http://localhost:8080/api/dingtalk/send/markdown \-H "Content-Type: application/json" \-d '{"title": "系统通知","text": "### 系统通知\\n- 时间: 2024-01-01 12:00:00\\n- 内容: 系统测试\\n- 状态: ✅ 正常","atMobiles": ["13800138000"]}'# 发送模板消息curl -X POST http://localhost:8080/api/dingtalk/send/template/alert \-H "Content-Type: application/json" \-d '{"level": "ERROR","service": "用户服务","metric": "响应时间","value": "2.5s","host": "server-01","detail": "接口响应时间超过阈值","suggestion": "检查数据库连接和索引","mobile": "13800138000","atAll": false}'
13.2 Java代码调用
@Servicepublic class MyService {@Autowiredprivate DingTalkRobotService dingTalkRobotService;public void sendOrderCreatedNotification(Order order) {String markdown = String.format("""### 🎉 新订单通知**订单信息**- 📦 **订单号**: %s- 👤 **用户**: %s- 💰 **金额**: ¥%.2f- 📅 **时间**: %s- 📍 **状态**: %s**商品清单**%s> 请及时处理订单!""",order.getOrderNo(),order.getUserName(),order.getAmount(),order.getCreateTime(),order.getStatus(),buildProductList(order.getProducts()));DingTalkResponse response = dingTalkRobotService.sendMarkdown("新订单通知",markdown);if (!response.isSuccess()) {// 记录日志或重试逻辑}}public void sendSystemAlert(String service, String metric, String value, String detail) {Map<String, Object> params = new HashMap<>();params.put("level", "ERROR");params.put("service", service);params.put("metric", metric);params.put("value", value);params.put("host", getHostName());params.put("detail", detail);params.put("suggestion", "检查相关服务日志和监控");params.put("mobile", "13800138000");params.put("atAll", false);DingTalkResponse response = dingTalkRobotService.sendTemplate("alert.json", params);if (!response.isSuccess()) {// 发送失败,记录日志}}private String buildProductList(List<Product> products) {return products.stream().map(p -> String.format("- %s × %d", p.getName(), p.getQuantity())).collect(Collectors.joining("\n"));}}
14. 部署和配置
14.1 Docker部署
Dockerfile:
FROM openjdk:17-jdk-slimWORKDIR /appCOPY target/dingtalk-robot-1.0.0.jar app.jarEXPOSE 8080ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml:
version: '3.8'services:dingtalk-robot:build: .ports:- "8080:8080"environment:- DINGTALK_WEBHOOK=${DINGTALK_WEBHOOK}- DINGTALK_SECRET=${DINGTALK_SECRET}- JAVA_OPTS=-Xmx512m -Xms256mrestart: unless-stopped
14.2 安全配置
在 application.yml 中添加安全配置:
# 安全配置security:# 启用API密钥认证api-key:enabled: truekeys:- name: adminkey: ${API_KEY:default-admin-key}roles: ADMIN- name: clientkey: ${CLIENT_KEY:default-client-key}roles: CLIENT# 限流配置rate-limit:enabled: true# 每分钟最大请求数capacity: 100# 令牌桶填充速率refill-rate: 10# 每次请求消耗令牌数tokens-per-request: 1
15. 总结
这个完整的钉钉机器人集成方案包含以下特点:
-
多机器人支持:支持配置多个钉钉机器人
-
多种消息类型:支持文本、Markdown、Link、ActionCard等消息类型
-
消息模板:支持模板化消息,便于复用
-
签名安全:支持加签安全验证
-
异常告警:系统异常自动发送钉钉告警
-
系统监控:集成系统状态监控和定时报告
-
重试机制:消息发送失败自动重试
-
RESTful API:提供完整的HTTP接口
-
完整日志:详细的日志记录
-
Docker支持:支持容器化部署
你可以根据实际需求进行调整和扩展,如添加消息队列、数据库存储、更复杂的模板等。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)