尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C#钉钉机器人消息推送实战:签名、消息构造与避坑指南
简介这是一份面向C#开发者的钉钉消息推送功能源码示例专门解决企业应用中接入钉钉机器人或应用消息通知的凭证获取与用户身份匹配问题。资源基于钉钉开放平台开发者应用演示如何通过AgentId、AppKey、AppSecret获取access_token并调用GetUser接口根据手机号获取userid适用于需要在内部系统、OA或管理后台中集成钉钉消息通知的团队。资源包共11个文件核心为3个C#源文件包含主要推送逻辑另有编译所需的两个DLLTopSdk.dll与Newtonsoft.Json.dll及配置说明XML同时附带Visual Studio解决方案文件、项目文件与缓存文件下载后可直接打开工程进行调试整体仅907KB结构紧凑。已有1700余人学习该资源适合初中级C#开发者快速理解钉钉开放接口调用流程并复制到实际项目中二次开发。1. 这个项目是干什么的一个 C# 实现的钉钉群机器人客户端凌晨两点某内部系统的批处理任务挂了没人知道生产环境磁盘冲到 95%告警邮件躺在没人读的邮箱里。最省事的解决办法就是往钉钉工作群推一条消息让所有人掏出手机就能看到。这个 C# 钉钉消息推送项目把“往钉钉群机器人发消息”做成了一套可直接用的源码只要配置 Webhook 地址和密钥几行代码就能把文本、Markdown、卡片消息推到指定群还能 成员、带跳转链接。它适合三类人想给内部系统加告警通道的运维开发、需要把构建结果同步到工作群的业务开发以及准备在自己的 C# 项目里接入钉钉能力的同学。这个项目把签名算法、消息构造、异常处理、单元测试一次补齐拿来就能跑也能拆出来并进自己的解决方案。2. 项目骨架与技术选型三个工程文件把职责拆清楚2.1 解压后看到的目录结构项目采用了我自己比较习惯的三层拆分方式打开解决方案后看到的是这样一组工程DingTalkPush.sln ├─ DingTalkPush.Core // 核心类库签名、消息模型、发送器 │ ├─ DingTalkRobot.cs │ ├─ Signer.cs │ ├─ Models/ │ │ ├─ DingTalkResponse.cs │ │ ├─ TextMessage.cs │ │ ├─ MarkdownMessage.cs │ │ └─ ActionCardMessage.cs │ └─ Exceptions/ │ └─ DingTalkApiException.cs ├─ DingTalkPush.App // 控制台演示程序读配置、发消息 │ ├─ Program.cs │ └─ appsettings.json └─ DingTalkPush.Tests // 单元测试签名向量、消息 JSON 结构 ├─ SignerTests.cs └─ MessageSerializationTests.csCore 不引用任何界面层App 只负责从配置文件读取参数然后调用 CoreTests 只关注 Core 的行为。这么拆的原因是告警推送这类功能最终大概率要被嵌进已有的后台服务、Windows 服务或者 Web API 里而不是永远跑在控制台程序中。把发送逻辑和入口程序切开将来别的项目引用 Core 编译出来的程序集就能用不用把整个演示程序一起带过去。依赖方面我用的目标框架是 .NET 6HTTP 请求走 HttpClientJSON 序列化优先用 System.Text.Json。代码里构造消息体时使用的是对象而不是预先把 JSON 写死成字符串这样做既兼容了项目里可能存在的 Newtonsoft.Json也保留了 System.Text.Json 的轻量特性。如果你还在维护 .NET Framework 4.x把 Core 的目标框架改回去HttpClient 换成 HttpWebRequest或者引入 System.Net.Http 包改动的范围集中在发送器内部不会影响消息模型和签名逻辑。2.2 核心类的职责边界这个项目真正核心的类只有四个我把它们各自的职责写在类注释里比任何 README 都管用。DingTalkRobot 是整个库的门面外部代码基本只和它打交道。它接收 Webhook 地址和加签密钥对外暴露 SendTextAsync、SendMarkdownAsync、SendActionCardAsync 三个方法内部再去拼 URL、算签名、发请求。调用方不需要关心签名是怎么算的也不需要理解钉钉接口的响应结构。Signer 是纯静态类只做一件事根据毫秒时间戳和 secret 计算 HMAC-SHA256 签名并完成 URL 编码。它不感知任何业务消息所以可以单独被单元测试覆盖这也是我后面做固定向量测试的基础。Models 目录下是一组消息类型定义对应钉钉自定义机器人接口里的 text、markdown、actionCard 等消息格式。每一类消息在序列化成 JSON 时都严格贴合接口要求的字段名比如 msgtype、text.content、markdown.text、at.atMobiles。DingTalkApiException 是自定义异常用来包装钉钉返回的 errcode 和 errmsg。上层代码只需要捕获这一个异常就能判断推送是否成功而不是去解析一段晦涩的响应字符串。为什么消息体不用字典而是用类我在开发中发现钉钉这种接口嵌套层级多、可空字段也多用字典写起来确实快但后面换一个消息类型就要重构一串字符串 Key编译器完全帮不上忙。定义成类字段拼错编译期就报错而且新增消息类型只是新增一个文件不影响已有调用方。2.3 与常见示例相比这个项目多做了哪几件事网上很多 C# 推送钉钉的示例都是“一个函数解决战斗”拿 HttpWebRequest 拼一段 JSON 发出去然后就不管返回值了。这个项目把这类示例经常忽略的三件事补齐了。第一HTTP 状态码不等于业务成功。钉钉接口在业务失败的时候也经常返回 HTTP 200真正的结果在响应体的 errcode 字段里。我在发送器里每次都会反序列化 DingTalkResponse只有 errcode 为 0 才视为成功否则抛异常。第二加签后的 URL 编码。官方文档示例里 sign 是 Base64 字符串拼接到 Query 上时必须做 URL 编码否则 Base64 里的加号在 URL 中会被解析成空格服务端算出来的签名跟你本地完全对不上。Signer 里显式调用了 Uri.EscapeDataString这一行就是很多踩坑帖的根源。第三发送超时和重试的边界。我在发送器里设置了合理的 HttpClient 超时时间并预留了重试入口但重试绝对不是无条件做——同一告警重复推送会刷屏这一块在第 5 章避坑里会详细说。3. 核心模块拆解签名、消息构造、发送的完整链路3.1 加签逻辑HMAC-SHA256 和时间戳的细节钉钉自定义机器人的加签安全设置要求在请求 URL 上带两个额外参数timestamp 和 sign。timestamp 是当前毫秒时间戳sign 是把“timestamp 换行符 secret”用 HMAC-SHA256 计算后做 Base64 的结果。核心代码就这几行public static class Signer { public static string ComputeSign(long timestamp, string secret) { string stringToSign timestamp \n secret; using var hmac new HMACSHA256(Encoding.UTF8.GetBytes(secret)); byte[] hashBytes hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); return Convert.ToBase64String(hashBytes); } public static string BuildSignedUrl(string webhook, string secret) { long timestamp DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); string sign ComputeSign(timestamp, secret); string separator webhook.Contains(?) ? : ?; return ${webhook}{separator}timestamp{timestamp}sign{Uri.EscapeDataString(sign)}; } }逻辑说明第一步把时间戳和 secret 用换行符拼成待签字符串这里必须用字符串类型的换行符 “\n”而不是运行时环境相关的换行常量否则在 Windows 和 Linux 下会算出不同的签名。第二步用 secret 作为 HMAC 算法的密钥注意这里不是把 secret 拼进待签内容而是当作密钥传入两件事一旦混了签名就废了。第三步做 Base64 转码最后通过 Uri.EscapeDataString 把 Base64 结果里的加号、等号、斜杠转成 URL 安全形式。参数说明secret 是创建机器人时得到的加签密钥形如一串随机字符timestamp 单位是毫秒不是秒我用 DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() 取值。钉钉服务端会校验时间窗口本地时间与服务器时间误差超过一小时就会拒绝请求这一点我放在避坑章节细说。3.2 三类消息体构造文本、Markdown、卡片在动手发消息之前先弄清楚三类消息分别该怎么选。我把它们整理成一个对照关系消息类型适用场景关键字段text简短提醒、单行状态text.contentmarkdown结构化告警、构建结果、报表markdown.title、markdown.textactionCard需要用户点击处理的告警actionCard.title、actionCard.btns文本消息是最低成本的告警载体结构最简单var payload new { msgtype text, text new { content 磁盘使用率超过 90%请检查 }, at new { atMobiles new[] { 13800000000 }, isAtAll false } };逻辑说明字段名必须保持小写System.Text.Json 的默认命名策略是原样输出所以匿名对象直接用和小写字段同名的属性最省事。atMobiles 传手机号数组isAtAll 控制是否 所有人。如果你只希望指定的人看到content 里最好同时带上对方的手机号或显示名否则在部分客户端版本里消息列表能看见但会话里被 的人显示不出来容易误以为功能有 bug。Markdown 消息适合结构化告警比如构建失败、发布结果var payload new { msgtype markdown, markdown new { title 构建失败告警, text ## 构建结果 \n- 项目某跨平台系统 \n- 分支release \n- 状态font color\red\失败/font \n[查看构建日志](https://ci.example.com/build/123) }, at new { atMobiles new[] { 13800000000 }, isAtAll false } };逻辑说明title 是钉钉会话列表里显示的消息摘要text 是完整 Markdown 正文两个字段都必填。文本里我用两个空格加换行实现 Markdown 换行因为钉钉客户端对单个换行的渲染不太稳定两个空格是兼容性最好的写法。actionCard 消息适合带操作按钮的场景例如“查看日志”“去处理”var payload new { msgtype actionCard, actionCard new { title 线上事故, text 订单接口超时率持续上升请尽快处理, btnOrientation 1, btns new[] { new { title 查看监控, actionURL https://monitor.example.com/orders }, new { title 进入聊聊, actionURL dingtalk://dingtalkclient/action/sendmsg?dingtalk_idxxx } } } };逻辑说明btnOrientation 为 “1” 表示按钮竖向排列“0” 是横向。actionURL 支持普通 HTTPS 链接也可以写钉钉客户端协议地址后者能直接拉起某个群聊或联系人。实际使用中我一般把“查看详情”放在第一位保证用户一眼看到主要操作。3.3 发送与响应处理把 errcode 当作第一判断标准发送逻辑统一走 HttpClient核心代码如下public async TaskDingTalkResponse SendAsync(object payload, CancellationToken ct default) { string url _signedUrlProvider.GetSignedUrl(); using var request new HttpRequestMessage(HttpMethod.Post, url); request.Content new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, application/json); using var response await _httpClient.SendAsync(request, ct); string body await response.Content.ReadAsStringAsync(ct); var result JsonSerializer.DeserializeDingTalkResponse(body) ?? throw new DingTalkApiException(响应为空); if (!response.IsSuccessStatusCode || result.Errcode ! 0) throw new DingTalkApiException(result.Errcode, result.Errmsg); return result; }逻辑说明这里把 HTTP 状态码和业务错误码分开判断两个条件任何一个不满足都抛异常。我在一线见过不少线上事故日志里明明显示 HTTP 200群里却没消息原因就是 errcode 非 0 的失败被调用方忽略。DingTalkResponse 只需要三个字段errcode、errmsg、messageIdmessageId 在后端排查时非常有价值。参数说明SendAsync 接收的 payload 可以是任何序列化后符合钉钉接口结构的类型这意味着后面扩展新的消息类型不需要修改发送器本身。HttpClient 我在上层用单例或者 IHttpClientFactory 管理避免频繁创建导致 socket 耗尽。重试逻辑我放在发送器外面由调用方根据业务场景决定因为同一告警的无脑重试很快就能刷爆一个群。4. 把项目跑起来从建群机器人到收到第一条消息4.1 在钉钉群里创建自定义机器人打开钉钉 PC 端进入目标群点击群设置里的“智能群助手”选择“添加机器人”找到“自定义”机器人并添加。安全设置这一步我建议直接选“加签”。添加完成后你会得到一个 Webhook 地址和密钥地址形如https://oapi.dingtalk.com/robot/send?access_tokenxxxxxxxx注意这个 Webhook 地址和 secret 只在添加成功时完整展示一次关掉窗口再进去就看不到 secret 了务必先复制保存。我通常把这一对密钥放进专门的文件里不提交到 Git 仓库。4.2 修改配置并运行演示程序appsettings.json 的配置结构如下{ DingTalk: { Webhook: https://oapi.dingtalk.com/robot/send?access_token你的token, Secret: 你的加签密钥, MessageType: markdown, AtMobiles: [ 13800000000 ] } }配置项说明配置项含义填写建议Webhook机器人完整的请求地址原样复制不要截断 access_tokenSecret加签用的密钥与 Webhook 一一对应别混用MessageType演示程序发送的消息类型支持 text、markdown、actionCardAtMobiles需要 的手机号列表可留空留空表示不 填写真实配置后执行dotnet run --project DingTalkPush.App如果你用的是 Visual Studio直接把 DingTalkPush.App 设为启动项目按 F5 运行即可。程序启动后会在控制台打印本次推送的响应内容看到 errcode 为 0 后回到钉钉群里看一眼应该已经有一条 Markdown 消息躺在里面了。4.3 如何确认不是“碰巧成功”想确认这条链路真正稳定可复现我会故意做两个负面测试。第一个把 Secret 改错一位再运行预期收到签名不匹配的报错第二个把 Webhook 里的 access_token 改成乱码预期收到 token 无效的错误。两个测试都报预期错误再把配置改回来跑通说明你的代码和配置都在真实地工作而不是消息被某个隐藏通道悄悄投递成功。如果你在运行阶段就遇到问题不要急着改代码。先看控制台打印的 errcode绝大多数配置问题都可以在钉钉官方错误码表里直接查到比如签名错误、token 不存在、消息内容不合法。日志里我特意把请求 URL 中的 timestamp 打了出来方便和本地时间做对比。5. 避坑记录签名不匹配、发了没反应、报警刷屏的根因5.1 “签名不匹配”先查这三处现象调用接口返回 errcode 310000错误信息里带“签名不匹配”。按顺序排查三处。第一确认计算签名时用的字符串是“时间戳 换行 secret”的顺序有人会下意识写成“secret 换行 时间戳”顺序反了必失败。第二确认拼接 URL 时 sign 有没有做 URL 编码。Base64 里的加号在 URL 中会被解析成空格服务端解码后跟你本地算出的签名完全对不上。第三确认 timestamp 用的是毫秒而且本地时间与钉钉服务器时间偏差不能超过一小时服务器时间慢了两分钟同样会被拒绝。解决先在本地用一个固定 timestamp 和 secret 算一遍签名再拿同样一组值构造完整 URL 发起调用这样能区分到底是算法问题还是时间问题。项目里的 SignerTests 就是干这件事的跑一遍固定向量对不上必然是代码被改坏了。5.2 HTTP 200 但群里没消息安全设置是叠加关系现象请求返回 errcode 0群聊里却毫无动静。这个坑我见过很多次典型场景是机器人同时开了“加签”和“关键字”两项安全设置调用方只在 URL 里带了签名消息内容却不包含任何关键字钉钉就把这条消息静默丢弃了。原因自定义机器人的安全设置不是三选一而是所有已开启的规则都要满足任何一个不通过消息都进不了群。解决回到智能群助手的机器人设置里确认为这个机器人开了哪些安全设置。如果要求的关键字是“告警”那么消息文本里必须真实出现“告警”两个字。另外如果开了 IP 白名单要确认发出请求的服务公网出口 IP 在白名单里很多开发环境的出口 IP 是浮动的换一个网络就出问题。还有一点容易混淆atMobiles 的 功能与安全设置是两条独立逻辑开了关键字但正文里没有关键字即使 了人也一样不推送。5.3 告警风暴是怎么刷爆群的现象网络抖动一次重试机制触发三次三条一模一样的告警同时出现在群里再赶上批量任务并发一个晚上群里上百条消息最后大家直接屏蔽这个群告警通道彻底失效。原因重试策略太激进把“推送失败重试”和“任务失败重试”混为一谈或者上游并发任务量大每一条都独立推送没有做任何聚合。解决我在发送器外层加了一个轻量去重同一告警消息的 DedupKey 在 60 秒窗口内只发送一次告警文本相同时合并成一条在内容里附上触发次数。压测的时候我还踩到过机器人每分钟 20 条消息的频率上限所以批量推送前必须先聚合而不是用 for 循环裸发。5.4 服务器时间偏差的隐蔽问题现象签名算法、密钥、URL 编码全都核对无误但每天总有几次推送失败日志时间集中在凌晨两点到四点之间白天正常。原因我遇到过一台内网服务器没有配置时间同步系统时钟慢慢漂移和钉钉服务器时间差一旦超过一小时加签就会被拒绝。因为签名错误和网络错误混在一起这种问题排查起来特别绕。解决给服务器配置 NTP 定期同步同时把每次请求的 timestamp 写进日志。发现失败时刻的时间戳比本地时间慢了一小时以上基本就能锁定是时钟漂移。另外在单元测试里固定 DateTimeOffset.UtcNow 的返回值避免测试结果跟着机器时间跑Signer 设计成纯静态方法就是为了方便测试替身注入固定时间。6. 二次开发把单次推送改成模板化告警通道6.1 新增一种消息类型改动控制在 30 行以内先别急着上策略模式钉钉的 link、feedCard 消息本质上都是一个匿名对象就能解决的问题。最直接的做法是在 Core 里加一个静态消息工厂每种类型对应一个方法调用方想换格式就换方法不用理解钉钉接口内部结构。例如新增 link 类型public static object BuildLink(string title, string text, string messageUrl, string picUrl null) { return new { msgtype link, link new { title, text, messageUrl, picUrl } }; }这个函数不需要改动 DingTalkRobot 的发送逻辑直接把它返回的对象传给 SendAsync 即可。我建议消息体构造全部走这类静态工厂避免调用方到处拼匿名对象将来钉钉接口字段发生变化时只改一处。6.2 封装成后台服务推送不再依赖人工触发一个自然的演进方向是把发送能力封装成后台队列消费者。业务模块只负责投递告警对象后台进程统一负责签名、限流、重试与去重批量告警被平滑地发出去。简化版的消费者核心代码如下while (await channel.Reader.WaitToReadAsync(ct)) { var alert await channel.Reader.ReadAsync(ct); if (_deduplicator.ShouldSend(alert.DedupKey)) { try { await _robot.SendMarkdownAsync(alert.Title, alert.Text, alert.AtMobiles, ct); } catch (DingTalkApiException ex) { _logger.LogError(ex, 钉钉推送失败 alertId:{AlertId}, alert.AlertId); } } }逻辑说明队列的写端是各业务模块读端每收到一条就尝试发送。ShouldSend 基于 DedupKey 做时间窗口去重异常被记录进日志而不是让后台服务崩溃。这样改造后上游即使一次性投递一百条告警队列消费者也能按自己的节奏发送不会把钉钉群瞬间刷爆。6.3 改造完怎么验证不炸群我每一次改完代码都会在测试群里用同一套配置跑一遍全量消息类型确认签名、消息格式、 功能都正常。然后再看一次程序日志里的 errcode和钉钉后台的消息记录对一遍。最后做一次故障演练把某条消息的 Webhook 故意改错确认异常捕获链路是通的。这个项目最麻烦的地方其实是“没有反馈”——发送成功了群里不一定有人注意失败了也常常悄无声息。所以我把所有推送结果都做成结构化日志任何一步失败都能在日志里找到对应的 errcode。从那以后我每次改完都先在测试群跑一遍完整流程再动线上群已经成了肌肉记忆希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Kafka与RabbitMQ选型实战:吞吐量、延迟、可靠性对比与决策指南

Kafka与RabbitMQ选型实战:吞吐量、延迟、可靠性对比与决策指南

消息中间件选型这件事,几乎每个后端团队在业务量涨到某个临界点的时候都会撞上。我经历过至少三次从零搭建消息链路的过程,有两次是RabbitMQ,一次是Kafka,还有一次是两个一起上——用RabbitMQ扛业务指令,用Kafka做数据…

📅 2026/10/20 1:24:10
Tailwind组件库实战:从原子类到主题定制与避坑指南

Tailwind组件库实战:从原子类到主题定制与避坑指南

简介:这是一份 Tailwind CSS 6.23 版本的组件库资源包,面向 Web 前端开发者与 UI 工程师,以 utility-first 为核心,汇总了高频组件、实用类和官方 Tailwind UI 最佳实践,帮助快速搭建响应式、无障碍的现代界面&#xf…

📅 2026/10/20 1:19:07
N_m3u8DL-RE流媒体下载工具实用指南:5分钟跑通 M3U8/MPD 点播与直播下载

N_m3u8DL-RE流媒体下载工具实用指南:5分钟跑通 M3U8/MPD 点播与直播下载

N_m3u8DL-RE流媒体下载工具实用指南:5分钟跑通 M3U8/MPD 点播与直播下载 【免费下载链接】N_m3u8DL-RE Cross-Platform, modern and powerful stream downloader for MPD/M3U8/ISM. English/简体中文/繁體中文. 项目地址: https://gitcode.com/GitHub_Trending/n…

📅 2026/10/20 1:19:07
MORE NEWS

更多资讯

📰

HyperDbg:Windows Ring0级内核调试代理原理与实践

简介:本资源为开源Ring0级内核调试器Hyperdbg的完整源码工程,面向Windows驱动开发者、系统安全研究员及操作系统学习者,用于深入理解与实践内核层调试技术,解决驱动开发调试、恶意代码行为分析、系统稳定性优化等高阶问题。压缩包…

📰

SootTest实战:Java静态分析从字节码到调用图与ICFG

简介:SootTest 是一份面向 Java 静态分析与编译优化学习者的实战示例资源,基于开源分析框架 Soot,演示如何生成调用图与过程间控制流程图(ICFG)。它适合已掌握 Java 基础、希望深入字节码分析与程序结构理解的开发者&a…

📰

AutoRemesher:3 步完成自动四边形网格重划分,高模秒变低模

AutoRemesher:3 步完成自动四边形网格重划分,高模秒变低模 【免费下载链接】autoremesher Automatic quad remeshing tool 项目地址: https://gitcode.com/GitHub_Trending/au/autoremesher AutoRemesher 是一款跨平台自动四边形网格重划分工具&a…

📰

GEFCOM2014-EPFL负荷数据集:电力时序建模的工业级实战指南

简介:本资源是面向数据科学学习者与电力行业建模人员的GEFCOM2014能源负荷预测实战包,聚焦R语言环境下的时间序列建模与预测全流程。资源提供EPFL参与构建的多地区小时级负荷数据集及配套R分析框架,覆盖数据加载、可视化(ggplot2/…

📰

智能瘦身系统怎么做?SpringBoot+小程序从零到一全流程拆解

在校招季和毕业季,我经常被问到同一个问题:“智能瘦身系统”这个题目到底该怎么做?后台挂Java和SpringBoot,前端接小程序,名字还挂着“智能”和“健康”,听起来像是个健康类APP,可拿到题目之后很…

📰

复杂网络核心参数与经典模型详解:从度分布到小世界网络

简介:复杂网络研究将万物抽象为节点与边的集合,度分布、平均路径长度、集聚系数和介数等基础指标构成量化分析的核心工具。从七桥问题到脑网络、基因调控与互联网基础设施,这一框架能够统一解释病毒传播、电网级联故障和语言演化等现象。通过…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬