企业微信 send_welcome_msg 不替换占位符——UI 与 API 是两套渲染引擎
一个 C 端运营场景:BA 在企业微信加客户为好友后,系统自动发一条个性化欢迎语,"您好 张三,欢迎光临 XX 门店!"。后台管理界面把欢迎语模板配好了,%NICKNAME% 和 %STORE_NAME% 占位符看着没啥毛病,上线测试——客户截图发过来:"啥意思?'您好 %NICKNAME%,欢迎光临 %STORE_NAME%!'"
现象
- BA 加好友触发
add_external_contact回调,后端调send_welcome_msgAPI,传入带占位符的text.content - 客户实际收到的消息里,
%NICKNAME%和%STORE_NAME%一字不差,没有任何替换 - 换成
${name}、{customer_name}等格式,还是不替换 - 去企微管理后台手动配"自动欢迎语",里面的
%NICKNAME%就能正常替换——同一个平台,行为截然不同
根因
企业微信有两套完全独立的欢迎语机制:
| 路径 | 占位符引擎 | 行为 |
|------|-----------|------|
| 企微管理后台 UI 配置"自动欢迎语" | 企微服务端模板渲染 | %NICKNAME% → 客户昵称 |
| send_welcome_msg API(应用主动调) | 无 | 字符串原样下发 |
| add_msg_template API(群发) | 无 | 字符串原样下发 |
官方文档 send_welcome_msg 的 text.content 字段说明只有一句话:"消息文本内容,最长为 4000 字节"——完全没有提占位符。UI 里能用 %NICKNAME% 是因为企微服务端在下发前做了模板渲染,而 API 路径下发的是最终成品,不会被二次解析。
企微管理后台 UI 支持 %NICKNAME% 占位符替换,和 send_welcome_msg API 的 text.content 是两套不同渲染管道,互不相通。用 API 发的内容是字面量,写什么发什么,不会被企微再处理一次。文档没有明示"不支持占位符",但沉默即不支持——只有明确写了占位符规范的接口才有替换能力。
正解
在应用层调 API 之前,先从 ExternalContact 接口拿到客户昵称,用 str_replace 做替换,然后把替换完的成品文本传给 send_welcome_msg。
步骤 1:在 add_external_contact 回调里拉客户档案并落表:
// handleExternalChange — add 分支
$resp = WecomApi::get('/cgi-bin/externalcontact/get', [
'external_userid' => $externalUserId,
]);
$name = $resp['external_contact']['name'] ?? '';
Db::name('wecom_external_contact')->insertUpdate([
'external_userid' => $externalUserId,
'name' => $name,
'updated_at' => date('Y-m-d H:i:s'),
]);步骤 2:发欢迎语前做应用层替换:
// 取模板、取昵称、替换、发送
$template = Db::name('wecom_welcome_config')
->where('user_id', $userId)
->value('text'); // "您好 %NICKNAME%,欢迎光临 %STORE_NAME%!"
$name = Db::name('wecom_external_contact')
->where('external_userid', $externalUserId)
->value('name') ?: ''; // 拿不到 fallback 空字符串
$storeName = /* 从 user → store 关联表取 */ '';
$content = str_replace(
['%NICKNAME%', '%STORE_NAME%'],
[$name, $storeName],
$template
);
WecomApi::post('/cgi-bin/externalcontact/send_welcome_msg', [
'welcome_code' => $welcomeCode,
'text' => ['content' => $content],
]);边缘情况处理:
name为空:企微约有 5% 概率在事件触发瞬间 ExternalContact 还没同步完,可以重试一次(sleep(2)+ 再拉一次),仍为空则 fallback 空字符串,不要因为拿不到昵称就放弃整条欢迎语(welcome_code24 小时过期)- 内容超限:客户昵称可能含 emoji 或很长,替换后要检查
strlen($content) > 4000并截断 str_replace大小写严格匹配:团队约定统一用大写%NICKNAME%,或改用str_ireplace不区分大小写,混用必踩
不要指望企微 API 帮你渲染占位符。在调 send_welcome_msg 之前,先从 ExternalContact 接口取到真实昵称,用 str_replace 在本地把模板变成成品,再把成品文本传给 API。占位符约定、fallback 逻辑、内容长度检查,全部在自己的代码里管。
一句话外卖
开放平台的 UI 配置界面和 API 接口往往是两套独立管道,UI 里能用的模板语法从不自动延伸到 API——沉默即不支持,动手前先搜文档里有没有"占位符"字样。