返回博客

企业微信 send_welcome_msg 不替换占位符——UI 与 API 是两套渲染引擎

一个 C 端运营场景:BA 在企业微信加客户为好友后,系统自动发一条个性化欢迎语,"您好 张三,欢迎光临 XX 门店!"。后台管理界面把欢迎语模板配好了,%NICKNAME%%STORE_NAME% 占位符看着没啥毛病,上线测试——客户截图发过来:"啥意思?'您好 %NICKNAME%,欢迎光临 %STORE_NAME%!'"

现象

  • BA 加好友触发 add_external_contact 回调,后端调 send_welcome_msg API,传入带占位符的 text.content
  • 客户实际收到的消息里,%NICKNAME%%STORE_NAME% 一字不差,没有任何替换
  • 换成 ${name}{customer_name} 等格式,还是不替换
  • 去企微管理后台手动配"自动欢迎语",里面的 %NICKNAME% 就能正常替换——同一个平台,行为截然不同

根因

企业微信有两套完全独立的欢迎语机制

| 路径 | 占位符引擎 | 行为 | |------|-----------|------| | 企微管理后台 UI 配置"自动欢迎语" | 企微服务端模板渲染 | %NICKNAME% → 客户昵称 | | send_welcome_msg API(应用主动调) | 无 | 字符串原样下发 | | add_msg_template API(群发) | 无 | 字符串原样下发 |

官方文档 send_welcome_msgtext.content 字段说明只有一句话:"消息文本内容,最长为 4000 字节"——完全没有提占位符。UI 里能用 %NICKNAME% 是因为企微服务端在下发前做了模板渲染,而 API 路径下发的是最终成品,不会被二次解析。

坑:把 UI 行为反推到 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_code 24 小时过期)
  • 内容超限:客户昵称可能含 emoji 或很长,替换后要检查 strlen($content) > 4000 并截断
  • str_replace 大小写严格匹配:团队约定统一用大写 %NICKNAME%,或改用 str_ireplace 不区分大小写,混用必踩
正解:应用层替换,传成品文本

不要指望企微 API 帮你渲染占位符。在调 send_welcome_msg 之前,先从 ExternalContact 接口取到真实昵称,用 str_replace 在本地把模板变成成品,再把成品文本传给 API。占位符约定、fallback 逻辑、内容长度检查,全部在自己的代码里管。

一句话外卖

开放平台的 UI 配置界面和 API 接口往往是两套独立管道,UI 里能用的模板语法从不自动延伸到 API——沉默即不支持,动手前先搜文档里有没有"占位符"字样。