2024年12月14日·阅读约1分钟

多渠道通知系统:模板、重试与偏好

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

多渠道通知系统:模板、重试与偏好

单一通知系统能解决什么问题

当电子邮件、短信和 Telegram 被当作互相独立的功能构建时,问题很快就会显现。所谓“相同”的警报往往出现措辞不同、时间不同、以及谁能收到的规则不同。支持团队于是要追踪三套真相:一套在邮件提供商,一套在短信网关,还有一套在机器人日志里。

一个多渠道通知系统通过把通知当作一个产品来解决这个问题,而不是三套集成。发生一个事件(重置密码、发票已付、服务器宕机),系统根据模板、用户偏好和投递规则决定如何跨渠道发送。消息在不同渠道可以有不同的格式,但在含义、数据和跟踪上保持一致。

大多数团队无论从哪个渠道开始,最终都需要相同的基础能力:带变量的版本化模板、投递状态跟踪(“sent、delivered、failed、为什么”)、合理的重试与回退、带同意与静默时段的用户偏好,以及支持团队可以查看的审计轨迹,而不用猜测。

成功看起来很平凡,但这是好事。消息可预测:合适的人在合适的时间通过他们允许的渠道收到合适的内容。当出现问题时,排查简单,因为每次尝试都有记录、带清晰的状态和原因码。

“新登录”警报就是一个好例子。你只创建一次,用相同的用户、设备和位置信息填充,然后将其作为电子邮件(用于详情)、短信(用于紧急)和 Telegram(用于快速确认)投递。如果短信提供商超时,系统会按计划重试、记录超时,并可以回退到另一个渠道,而不是丢弃警报。

核心概念与简单数据模型

当你把“为什么要通知”与“如何投递”分开时,多渠道通知系统更易于管理。也就是说保持一小组共享对象,只在真正不同的地方保留渠道特定的细节。

从一个事件开始。事件是一个命名触发,如 order_shippedpassword_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),以防上游系统的重试产生重复。如果工作流步骤重跑,幂等键可以防止用户收到两条短信。

长期只存审计所需的数据(事件、通知、最终状态、时间戳)。短期投递队列和原始提供商负载只保留到运行和排查所需的时间。

实用的端到端流程(逐步)

当你把“决定要发送什么”与“发送它”分开时,多渠道通知系统效果最佳。这能让应用保持快速并简化故障处理。

一个实用流程如下:

  1. 事件产生者创建通知请求。 这可以是 “password reset”、“invoice paid” 或 “ticket updated”。请求包含用户 ID、消息类型和上下文数据(订单号、金额、客服姓名)。立即存储请求以保留审计轨迹。

  2. 路由器加载用户与消息规则。 它查找用户偏好(允许的渠道、已选择项、静默时段)和消息规则(例如:安全警报必须先尝试邮件)。路由器决定渠道计划,比如先 Telegram、再 SMS、最后 email。

  3. 系统为每个渠道入列发送任务。 每个任务包含模板键、渠道和变量。任务进入队列,这样用户操作就不会被发送阻塞。

  4. 渠道工作进程通过提供商投递。 邮件走 SMTP 或邮件 API,短信走 SMS 网关,Telegram 走你的机器人。工作进程应具备幂等性,重试同一任务不会造成重复发送。

  5. 状态更新汇总到一个地方。 工作进程记录 queued、sent、failed,以及可用时的 delivered。如果提供商仅确认 “accepted”,也要记录并与 delivered 区分对待。

  6. 回退与重试基于相同状态运行。 如果 Telegram 失败,路由器(或重试工作进程)可以在不丢失上下文的情况下安排接下来发送 SMS。

示例:用户更改密码。后端发出一个包含用户和 IP 地址的请求。路由器看到用户偏好是 Telegram,但静默时段在夜间阻止它,于是现在安排发送邮件并在早晨安排 Telegram,同时把两者都记录到同一条通知记录下。

如果你在 AppMaster 中实现,建议把请求、任务和状态表放在 Data Designer 中,在 Business Process Editor 中表达路由和重试逻辑,发送操作异步处理以保持 UI 响应。

跨渠道通用的模板结构

一个好的模板系统从一个想法出发:你是在为一个事件发送通知,而不是“发送一封邮件”或“发送一条短信”。为每个事件创建一个模板(密码重置、订单发货、支付失败),然后在同一事件下存储各渠道的变体。

在每个渠道变体中保持相同的变量名。如果电子邮件使用 first_nameorder_id,短信和 Telegram 应使用完全相同的名称。这能避免某个渠道渲染正常而另一个显示空白的细微错误。

一个简单且可复用的模板形态

对每个事件,为每个渠道定义一小组字段:

  • Email:subject、preheader(可选)、HTML body、text fallback
  • SMS:plain text body
  • Telegram:plain text body,加上可选按钮或短元数据

渠道间唯一变化的是格式,而非含义。

短信需要特别规则,因为它短小。事先决定内容过长时的处理方式并保持一致:设置字符限制、选择截断规则(截断并加 … 或先丢弃可选行)、避免长 URL 和多余标点,把关键动作放在前面(验证码、截止时间、下一步)。

把语言当参数而不是业务逻辑复制

将语言视为参数而非独立工作流。为每个事件和渠道存储翻译,然后用相同变量渲染。Order shipped 的逻辑保持不变,而主题和正文随语言变化。

预览模式非常值得投入。用示例数据(包括长名等边缘情况)渲染模板,让支持团队在上线前验证电子邮件、短信和 Telegram 的变体。

可信且易调试的投递状态

从构建到部署
准备好运行时,可部署到 AppMaster Cloud 或你自己的云环境。
部署应用

通知只有在你后来能回答“它发生了什么”时才有用。好的多渠道通知系统把你想发的消息与每次投递尝试分开。

从一小组跨渠道共享的状态开始,含义在电子邮件、短信和 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,记录尝试、提供商状态和掩码的收件人,但避免在数据库或日志中存储重置链接本身。

示例场景:一个警报,三条渠道,真实结果

快速实现多渠道投递
在同一地方接入 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 连接为集成,同时把状态跟踪保持在同一处。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧
多渠道通知系统:模板、重试与偏好 | AppMaster