钉钉消息推送完全指南

0 阅读7分钟

概述

消息推送是企业内部应用与钉钉集成的核心功能之一。本文全面介绍钉钉消息推送的多种实现方式,包括接口推送和机器人推送,帮助开发者根据业务场景选择最合适的方案。

一、消息推送方式对比

钉钉提供两种主要的消息推送方式,适用于不同的业务场景:

对比项企业内部应用推送机器人Webhook推送
认证方式AccessTokenWebhook地址
推送目标指定用户/部门/所有人指定群聊
消息类型丰富的业务消息类型通知类消息
交互能力支持回调交互单向推送
频率限制根据接口不同每分钟20条
适用场景审批通知、待办提醒监控报警、群通知

flowchart LR

二、企业内部应用消息推送

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纯文本简单通知
markdownMarkdown格式富文本通知
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}&timestamp={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 方案选型决策树

flowchart TD

6.2 关键建议

  1. 合理选择推送方式

    • 群通知使用机器人
    • 需要交互的审批、待办使用应用消息
  2. 重视频率控制

    • 机器人每分钟20条限制
    • 高频场景使用消息队列缓冲
  3. 做好异常处理

    • Token过期自动刷新
    • 发送失败重试机制
    • 完整的日志记录
  4. 注意用户体验

    • 避免深夜高频推送
    • 消息内容简洁清晰
    • 提供快捷操作入口