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.0          http://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: 8080  servlet:    context-path: /spring:  application:    name: dingtalk-robot-demo  jackson:    time-zone: Asia/Shanghai    date-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: false        at-mobiles: []        at-user-ids: []      markdown:        title: 系统通知        text: ''      template-path: classpath:templates/dingtalk/# 应用配置app:  env: dev  name: DingTalk机器人服务  version: 1.0.0# 日志配置logging:  level:    com.example: debug  file:    name: logs/dingtalk-robot.log  pattern:    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();    @Data    public 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;    }    @Data    public 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";    }    @Data    public static class AtConfig {        /**         * 是否@所有人         */        private Boolean atAll = false;        /**         * 被@人的手机号         */        private List<String> atMobiles = List.of();        /**         * 被@人的用户ID         */        private List<String> atUserIds = List.of();    }    @Data    public 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 + "&timestamp=" + 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    @AllArgsConstructor    public static class Text {        private String content;    }    @Data    @Builder    @NoArgsConstructor    @AllArgsConstructor    public static class Markdown {        private String title;        private String text;    }    @Data    @Builder    @NoArgsConstructor    @AllArgsConstructor    public static class Link {        private String title;        private String text;        private String messageUrl;        private String picUrl;    }    @Data    @Builder    @NoArgsConstructor    @AllArgsConstructor    public 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        @AllArgsConstructor        public static class Button {            private String title;            private String actionURL;        }    }    @Data    @Builder    @NoArgsConstructor    @AllArgsConstructor    public static class FeedCard {        private List<LinkItem> links;        @Data        @Builder        @NoArgsConstructor        @AllArgsConstructor        public static class LinkItem {            private String title;            private String messageURL;            private String picURL;        }    }    @Data    @Builder    @NoArgsConstructor    @AllArgsConstructor    public 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;    }    @Override    public DingTalkResponse sendText(String content) {        return sendText(content, null, null, false);    }    @Override    public DingTalkResponse sendText(String content, List<String> atMobiles,                                     List<String> atUserIds, Boolean isAtAll) {        DingTalkMessage message = DingTalkMessage.text(content, atMobiles, atUserIds, isAtAll);        return sendMessage(message);    }    @Override    public DingTalkResponse sendMarkdown(String title, String text) {        return sendMarkdown(title, text, null, null, false);    }    @Override    public 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);    }    @Override    public DingTalkResponse sendLink(String title, String text, String messageUrl, String picUrl) {        DingTalkMessage message = DingTalkMessage.link(title, text, messageUrl, picUrl);        return sendMessage(message);    }    @Override    public 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);    }    @Override    public DingTalkResponse sendMessage(DingTalkMessage message) {        return sendToRobot("default", message);    }    @Override    public 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);        }    }    @Override    public 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);            // 获取签名的webhook            String 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 {    @Autowired    private 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);    }    // 请求参数类    @Data    public static class TextMessageRequest {        @NotBlank(message = "消息内容不能为空")        private String content;        private List<String> atMobiles;        private List<String> atUserIds;        private Boolean isAtAll = false;    }    @Data    public 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;    }    @Data    public static class LinkMessageRequest {        @NotBlank(message = "标题不能为空")        private String title;        @NotBlank(message = "内容不能为空")        private String text;        @NotBlank(message = "消息链接不能为空")        private String messageUrl;        private String picUrl;    }    @Data    public 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 {    @Autowired    private 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 {    @Autowired    private DingTalkRobotService dingTalkRobotService;    @Autowired    private 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:8080            API文档: 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 {    @Autowired    private DingTalkRobotService dingTalkRobotService;    @Test    void testSendText() {        DingTalkResponse response = dingTalkRobotService.sendText(            "测试文本消息\n当前时间: " + java.time.LocalDateTime.now()        );        System.out.println("文本消息发送结果: " + response.isSuccess());    }    @Test    void 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 {    @Autowired    private 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 -Xms256m    restart: unless-stopped

14.2 安全配置

在 application.yml 中添加安全配置:

# 安全配置security:  # 启用API密钥认证  api-key:    enabled: true    keys:      - name: admin        key: ${API_KEY:default-admin-key}        roles: ADMIN      - name: client        key: ${CLIENT_KEY:default-client-key}        roles: CLIENT# 限流配置rate-limit:  enabled: true  # 每分钟最大请求数  capacity: 100  # 令牌桶填充速率  refill-rate: 10  # 每次请求消耗令牌数  tokens-per-request: 1

15. 总结

这个完整的钉钉机器人集成方案包含以下特点:

  1. 多机器人支持:支持配置多个钉钉机器人

  2. 多种消息类型:支持文本、Markdown、Link、ActionCard等消息类型

  3. 消息模板:支持模板化消息,便于复用

  4. 签名安全:支持加签安全验证

  5. 异常告警:系统异常自动发送钉钉告警

  6. 系统监控:集成系统状态监控和定时报告

  7. 重试机制:消息发送失败自动重试

  8. RESTful API:提供完整的HTTP接口

  9. 完整日志:详细的日志记录

  10. Docker支持:支持容器化部署

你可以根据实际需求进行调整和扩展,如添加消息队列、数据库存储、更复杂的模板等。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐