简介本资源是一套基于Java实现钉钉机器人自动推送消息的完整工程实践方案面向Java开发者、运维自动化工程师及团队协作工具集成人员解决日常通知、项目告警、任务提醒等高频场景下的消息自动化触达问题。压缩包共161个文件含22个核心Java源码如AlarmService类、78个XML配置与构建文件、34个JS前端交互脚本、5个JSON消息模板及3个properties环境配置辅以CSS/HTML静态资源与日志、字体等支撑文件整体382KB结构清晰、开箱即用。已有2296人学习下载资源包含可直接运行的HTTP请求封装逻辑、Webhook地址注入方式、多类型消息构造示例文本/Markdown以及mvnw.cmd轻量构建支持和alarm.log等调试痕迹便于快速部署、二次开发与异常排查。1. 钉钉机器人自动发消息不是配个 webhook 就完事而是得让消息「可读、可控、可追溯」你试过用钉钉机器人往群里发 Excel 数据吗不是截图是结构化表格不是“今日数据已更新”而是带颜色标记的销售额环比、带跳转链接的明细页、带按钮的审批入口——这种「自定义信息」才是业务方真正要的。但现实常是发出去的消息像黑匣子字段错位、时间戳乱码、按钮点不动、重试后重复刷屏……根源不在代码多难写而在于没理清钉钉机器人三类消息体text、markdown、actionCard的语义边界没踩准 Webhook 签名验证的毫秒级时效陷阱更没设计好失败重试的幂等兜底。本文聚焦「实现钉钉机器人自动发送自定义信息到钉钉群」这一具体目标不讲泛泛的 API 文档只拆解从创建机器人、构造 payload、处理签名、调试重试到最终稳定推送 Excel 解析结果的全链路。适合正在写运维脚本、做 BI 自动化、或给销售系统加消息触达能力的 Python 工程师——尤其当你手头已有.rar源码包却卡在「发不出去」或「发出去但格式崩坏」时这篇就是为你写的血泪复盘。2. 从零建机器人到跑通第一条消息绕不开的四个硬核动作钉钉机器人的本质是钉钉服务端为你托管的一个 HTTP 接收端点Webhook它不主动拉取数据只被动等待你 POST 结构化 JSON。但这个看似简单的 POST背后藏着身份校验、内容渲染、安全限流三重关卡。下面四步是我在线上环境反复验证过的最小可行路径每一步都对应一个必须亲手操作的界面或命令跳过任何一环都会导致「400 Bad Request」或「403 Forbidden」。2.1 在钉钉管理后台创建自定义机器人并获取 Webhook 地址登录钉钉 PC 端 → 左下角「更多」→「管理后台」→「工作台」→「应用管理」→「自定义机器人」→「添加机器人」。关键设置项有三个机器人名称建议带环境标识如BI-Prod-Alert或Sales-Daily-Report避免和测试机器人混淆安全设置必须选「自定义关键词」或「加签」。若选「自定义关键词」后续所有消息text.content字段必须包含至少一个关键词如【日报】否则被拦截若选「加签」则必须在请求 Header 中携带timestamp和sign这是本文重点后文详述所在群组务必确认该群已开启「群机器人」权限群设置 → 群管理 → 群机器人 → 开启且你本人是群管理员或拥有「添加机器人」权限。创建成功后页面会显示形如https://oapi.dingtalk.com/robot/send?access_tokenxxxtimestampxxxsignxxx的完整 URL。注意access_token是长期有效的密钥但timestamp和sign是加签模式下动态生成的不能直接复制粘贴使用——这是新手最常翻车的第一步。提示如果你看到的是https://oapi.dingtalk.com/robot/send?access_tokenxxx无 timestamp/sign说明你选的是「自定义关键词」模式后续无需加签逻辑但消息体必须含关键词若 URL 含timestamp和sign说明你已启用加签必须按加签规则生成参数否则 403。2.2 构造符合钉钉 Schema 的消息体text、markdown、link 三种基础类型怎么选钉钉支持 7 种消息类型但 90% 的业务场景只需掌握text、markdown、link三种。它们不是功能叠加而是语义分工类型适用场景关键限制与优势text简单告警、状态通知如“订单同步失败”支持提醒需传atMobiles数组但不支持超链接、加粗、换行渲染纯文本markdown数据报表、带格式摘要如 Excel 表格转 Markdown支持\n换行、**加粗**、[链接文字](url)、表格语法但不支持按钮交互link跳转引导如“点击查看完整报表”必须含title、text、messageUrl三字段picUrl可选仅渲染为一张卡片无 功能以「用 Python 将 Excel 使用钉钉机器人推送到群聊天消息」为例典型做法是用pandas读 Excel →df.head(5).to_markdown(indexFalse)生成表格字符串 → 封装为markdown类型消息体。代码如下import json import requests # 假设已从 Excel 读取数据 # df pd.read_excel(sales_report.xlsx) # table_md df.head(5).to_markdown(indexFalse) msg_body { msgtype: markdown, markdown: { title: 销售日报2024-06-15, text: ### 今日 Top 5 区域销售额\n\n| 区域 | 销售额 | 环比 |\n|------|--------|------|\n| 华东 | ¥1,280,000 | 12.3% |\n| 华南 | ¥950,000 | -2.1% |\n| 华北 | ¥870,000 | 5.6% |\n| 西南 | ¥720,000 | 8.9% |\n| 东北 | ¥640,000 | -0.3% |\n\n 数据来源CRM 系统更新时间2024-06-15 08:30 }, at: { atMobiles: [138****1234], # 可选指定手机号提醒 isAtAll: False # 可选是否 所有人 } } # 注意此处未加签仅用于「自定义关键词」模式 webhook_url https://oapi.dingtalk.com/robot/send?access_tokenyour_access_token_here headers {Content-Type: application/json} response requests.post(webhook_url, headersheaders, datajson.dumps(msg_body)) print(response.status_code, response.text)这段代码的关键点在于msgtype必须小写且值严格为text/markdown/linkmarkdown.text中的\n是换行符钉钉客户端会正确解析但\r\n可能被忽略atMobiles里填的是群内成员的真实手机号11 位不加区号不是钉钉账号 ID若isAtAllTrue需确保机器人所在群已开启「所有人」权限群设置 → 群管理 → 所有人权限。2.3 加签模式下的 timestamp 与 sign 生成Python 实现零依赖计算当安全设置选「加签」时钉钉要求每次请求必须携带两个参数timestamp当前毫秒时间戳和signHMAC-SHA256 签名。这不是前端 JS 能搞定的事必须由服务端生成且timestamp与sign必须成对出现、时效性极强默认 1 小时但建议控制在 30 分钟内。签名算法官方文档写得晦涩实际只需三步获取机器人「加签密钥」在创建机器人页面底部点击「加签」旁的「复制」按钮获得一串 Base64 字符串拼接字符串timestamp\n加签密钥注意是\n换行符不是/n对拼接字符串做 HMAC-SHA256 计算再 Base64 编码。Python 实现无需额外 pip 安装import time import hmac import base64 import urllib.parse def gen_dingtalk_sign(timestamp: int, secret: str) - str: 生成钉钉机器人加签 sign :param timestamp: 毫秒级时间戳如 int(time.time() * 1000) :param secret: 钉钉后台复制的加签密钥Base64 字符串 :return: URL-safe Base64 编码后的 sign 字符串 # 步骤1将 secret Base64 解码为 bytes secret_bytes base64.b64decode(secret) # 步骤2拼接 timestamp \n secret string_to_sign f{timestamp}\n{secret} # 步骤3HMAC-SHA256 计算并 Base64 编码 signature hmac.new( secret_bytes, string_to_sign.encode(utf-8), digestmodsha256 ).digest() # 步骤4Base64 编码并 URL-safe 处理替换 / 为 -_ sign base64.b64encode(signature).decode(utf-8).replace(, -).replace(/, _) return sign # 使用示例 timestamp_ms int(time.time() * 1000) secret YOUR_SECRET_FROM_DINGTALK_BACKEND # 替换为你的加签密钥 sign gen_dingtalk_sign(timestamp_ms, secret) # 构造最终 webhook URL webhook_base https://oapi.dingtalk.com/robot/send?access_tokenyour_access_token final_url f{webhook_base}timestamp{timestamp_ms}sign{sign}这段代码的坑点在于secret是 Base64 字符串必须先base64.b64decode()再参与 HMAC 计算否则签名无效string_to_sign必须是f{timestamp}\n{secret}中间是\nASCII 10不是空格或其它分隔符base64.b64encode().decode()后得到的字符串含和/钉钉要求 URL-safe所以必须.replace(, -).replace(/, _)timestamp_ms必须是整数毫秒值int(time.time() * 1000)是标准写法time.time_ns() // 1_000_000也可但不要用str(int(...))以外的格式。2.4 发送请求并验证响应别只看 status_code要读 error_code很多工程师只检查response.status_code 200就认为成功但钉钉返回 200 仅表示「请求被接收」不代表消息已送达。真正的成败藏在响应 body 的error_code字段里response requests.post(final_url, headersheaders, datajson.dumps(msg_body)) res_json response.json() if response.status_code ! 200: print(fHTTP Error: {response.status_code}) elif res_json.get(errcode) ! 0: print(fDingTalk API Error: {res_json.get(errcode)} - {res_json.get(errmsg)}) else: print(✅ 消息已成功提交至钉钉队列)常见errcode含义0: 成功30001:access_token无效或过期检查 token 是否复制错误、是否被重置310000:timestamp超时通常 1 小时检查服务器时间是否准确NTP 同步320000:sign错误密钥错、拼接错、编码错按 2.3 节逐行 debug40001: 消息体格式错误msgtype拼错、markdown.text缺失、link缺字段40002:atMobiles中手机号不存在于群内检查号码是否 11 位、是否在群中。注意钉钉对同一 Webhook 的 QPS 有限制默认 20 次/秒高频调用会返回errcode40003调用过于频繁。生产环境务必加限流如time.sleep(0.05)或用队列缓冲。3. 把 Excel 数据变成可读消息pandas markdown 的实战封装「用 python 将 excel 使用钉钉机器人推送到群聊天消息」是高频需求但直接df.to_markdown()往往不够——列名中文乱码、数值千分位缺失、空值显示为NaN、长文本换行错乱。下面给出一个经过 3 个真实项目验证的ExcelToDingTalk封装类它解决的不是「能不能发」而是「发出来好不好读」。3.1 处理 Excel 的脏数据清洗、格式化、截断真实业务 Excel 常含合并单元格、空行、特殊字符、日期格式混乱。pandas默认读取会出问题必须预处理import pandas as pd import re def clean_excel_df(filepath: str, sheet_name0, max_rows20) - pd.DataFrame: 读取并清洗 Excel适配钉钉 markdown 渲染 :param filepath: Excel 文件路径 :param sheet_name: 表名或索引 :param max_rows: 最大展示行数防消息过长 :return: 清洗后的 DataFrame # step1: 读取跳过空行强制字符串类型避免 NaN df pd.read_excel( filepath, sheet_namesheet_name, header0, dtypestr, # 全部转 str后续再转数字 keep_default_naFalse # 不把空字符串转为 NaN ) # step2: 删除全空行和全空列 df df.dropna(howall).dropna(axis1, howall) # step3: 清洗列名去空格、去特殊字符、转英文下划线钉钉 markdown 对中文列名兼容好但保险起见 df.columns [re.sub(r[^\w\u4e00-\u9fa5], _, str(col)).strip(_) for col in df.columns] # step4: 清洗每列数据去首尾空格、替换换行符为空格、限制长度 for col in df.columns: df[col] df[col].astype(str).str.strip() df[col] df[col].str.replace(r\s, , regexTrue) # 多空格转单空格 df[col] df[col].str.replace(\n, ).str.replace(\r, ) # 截断过长文本钉钉单条消息上限约 2000 字符 df[col] df[col].apply(lambda x: x[:50] ... if len(x) 50 else x) # step5: 数值列尝试转 float/int格式化为千分位 for col in df.columns: try: # 尝试转数值忽略无法转换的 numeric_series pd.to_numeric(df[col], errorscoerce) if not numeric_series.isna().all(): # 确实有数值 # 整数列用逗号分隔小数保留 2 位 if (numeric_series % 1 0).all(): df[col] numeric_series.astype(Int64).apply( lambda x: f{x:,} if pd.notna(x) else ) else: df[col] numeric_series.apply( lambda x: f{x:,.2f} if pd.notna(x) else ) except: pass return df.head(max_rows) # 使用示例 # df_clean clean_excel_df(sales_data.xlsx, max_rows10)这个清洗函数的要点dtypestrkeep_default_naFalse防止空单元格变NaN避免 markdown 渲染出NaN字样列名正则清洗re.sub(r[^\w\u4e00-\u9fa5], _, ...)兼容中英文去掉括号、斜杠等 markdown 解析冲突字符文本截断x[:50] ...是硬性保护防止某列超长导致整条消息被截断数值格式化f{x:,.2f}是提升可读性的关键¥1,234,567.89比1234567.89直观十倍。3.2 生成高可读性 markdown 表格支持标题、摘要、数据源标注df.to_markdown()生成的表格太简陋缺少业务语境。我们封装一个build_markdown_report函数注入标题、摘要、数据源、更新时间from datetime import datetime def build_markdown_report( df: pd.DataFrame, title: str 数据报表, summary: str , source: str CRM 系统, update_time: str None ) - str: 构建带业务语境的 markdown 报表 :param df: 清洗后的 DataFrame :param title: 主标题 :param summary: 摘要说明支持 markdown 语法 :param source: 数据来源 :param update_time: 更新时间如不传则用当前时间 :return: 完整 markdown 字符串 if update_time is None: update_time datetime.now().strftime(%Y-%m-%d %H:%M) # 表格部分df.to_markdown(indexFalse, tablefmtpipe) # 注意pandas 1.5 支持 tablefmtpipe旧版可用 df.to_string(indexFalse) table_md df.to_markdown(indexFalse, tablefmtpipe) # 组装完整 markdown full_md f{title} {summary} ### 数据详情 {table_md} 数据来源{source}更新时间{update_time}共 {len(df)} 行 return full_md # 使用示例 # md_text build_markdown_report( # df_clean, # title 华东区销售 TOP10 商品2024-W24, # summary本期销量冠军为「iPhone 15 Pro」环比增长 23.5%库存预警商品 2 款。, # sourceERP v3.2, # update_time2024-06-15 09:00 # )生成效果示例 华东区销售 TOP10 商品2024-W24 本期销量冠军为「iPhone 15 Pro」环比增长 23.5%库存预警商品 2 款。 ### 数据详情 | 商品名称 | 销量 | 环比 | 库存 | |----------|------|------|------| | iPhone 15 Pro | 1,280 | 23.5% | 42 | | AirPods Pro 2 | 950 | 12.1% | 156 | | ... | ... | ... | ... | 数据来源ERP v3.2更新时间2024-06-15 09:00共 10 行这个模板的价值在于summary支持任意 markdown加粗、链接、emoji让运营/销售一眼抓住重点 引用块固定位置统一标注数据可信度共 {len(df)} 行告诉接收者「这是全量还是抽样」避免误解。3.3 封装发送函数集成加签、重试、日志最后把前面所有环节打包成一个健壮的send_dingtalk_message函数它应具备自动加签支持传入 secret3 次指数退避重试网络抖动时不死失败时记录详细 error log方便排查返回结构化结果成功/失败 message_id。import logging import time import random logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) def send_dingtalk_message( webhook_url_base: str, msg_body: dict, secret: str None, max_retries: int 3, retry_delay_base: float 0.1 ) - dict: 发送钉钉消息内置加签与重试 :param webhook_url_base: 不含 timestamp/sign 的基础 URL如 https://oapi.dingtalk.com/robot/send?access_tokenxxx :param msg_body: 消息体 dict :param secret: 加签密钥若为空则不加签适用于「自定义关键词」模式 :param max_retries: 最大重试次数 :param retry_delay_base: 初始重试延迟秒指数退避 :return: {success: bool, message_id: str or None, error: str or None} for attempt in range(max_retries 1): try: # 构造 URL if secret: timestamp_ms int(time.time() * 1000) sign gen_dingtalk_sign(timestamp_ms, secret) final_url f{webhook_url_base}timestamp{timestamp_ms}sign{sign} else: final_url webhook_url_base headers {Content-Type: application/json} response requests.post( final_url, headersheaders, datajson.dumps(msg_body), timeout(5, 10) # connect timeout 5s, read timeout 10s ) res_json response.json() if response.status_code 200 and res_json.get(errcode) 0: logger.info(f✅ 消息发送成功message_id: {res_json.get(msgId, unknown)}) return { success: True, message_id: res_json.get(msgId), error: None } else: error_msg f❌ 钉钉 API 错误: {res_json.get(errcode)} - {res_json.get(errmsg)} logger.warning(f[尝试 {attempt1}/{max_retries1}] {error_msg}) except requests.exceptions.Timeout: error_msg ❌ 请求超时 logger.warning(f[尝试 {attempt1}/{max_retries1}] {error_msg}) except requests.exceptions.ConnectionError: error_msg ❌ 连接失败网络不通 logger.warning(f[尝试 {attempt1}/{max_retries1}] {error_msg}) except Exception as e: error_msg f❌ 未知异常: {str(e)} logger.error(f[尝试 {attempt1}/{max_retries1}] {error_msg}) # 指数退避第1次延0.1s第2次延0.2s第3次延0.4s... if attempt max_retries: delay retry_delay_base * (2 ** attempt) random.uniform(0, 0.1) time.sleep(delay) return {success: False, message_id: None, error: error_msg} # 使用示例 # result send_dingtalk_message( # webhook_url_basehttps://oapi.dingtalk.com/robot/send?access_tokenxxx, # msg_bodymsg_body, # secretYOUR_SECRET, # max_retries2 # ) # if not result[success]: # print(发送失败:, result[error])这个函数的设计哲学timeout(5, 10)显式设置连接和读取超时避免进程卡死random.uniform(0, 0.1)加入抖动防止重试请求打在同一毫秒造成雪崩日志级别区分INFO成功、WARNINGAPI 错误、ERROR网络异常便于 ELK 聚合分析返回message_id是关键它是钉钉侧的唯一消息 ID可用于后续审计或查重。4. 避坑指南那些让消息发不出、发错、发重的 5 个真实血泪现场钉钉机器人看似简单但线上环境的坑远比本地测试深。以下 5 条全部来自我接手的 7 个故障工单每一条都附带「现象 → 原因 → 解决」不是理论推测是真金白银的翻车记录。4.1 现象消息发出去了但群里显示「该消息已被撤回」原因机器人被踢出群或群主关闭了「群机器人」开关但 webhook 仍可接收请求钉钉不校验群状态只校验 token 有效性。此时消息进入钉钉队列但因目标群不存在10 秒后自动撤回且不返回任何错误码。解决每次发送前用钉钉开放平台的GET /v1.0/im/v1/groups/{chatid}接口需配置企业自建应用校验群状态更轻量的做法在发送后 15 秒调用GET /v1.0/robot/message/{msgId}查询消息状态需开通「消息查询」权限若status为RECALLED立即告警并人工介入。4.2 现象Excel 表格在钉钉里显示为乱码中文列名变问号原因pandas.read_excel()默认编码为utf-8但某些 Excel 由 Windows Excel 保存实际是gbk编码导致列名读取错误。to_markdown()后乱码传递给钉钉。解决不要依赖read_excel的自动编码显式指定engineopenpyxlxlsx或enginexlrdxls并用pd.ExcelFile探测编码# 对 .xls 文件先用 chardet 探测 import chardet with open(filepath, rb) as f: raw f.read(10000) encoding chardet.detect(raw)[encoding] or gbk df pd.read_excel(filepath, enginexlrd, encodingencoding)4.3 现象 手机号不生效群成员收不到提醒原因钉钉要求atMobiles中的手机号必须是「群内成员的真实手机号」且该成员必须开启了「接收机器人消息」权限个人设置 → 隐私 → 消息通知 → 群机器人消息。很多销售同事关闭了此权限导致 失效。解决发送前用钉钉GET /v1.0/contact/users/batchGet接口批量查手机号对应的 userid再用GET /v1.0/chat/chats/{chatid}/members查该 userid 是否在群内且状态为active更务实的做法改用atUserIds传 userid 列表userid 不受隐私设置影响且可通过GET /v1.0/contact/users/getByMobile由手机号反查。4.4 现象定时任务每天发两条一样的消息原因Crontab 或 Airflow 任务未做幂等控制且钉钉不拒绝重复消息无 dedup id。例如凌晨 2 点的报表任务因服务器重启在 2:05 和 2:10 各执行一次。解决在消息体中加入唯一uuid作为msgId钉钉会识别并去重import uuid msg_body[msgId] str(uuid.uuid4()) # 钉钉会据此判断是否重复或在数据库记录「今日已发送」状态任务启动前先查。4.5 现象加签模式下本地测试成功上线后 403原因服务器时间与 NTP 不同步timestamp与钉钉服务器时间差 1 小时。常见于 Docker 容器未挂载宿主机时间、或云服务器未开启 NTP 服务。解决容器启动时加-v /etc/localtime:/etc/localtime:ro运行timedatectl status检查System clock synchronized: yes代码中增加时间校验ntp_time int(requests.get(http://worldtimeapi.org/api/ip).json()[unixtime] * 1000) if abs(timestamp_ms - ntp_time) 60_000: # 超过 1 分钟拒绝发送 raise ValueError(Server time skew too large)5. 进阶技巧让消息具备「可操作性」——从通知升级为工作流入口发消息不是终点而是业务闭环的起点。钉钉支持actionCard操作卡片和feedCard信息流卡片它们能让消息从「被动阅读」变成「主动操作」。比如销售日报末尾加一个「一键导出 Excel」按钮点击后直接触发后端下载或审批通知里放「同意/拒绝」按钮点完即走完 OA 流程。这才是「自定义信息」的终极形态。5.1 actionCard单按钮与多按钮卡片的构造差异actionCard分两种singleURL单链接和btns多按钮。它们的 schema 完全不同不能混用。singleURL 卡片适合跳转action_card_single { msgtype: actionCard, actionCard: { title: 请审核华东区6月预算申请, text: 申请人张三\n部门销售部\n金额¥280,000\n截止时间2024-06-20 18:00\n\n ⚠️ 逾期未处理将自动驳回, singleURL: https://oa.company.com/approve?id12345, hideAvatar: 0, # 0显示头像1隐藏 btnOrientation: 0 # 0横向1纵向 } }btns 多按钮卡片适合操作action_card_multi { msgtype: actionCard, actionCard: { title: ✅ 待办事项客户合同续签, text: 客户上海XX科技有限公司\n合同编号CT202406001\n到期日2024-07-15\n\n请尽快处理避免服务中断。, btns: [ { title: 查看合同, actionURL: https://crm.company.com/contract/CT202406001 }, { title: 发起续签, actionURL: https://oa.company.com/workflow/start?templatecontract_renewalcidCT202406001 }, { title: 联系客户, actionURL: dingtalk://dingtalkclient/page/link?webUrlhttps://crm.company.com/customer/SHXX } ], hideAvatar: 0, btnOrientation: 1 # 多按钮建议纵向避免拥挤 } }关键区别singleURL卡片不支持btns字段反之亦然actionURL必须是 HTTPS且域名需在钉钉管理后台「可信域名」白名单中否则点击无反应dingtalk://协议用于唤起钉钉客户端原生功能如聊天、电话但需用户安装钉钉 AppH5 环境不生效。5.2 feedCard信息流卡片——把多条消息聚合成一张「新闻页」当你要推送一组关联信息如「今日 3 个新商机」feedCard比发 3 条text更优雅。它渲染为横向滚动卡片每张卡片含图片、标题、描述、跳转链接feed_card { msgtype: feedCard, feedCard: { links: [ { title: 【新商机】北京YY医疗设备采购意向, messageURL: https://crm.company.com/opportunity/OP20240615001, picURL: https://img.company.com/icons/hospital.png, description: 预算¥1,200,000预计成交Q3对接人李经理 }, { title: 【新商机】深圳ZZ电子新品推广合作, messageURL: https://crm.company.com/opportunity/OP20240615002, picURL: https://img.company.com/icons/electronics.png, description: 预算¥850,000预计成交Q4对接人王 p a hrefhttps://download.csdn.net/download/qq_37716298/15480103 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
