ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

钉钉服务端API工作通知实践:从access_token缓存到Zabbix告警联动

钉钉服务端API工作通知实践:从access_token缓存到Zabbix告警联动 调用钉钉服务端API发送工作通知消息听起来是个很小的功能点但真正落地的时候涉及到的坑远比想象中多。我自己早年第一次接的时候想着不就是POST一个JSON过去嘛结果从企业自建应用的权限点申请到access_token的缓存失效再到工作通知的跳转链接没带参数被客户端打开空白页前后折腾了两三天。这篇文章不打算写成一个泛泛的开发指南而是把我实际踩过的坑、验证过的方案、以及最后稳定跑了很久的代码结构全部摊开来讲。如果你正在做钉钉和企业内部系统的对接尤其是想把监控告警、工单通知、数据推送这类消息直接打到员工钉钉上这篇文章应该能帮你省下不少排查时间。1. 为什么绕不开服务端API这个方案先说结论如果你的目标只是“在群里发一条通知”那自定义机器人就够用没必要碰服务端API。但凡是遇到下面这几种情况服务端API基本是绕不开的消息需要发送给指定的人而不是一个群。工作通知天然以userId或者部门id为接收对象精准投递。消息要以应用的身份触达用户比如“审批待办”“巡检异常”“工单指派”这类消息希望出现在钉钉的“工作通知”会话里带有应用头像和名称而不是一个冷冰冰的webhook机器人名字。需要跳转到企业内部系统的详情页比如告警详情、工单详情。工作通知支持点击消息直接跳转URL还可以配合action多按钮布局而自定义机器人的链接跳转体验会弱一些。需要拿到消息的发送结果和阅读状态。服务端API有对应的结果查询接口能追踪到“这条消息到底有没有送达到这个人”机器人消息基本做不到这种闭环。还有一点容易被忽略自定义机器人有频次限制比如每分钟最多20条这种量级一旦告警爆发消息就会直接丢失。而服务端API配合企业应用频次上限高得多整体更抗压。顺带提一个我在热搜词里看到的高频场景很多人想把Zabbix、Prometheus这类监控系统联到钉钉上。如果你只是“群里吼一嗓子”那机器人够用但如果你想做到“不同级别的告警发给不同的负责人”那服务端API配合组织架构的查询能力才算是真正贴合需求。2. 接入前的逆向梳理先搞清楚消息链路的完整路径很多初次接触的人会直接去网上一顿搜抄一段Python或者Java代码就跑最后卡在报错上完全不知道怎么回事。我个人的建议是别急着写代码先把一条消息从发起端到用户钉钉App的完整链路画出来。这条链路大致是这样你的服务端程序调用钉钉开放平台的/topapi/message/corpconversation/asyncsend_v2接口请求里必须带一个有效的access_token钉钉网关校验token、权限点、参数格式校验通过后消息进入钉钉的消息队列由后端推送到目标用户的钉钉客户端。在这条链路里每一个环节都可能出错。最常见的卡点有三个拿不到token或者token失效应用没有开通对应的权限点请求里的agentId、userId、部门id等参数用错。从反面经验来看大多数人写的第一版代码问题往往不是“代码写错了”而是“接口的调用上下文没搞明白”。比如有人拿的是旧版开放平台AppKey有人拿的是内部应用却传了第三方应用的suiteKey还有人压根没搞清楚自己用的是“企业内部应用”还是“第三方企业应用”直接在请求参数上张冠李戴。所以我建议你接入前先登录钉钉开放平台后台确认三件事当前登录的企业是不是目标企业创建的应用类型是企业内部应用还是第三方应用应用详情里的AppKey、AppSecret、AgentId分别是什么先核对一遍。3. 获取access_token的关键细节与缓存策略access_token是调用钉钉服务端API的通行证所有业务接口都离不开它。获取方式很简单官方文档写得很清楚调用/gettoken接口传appkey和appsecret即可。但这里有一个非常核心的问题access_token有效期是7200秒但如果你每次请求都现去获取一次一是慢二是容易触发频次限制三是会被判定为异常调用。我见过最夸张的一个案例同事在告警循环里每次都先请求一次token再发消息每分钟几十次token请求直接把应用的调用量打上去了后来被临时限流。当时排查了半天还以为是出口IP被封了。正确的做法是写一个带缓存的token管理器。我的Java实现大致是这样的public class DingTalkTokenManager { private static volatile String accessToken; private static volatile long expireTime 0; public static String getAccessToken() { if (accessToken ! null System.currentTimeMillis() expireTime - 60000) { return accessToken; } synchronized (DingTalkTokenManager.class) { if (accessToken ! null System.currentTimeMillis() expireTime - 60000) { return accessToken; } refreshToken(); return accessToken; } } private static void refreshToken() { String url https://oapi.dingtalk.com/gettoken?appkey appKey appsecret appSecret; // 发起HTTP请求解析JSON取出access_token和expires_in // 设置 accessToken ...; expireTime System.currentTimeMillis() expires_in * 1000; } }注意两个细节第一我预留了60秒的缓冲时间防止刚好在过期边缘发出去一个带旧token的请求第二用volatile加synchronized双重检查保证多线程环境下不会重复刷新。这里我再补充一个容易被忽略的点钉钉老版本接口和新版本接口对token的处理策略有点差异。部分新接口支持在请求体里直接传token参数不再单向依赖查询字符串但从我个人的实际经验来说稳定起见统一用?access_tokenxxx的追加方式走老接口通道兼容性更强而且大厂的网关对这个方式支持最稳定。4. 组装工作通知消息字段级别的一步步拆解拿到access_token之后就是组装消息本身了。这是整个流程里最容易出细节问题的地方。4.1 接口选型发送工作通知最常用的是/topapi/message/corpconversation/asyncsend_v2这个接口是异步发送服务端返回成功只代表“钉钉接收了发送请求”并不代表“用户已经收到”。如果你因为返回成功就觉得万事大吉后面会有很大偏差。同步发送不是没有但对普通场景没必要异步足够。如果你想确认用户是否真的收到了官方还有专门的结果查询接口这个后面会讲。4.2 必填参数简单列一下我每次都会做一个参数清单agent_id应用的AgentId必须是你自己应用的userid_list接收者的userId列表多个用英文逗号分隔dept_id_list部门id列表和userid_list至少传一个msg消息内容体不同的消息类型传不同的JSON结构msgtype消息类型取值如text、link、markdown、oa。这几个字段里最坑的是msg和msgtype的匹配关系。msgtype传了textmsg里面就要带content字段传了linkmsg里面就要带title、text、messageUrl、picUrl。我后来统一封装了一个消息工厂不再手写JSON减少低级错误public class MessageFactory { public static String textMsg(String content) { return {\msgtype\:\text\,\text\:{\content\:\ content \}}; } public static String linkMsg(String title, String text, String messageUrl) { return {\msgtype\:\link\,\link\:{\title\:\ title \,\text\:\ text \,\messageUrl\:\ messageUrl \}}; } }4.3 接收人设计的微妙之处这里要专门说一下userId的问题。很多第一次接的人会想当然地用手机号或者工号去传然后怎么调都报“用户不存在”。钉钉的userId是一个很长的字符串和手机号不一样它需要通过通讯录接口把手机号或工号映射成userId。你需要调用/topapi/v2/user/getbymobile传手机号获取对应的userid。这个映射关系我建议做完之后缓存下来别每次都查。毕竟通讯录的变动频率不高没必要为每一次发送都付出一次接口调用的代价。4.4 部门通知还是个人通知还有一个常见问题如果你的目标是“通知所有某部门的人”是不是传dept_id_list就行了理论上可以但实际运维中我更喜欢传一份“处理人名单”。原因很简单工作通知消息如果发给整个部门很容易造成“三个和尚没水喝”的效果尤其是告警通知发了等于没发。更合理的方案是消息主题同步到部门群并且额外给当前值班人单独发一条。5. 从脚本到zabbix联动告警的实际落地前几节是基础能力这节我讲讲怎么把它真正用到运维场景里。热搜词里反复出现“zabbix 7.0 联动钉钉”说明这是个典型刚需。Zabbix和钉钉的联动方式很多最简单的方案是在Zabbix的报警媒介类型里写脚本触发动作时调用我们写好的服务端API程序。5.1 Zabbix告警脚本的基本验证我用的方式是在Zabbix Server上放一个Python脚本脚本读取传入的参数再调用一个统一的通知服务接口。这样做的好处是通知逻辑和Zabbix解耦你以后换Prometheus、换夜莺都不用改底层发送逻辑。核心脚本示意import sys import requests def main(): userid sys.argv[1] # 接收人 title sys.argv[2] # 标题 content sys.argv[3] # 内容 resp requests.post( http://your-notify-service/api/send_dingtalk, json{userid: userid, title: title, content: content} ) print(resp.json()) if __name__ __main__: main()实际操作里有一点很关键Zabbix的媒介类型脚本传参顺序要和脚本接收顺序完全对上。我见过太多人脚本写得没问题就是参数顺序反了导致告警内容张冠李戴。经验是在脚本第一行就把参数全部print出来打日志先跑一次“测试发送”确认日志里的值和预期一致再去接告警动作。5.2 工作通知消息里的告警上下文告警场景下光发一句“CPU过高”毫无意义。真正能减少处理时间的是把“哪台机器、哪个指标、当前值、持续多久、历史趋势链接”全部塞进消息里。所以我在告警网关里做了一层模板渲染。一个典型的markdown消息是这样拼的{ msgtype: markdown, markdown: { title: 生产环境-XX服务器CPU告警, text: #### 告警详情 \n - 主机: 10.10.12.33 \n - 指标: CPU使用率 \n - 当前值: 96.5% \n - 阈值: 85% \n - 持续时间: 15分钟 \n - 跳转链接: [查看详情](http://monitor.internal/alert/12345) } }这里有一个我自己反复踩过的坑markdown消息里的链接域名必须能被钉钉客户端正常打开。如果你写的链接是内网地址手机端打开就直接提示“无法访问”体验极差。后来我们专门在消息网关里加了一个“外链转换”的步骤把内网地址映射成公司网关外的可访问地址消息才有真正的行动力。5.3 告警升级策略在消息层的体现告警联动不能只做“发消息”这一步还要考虑消息没人处理的场景。我在通知服务里做了一档升级逻辑P3级别只发给值班人P2级别发给值班人直属LeaderP1级别直接拉群并且在群里所有人同时工作通知再给负责人补一条。这套逻辑不复杂但收益非常大。以前P1告警只发一次凌晨经常没人响应加了升级策略之后值班人10分钟未确认系统自动再往上一级发响应速度明显上来了。6. 高频错误码排障链路与稳定性加固这节放在最后因为“代码能跑通”只是开始真正稳定运行半年、一年才是目标。我整理一下我遇到过的高频错误码和排查路径按出现频率排序。6.1 错误码88xxx系列钉钉错误码里88开头的占了绝大多数常见的是错误码含义真实原因88系统内部错误多数是参数格式问题仔细检查JSON400参数无效userid不存在、agentId不匹配999系统繁忙触发限流稍后重试500服务器内部错误偶发重试一般能过很多人拿到错误码就慌其实第一步永远是回到原始请求逐个字段核对。我用的是最笨但最有效的办法把这个请求的完整URL、请求体、响应体全部打到日志里看日志定位。6.2 “token无效”的隐藏原因你明明刚get完token为什么提交消息时还是报“token无效”我在线上遇到过一种情况服务器时间不准。钉钉网关校验token时有时候会参考请求方的时间戳服务器时间偏差太大token会莫名其妙失效。排查方法很简单在应用服务器上执行date命令和北京时间对一下。如果偏差超过几分钟建议开启NTP同步。这个坑非常隐蔽因为平时不对比时间根本不会注意。6.3 重试策略设计消息发送属于可以做成“最终一致”的场景所以重试是必须的但不能无脑重试。我设计的重试逻辑是第一次失败等1秒重试第二次失败等3秒重试第三次失败等10秒重试三次都失败把消息写到本地失败队列由定时任务每5分钟扫描一次再补发。这套策略跑下来在钉钉接口偶尔抖动的情况下消息丢失率基本为0。注意重试的时候一定要判断错误码类型参数类错误如用户不存在重试一万次也没用立刻放弃并告警人工介入。6.4 查询消息送达状态的闭环异步发送成功后有没有办法知道用户到底收到没有钉钉提供了/topapi/message/corpconversation/getsendresult接口传入发送任务ID可以查询发送状态。我的建议是不要对每条消息都查状态只需要对重点告警做确认闭环。普通通知类消息全量查不仅浪费接口配额还会把简单问题搞复杂。最后再分享一个小技巧在整个接入过程里我发现很多时候真正拖慢进度的不是接口文档看不懂而是“没有一套本地的完整测试环境”。后来我痛定思痛搭建了一套针对钉钉API的本地模拟服务把获取token、发送消息、错误码返回全部mock出来所有代码先在mock环境跑通再切到真实环境。这套模拟服务不仅开发时能自测还能在钉钉接口故障时作为本地降级方案保证核心通知不中断。还有一个小建议钉钉开放平台后台有一个“接口调试工具”联调的时候尽量多用它验证参数它能帮你把权限点、参数、调用链路的问题一次性暴露出来。等你在调试工具里把请求调通了再回代码里去复现绝对比在代码里瞎试高效得多。如果这篇文章帮你少走了一点弯路那对我来说就是值得的。
返回列表