在这篇博客中,我们将详细介绍如何使用 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 + "×tamp=" + 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;
}