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

devtools/2025/1/8 2:51:19/

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

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


功能概述

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

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

实现步骤

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

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

完成后拿到Webhook

2. 配置依赖

在项目的 pom.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同步数据)

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

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

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

4. 配置markdown消息

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@AllArgsConstructorpublic class MarkDown {/*** 首屏会话透出的展示内容*/@NotBlank@Setterprivate String title = "客户信息";/*** markdown格式的消息* note: 如果钉钉配置中使用了keyword,那么消息体中必须包含keyword;* 如果使用了@功能,也要包含@人的手机号,格式为@150xxxxxxxx*/@NotBlankprivate 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@AllArgsConstructorpublic class AT {/*** 被@人的手机号* note: 在text内容里要有@人的手机号*/private String[] atMobiles;/*** 被@人的用户userid*/private String[] atUserIds;/*** 是否@所有人*/private boolean isAtAll;}
}

5. 创建服务实现类

服务类 SendDingServiceImpl 核心逻辑如下:

实现核心功能

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();}@Overridepublic 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

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;}

测试


http://www.ppmy.cn/devtools/148791.html

相关文章

26考研资料分享 百度网盘

26考研资料分享考研资料合集 百度网盘&#xff08;仅供参考学习&#xff09; 基础班&#xff1a; 通过网盘分享的文件&#xff1a;2026【考研英语】等3个文件 链接: https://pan.baidu.com/s/1Q6rvKop3sWiL9zBHs87kAQ?pwd5qnn 提取码: 5qnn --来自百度网盘超级会员v3的分享…

【Blackbox Exporter】prober.Handler源码详细分析

http.HandleFunc(path.Join(*routePrefix, "/probe"), func(w http.ResponseWriter, r *http.Request) {sc.Lock()conf : sc.Csc.Unlock()prober.Handler(w, r, conf, logger, rh, *timeoutOffset, nil, moduleUnknownCounter, allowedLevel)})我们了解到blackbox_ex…

在调用 borrowObject 方法时,Apache Commons Pool 会根据连接池的配置触发一系列相关的方法

在调用 borrowObject 方法时&#xff0c;Apache Commons Pool 会根据连接池的配置触发一系列相关的方法 1. GrpcChannel 的概念 GrpcChannel 是 gRPC 客户端与服务器之间通信的核心组件。它是基于 HTTP/2 的连接&#xff0c;支持多路复用&#xff0c;即通过单个通道可以发送多…

Leetcode 3409. Longest Subsequence With Decreasing Adjacent Difference

Leetcode 3409. Longest Subsequence With Decreasing Adjacent Difference 1. 解题思路2. 代码实现 题目链接&#xff1a;3409. Longest Subsequence With Decreasing Adjacent Difference 1. 解题思路 这一题做的很失败&#xff0c;虽然只是一个medium的题目&#xff0c;但…

掌控ctf-2月赛

没事干 随便刷刷题 1伪协议读取系统进程 源码 <?php highlight_file(__FILE__); require_once flag.php; if(isset($_GET[file])) {require_once $_GET[file]; } 伪协议读取flag.php&#xff0c;/proc/self指向当前进程的 exp ?filephp://filter/readconvert.base64…

初始值变量类型

状态名同步位置初始值变量类型不支持的UL刷新注意事项State父组件必填Object、classstring、number、boolean、enum类型&#xff0c;以及这些类型的数组。支持Date类型。对象的对象数组属性更新数组对象的属性更新 State装饰的变量必须初始化&#xff0c;否则编译期会报错。Sta…

PADS Layout 如何快速高效的学习,学习的重点,难点,目标

以最短的时间去操作PADS的流程,走线需要注意哪些规则? 布局需要注意哪些规范和规则要求? 开始布第一块的高速板子,肯定是有大把的问题的,这些问题就是在严格的行业规则和规范下面是不符合要求的。 我们一定要把PADS Layout的速度练习起来,到后面的话,就会越来越快,对…

3.1 vue基础1

template 和 render()函数 buildTemplate > render() js <template> <div class"data1">{{data1}}</div> </template> // 更改 render() { return ( <div class"data1">{ data1 }</div> ) } // 优化能力 - domdi…