
上一篇讲了撤回接口的机制和时效这篇从发错这个事故本身出发讲怎么设计防错体系——撤回是最后一道防线更好的做法是让消息根本发不错。一、发错消息的三种典型事故对象错误消息内容对但发给了错误的人或群。原因通常是路由逻辑 bug、wxid 映射错乱。内容错误对象对但内容有问题——模板变量没填充尊敬的{name}、知识库命中错误、敏感内容漏审。时机错误内容和对象都对但时间不对——深夜给客户发营销消息、活动没开始就发了通知。二、第一道防线发送前校验对象校验发送前确认 toUser 在允许列表内营销消息只发给打了对应标签的客户群消息确认目标群 ID。内容校验模板渲染后检查是否还有未填充的占位符敏感词过滤长度检查。时机校验营销类消息检查发送时间窗口如只在 9:00-21:00 发送。三道校验拦住大部分事故成本远低于发错后撤回。三、第二道防线灰度和延迟发送批量消息不直接全发先发一小批如5人观察确认无误再继续。更稳妥的是延迟队列消息先进队列暂存30秒这期间人工或监控发现问题可以从队列里撤下超时后才真正发出。30秒延迟对营销通知几乎无影响但给了纠错窗口。四、第三道防线撤回兜底前两道没拦住的靠撤回接口补救。撤回只对时效内消息有效所以监控要实时——发送后自动审计消息内容发现异常立即触发撤回流程。三道防线对照防线拦截阶段拦截事故类型成本发送前校验消息发出前对象/内容/时机错误低灰度延迟队列暂存期批量事故中撤回兜底发出后时效内漏网事故高客户已见防错发送体系import time def safe_send(to_user, content, scenenormal): # 第一道发送前校验 if not validate_target(to_user, scene): return None, 目标校验失败 rendered validate_content(content, to_user) if isinstance(rendered, str) and rendered.startswith(ERR): return None, rendered if scene marketing and not in_send_window(): return None, 不在发送时间窗口 # 第二道营销消息进延迟队列 if scene marketing: job_id delay_q.push({ to: to_user, content: content, ready_at: time.time() 30 }) return job_id, 已进入30秒延迟队列 # 普通消息直发但存msgId支持撤回 return send_with_recall(to_user, content), 已发送 def validate_content(template, to_user): 模板渲染内容检查 contact db.query(contacts, wxidto_user) rendered template.format(namecontact.get(nickname, 朋友)) if { in rendered or } in rendered: return ERR_占位符未填充 for word in SENSITIVE_WORDS: if word in rendered: return fERR_敏感词:{word} return rendered def in_send_window(): h datetime.now().hour return 9 h 21落地建议撤回是事故发生后的补救客户已经看到了消息体验损伤已经造成。工程上正确的投入顺序是先做发送前校验成本最低、收益最大批量场景加延迟队列最后才是撤回兜底。三道防线配合发错消息的概率能压到极低。撤回接口说明参考 Eyun 开发文档。