以下是使用 Spring Boot 对接钉钉发送消息的完整实现方案,包含多种消息类型(文本、链接、Markdown 等)的发送方式:
1. 准备工作
-
创建钉钉应用:
- 登录钉钉开放平台
- 创建企业内部应用(或第三方企业应用)
- 获取
AppKey
和AppSecret
- 开启「机器人」权限(如需使用自定义机器人则需单独配置)
-
安全配置:
- 设置IP白名单(重要!)
- 启用加签(可选,推荐生产环境使用)
2. 项目配置
2.1 application.yml
配置
bash
dingtalk:
app:
app-key: your_app_key
app-secret: your_app_secret
access-token-url: https://api.dingtalk.com/v1.0/oauth2/accessToken
robot-webhook: https://oapi.dingtalk.com/robot/send
2.2 配置类
less
@Data
@Configuration
@ConfigurationProperties(prefix = "dingtalk.app")
public class DingTalkConfig {
private String appKey;
private String appSecret;
private String accessTokenUrl;
private String robotWebhook;
}
3. AccessToken 管理
3.1 带缓存的Token管理
ini
@Component
public class DingTalkTokenManager {
private static final Logger logger = LoggerFactory.getLogger(DingTalkTokenManager.class);
@Autowired
private DingTalkConfig config;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String ACCESS_TOKEN_KEY = "dingtalk:access_token";
public String getAccessToken() {
String token = redisTemplate.opsForValue().get(ACCESS_TOKEN_KEY);
if (StringUtils.isNotBlank(token)) {
return token;
}
return refreshAccessToken();
}
private synchronized String refreshAccessToken() {
String url = config.getAccessTokenUrl();
Map<String, String> params = new HashMap<>();
params.put("appKey", config.getAppKey());
params.put("appSecret", config.getAppSecret());
try {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> request = new HttpEntity<>(params, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
JsonObject json = JsonParser.parseString(response.getBody()).getAsJsonObject();
String newToken = json.get("accessToken").getAsString();
int expiresIn = json.get("expireIn").getAsInt();
redisTemplate.opsForValue().set(
ACCESS_TOKEN_KEY,
newToken,
expiresIn - 300, // 提前5分钟过期
TimeUnit.SECONDS
);
return newToken;
} catch (Exception e) {
logger.error("刷新钉钉Token失败", e);
throw new RuntimeException("钉钉服务不可用");
}
}
}
4. 消息发送服务
4.1 通用消息发送基类
typescript
@Service
public class DingTalkService {
@Autowired
private DingTalkConfig config;
@Autowired
private DingTalkTokenManager tokenManager;
// 发送工作通知(需用户ID)
public void sendToUser(String userId, String msg) {
String accessToken = tokenManager.getAccessToken();
String url = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
Map<String, Object> params = new HashMap<>();
params.put("robotCode", config.getAppKey());
params.put("userIds", Collections.singletonList(userId));
params.put("msgKey", "sampleText");
params.put("msgParam", "{"content":"" + msg + ""}");
sendRequest(url, accessToken, params);
}
// 发送群机器人消息(无需用户ID)
public void sendRobotMessage(Object messageBody, String secret) {
long timestamp = System.currentTimeMillis();
String sign = generateSign(timestamp, secret);
String url = config.getRobotWebhook() + "?access_token=" + tokenManager.getAccessToken()
+ "×tamp=" + timestamp + "&sign=" + sign;
sendRequest(url, null, messageBody);
}
private void sendRequest(String url, String token, Object params) {
try {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
if (token != null) {
headers.set("x-acs-dingtalk-access-token", token);
}
HttpEntity<Object> request = new HttpEntity<>(params, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
handleResponse(response.getBody());
} catch (Exception e) {
throw new RuntimeException("钉钉消息发送失败", e);
}
}
// 生成加签(需要时使用)
private String generateSign(Long timestamp, String secret) {
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));
return URLEncoder.encode(new String(Base64.getEncoder().encode(signData)), "UTF-8");
}
private void handleResponse(String responseBody) {
JsonObject json = JsonParser.parseString(responseBody).getAsJsonObject();
if (json.has("errcode") && json.get("errcode").getAsInt() != 0) {
throw new RuntimeException("钉钉接口错误: " + responseBody);
}
}
}
5. 消息类型封装
5.1 文本消息
typescript
public class TextMessage {
private String msgtype = "text";
private TextContent text;
private AtInfo at;
public TextMessage(String content, List<String> atMobiles) {
this.text = new TextContent(content);
this.at = new AtInfo(atMobiles);
}
@Data
@AllArgsConstructor
private static class TextContent {
private String content;
}
@Data
@AllArgsConstructor
private static class AtInfo {
private List<String> atMobiles;
private boolean isAtAll = false;
}
}
5.2 Markdown消息
arduino
public class MarkdownMessage {
private String msgtype = "markdown";
private MarkdownContent markdown;
private AtInfo at;
public MarkdownMessage(String title, String text, List<String> atMobiles) {
this.markdown = new MarkdownContent(title, text);
this.at = new AtInfo(atMobiles);
}
@Data
@AllArgsConstructor
private static class MarkdownContent {
private String title;
private String text;
}
@Data
@AllArgsConstructor
private static class AtInfo {
private List<String> atMobiles;
private boolean isAtAll = false;
}
}
6. 控制器调用示例
less
@RestController
@RequestMapping("/dingtalk")
public class DingTalkController {
@Autowired
private DingTalkService dingTalkService;
// 发送文本消息
@PostMapping("/send-text")
public ResponseEntity<String> sendText(@RequestParam String userId) {
dingTalkService.sendToUser(userId, "您的订单已处理完成");
return ResponseEntity.ok("消息已发送");
}
// 发送机器人Markdown消息
@PostMapping("/send-markdown")
public ResponseEntity<String> sendMarkdown(@RequestParam String secret) {
MarkdownMessage message = new MarkdownMessage(
"项目通知",
"### 服务器状态告警\n> **CPU使用率**: 95%\n> **内存使用率**: 80%\n> 请及时处理!",
Collections.singletonList("18812345678")
);
dingTalkService.sendRobotMessage(message, secret);
return ResponseEntity.ok("机器人消息已发送");
}
}
7. 注意事项
-
消息类型限制:
- 单次消息最大长度:文本消息20000字符,Markdown消息5000字符
- 频率限制:每个机器人每分钟最多发送20条消息
-
安全策略:
- 推荐使用加签方式(与钉钉机器人设置一致)
- 敏感信息不要明文传输
-
用户识别:
- 通过手机号或用户ID发送需确保用户已在组织内
- 使用
userid
需先通过钉钉API获取用户信息
-
错误代码处理:
- 300001:无效的access_token
- 310000:消息内容超过长度限制
- 330001:机器人被禁用
8. 高级功能扩展
-
消息卡片:
arduinopublic class ActionCardMessage { private String msgtype = "actionCard"; private String title; private String text; private String singleTitle; private String singleURL; }
-
异步消息:
typescript@Async public void asyncSendMessage(String userId, String message) { dingTalkService.sendToUser(userId, message); }
-
消息模板:
typescriptpublic class TemplateMessage { private String msgtype = "template"; private String templateId; private Map<String, String> data; }
9. 完整调用流程
- 获取用户ID(通过钉钉API或扫码登录)
- 选择消息类型并构造消息体
- 获取有效的access_token
- 调用钉钉消息接口发送
- 处理发送结果(成功/失败)
通过以上步骤即可实现 Spring Boot 与钉钉的消息对接,建议根据实际业务需求选择适合的消息类型和安全策略。