如何接入企业微信异常通知机器人(四)
摘要:在无人值守的后台服务里,"出了问题有人知道"比"程序不崩"更重要。本文以自动下单客户端的企业微信告警模块为例,从群机器人 Webhook 原理讲起,手把手实现一个支持文本/Markdown 双通道、按类型限流、可 @ 指定成员的告警服务,并给出三个真实业务场景的接入示例。
一、企业微信通知机器人创建步骤
- 打开企业微信客户端,进入(或创建一个)用于接收告警的群聊;
- 点击群聊右上角 "..." → 群机器人 → 添加机器人;



- 填写机器人名称(如"XX客户端告警机器人"),复制Webhook地址,点击"保存";

- 添加成功后,复制生成的 Webhook 地址,形如:
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
二、背景与痛点
桌面客户端跑在用户电脑上,通常无人值守。深夜账号掉线、全部账号离线这类异常,如果只是写日志,第二天才发现往往已经错过处理时机:
- 日志只在出问题的机器上,远程看日志成本高、不及时;
- 邮箱告警需要申请、配置 SMTP,维护成本重;
- 短信/电话告警费用高,不适合高频事件。
把关键事件推送到手机上,让运维在微信里直接收到结构化告警,是最低成本、零运维的方案。企业微信群机器人恰好提供了开箱即用的 Webhook 接口——无需申请企业微信应用、无需 AppSecret,拉个群扫码就能用,5 分钟即可接入。
三、企业微信机器人 Webhook 原理
在企业微信群中添加"群机器人"后,会得到一个形如:
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
的 Webhook 地址。向该地址 POST 一个 JSON 即可发消息,支持的主要消息类型如下:
| 类型 | 用途 | 典型场景 |
|---|---|---|
text | 纯文本,可指定 mentioned_list(userid)或 mentioned_mobile_list(手机号)@ 成员 | 普通通知、连通性测试 |
markdown | Markdown 富文本,支持标题、加粗、引用、表格 | 结构化告警、状态汇总 |
news | 图文卡片 | 需要跳转链接的推送 |
file | 文件消息,需先上传获得 media_id | 日志、截图 |
使用中需注意两个关键限制:
- 发送频率限制:每个机器人 20 条/分钟,超限返回
errcode: 45009; - 只能主动推送:机器人消息不支持被动回复(对告警场景够用)。
发送成功时响应体:
{ "errcode": 0, "errmsg": "ok" }
四、基础封装:文本与 Markdown 两个通道
4.1 文本消息,支持 @ 成员
/// <summary>
/// 发送文本消息
/// </summary>
/// <param name="content">消息内容</param>
/// <param name="mentionedList">@的成员列表(手机号或 userid)</param>
/// <returns>是否发送成功</returns>
public async Task<bool> SendTextMessageAsync(string content, List<string>? mentionedList = null)
{
try
{
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var payload = new
{
msgtype = "text",
text = new
{
content = content,
mentioned_list = mentionedList ?? new List<string>()
}
};
var json = JsonSerializer.Serialize(payload);
var contentData = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(WebhookUrl, contentData);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
using var doc = JsonDocument.Parse(responseContent);
var errcode = doc.RootElement.GetProperty("errcode").GetInt32();
if (errcode == 0)
{
_logger.LogInformation("企业微信消息推送成功");
return true;
}
else
{
var errmsg = doc.RootElement.GetProperty("errmsg").GetString();
_logger.LogError("企业微信消息推送失败: errcode={Errcode}, errmsg={Errmsg}", errcode, errmsg);
return false;
}
}
else
{
_logger.LogError("企业微信消息推送HTTP请求失败: StatusCode={StatusCode}", response.StatusCode);
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "企业微信消息推送异常");
return false;
}
}
要点:
- 永远返回
bool而不是抛异常。通知是旁路逻辑,推送失败不能影响主流程(如下单); mentioned_list传空数组而不是null,否则微信侧可能报参数错误;- 若按手机号 @,请使用
mentioned_mobile_list字段,二者互斥。
4.2 Markdown 消息,构建富文本告警
/// <summary>
/// 发送Markdown消息
/// </summary>
/// <param name="content">Markdown内容</param>
/// <returns>是否发送成功</returns>
public async Task<bool> SendMarkdownMessageAsync(string content)
{
try
{
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var payload = new
{
msgtype = "markdown",
markdown = new
{
content = content
}
};
var json = JsonSerializer.Serialize(payload);
var contentData = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(WebhookUrl, contentData);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
using var doc = JsonDocument.Parse(responseContent);
var errcode = doc.RootElement.GetProperty("errcode").GetInt32();
if (errcode == 0)
{
_logger.LogInformation("企业微信Markdown消息推送成功");
return true;
}
else
{
var errmsg = doc.RootElement.GetProperty("errmsg").GetString();
_logger.LogError("企业微信Markdown消息推送失败: errcode={Errcode}, errmsg={Errmsg}", errcode, errmsg);
return false;
}
}
else
{
_logger.LogError("企业微信Markdown消息推送HTTP请求失败: StatusCode={StatusCode}", response.StatusCode);
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "企业微信Markdown消息推送异常");
return false;
}
}
content 中可以组合企业微信支持的 Markdown 语法,例如:
## 采购账号全部离线告警
> <font color="warning">⚠️ 检测到所有账号在 5 分钟内无可用状态</font>
- **告警时间**:2026-08-21 03:12:45
- **账号数量**:8
- **当前状态**:全部离线
---
> 请及时登录账号,避免漏单
五、告警层:限流是灵魂
直接暴露 SendTextMessageAsync 给业务方有个问题:高频异常会刷屏。比如账号反复掉线重登,每分钟可能触发 10 条告警,不仅骚扰,还容易撞上 20 条/分钟的接口上限,导致真正致命的告警被挤掉。
项目的解法是按 alertType 做时间窗口限流:
/// <summary>
/// 发送异常告警消息(带推送频率限制)
/// </summary>
/// <param name="alertType">告警类型(用于频率限制key)</param>
/// <param name="title">告警标题</param>
/// <param name="message">告警消息</param>
/// <param name="details">详细信息</param>
/// <returns>是否发送成功(如果被限流则返回true表示跳过)</returns>
public async Task<bool> SendAlertAsync(string alertType, string title, string message, string? details = null)
{
// 检查推送频率限制
var now = DateTime.Now;
if (_lastPushTime.TryGetValue(alertType, out var lastTime))
{
if (now - lastTime < MinPushInterval)
{
_logger.LogInformation("告警推送被限流: AlertType={AlertType}, 距离上次推送仅 {Seconds} 秒",
alertType, (int)(now - lastTime).TotalSeconds);
return true;
}
}
var timestamp = now.ToString("yyyy-MM-dd HH:mm:ss");
var markdownContent = $"## ⚠️ {title}\n\n" +
$"> **时间**: {timestamp}\n" +
$"> **描述**: {message}\n";
if (!string.IsNullOrEmpty(details))
{
markdownContent += $"> **详情**: {details}\n";
}
markdownContent += "\n---\n";
var success = await SendMarkdownMessageAsync(markdownContent);
if (success)
{
_lastPushTime[alertType] = now;
}
return success;
}
配套字段定义:
// 防止重复推送的时间间隔(秒)
private static readonly Dictionary<string, DateTime> _lastPushTime = new();
private static readonly TimeSpan MinPushInterval = TimeSpan.FromMinutes(5);
设计细节解析
-
限流键是
alertType而非全局。"AllAccountsOffline"(全部离线)和"LoginTimeout_10086"(某账号掉线)互不干扰,一个账号反复掉线不会淹没全局告警。更精细的做法是$"LoginTimeout_{AccountId}",把粒度直接打到账号级。 -
被限流时返回
true。语义是"本次告警按预期被合并/跳过,不算失败",避免业务侧看到false误以为推送通道故障,再触发补偿逻辑,形成告警风暴。 -
_lastPushTime用普通Dictionary而非ConcurrentDictionary。告警场景低频(分钟级),单线程执行下无需加锁;如果告警频率很高,换成ConcurrentDictionary即可,成本为零。 -
失败不记时间。只有发送成功才更新
_lastPushTime,网络抖动导致的单次失败不会白白消耗限流窗口。
六、接入业务场景
项目中有三个典型接入点,覆盖"验证、主动通知、异常告警"三类用法。
6.1 设置页连通性测试
UI 层点"测试推送"按钮时,走文本通道验证 Webhook 可用:
var testMessage = $"✅ 企业微信通知测试成功!\n" +
$"📌 项目:采购下单客户端\n" +
$"⏰ 发送时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}";
var success = await _notificationService.SendTextMessageAsync(testMessage);
if (success)
{
messageService?.SendMessage("企业微信测试推送成功",
severity: SpeedyOrderPlacement.Core.Models.InfoBarSeverity.Success);
}
6.2 账号掉线超时告警(按账号维度限流)
TaobaoCookieRefreshService 中,自动登录超时即触发告警:
// 自动登录超时(> 1 分钟),说明需要人工介入
var alertMessage = $"采购账号 [{target.DisplayName}] (ID: {target.AccountId}) " +
$"自动登录耗时超过一分钟(已耗时 {elapsedSeconds} 秒),需要人工介入";
await _notificationService.SendAlertAsync(
alertType: $"LoginTimeout_{target.AccountId}", // 每个账号独立限流
title: "采购账号掉线超时告警",
message: alertMessage,
details: $"登录开始时间: {loginStartTime:HH:mm:ss}, " +
$"超时时间: {_autoLoginTimeout.TotalSeconds}秒");
6.3 全局离线告警(单例限流)
PurchaseOrderPollingService 中检测到所有账号连续 5 分钟无可用时触发,5 分钟内只推一次:
var alertMessage = $"轮询出现长时间无可用采购账号!所有账号均未登录。" +
$"总账号数: {totalCount}, 已登录: {loggedInCount}, " +
$"未登录: {notLoggedInCount}, 已禁用: {disabledCount}, " +
$"持续时间: {(int)elapsed.TotalMinutes} 分钟";
await _notificationService.SendAlertAsync(
alertType: "AllAccountsOffline", // 全局事件固定类型名
title: "采购账号全部离线告警",
message: alertMessage,
details: $"告警开始时间: {_noAvailableAccountStartTime.Value:HH:mm:ss}, " +
$"当前时间: {now:HH:mm:ss}");
两个场景正好演示了限流键的设计差异:账号级事件用 {类型}_{账号Id} 做键,全局事件用固定类型名做键。
七、依赖注入与配置
服务通过构造函数注入 ILogger,在模块中注册为单例:
services.AddSingleton<WeChatWorkNotificationService>();
Webhook 地址建议从 AppSettings / 配置文件读取,不要把 key 硬编码进代码,否则换群就得改代码重新发布:
{
"WeChatWork": {
"WebhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx"
}
}
八、可继续深化的方向
- 图片告警:先调
webhook/upload_media上传图片拿到media_id,再发image消息——非常适合"滑块出现时自动截图推送"; - 告警分级:
SendAlertAsync增加level参数,error/warning/info对应不同限流窗口(如 error 5 分钟、warning 30 分钟); - 失败重试:
errcode == 45009(限流)时指数退避重试,45033(webhook 被封禁)时直接停服上报,避免无效重试; - 消息队列化:高频事件先入
Channel,后台消费者按批合并推送,进一步降低触发频率; - 多通道兼容:把"限流 + 组装 + 发送"抽象成接口,同一套逻辑可复用于钉钉、飞书、Telegram Bot 等 Webhook 渠道。
九、总结
一套可靠的企业微信告警,核心其实不在 Webhook 本身(本质只是一个 POST),而在旁路设计和限流策略:
| 设计原则 | 实现手段 | 收益 |
|---|---|---|
| 推送失败不影响主流程 | 返回 bool 不抛异常 | 告警是旁路,不能拖垮业务 |
| 重要的必达 | 按 alertType 分类限流 | 全局异常不会被账号级异常淹没 |
| 重复的不刷屏 | 时间窗口限流(5 分钟) | 不骚扰、不撞接口上限 |
| 限流窗口不被浪费 | 仅发送成功后更新时间戳 | 网络抖动不消耗告警配额 |
这套模式稍作改造即可复用到钉钉、飞书、Telegram Bot 等任意 Webhook 类推送渠道,建议直接沉淀为团队通用的通知组件。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)