SMS 事件告警的正确姿势:先写记录、幂等发送、轮询状态、策略前置
把 SMS 当新订单通知的辅助或紧急渠道时,消息模板留在你的应用里,冷却时间、国家白名单、花费阈值与投递轮询都做成通知 worker 的显式部分。供应商能发送并上报状态,但安全策略是你的产品自己的事——这是作者一个小型 eval harness 的结论。
为什么不能同步 send() 一把梭
最初模型是订单事务里一次同步 send() 调用,10 秒客户端超时,无持久 attempt id。笔记本里看着整洁,重试却变含糊:超时可能意味着运营商已接受消息,而数据库仍写着"未发送"。选定设计改为:先写 alert 记录、用幂等键发送、轮询状态直到 UI 能显示 sent/delivered/failed/undeliverable。多一点管道,但状态转换可测、可重放。
测试夹具:四条准入检查
以"市场卖家通知"为夹具(一单、一卖家、一目的国、一条确定消息),harness 在接纳一个供应商集成前检查四件事:
- 发送响应可归属到订单;
- 后续读取给出稳定投递状态;
- 可恢复失败能重发而不重复订单告警;
- 策略拒绝发生在任何付费尝试之前——这条最容易被低估。
同一个事件,美国卖家与欧盟卖家收到的待遇可以不同:业务规则按国家、同意记录、静默时段与预算区分。地理围栏与国家价格熔断不在供应商管理范围,worker 必须在调 SMS API 前评估它们。每用户冷却也属于这一层,否则一连串订单更新会变成意外垃圾循环。
文案保持平淡且有界:"Order 18427 from Alex is ready to review." 一句话就够。把订单 id、模板版本、国家决策与策略版本存在自己的数据库里——API 不做按 tag 的成本聚合,这些字段才是日后解释"为什么发了、花了多少"的依据。
状态机:数据库先决策
即使生产 worker 是 Node.js,控制流也应该是语言无关的:建 alert 行 → 执行策略 → 发一次 → 轮询。429 时尊重 Retry-After(有则用,无则指数退避);重试必须带同一个 client id/幂等键,否则一次瞬时网络超时会变成发给卖家两条短信。作者给了紧凑的 Python 版(只用 send 和 status 两个路由,可平移到 Node fetch wrapper):post_with_backoff(幂等键 + 429 退避)→ 发 → 循环查状态最多 6 次、每次间隔 5 秒,直到 delivered/failed/undeliverable。
应用应持久化每个观察到的状态,而不是把最后一次轮询当唯一真相。可恢复失败时,用同一条 alert 记录和新 attempt 号调 resend;绝不静默创建新的业务事件。取消只适用于待发送的定时 SMS 流(产品明确给用户停止动作),不是已交给运营商的召回机制。没有 webhook 事件时轮询就是投递机制,轮询间隔要匹配订单告警的紧迫性与请求预算。
模板所有权是决定性约束
不是发送延迟排行榜。应用自有模板让你能审阅文案、本地化、挂订单 id、发布前跑确定性测试;供应商托管模板方便支持/运营团队不改代码就改文案,但引入第二个版本存储和变更审批路径。
实践建议
- 先建记录再发送:超时≠失败,幂等键 + 状态轮询才能消歧;
- 策略(国家/同意/静默/预算/冷却)在 worker 层、调用 API 之前执行;
- 重试沿用同一幂等键,取消不是召回。
来源:SMS Event Alerts: Delivery Polling, Resends, Cancellation, and EU/US Guardrails - DEV Community