
最近陆陆续续有朋友问我同一个问题怎么把自己系统里的告警、定时任务结果、交易动态这些信息第一时间推到手机上。我的答案一直很固定用 Telegram Bot 配合 Azure Functions。这个组合几乎零成本不需要单独买一台服务器跑常驻程序也不用盯着某个进程是不是挂了函数触发就跑跑完就睡省心。这篇文章就把这套方案的完整思路、代码实现和我在实际使用中踩过的坑一次性讲清楚适合想给自己的项目加上消息通知能力、又不想维护后端服务的人参考。1. 整体设计与思路拆解1.1 为什么用 Azure Functions 做消息推送先说结论消息推送这个场景本质上是一个“短平快”的请求转发动作。把一段文本发到 Telegram从请求发出到响应返回正常情况下一秒钟内就能完成不需要长期占用任何资源。如果为了这种低频操作专门开一台云服务器成本上非常不划算。Azure Functions 属于无服务器计算计费方式按执行次数和资源消耗算个人项目一个月跑几百次很多时候花费几乎可以忽略不计。而且它天然支持多种触发方式HTTP 请求、定时器、队列消息都能作为入口这正好和消息推送的多样化需求匹配。比如我想做一个每天早上 8 点推送天气信息的功能这就属于定时触发想做一个别人访问某个网址就通知我的功能这就属于 HTTP 触发。两种场景用同一个运行环境、同一套代码结构就能覆盖不用分开部署两套系统。还有一个很实际的好处Azure Functions 自带日志和监控面板每次执行是否成功、耗时多少、有没有报错在 Azure Portal 里都能直接看到。消息发送这种场景最怕的就是“悄悄失败了”有日志系统做后盾排查问题会轻松很多。1.2 Telegram Bot 的工作原理三个关键要素用 Telegram Bot 发送消息核心要理解三个东西Bot Token、Chat ID、HTTP API。Bot Token 是你创建机器人时拿到的一串密钥格式大概是123456789:AAF...这样前面是数字 ID后面是字母数字混合的令牌。这串密钥相当于机器人的身份证加门禁卡所有 API 调用都要带着它。要特别注意保密谁拿到这个 Token谁就能控制你的机器人。Chat ID 是聊天窗口的唯一标识。很多人第一次做的时候在这里卡住明明 Token 是对的消息就是发不出去多半是 Chat ID 填错了。需要注意与机器人对话的那个窗口Chat ID 可能是一串正数或负数群组通常是负数个人用户通常是正数不能想当然地用手机号或者用户名代替。HTTP API 是 Telegram 官方提供的接口。你只需要向https://api.telegram.org/bottoken/sendMessage发送一个 POST 请求body 里带上chat_id和text两个参数消息就出去了。整个链路非常简单没有任何 SDK 依赖这也是它适合放在 Functions 里的原因。还有一个生活化的类比Bot Token 是你的银行卡和密码Chat ID 是收款方的银行卡号HTTP API 是转账通道。三者缺一不可任何一个错了钱都到不了账“转账失败”的提示还不会特别精确。1.3 触发器选型什么时候用 HTTP什么时候用 Timer写代码之前先想清楚一个设计问题你的消息发送动作由什么来触发如果消息是因为某个外部事件产生的比如网站有用户注册、服务器发生告警、支付平台回调成功那就用 HTTP 触发。外部系统往你的 Function URL 发一条请求函数收到请求后把消息内容转推给 Telegram。如果是周期性的任务比如每天早上推送新闻、每周五汇总本周数据那就用 Timer 触发器配置一个 CRON 表达式控制执行时间完全不需要外部介入。还有一种情况适合用 Queue 触发器比如你那套系统本身就在用 Azure 服务总线或者存储队列做异步解耦消息先丢进队列由函数消费后转发到 Telegram。这样即使瞬间产生大量消息队列也能起到削峰填谷的作用不至于把 Telegram API 打到限流。这三个触发器没有哪个绝对好纯粹看你当前的系统架构。如果是新项目优先 HTTP因为它最灵活任何语言、任何平台都能通过一个网址调用后面要接定时任务也只是加一个单独的函数而已。2. 环境准备与前置条件2.1 创建一个 Telegram Bot 并拿到 Token准备工作第一步在 Telegram 里找到 BotFather这是官方用来管理机器人的“管理员”。对它发送/newbot命令按提示输入机器人的显示名称和用户名用户名必须以bot结尾比如my_alert_bot。创建成功后BotFather 会返回一条消息里面就是你的 Bot Token。这里提醒一个常见的注册问题如果 BotFather 一直不回复可能是当时网络连接异常导致请求没送达可以换个时间再试或者彻底退出 Telegram 客户端重进而不是反复发送同样命令。另外 BotFather 的对话是交互式的需要耐心一步步跟着引导走别在它没问完的时候就发下一个指令。拿到 Token 后先找个地方记下来这串字符后面写代码和配置都要用。我自己的习惯是存到密码管理器里不放在明文文本里避免哪天手滑提交到 GitHub 泄漏出去。2.2 确定 Chat ID向自己的机器人发一条消息这一步困扰过非常多新手。正确做法分两步。第一先用你的个人 Telegram 账号找到刚创建的机器人点进去点一下Start或发送任意文本消息比如发个hello。这一步的目的是在机器人后台生成一个聊天记录否则机器人不知道有你这个用户存在。第二在浏览器里打开或者用 curl 请求这个地址https://api.telegram.org/bot你的Token/getUpdates。返回的 JSON 里会有一个chat对象里面id字段就是你的 Chat ID。如果你刚才发过消息这里就能看到。有人问有没有替代办法比如加第三方的用户信息查询机器人能直接发消息给你反查 ID。当然可以但我还是推荐官方 API 的 getUpdates 方式因为不依赖第三方服务理解原理之后也更踏实。2.3 本地开发环境搭建VSCode 加 Functions Core Tools写代码之前先把本地开发环境准备好。我目前最常用的组合是 Visual Studio Code 加 Azure Functions Core Tools 加 .NET 8 SDK。Azure Functions Core Tools 是微软官方的本地调试工具它能在你的电脑上模拟 Functions 运行时环境函数写好之后直接在本地跑起来测不需要一上来就部署到云端。安装方式很简单Windows 上用 winget 命令winget install Azure.Functions.CoreToolsmacOS 上用 Homebrew 执行brew tap azure/functions brew install azure-functions-core-tools4。VSCode 里需要安装 Azure Functions 扩展装好后左侧会出现 Azure 图标可以在 VSCode 里直接创建函数项目、选择模板、写代码、一键部署体验很流畅。还有一点很重要确认你的 .NET SDK 版本和 Functions 运行时版本兼容.NET 8 配 Functions v4 是目前比较稳的组合。如果你更熟悉命令行操作不想依赖 VSCode 插件那直接用func init和func new命令创建项目也是一样的功能上没有任何差别。3. 核心实现用 C# 编写一个消息发送函数3.1 初始化项目选择 Isolated Worker 模型打开终端找一个干净目录执行下面这几条命令func init TelegramBotSender --worker-runtime dotnet-isolated cd TelegramBotSender func new --name SendTelegramMessage --template HTTP trigger --authlevel function解释一下上面的参数。dotnet-isolated是 .NET 在 Azure Functions 中的进程隔离模型和旧版 In-Process 模型相比它让你可以自己控制依赖注入和配置不受 Functions 宿主版本限制是目前官方主推的方式新项目建议直接选它。模板选择HTTP trigger意味着我们创建的是一个 HTTP 接口函数外部访问时需要携带authlevel function指定的函数密钥。注意这个密钥和 Telegram Bot Token 不同它是 Azure Functions 层面的访问控制用来防止你的函数被任何人无限制调用。项目创建好之后会生成一个默认的SendTelegramMessage.cs文件。里面的代码结构会是一个静态类或者实例类包含一个FunctionName特性标记的和HttpTrigger特性标记的入口方法。先不急着改逻辑我们先理解发送消息这个核心动作。3.2 发送逻辑用 HttpClient 调用 Telegram APITelegram 的 sendMessage 接口可以用 URL 编码的表单方式调用也可以提交 JSON。我实测下来用表单的方式最不容易出问题因为反正参数也不复杂省得处理 JSON 序列化和 Content-Type 的坑。下面是完整的执行函数代码using System.Net; using System.Net.Http; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; public static class SendTelegramMessage { private static readonly HttpClient httpClient new HttpClient(); [Function(SendTelegramMessage)] public static async TaskHttpResponseData Run( [HttpTrigger(AuthorizationLevel.Function, post)] HttpRequestData req, FunctionContext executionContext) { var logger executionContext.GetLogger(SendTelegramMessage); var requestBody await new StreamReader(req.Body).ReadToEndAsync(); string botToken Environment.GetEnvironmentVariable(TELEGRAM_BOT_TOKEN); string chatId Environment.GetEnvironmentVariable(TELEGRAM_CHAT_ID); if (string.IsNullOrEmpty(botToken) || string.IsNullOrEmpty(chatId)) { logger.LogError(Telegram token or chat id is missing.); } var formContent new FormUrlEncodedContent(new[] { new KeyValuePairstring, string(chat_id, chatId), new KeyValuePairstring, string(text, requestBody) }); string apiUrl $https://api.telegram.org/bot{botToken}/sendMessage; var response await httpClient.PostAsync(apiUrl, formContent); var responseBody await response.Content.ReadAsStringAsync(); logger.LogInformation($Telegram API response: {(int)response.StatusCode} - {responseBody}); var result req.CreateResponse(response.StatusCode); await result.WriteStringAsync(responseBody); return result; } }这段代码有几个细节值得说。第一httpClient被声明为静态字段这是《编程实践》里反复强调的标准做法每次请求都 new 一个 HttpClient 容易导致端口资源耗尽。第二所有敏感信息都从环境变量读取代码里不出现任何硬编码的 Token。第三函数把 Telegram API 的原始响应原样返回给调用方这样外部系统能清楚地知道是否发送成功。3.3 敏感信息配置local.settings.json 与 Application Settings本地调试时配置文件是local.settings.json。打开这个文件把刚才获取的 Token 和 Chat ID 填入Values节点{ IsEncrypted: false, Values: { AzureWebJobsStorage: , FUNCTIONS_WORKER_RUNTIME: dotnet-isolated, TELEGRAM_BOT_TOKEN: 123456789:AAF..., TELEGRAM_CHAT_ID: 987654321 } }这个文件默认会被.gitignore忽略不会上传到代码仓库方便本地各人配置不同的值。但千万注意local.settings.json只在本地开发时生效部署到 Azure 后函数运行环境读的是 Azure Portal 里 Function App 的“环境变量”配置也就是 Application Settings。部署完成后需要在门户里找到你的 Function App进入“设置 - 环境变量”添加两个键值对TELEGRAM_BOT_TOKEN和TELEGRAM_CHAT_ID内容和 local.settings.json 一致。不配置这一步的话函数跑起来会报错说环境变量为空消息也就发不出去。我去年代别人排查过一次事故最后发现就是部署时忘了在云端配置环境变量本地明明是好的上了云就失效非常典型的坑。3.4 本地验证与部署上线在项目根目录执行func startCore Tools 会启动本地运行时并输出函数地址。默认地址类似http://localhost:7071/api/SendTelegramMessage。用 curl 发一条测试请求curl -X POST http://localhost:7071/api/SendTelegramMessage \ -H Content-Type: text/plain \ -d 这是一条来自 Azure Functions 的测试消息如果一切正常你的 Telegram 会收到一条消息同时终端会打印 Telegram API 的返回 JSON里面ok字段是true。这是整套流程里最快乐的时刻。本地验证通过后部署上云。如果你用的是 VSCode右键点击当前项目选择“部署到函数应用”按提示选择或创建资源组、存储账户和 Function App 即可。部署完成后Azure 会给你一个 HTTPS 地址外部系统直接用这个地址调用就大功告成。这里提醒一下创建 Function App 时可以选择 Windows 或 Linux它们的计费方式和运行环境略有差异但对我们这种消息推送的轻量场景没有本质区别选哪个都行看你对哪个环境更熟悉。4. 常见问题与排查技巧实录4.1 消息发不出去网络连通、编码格式和 Token 问题我在实操中总结了一套排查思路按顺序检查基本可以解决 90% 的问题。首先是看日志。Azure Portal 的“日志流”页面能实时看到函数执行时的输出如果函数本身没执行问题在触发端如果执行了但报错看具体报错信息。最典型的错误是请求报 400 或 404通常是 Token 格式不对或者用户根本没有先给机器人发过消息。404 这种经常让人误以为是网址写错了其实是 Telegram 在告诉你“这个 Token 无效”。我把 Token 复制到文本编辑器里检查过好多次发现是复制的时候把空格带进去了所以建议你粘贴时用引号括起来避免意外空白。还有一种情况消息内容里带了特殊字符比如 JSON 格式的字符串中含有很多{}和引号。如果用表单提交方式一般没问题但如果用 JSON 方式传参必须确保转义正确。我最开始测试时传了一段 JSON 文本结果 Telegram 返回 400检查半天发现是我的 JSON 序列化过程出了问题多加了一层引号。改回表单方式后就再也没踩过这个坑。4.2 429 限流问题消息发送过于频繁怎么办消息推送做多了会撞上一个经典错误Telegram API 返回429 Too Many Requests错误文本类似“Too Many Requests: retry after X”。这和你在其他平台上遇到“消息发送过于频繁请稍后重试”的提示是一个原理都是服务端对调用频率做了限制。Telegram 对 Bot 的限制规则大致是同一个群组或用户每秒最多发送约 30 条消息广播场景如果短时间内向大量用户推送也会触发限流。解决方法其实不复杂。第一个办法是在代码里做退避重试。当你收到 429 响应时从响应头的Retry-After字段读取等待秒数然后Task.Delay等待相应时间再重试。这里注意别用太激进的策略等待时间必须是服务端告诉你多少就等多少否则容易加重对方负载。第二个办法是合并发送。如果消息内容是给同一批用户的重复性通知就不要逐条调用 sendMessage而是先把多条内容合并成一条长文本一次性发出。Telegram 单条消息的长度上限大约是 4096 个字符只要不超这个限制就尽量合并。我之前做群里告警通知时把 5 分钟内的告警缓存到一个 List 里定时器每 5 分钟汇总发送一次既解决了限流也减少了噪音。4.3 怎么确认消息真的发送成功了有朋友问“消息发出去了但不知道怎么判断成不成功”这个问题在企业微信、钉钉这类应用开放平台的场景里同样常见。确认 Telegram 消息是否成功核心就是看 HTTP 响应。Telegram sendMessage 接口的返回格式是一个包含ok布尔字段的 JSON。ok为true时消息就发送成功了ok为false时响应体里会有description字段说明失败原因。所以你的函数在处理完请求后一定要把 Telegram 返回的状态记录到日志里方便事后回溯。更进一步如果你想确保“执行成功且消息已发送”可以在函数里判断 Telegram 响应的ok字段如果为false就把函数返回状态码改成 500这样 Azure 的监控体系就会把它记录为一次失败调用你可以在 Alert 规则里配置失败告警。这是生产环境里比较稳妥的确认方式。4.4 定时触发的时区问题CRON 表达式用了 UTC如果要做定时推送功能最容易忽略的是时区问题。Azure Functions 的 Timer 触发器默认使用 UTC 时区CRON 表达式里的时间就是 UTC 时间不是本地时间。比如我最初设置每天早上 9 点推送填了0 0 9 * * *结果实际收到消息的时间是北京时间下午 5 点整个节奏全乱了。解决办法是计算时区偏移。北京时间比 UTC 快 8 小时所以想在北京时间早上 9 点执行CRON 表达式里应该写0 0 1 * * *。如果觉得换算麻烦可以在函数代码里自己处理时间判断比如用TimeZoneInfo.ConvertTime把当前 UTC 时间转成目标时区然后判断小时是否等于目标小时。这样 CRON 只需设置成每分钟跑一次成本极低但逻辑会更灵活。5. 进阶玩法与扩展方向5.1 消息内容格式化给通知增加可读性Telegram 的 sendMessage 接口支持两种解析模式HTML和MarkdownV2。开启后可发送带格式的文本。目前我用得最多的是 HTML 模式贴一段示例{ chat_id: 987654321, text: b告警通知/b\npre服务器 CPU 使用率超过 90%/pre\n时间2025-01-20 14:30:00, parse_mode: HTML }使用 HTML 模式时有一个容易被坑的点发送方传入的文本中出现或这类字符时必须转义成lt;和amp;否则消息会发送失败或者显示异常。比如日志里常见的异常信息ArgumentNullException: Value cannot be null. (Parameter key)本身不含尖括号所以没事但如果消息内容是代码片段里面含有泛型符号就必须做转义。MarkdownV2 模式更强大但也更严格转义规则极其繁琐下划线、星号、方括号都会被特殊处理我一开始折腾过几次后来全部切换到 HTML 模式省心很多。5.2 发送图片和文档从纯文本升级到富媒体有些场景不只发文字比如定时把服务器上的报表图片推送到手机。Telegram 提供了sendPhoto和sendDocument接口且两者都支持 URL 方式直接发送不需要把文件上传到 Telegram。代码上可以复用现有的 HttpClientvar formContent new FormUrlEncodedContent(new[] { new KeyValuePairstring, string(chat_id, chatId), new KeyValuePairstring, string(photo, https://example.com/chart.png), new KeyValuePairstring, string(caption, 今日站点流量趋势) }); var apiUrl $https://api.telegram.org/bot{botToken}/sendPhoto; var response await httpClient.PostAsync(apiUrl, formContent);这里的实现思路和 sendMessage 几乎一模一样区别仅仅是把接口名和表单字段换了一下。如果你的图片不是公网 URL而是生成在函数内存里的字节数组那需要用到 MultipartFormDataContent代码会复杂一些但原理相似。我项目里有一个每日汇总报告功能就是先在一个函数里生成 HTML 报表再用另一个函数把它通过 sendDocument 发到群里全程自动化。5.3 和 IoT 场景联动设备状态上云后再转推现在很多嵌入式设备能联网比如 ESP01S 这类 Wi-Fi 模块。常见玩法是设备在按键触发或传感器变化时通过 TCP 发一段数据到云服务器如果我们要用 Telegram 接收通知最简单的方式是让设备直接调用一个 HTTP 接口。这里的建议是嵌入式设备不要直接调 Telegram API。原因有两个一是设备代码里保存 Bot Token 有泄露风险二是不方便做频率控制和数据校验。更合理的流程是设备先向 Azure Functions 的 HTTP 接口发送一条请求函数收到请求后先解析和校验数据再以自己的身份调用 Telegram API 完成通知。这样一个设备数据入口对应多个下游动作将来接企业微信、钉钉或者邮件只需要在函数里扩展分发逻辑不需要改设备端程序。5.4 其他语言实现速览Python 和 Node.js如果你的主要开发语言不是 C#换语言实现起来也不麻烦。核心逻辑都是读环境变量、构造 HTTP 请求、调用 Telegram API。Python 版本用requestsimport os, requests token os.environ[TELEGRAM_BOT_TOKEN] chat_id os.environ[TELEGRAM_CHAT_ID] def main(req): payload {chat_id: chat_id, text: Python 函数消息} resp requests.post( fhttps://api.telegram.org/bot{token}/sendMessage, datapayload, timeout10 ) return resp.json()这里非常重要的一点务必设置timeout参数告诉 requests 如果 API 在指定时间内不响应就放弃。否则一旦 Telegram 那边网络抖动你的函数会一直挂在那里等待白白浪费执行时长在无服务器环境里超时是要消耗资源和费用的。我用 Azure Functions 做 Telegram 通知这件事已经有两年多。最开始只是为了解决自己的服务器告警现在把它扩展成了一个小型通知中心数据库备份结果、每日报表、订单动态、甚至智能家居的传感器报警全都通过这一套管道推送到手机。整个系统的维护成本极低除了偶尔看一眼执行日志基本不用管它。如果你也想尝试给自己的项目加一条“会主动找你说话”的通知通道这个方案不需要买服务器、不需要备案、不需要复杂的架构设计从零开始到第一条消息落地通常半小时内就能完成。动手试一下你会发现原来“被系统主动汇报”是这么一件让人安心的事情。