使用 Spring Boot 实现钉钉消息发送消息

钉钉官方文档(Webhook同步数据)

在这篇博客中,我们将详细介绍如何使用 Spring Boot 集成钉钉机器人,构建一个发送钉钉消息的服务,并通过 OkHttp 实现 HTTP 请求,同时使用 Hutool 提供便捷的 POST 请求工具。


功能概述

本服务的主要功能是通过钉钉机器人接口发送 Markdown 格式的消息。

  • 自动化生成签名(sign)以保障接口安全。
  • 支持 @ 特定用户或全体成员。
  • 使用 OkHttpClient 发送 POST 请求。
  • 支持以 JSON 格式组织消息内容。

实现步骤

1. 新建群聊(添加机器人)

设置关键词keyword,加签获取secret

完成后拿到Webhook

2. 配置依赖

在项目的 pom.xml 文件中引入必要的依赖:

xml 复制代码
<dependencies>
  <!-- Spring Boot Starter -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <!-- Hutool 工具类 -->
  <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.10</version>
  </dependency>

  <!-- OkHttp -->
  <dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.11.0</version>
  </dependency>

  <!-- FastJSON 2 -->
  <dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.36</version>
  </dependency>
</dependencies>

3. 配置钉钉机器人参数

钉钉机器人支持text、link、markdown、actionCard、feedCard 这几种消息类型,本文使用的markdown, 如果需要使用其他类型可以查看官方文档钉钉官方文档(Webhook同步数据)

参数 参数类型 是否必填 说明
msgtype String 消息类型,此时固定为:markdown。
title String 首屏会话透出的展示内容。
text String markdown格式的消息。
atMobiles Array 在content里添加被@人的手机号。 提示:只有在群内的成员才可被@,非群内成员手机号会被脱敏
atUserIds Array 在content里添加被@人的用户userid。
isAtAll Boolean 是否@所有人。

application.yml 文件中配置钉钉机器人的参数:

yaml 复制代码
ding:
  webhook: "https://oapi.dingtalk.com/robot/send?access_token=add7e41373b48a88ae9be86a2065e525xxxxxxxxx"
  keyword: "收到客户提单"
  secret: "xxxxxxxxxxx9fe24ff0d72ccxxxxxxx"
  white-ip: []
  at-all: true
  at-user-ids: []
  at-mobiles: []

4. 配置markdown消息

java 复制代码
import cn.hutool.core.util.ArrayUtil;
import lombok.*;
import javax.validation.constraints.NotBlank;
import java.util.Arrays;

@Data
public class MarkDownMessage {

    private static String keyword = "[收到客户提单]";

    /**
     * 消息类型,固定为markdown
     */
    @Setter(AccessLevel.NONE)
    private final String msgtype = "markdown";

    private MarkDown markdown = new MarkDown();

    private AT at = new AT();

    /**
     * 消息内容
     */
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public class MarkDown {
        /**
         * 首屏会话透出的展示内容
         */
        @NotBlank
        @Setter
        private String title = "客户信息";
        /**
         * markdown格式的消息
         * note: 如果钉钉配置中使用了keyword,那么消息体中必须包含keyword;
         * 如果使用了@功能,也要包含@人的手机号,格式为@150xxxxxxxx
         */
        @NotBlank
        private String text;

        public void setText(String text) {
            StringBuilder sb = new StringBuilder();
            if (ArrayUtil.isNotEmpty(at.getAtMobiles())) {
                Arrays.stream(at.getAtMobiles()).forEach(sb::append);
            }
            sb.append("\n")
                    .append(keyword)
                    .append("\n")
                    .append(text);
            this.text = sb.toString();
        }

        public void setText(String serverName, String env, String url, String msg, String level,
                            String scene) {
            StringBuilder sb = new StringBuilder();
            if (ArrayUtil.isNotEmpty(at.getAtMobiles())) {
                Arrays.stream(at.getAtMobiles()).forEach(mobile -> {
                    sb.append("@")
                            .append(mobile);
                });
            }
            sb.append("\n")
                    .append(keyword)
                    .append("\n")
                    .append("客户信息:" + msg);

            this.text = sb.toString();
        }
    }

    /**
     * 使用@消息配置
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class AT {
        /**
         * 被@人的手机号
         * note: 在text内容里要有@人的手机号
         */
        private String[] atMobiles;
        /**
         * 被@人的用户userid
         */
        private String[] atUserIds;
        /**
         * 是否@所有人
         */
        private boolean isAtAll;
    }
}

5. 创建服务实现类

服务类 SendDingServiceImpl 核心逻辑如下:

实现核心功能

java 复制代码
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.domain.entity.sms.MessageReq;
import com.ruoyi.common.core.domain.model.msg.MarkDownMessage;
import com.ruoyi.common.core.domain.model.resp.DingRep;
import com.ruoyi.system.service.SendDingMsgService;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.URLEncoder;

@Slf4j
@Service
public class SendDingServiceImpl implements SendDingMsgService {

    @Value("${ding.webhook}")
    private String webhook;

    @Value("${ding.at-all}")
    private boolean isAll;

    @Value("${ding.at-user-ids}")
    private String[] ids;

    @Value("${ding.at-mobiles}")
    private String[] mobiles;

    @Value("${ding.secret}")
    private String secret;

    private final OkHttpClient okHttpClient;

    public SendDingServiceImpl() {
        okHttpClient = new OkHttpClient();
    }


    @Override
    public void send(MessageReq req) {

        String type = "application/json; charset=utf-8";

        MarkDownMessage markDownMsg = new MarkDownMessage();
        MarkDownMessage.AT at = markDownMsg.getAt();
        at.setAtAll(isAll);
        at.setAtMobiles(mobiles);
        at.setAtUserIds(ids);

        MarkDownMessage.MarkDown markdown = markDownMsg.getMarkdown();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("客户姓名", req.getName());
        jsonObject.put("联系方式", req.getPhone());
        jsonObject.put("公司名称", req.getCorporate());
        jsonObject.put("工作邮箱", req.getMailbox());
        jsonObject.put("需求信息", req.getDemand());
        markdown.setText("", "", "", jsonObject.toJSONString(), "", "");

        try {
            RequestBody body = RequestBody.create(MediaType.parse(type), JSONObject.toJSONString(markDownMsg));
            Request.Builder builder = new Request.Builder().url(webhook + "&timestamp=" + System.currentTimeMillis() + "&sign=" + getSign(secret));
            builder.addHeader("Content-Type", type).post(body);
            Request request = builder.build();
            Response response = okHttpClient.newCall(request).execute();
            String rep = response.body().string();
            DingRep dingRep = JSONObject.parseObject(rep, DingRep.class);
            if (dingRep.getErrcode() == 0 || dingRep.getErrmsg().equals("ok")) {
                log.info("钉钉发送消息成功,发送内容:{}", markDownMsg.toString());
            } else {
                log.warn("钉钉发送消息失败,发送内容:{}, 失败内容:{}", markDownMsg.toString(), dingRep.toString());
            }
        } catch (IOException e) {
            log.error("钉钉发送消息失败,失败内容:{}", e.getMessage());
        }
    }

    public String getSign(String secret) {

        String sign = null;
        try {

            Long timestamp = System.currentTimeMillis();
            String stringToSign = timestamp + "\n" + secret;
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
            byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
            sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");

        } catch (Exception e) {

            log.error("获取签名失败,失败内容:{}", e.getMessage());
        }

        return sign;
    }
}

6. MessageReq

java 复制代码
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MessageReq implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty("用户姓名")
    private String name;

    @ApiModelProperty("联系方式")
    private String phone;

    @ApiModelProperty("公司名称")
    private String corporate;

    @ApiModelProperty("工作邮箱")
    private String mailbox;

    @ApiModelProperty("需求")
    private String demand;

}

测试

相关推荐
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
dkbnull1 小时前
Spring Boot请求处理组件对比详解
java·spring boot
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
不能只会打代码1 小时前
Day 011 — Spring 全家桶深度拆解
spring boot·mybatis·spring aop·spring mvc·spring ioc
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构