刚接触企微私域自动化开发的同学,几乎都会踩中同一个“连环坑”:客户明明只扫码添加了一次客服机器人,后台的日志却显示瞬间收到了两三次一模一样的回调请求。
带来的直接后果就是——系统给客户连发了三遍一样的欢迎语。客户体验极差,甚至反手就是一个拉黑。
到底为啥会收到多次推送?遇到这种情况业务代码该怎么防重?今天咱们就直接剖析一下底层的推送逻辑。
真相一:你触发了企微的“超时重试机制”
这是导致重复回调最、最、最常见的原因。
企微底层的 Webhook 推送是个“急性子”。当有新客户加你时,系统把加密报文推送到你的服务器,它最多只等你 5 秒钟。
很多新手的代码逻辑是串行的:
接收密文,解密
解析出
ExternalUserID拿着 ID 去查内部的 MySQL 数据库,看这人是不是老客户
调用业务大模型生成一段专属欢迎语
调用发消息接口,把欢迎语发出去
最后才给回调接口 return 一个
success
这套流程走下来,往往需要两三秒甚至更久。一旦网络稍微波动,超过了 5 秒,企微网关就会判定:“这台服务器没响应,推送失败,我再重试一次吧。” 于是,第二次、第三次相同的数据包就砸过来了。
破局思路: 永远不要在回调的当前线程里干重活!解密拿到数据后,立刻给网关响应""(空字符串)或者success结束会话。真正的发欢迎语逻辑,丢到异步队列(比如 Redis 的 List 或者 Celery)里去慢慢跑。
真相二:真的不是重试,而是“事件连发”
有时候你查了日志,发现自己明明 1 秒内就返回了 success,为啥还是有多个回调? 这时候请仔细看解密后的明文 JSON,重点盯住ChangeType这个字段。
在真实的业务环境里,一个加好友的动作,可能会同时触发多个不同类型的状态变更:
第一个到达的回调:
ChangeType: "add_external_contact"(添加了外部联系人)紧随其后的第二个回调:
ChangeType: "edit_external_contact"(因为你在后台设置了自动打标签,客户加上的一瞬间被贴了标签,触发了信息修改事件)
如果你的代码不够严谨,没有去精确拦截ChangeType,而是粗暴地只要看到有客户 ID 就去发欢迎语,那自然就会发重。
终极防御:做业务的“幂等性”控制
网络环境是极其复杂的,就算你搞定了异步处理和类型拦截,依然无法绝对保证底层的网络层不出现偶尔的重复发包。在企业级开发中,要想彻底解决这类回调并发问题,唯一靠谱的方案就是做幂等校验。
一个简单的防重思路(Redis 方案):当解析出add_external_contact事件时,拿到目标客户的ExternalUserID和当前时间。 每次准备发欢迎语前,先去 Redis 里塞一个 Key(比如welcome_sent_{ExternalUserID}),并设置一个 10 秒左右的过期时间。
代码逻辑大致如下:
Python
# 伪代码演示防重逻辑 lock_key = f"welcome_sent_{external_user_id}" # SETNX: 如果这个 Key 不存在就设置成功返回 1,存在就返回 0 is_first_time = redis.setnx(lock_key, "1") redis.expire(lock_key, 10) # 加上 10秒 锁过期时间 if is_first_time: # 证明是第一次收到,放行,去调用发消息接口 send_welcome_msg(...) else: # 证明这 10 秒内已经处理过该用户的加好友请求了,直接 return 丢弃 return "ignore duplicate request"随手总结
写回调接口,别对网络和底层的只推一次(Exactly-once)抱有幻想。随时翻看 API文档 确认报文结构,做到“异步响应防超时,事件类型精准拦,Redis 加锁做防重”。
只要把这三板斧落实到代码里,不管前端的扫码动作再怎么高并发,你的后端也能稳如老狗,绝对不会再出现连发三遍欢迎语的尴尬场面。