多渠道通知系统:模板、重试与偏好
为电子邮件、短信和 Telegram 设计一个多渠道通知系统:包含模板、投递状态、重试策略与保持一致的用户偏好。

单一通知系统能解决什么问题
当电子邮件、短信和 Telegram 被当作互相独立的功能构建时,问题很快就会显现。所谓“相同”的警报往往出现措辞不同、时间不同、以及谁能收到的规则不同。支持团队于是要追踪三套真相:一套在邮件提供商,一套在短信网关,还有一套在机器人日志里。
一个多渠道通知系统通过把通知当作一个产品来解决这个问题,而不是三套集成。发生一个事件(重置密码、发票已付、服务器宕机),系统根据模板、用户偏好和投递规则决定如何跨渠道发送。消息在不同渠道可以有不同的格式,但在含义、数据和跟踪上保持一致。
大多数团队无论从哪个渠道开始,最终都需要相同的基础能力:带变量的版本化模板、投递状态跟踪(“sent、delivered、failed、为什么”)、合理的重试与回退、带同意与静默时段的用户偏好,以及支持团队可以查看的审计轨迹,而不用猜测。
成功看起来很平凡,但这是好事。消息可预测:合适的人在合适的时间通过他们允许的渠道收到合适的内容。当出现问题时,排查简单,因为每次尝试都有记录、带清晰的状态和原因码。
“新登录”警报就是一个好例子。你只创建一次,用相同的用户、设备和位置信息填充,然后将其作为电子邮件(用于详情)、短信(用于紧急)和 Telegram(用于快速确认)投递。如果短信提供商超时,系统会按计划重试、记录超时,并可以回退到另一个渠道,而不是丢弃警报。
核心概念与简单数据模型
当你把“为什么要通知”与“如何投递”分开时,多渠道通知系统更易于管理。也就是说保持一小组共享对象,只在真正不同的地方保留渠道特定的细节。
从一个事件开始。事件是一个命名触发,如 order_shipped 或 password_reset。保持名称一致:小写、下划线,必要时用过去式。把事件视为模板和偏好规则依赖的稳定契约。
基于一个事件,创建一个通知记录。它代表面向用户的意图:为谁、发生了什么以及渲染内容所需的数据(订单号、送达日期、重置码)。在这里存共享字段,比如 user_id、event_name、locale、priority 和 scheduled_at。
然后按渠道拆分为消息。一次通知可能产生 0 到 3 条消息(电子邮件、短信、Telegram)。消息包含渠道特定字段,比如目的地(电子邮件地址、手机号、Telegram chat_id)、template_id 和已渲染的内容(电子邮件的主题/正文、短信的短文本)。
最后,跟踪每次发送作为一个投递尝试。尝试记录 provider request_id、时间戳、响应码和一个归一化状态。这正是你在用户说“我没收到”时要检查的东西。
一个简单模型通常可以用四张表或集合来表示:
- Event(允许的事件名称与默认项目录)
- Notification(每个用户意图一条)
- Message(每个渠道一条)
- DeliveryAttempt(每次尝试一条)
及早考虑幂等性。为每个通知提供确定性的键,例如 (event_name, user_id, external_ref),以防上游系统的重试产生重复。如果工作流步骤重跑,幂等键可以防止用户收到两条短信。
长期只存审计所需的数据(事件、通知、最终状态、时间戳)。短期投递队列和原始提供商负载只保留到运行和排查所需的时间。
实用的端到端流程(逐步)
当你把“决定要发送什么”与“发送它”分开时,多渠道通知系统效果最佳。这能让应用保持快速并简化故障处理。
一个实用流程如下:
-
事件产生者创建通知请求。 这可以是 “password reset”、“invoice paid” 或 “ticket updated”。请求包含用户 ID、消息类型和上下文数据(订单号、金额、客服姓名)。立即存储请求以保留审计轨迹。
-
路由器加载用户与消息规则。 它查找用户偏好(允许的渠道、已选择项、静默时段)和消息规则(例如:安全警报必须先尝试邮件)。路由器决定渠道计划,比如先 Telegram、再 SMS、最后 email。
-
系统为每个渠道入列发送任务。 每个任务包含模板键、渠道和变量。任务进入队列,这样用户操作就不会被发送阻塞。
-
渠道工作进程通过提供商投递。 邮件走 SMTP 或邮件 API,短信走 SMS 网关,Telegram 走你的机器人。工作进程应具备幂等性,重试同一任务不会造成重复发送。
-
状态更新汇总到一个地方。 工作进程记录 queued、sent、failed,以及可用时的 delivered。如果提供商仅确认 “accepted”,也要记录并与 delivered 区分对待。
-
回退与重试基于相同状态运行。 如果 Telegram 失败,路由器(或重试工作进程)可以在不丢失上下文的情况下安排接下来发送 SMS。
示例:用户更改密码。后端发出一个包含用户和 IP 地址的请求。路由器看到用户偏好是 Telegram,但静默时段在夜间阻止它,于是现在安排发送邮件并在早晨安排 Telegram,同时把两者都记录到同一条通知记录下。
如果你在 AppMaster 中实现,建议把请求、任务和状态表放在 Data Designer 中,在 Business Process Editor 中表达路由和重试逻辑,发送操作异步处理以保持 UI 响应。
跨渠道通用的模板结构
一个好的模板系统从一个想法出发:你是在为一个事件发送通知,而不是“发送一封邮件”或“发送一条短信”。为每个事件创建一个模板(密码重置、订单发货、支付失败),然后在同一事件下存储各渠道的变体。
在每个渠道变体中保持相同的变量名。如果电子邮件使用 first_name 和 order_id,短信和 Telegram 应使用完全相同的名称。这能避免某个渠道渲染正常而另一个显示空白的细微错误。
一个简单且可复用的模板形态
对每个事件,为每个渠道定义一小组字段:
- Email:subject、preheader(可选)、HTML body、text fallback
- SMS:plain text body
- Telegram:plain text body,加上可选按钮或短元数据
渠道间唯一变化的是格式,而非含义。
短信需要特别规则,因为它短小。事先决定内容过长时的处理方式并保持一致:设置字符限制、选择截断规则(截断并加 … 或先丢弃可选行)、避免长 URL 和多余标点,把关键动作放在前面(验证码、截止时间、下一步)。
把语言当参数而不是业务逻辑复制
将语言视为参数而非独立工作流。为每个事件和渠道存储翻译,然后用相同变量渲染。Order shipped 的逻辑保持不变,而主题和正文随语言变化。
预览模式非常值得投入。用示例数据(包括长名等边缘情况)渲染模板,让支持团队在上线前验证电子邮件、短信和 Telegram 的变体。
可信且易调试的投递状态
通知只有在你后来能回答“它发生了什么”时才有用。好的多渠道通知系统把你想发的消息与每次投递尝试分开。
从一小组跨渠道共享的状态开始,含义在电子邮件、短信和 Telegram 中一致:
- queued:被系统接受,等待工作进程
- sending:正在进行投递尝试
- sent:已成功交给提供商 API
- failed:尝试以可以处理的错误结束
- delivered:有证据表明到达用户(若可用)
把这些状态放在主消息记录上,同时在历史表中跟踪每次尝试。正是这些历史让调试变得简单:尝试 #1 超时、尝试 #2 成功,或短信成功而邮件一直退回。
每次尝试需存哪些内容
把提供商响应归一化,这样即便提供商用不同措辞,你也能搜索和归类问题。
- provider_name 与 provider_message_id
- response_code(归一化码,如 TIMEOUT、INVALID_NUMBER、BOUNCED)
- raw_provider_code 与 raw_error_text(供支持排查)
- started_at、finished_at、duration_ms
- channel(email、sms、telegram)和 destination(掩码显示)
为部分成功做好准备。一次通知可能产生三条渠道消息,它们共享相同的 parent_id 和业务上下文(order_id、ticket_id、alert_type)。如果短信发送成功但邮件失败,你仍希望在一个地方看到完整的故事,而不是三个无关联的事件。
“已送达”真正意味着什么
“已发送”不等于“已送达”。对于 Telegram,你可能只能知道 API 接受了消息。对于短信和邮件,投递往往依赖 webhook 或提供商回调,而且不同提供商的可靠性不一。
提前为每个渠道定义 delivered 的含义。可用时使用 webhook 确认投递;否则把 delivered 视为未知并继续报告 sent。这样你的报告更诚实,支持给出的答复也一致。
重试、回退与何时停止
重试往往是通知系统出错的地方。重试太快会造成风暴;重试无限会造成重复和支持问题。目标很简单:在有实际成功机会时重试,在无望时停止。
先对失败进行分类。邮件提供商超时、短信网关 502、或 Telegram 临时错误通常可重试。格式错误的邮箱、校验失败的手机号、或把你的机器人屏蔽的 Telegram 聊天不是可重试的。把它们混为一谈会浪费钱并淹没日志。
一个实用的重试方案是有界且带回退:
- 尝试 1:立即发送
- 尝试 2:30 秒后
- 尝试 3:2 分钟后
- 尝试 4:10 分钟后
- 在最大时效后停止(例如,告警类 30–60 分钟内停止)
在数据模型中真实地表示停止。超过重试限制后把消息标记为 dead-letter(或 failed-permanently)。保留最后的错误码和短错误信息,以便支持无需猜测即可行动。
用幂等性防止成功后重复发送。为逻辑消息创建幂等键(通常为 notification_id + user_id + channel)。如果提供商迟到响应而你重试,第二次尝试应被识别为重复并跳过。
回退应是有意的,而不是恐慌式的自动行为。根据严重性和时间定义升级规则。例如:密码重置不应回退到其他渠道(存在隐私风险),但生产事件告警可能在两次 Telegram 失败后尝试短信,再在 10 分钟后尝试邮件。
用户偏好、同意与静默时段
当通知系统尊重用户时,它看起来“聪明”。最简单的方式是让用户按通知类型选择渠道。许多团队把类型分桶(安全、账户、产品、营销),因为规则和法律要求不同。
从一个即使某渠道不可用也能工作的偏好模型开始。用户可能有电子邮件但没有手机号,或尚未连接 Telegram。你的多渠道通知系统应把这些视为正常情况,而不是错误。
大多数系统最终需要一组紧凑字段:通知类型(security、marketing、billing)、每类型允许的渠道(email、SMS、Telegram)、每渠道的同意记录(日期/时间、来源、必要时的证明)、每渠道退订原因(用户选择、邮箱退信、回复 “STOP”)以及静默时段规则(开始/结束与用户时区)。
静默时段是系统经常出错的地方。存储用户的时区(而非仅偏移量),以免夏令时等变更导致意外。当消息安排在静默时段,别将其视为失败。标记为 deferred 并选择下一个允许的发送时间。
默认值很重要,尤其是关键告警。常见做法是:安全通知忽略静默时段(但仍遵守法律要求的硬性退订),而非关键更新遵循静默时段与通道选择。
示例:密码重置应立即通过最快的允许渠道发送。周报应等到早晨并跳过短信,除非用户明确启用短信。
运营:监控、日志与支持工作流
当通知触及电子邮件、短信与 Telegram 时,支持团队需要快速得到答案:我们发了吗?到达了吗?哪里出错了?多渠道通知系统应当感觉像一个排查入口,即使背后使用了多个提供商。
从一个简单的管理视图开始,任何人都能使用。按用户、事件类型、状态和时间窗口可搜索,并把最近的尝试放在前面。每一行应展示渠道、提供商响应和下一步计划动作(重试、回退或停止)。
提前发现问题的指标
故障通常不会表现为单一清晰的错误。跟踪一小组关键数值并定期查看:
- 每渠道发送速率(每分钟消息数)
- 每提供商与每类错误码的失败率
- 重试率(多少消息需要第二次尝试)
- 到达时间(队列到 delivered,p50 与 p95)
- 丢弃率(因用户偏好、同意或超过最大重试停止)
把一切关联起来。事件发生时生成一个 correlation ID(比如 “invoice overdue”),并把它传到模板渲染、队列、提供商调用与状态更新。在日志中,这个 ID 就是当一个事件扩散到多个渠道时的线索。
支持友好的重放而不意外骚扰用户
重放很重要,但需要防护来避免骚扰用户或重复计费。一个安全的重放流程通常包括:只重新发送特定的消息 ID(而不是整个事件批次)、在发送前展示确切的模板版本与渲染内容、要求填写原因并记录触发者、如果消息已被投递则阻止重放(除非明确强制)、并对每用户与每渠道施加速率限制。
通知的安全与隐私基础
多渠道通知系统涉及个人数据(邮箱、手机号、聊天 ID)并常常覆盖敏感时刻(登录、支付、支持)。假设每条消息正文和每行日志未来都可能被查看,因此在设计时限制存储与访问范围。
尽量把敏感数据从模板中剥离。模板应可复用且“无趣”:“Your code is {{code}}” 是可以的,但避免把完整账户详情、长 token 或任何可能被用来接管账户的信息放进去。如果必须包含一次性代码或重置 token,只存验证所需的信息(例如哈希与过期时间),而不是原始值。
在存储或记录通知事件时,尽可能做掩码。支持人员通常只需要知道代码已发送,而不需要看到代码本身。手机号和邮箱同理:为了投递保留完整值,但在大多数界面只展示掩码版本。
防止大多数事故的最低控制项
- 基于角色的访问:只有少数角色可以查看消息正文与完整收件信息。
- 把调试访问与支持访问分开,避免排查变成隐私泄露。
- 保护 webhook 端点:使用签名回调或共享密钥、校验时间戳并拒绝未知来源。
- 静态加密敏感字段并在传输中使用 TLS。
- 定义保留规则:短时间保留详细日志,随后仅保留汇总或已哈希的标识符。
一个实用例子:如果密码重置短信失败并回退到 Telegram,记录尝试、提供商状态和掩码的收件人,但避免在数据库或日志中存储重置链接本身。
示例场景:一个警报,三条渠道,真实结果
客户 Maya 启用了两类通知:密码重置和新登录。她偏好先 Telegram,再邮件。只有在密码重置时她才把短信作为回退渠道。
一天晚上,Maya 请求密码重置。系统创建一条带稳定 ID 的通知记录,然后根据她的偏好扩展为渠道尝试。
Maya 看到的是简单的:几秒内收到 Telegram 消息,包含一个短重置码和过期时间。没有其他消息到达,因为 Telegram 成功且无需回退。
系统记录的更详细:
- Notification:type=PASSWORD_RESET, user_id=Maya, template_version=v4
- Attempt #1:channel=TELEGRAM, status=SENT then DELIVERED
- 没有创建邮件或短信尝试(策略:首次成功则停止)
同周稍晚,触发一次新登录告警,来源为新设备。Maya 对登录告警的偏好是仅 Telegram。系统发送 Telegram,但提供商返回临时错误。系统按回退策略重试两次后标记为 FAILED 并停止(该类型告警不允许回退)。
再举一个真实故障:Maya 在旅行中再次请求密码重置。Telegram 已发送,但配置为若 Telegram 在 60 秒内未投递则回退到 SMS。SMS 提供商超时。系统记录超时、重试一次,第二次尝试成功。Maya 在大约一分钟后收到了短信验证码。
当 Maya 联系支持时,支持按用户和时间窗口搜索,立刻看到尝试历史:时间戳、提供商响应码、重试次数与最终结果。
快速清单、常见错误与下一步
当你能快速回答两问时,多渠道通知系统更容易运行:“我们到底尝试发送了什么?”以及“之后发生了什么?”在添加更多渠道或事件前,请使用下列清单。
快速清单
- 明确的事件名称与责任人(例如
invoice.overdue归账单团队所有) - 模板变量一次定义(必需与可选、默认值、格式化规则)
- 事先约定的状态(created、queued、sent、delivered、failed、suppressed)及其含义
- 重试限制与回退(最大尝试次数、间隔、停止规则)
- 保留规则(消息正文、提供商响应及状态历史保存多长时间)
如果只能做一件事,请把 sent 与 delivered 的区别用白话写清楚。Sent 是系统所做的动作。Delivered 是提供商报告的结果(它可能延迟或缺失)。混淆这两者会让支持与干系人困惑。
常见错误要避免
- 把 sent 当作成功并夸大投递率
- 让渠道特定模板逐渐分歧,导致电子邮件、短信与 Telegram 相互矛盾
- 在没有幂等性的情况下重试,导致提供商超时后又接受消息时产生重复
- 无限重试,把临时故障变成噪声事件
- 在日志与状态记录中存储过多个人数据“以备不时之需”
从一个事件和一个主渠道开始,然后把第二个渠道作为回退(而非并行轰炸)。当流程稳定后逐个事件扩展,保持模板和变量的共享,以确保消息一致。
如果你希望在不手写全部代码的情况下构建,AppMaster (appmaster.io) 对核心部分很适合:在 Data Designer 中建模事件、模板与投递尝试,在 Business Process Editor 中实现路由与重试,并将电子邮件、短信和 Telegram 连接为集成,同时把状态跟踪保持在同一处。


