企业微信 41051:欢迎语发过一次就永久封死,换 welcome_code 没用
在给某后台管理系统对接企业微信「客户加好友自动发欢迎语」的功能时,我踩到了一个让整个团队反复绕圈子的坑。功能上线后,联调测试客户加 BA 好友一切正常,欢迎语发出去了。但当我们做「删好友再加」的回归测试时,企微回调确实下发了新的 welcome_code,应用却死活收到 errcode: 41051,欢迎语发不出去。
现象
测试客户删掉 BA 好友再重新加好友,企微后台回调正常触发 add_external_contact 事件,字段里携带了全新的 welcome_code(base64 解开能看到时间戳是新的),但应用调用 cgi-bin/externalcontact/send_welcome_msg 时稳定返回:
{ "errcode": 41051, "errmsg": "externaluser has started chatting" }与此同时,企微管理端 UI 后台配置的「自动欢迎语」在相同的测试场景下却能正常重发——客户再次加好友后会收到那条通用欢迎语。两条路径表现截然不同,团队开始怀疑是代码里 welcome_code 复用的 bug,花了大量时间排查缓存、幂等键、数据库重复写入等方向,全部无功而返。
根因
官方文档对 41051 的描述是这样写的:
如果欢迎语已成功下发给客户,应用再调用该接口时,将返回 41051。
这句话里「再调用」的语义含混。团队的第一直觉是「welcome_code 用过一次就失效了,换新的就好」,但实测的真实边界是:
阻断维度 = (external_userid, agent_id)
↑ 客户的企微 ID ↑ 自建应用的 ID
只要这对组合曾经 send_welcome_msg 返回过 errcode=0,这个客户对这个应用就永久 lockout——不管 welcome_code 是不是新的,不管隔了多少天再加好友,结果都是 41051。
41051 的触发条件是「企微视角 send 返回 errcode=0」,不需要客户实际完整收到内容。如果应用在发送时内部逻辑有 bug(比如动态渲染字段落空、欢迎语文本漏字),企微仍然记录「已成功下发」,客户-BA 关系就被永久锁定。事后想重试补发,已经来不及了。
和它容易混淆的 41057(welcome_code 已使用)才是真正的「单次 token 幂等」;41051 是关系维度的永久状态。
至于为什么管理端 UI 的自动欢迎语能重发:那套机制走的是企微服务端自行触发,根本不调用 send_welcome_msg API,是完全独立的两套实现,行为不能类比。
排查
误判信号梳理:
| 现象 | 第一直觉 | 真相 | |------|---------|------| | 企微下发了新 welcome_code | 换新 code 就能发 | welcome_code 与 41051 无关 | | 管理端 UI 欢迎语能重发 | API 也应该能 | UI 走的是另一套服务端机制 | | 换测试号 B 第一次加 → OK | A 号有 cookie 问题 | A 历史已发过,B 是首次 | | 退后重授权 | 是权限问题 | 41051 不是权限问题 |
关键验证动作:用一个从未发过欢迎语的全新测试微信号加好友 → 返回 200 OK。用已经成功过一次的测试号 → 稳定 41051。到这里根因清晰。
正解
接受 41051 是永久状态这个现实,不要重试。重试 100 次结果都一样,还会消耗 API 配额、撑爆日志。正确做法是静默放过,同时在日志表里落 status='blocked_41051',让运营可以查询「哪些客户因复加被跳过」。
public function sendWelcome(string $userId, string $externalUserId, string $welcomeCode): void
{
$resp = WecomApi::post('/cgi-bin/externalcontact/send_welcome_msg', [
'welcome_code' => $welcomeCode,
'text' => ['content' => $this->buildContent($userId)],
]);
if (($resp['errcode'] ?? -1) === 41051) {
// 永久 lockout,不重试,记可观测日志
Log::info('welcome_msg 41051 permanent block', [
'user' => $userId,
'external' => $externalUserId,
]);
$this->logWelcome($userId, $externalUserId, 'blocked_41051');
return;
}
if (($resp['errcode'] ?? 0) !== 0) {
Log::error('welcome_msg fail', $resp);
return;
}
$this->logWelcome($userId, $externalUserId, 'sent');
}如果业务上确实需要给「复加的老客户」发一条消息,可以 fallback 到 /externalcontact/add_msg_template(群发模板),但要注意它有每客户 24h 1 条的频控,且语义从「欢迎语」变成了「群发」,需要运营同意。
另一个值得注意的隐患:发送时的渲染依赖必须前置完成。如果欢迎语里有动态内容(客户昵称、门店名等),要确保这些字段在调用 send_welcome_msg 之前已经就绪。一旦渲染有问题发出了一条漏字的欢迎语,企微仍然记 errcode=0,这个客户-BA 关系就被消耗了,无法挽回。
测试策略也需要跟着调整:已经和某个测试号完成过首次发送的号就算「burn 掉了」,永远是 41051 状态,只能换新的测试微信号。
一句话外卖
企微
send_welcome_msg的 41051 是 (客户 ID, 应用 ID) 维度的永久阻断,不是 welcome_code 的单次幂等——文档里「再调用」三个字蒙过了很多人。