概述
消息推送是企业内部应用与钉钉集成的核心功能之一。本文全面介绍钉钉消息推送的多种实现方式,包括接口推送和机器人推送,帮助开发者根据业务场景选择最合适的方案。
一、消息推送方式对比
钉钉提供两种主要的消息推送方式,适用于不同的业务场景:
| 对比项 | 企业内部应用推送 | 机器人Webhook推送 |
|---|---|---|
| 认证方式 | AccessToken | Webhook地址 |
| 推送目标 | 指定用户/部门/所有人 | 指定群聊 |
| 消息类型 | 丰富的业务消息类型 | 通知类消息 |
| 交互能力 | 支持回调交互 | 单向推送 |
| 频率限制 | 根据接口不同 | 每分钟20条 |
| 适用场景 | 审批通知、待办提醒 | 监控报警、群通知 |
二、企业内部应用消息推送
2.1 获取AccessToken
消息推送前必须先获取有效的AccessToken:
import requests
from datetime import datetime
import time
class DingTalkClient:
"""钉钉API客户端"""
def __init__(self, app_key, app_secret):
self.app_key = app_key
self.app_secret = app_secret
self.base_url = "https://api.dingtalk.com/v1.0"
self._token = None
self._token_expire_time = 0
def get_access_token(self):
"""
获取AccessToken,带缓存机制
注意:Token有效期为2小时,提前刷新避免过期
"""
now = time.time()
# 缓存的Token未过期(提前10分钟刷新)
if self._token and now < self._token_expire_time - 600:
return self._token
# 请求新的Token
url = "https://api.dingtalk.com/v1.0/oauth2/accessToken"
payload = {
"appKey": self.app_key,
"appSecret": self.app_secret
}
response = requests.post(url, json=payload)
result = response.json()
if result.get("accessToken"):
self._token = result["accessToken"]
self._token_expire_time = now + result.get("expireIn", 7200)
return self._token
raise Exception(f"获取AccessToken失败: {result}")
2.2 发送文本消息
向指定用户发送文本消息:
def send_text_to_user(self, user_id, content, agent_id):
"""
发送文本消息给指定用户
:param user_id: 用户ID
:param content: 消息内容
:param agent_id: 应用AgentID
"""
url = f"{self.base_url}/im/v1/messages"
# 消息ID用于幂等控制
message_id = f"{int(time.time() * 1000)}{user_id}"
headers = {
"x-acs-dingtalk-access-token": self.get_access_token(),
"Content-Type": "application/json"
}
payload = {
"robotCode": self.app_key, # 机器人编码
"userIdList": [user_id],
"msg": {
"msgType": "text",
"text": {
"content": content
}
}
}
response = requests.post(url, headers=headers, json=payload)
return response.json()
2.3 发送卡片消息
支持交互式卡片消息,可包含按钮:
def send_interactive_card(self, user_id, title, content, buttons):
"""
发送卡片消息
:param buttons: 按钮配置列表
"""
url = f"{self.base_url}/im/v1/messages"
headers = {
"x-acs-dingtalk-access-token": self.get_access_token(),
"Content-Type": "application/json"
}
# 构造卡片内容
card_content = {
"title": title,
"form": [
{"title": "内容", "value": content}
],
"actions": [
{
"name": btn["name"],
"actionType": btn.get("type", "openUrl"),
"actionUrl": btn.get("url", "")
}
for btn in buttons
],
"btnOrientation": "1" # 横向排列
}
payload = {
"robotCode": self.app_key,
"userIdList": [user_id],
"msg": {
"msgType": "interactive",
"interactive": card_content
}
}
response = requests.post(url, headers=headers, json=payload)
return response.json()
2.4 发送工作通知
支持向部门或所有员工发送消息:
def send_work_notification(self, content, dept_ids=None, user_ids=None):
"""
发送工作通知
:param content: 消息内容
:param dept_ids: 部门ID列表,传入则发送给部门内所有员工
:param user_ids: 用户ID列表
"""
url = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"
params = {"access_token": self.get_access_token()}
payload = {
"agent_id": self.agent_id,
"msg": {
"msgtype": "text",
"text": {
"content": content
}
}
}
# 设置接收范围
if dept_ids:
payload["dept_id_list"] = dept_ids
if user_ids:
payload["userid_list"] = user_ids
# 如果都没有设置,发送给全部
if not dept_ids and not user_ids:
payload["to_all_user"] = True
response = requests.post(url, params=params, json=payload)
return response.json()
三、机器人消息推送
3.1 支持的消息类型
通过机器人Webhook发送的消息类型:
| 消息类型 | 说明 | 典型应用 |
|---|---|---|
| text | 纯文本 | 简单通知 |
| markdown | Markdown格式 | 富文本通知 |
| link | 链接卡片 | 文章分享 |
| actionCard | 按钮卡片 | 需要用户操作 |
| feedCard | 图文列表 | 多内容聚合 |
3.2 统一发送函数
import json
import hmac
import hashlib
import base64
import urllib.parse
from datetime import datetime
class DingTalkBot:
"""钉钉群机器人客户端"""
def __init__(self, webhook_url, secret=None):
self.webhook_url = webhook_url
self.secret = secret
def _sign(self, timestamp):
"""生成签名"""
if not self.secret:
return "", timestamp
string_to_sign = f"{timestamp}\n{self.secret}"
hmac_code = hmac.new(
self.secret.encode('utf-8'),
string_to_sign.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
sign = base64.b64encode(hmac_code).decode('utf-8')
return urllib.parse.quote_plus(sign), timestamp
def send(self, payload):
"""
发送消息通用方法
:param payload: 消息体字典
"""
timestamp = str(int(datetime.now().timestamp() * 1000))
sign, timestamp = self._sign(timestamp)
url = f"{self.webhook_url}×tamp={timestamp}&sign={sign}"
response = requests.post(
url,
headers={'Content-Type': 'application/json'},
data=json.dumps(payload)
)
return response.json()
def send_text(self, content, at_phones=None, at_all=False):
"""发送文本消息"""
return self.send({
"msgtype": "text",
"text": {"content": content},
"at": {
"atMobiles": at_phones or [],
"isAtAll": at_all
}
})
def send_markdown(self, title, content):
"""发送Markdown消息"""
return self.send({
"msgtype": "markdown",
"markdown": {
"title": title,
"content": content
}
})
def send_link(self, title, text, url, pic_url=None):
"""发送链接消息"""
return self.send({
"msgtype": "link",
"link": {
"title": title,
"text": text,
"picUrl": pic_url or "",
"messageUrl": url
}
})
def send_action_card(self, title, content, btn_title, btn_url,
orientation="0", extra_btns=None):
"""
发送卡片消息
:param orientation: 0竖向,1横向
"""
card = {
"title": title,
"text": content,
"singleTitle": btn_title,
"singleURL": btn_url,
"btnOrientation": orientation
}
if extra_btns:
card["btns"] = extra_btns
return self.send({
"msgtype": "actionCard",
"actionCard": card
})
3.3 实际应用示例
部署通知场景
def send_deploy_notification(bot, deploy_info):
"""
发送部署通知
:param deploy_info: 包含项目名、环境、版本、状态等信息
"""
status_emoji = "✅" if deploy_info["status"] == "success" else "❌"
content = f"""
### {status_emoji} 项目部署通知
**项目名称**:{deploy_info['project']}
**部署环境**:{deploy_info['env']}
**部署版本**:{deploy_info['version']}
**部署状态**:{deploy_info['status'].upper()}
**部署人员**:{deploy_info['operator']}
**部署时间**:{deploy_info['time']}
{"**变更内容**:" + deploy_info.get('changes', '无') if deploy_info.get('changes') else ''}
"""
return bot.send_markdown("部署通知", content)
监控告警场景
def send_alert(bot, alert_data):
"""
发送监控告警
:param alert_data: 告警信息字典
"""
level = alert_data.get("level", "warning")
level_text = {"critical": "严重", "warning": "警告", "info": "通知"}
content = f"""
### {level_text.get(level, '通知')}告警
**告警名称**:{alert_data['name']}
**告警级别**:{level_text.get(level, '通知')}
**发生时间**:{alert_data['time']}
---
**告警详情**:
{alert_data.get('description', '无详细描述')}
**处理建议**:
{alert_data.get('suggestion', '请及时处理')}
"""
return bot.send_markdown(
f"{level_text.get(level, '通知')}告警:{alert_data['name']}",
content
)
四、高级功能
4.1 消息模板系统
from string import Template
class MessageTemplate:
"""消息模板管理器"""
TEMPLATES = {
"deploy": """### ${status_emoji} 项目部署通知
**项目名称**:${project}
**部署环境**:${env}
**部署版本**:${version}
**部署状态**:${status}
> 部署人员:${operator} | ${time}
""",
"alert": """### ${level_emoji} ${level_name}告警
**告警名称**:${name}
**发生时间**:${time}
${description}
**建议措施**:
${suggestion}
""",
"task": """### 新任务分配
**任务名称**:${title}
**负责人**:${assignee}
**截止时间**:${due_date}
${description}
[查看详情](${detail_url})
"""
}
@classmethod
def render(cls, template_name, **kwargs):
"""渲染消息模板"""
template = Template(cls.TEMPLATES.get(template_name, ""))
return template.safe_substitute(**kwargs)
4.2 消息队列集成
import redis
import json
import threading
class AsyncMessageQueue:
"""异步消息队列"""
def __init__(self, bot, redis_url="redis://localhost:6379/0"):
self.bot = bot
self.redis_client = redis.from_url(redis_url)
self.queue_name = "dingtalk:message:queue"
self.running = False
self.worker_thread = None
def push(self, msg_type, **kwargs):
"""添加消息到队列"""
message = {
"type": msg_type,
"data": kwargs,
"timestamp": time.time()
}
self.redis_client.rpush(
self.queue_name,
json.dumps(message, ensure_ascii=False)
)
def _worker(self):
"""队列处理worker"""
while self.running:
# 阻塞获取消息
result = self.redis_client.blpop(self.queue_name, timeout=1)
if result:
_, message_json = result
message = json.loads(message_json)
try:
if message["type"] == "text":
self.bot.send_text(**message["data"])
elif message["type"] == "markdown":
self.bot.send_markdown(**message["data"])
except Exception as e:
# 失败消息放入重试队列
self.redis_client.rpush(
"dingtalk:message:retry",
message_json
)
def start(self):
"""启动队列处理"""
self.running = True
self.worker_thread = threading.Thread(target=self._worker)
self.worker_thread.daemon = True
self.worker_thread.start()
def stop(self):
"""停止队列处理"""
self.running = False
if self.worker_thread:
self.worker_thread.join(timeout=5)
五、消息状态追踪
5.1 发送结果处理
def handle_send_result(result):
"""
处理消息发送结果
:param result: 发送接口返回结果
:return: (success, message)
"""
if not result:
return False, "网络请求失败"
errcode = result.get("errcode", result.get("code"))
if errcode == 0 or errcode == "0":
return True, "发送成功"
# 常见错误码处理
error_messages = {
40001: "AccessToken无效,请重新获取",
40014: "无效的AccessToken",
40078: "机器人未上线或已被移除",
40079: "机器人已下线",
43004: "授权码已过期",
60020: "发送目标不在应用可见范围内"
}
msg = error_messages.get(errcode, result.get("errmsg", "未知错误"))
return False, f"发送失败({errcode}): {msg}"
5.2 消息追踪表设计
CREATE TABLE dingtalk_message_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message_id VARCHAR(64) NOT NULL COMMENT '消息ID',
msg_type VARCHAR(32) NOT NULL COMMENT '消息类型',
target_type VARCHAR(16) COMMENT '发送目标类型: user/dept/all',
target_ids TEXT COMMENT '目标ID列表',
content TEXT COMMENT '消息内容摘要',
status VARCHAR(16) DEFAULT 'pending' COMMENT '状态: pending/success/failed',
error_code VARCHAR(32) COMMENT '错误码',
error_msg VARCHAR(255) COMMENT '错误信息',
send_time DATETIME COMMENT '发送时间',
callback_time DATETIME COMMENT '回调确认时间',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_message_id (message_id),
INDEX idx_status (status),
INDEX idx_send_time (send_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
六、最佳实践总结
6.1 方案选型决策树
6.2 关键建议
-
合理选择推送方式
- 群通知使用机器人
- 需要交互的审批、待办使用应用消息
-
重视频率控制
- 机器人每分钟20条限制
- 高频场景使用消息队列缓冲
-
做好异常处理
- Token过期自动刷新
- 发送失败重试机制
- 完整的日志记录
-
注意用户体验
- 避免深夜高频推送
- 消息内容简洁清晰
- 提供快捷操作入口