消息模板一开始都是代码里的一个字符串常量,改一次文案发一次版。等到同一句通知要发给十几个群、运营每周要改两次措辞、某天发出去一条称呼位置空着的消息,才会有人想起来它该是个被管理的对象。这篇讲什么该做成模板、变量按什么分层、多群复用时差异往哪放,以及渲染该发生在哪一步 —— 最后这条是取舍,没有标准答案。链路按 wecomapi 的发送接口与事件回调来写。
什么该做成模板,什么不该
企业微信消息模板不是越多越好。判断一段文案该不该进模板库,只有两条线:这个结构是不是被三个以上的触发点复用,以及改它的人是不是工程师。两条都不满足的,留在代码里比进模板库便宜 —— 模板库自己也有维护成本,条目一多,找不到的人就会新建一条相似的,然后你有了两条意思一样的模板,谁也不敢删。
- 一次性文案:某场活动的专属话术,用完作废。它属于内容资产,不属于模板库,混进来只会把库撑大。
- 结构固定、变量变化:订单状态、审批结果、值班日报。这一类才是模板,也是模板机制真正能省下人力的地方。
- 结构本身在变:每次排版都不一样、字段组合不固定。硬塞进模板会长出一串条件分支,那是内容编排问题,不是模板问题。
还有一个常被跳过的判断是粒度。把一类通知做成一个大模板、用变量控制里面显示哪几段,看起来省事,实际比拆成三条小模板难维护得多 —— 大模板的每一次修改都要回归全部分支,而分支组合会随着需求指数增长。宁可多几条模板,也不要在模板里写条件。真到了必须按条件切内容的时候,切的应该是「选哪条模板」这一步,而不是模板内部。
变量按取数成本分层,缺值策略写进模板
变量真正该分类的维度不是数据类型,而是「取不到的时候会发生什么」。按这个分只有三层,三层的失败概率差一到两个数量级,却常被同一段渲染代码平等对待,于是一个偶发的慢查询就能把整批通知拖住。
- 1事件自带:用 wecomapi 的事件回调拿到消息或成员变更时,触发上下文里已经带着的那部分。它必然存在,不需要兜底,也不该再回查一遍。
- 2回查自有业务库:客户称呼、订单号、负责人。查不到是正常情况 —— 数据还没同步、记录被改过、人已经离职 —— 必须给回退值。
- 3跨系统实时拉取:库存、余额、排队人数。这一层还会超时,要有明确的超时上限,超时之后走回退值或者放弃这一条,不能让一个慢依赖卡住整条发送队列。
缺值策略要写在模板定义里,不写在渲染代码里。每个变量声明三件事:必填还是可空、缺失时的回退值、缺失时这条消息是拦住还是照发。写在模板里,运营改文案的时候就看得见;写在代码里,只有出事那天才有人翻得出来,而那天通常已经发出去几百条了。
const tpl = {
id: "notify.order_shipped",
version: 7,
body: "{{title}}|{{orderNo}} 已发出{{note}}",
vars: {
title: { from: "event", required: true },
orderNo: { from: "db", required: true, onMissing: "block" },
note: { from: "remote", required: false, fallback: "", timeoutMs: 300 },
},
};
// 渲染必须是纯函数:同一 version + 同一组取值,输出永远一致
const content = render(tpl, values);
// 出口调用(示意,精确字段与端点以线上接口文档为准)
await post("https://manager.wecomapi.com/message/sendText", { guid, toId, content });缺值率值得按模板维度单独记一个数。它会自己漂移 —— 上游改了一个字段,某条模板的缺值率第二天就上去了,而消息照发,只是里面少了一句话。这个指标比任何一次模板评审都早发现问题。
多群复用:一个基础模板加一层覆盖
同一条通知发到十几个群,差异通常不在结构上,而在称呼、@ 谁、要不要带链接、要不要展开明细这几件小事上。最省事的做法是复制十几份各改一改,三个月后你会有十几份互相漂移的企业微信群通知模板,没人说得清哪一份是基准,改一次公共措辞要挨个找。
正确的形状是基础模板加覆盖层:基础模板定义结构和完整的变量集,覆盖层按接收方维度只覆盖字面文案片段和显示开关。关键约束在覆盖层能改什么 —— 只能改文案和开关,不能引入新变量。一旦允许覆盖层带自己的变量,它就是第二套模板,你会同时维护两套渲染逻辑和两套缺值策略。
- 覆盖层必须是稀疏的。没有覆盖项的群完全跟随基础模板,不要为每个群生成一份完整副本 —— 副本一旦生成,基础模板后续的修改就传不下去了,而这件事没有任何报错。
- 覆盖项要能被列出来:随时回答「哪些群偏离了基础模板、偏在哪一项」。列不出来的覆盖等于已经分叉,只是还没人发现。
- 单聊和群里的同一份内容,称呼和信息密度通常不一样。这一层差异要做成两条基础模板,不要用覆盖层硬扛,否则覆盖项会把两种形态的差异全吃进去。
覆盖项的总数是个健康指标。它超过基础模板条目数的两三倍,说明抽象切错了位置 —— 该回去重新划分基础模板,而不是继续往覆盖层里加。
渲染放在提交时还是发送时
这是模板管理里唯一一个真正的取舍,两边都有代价。提交时渲染,队列里存的是成品文本:能在发出去之前全量校验、能人工抽检、失败点集中在一处;代价是内容在排队期间会过期,真发出去的时候数字已经不对了。发送时渲染,内容永远最新;代价是校验窗口没了,渲染失败发生在链路最末端、最不好处理的位置。
按消息的时效语义分,结论其实很清楚:营销与播报类提前渲染,它们的价值在文案本身,而且往往必须支持发前审核;状态与查询类延迟渲染,它们的价值在数字准确,发出一个两小时前的余额比不发更糟。这条判断和消息量、和用哪种发送形态都无关,只跟内容会不会过期有关。
混合场景按变量分,不按消息分:能提前确定的部分先渲染成成品,必须实时的那两三个变量留成占位,在调用 wecomapi 的发送接口之前的最后一步补齐。这样审核看到的是接近成品的内容,实时性也保住了。占位变量的数量要卡死在个位数,多了等于回到发送时渲染,只是多了一层假的安全感。
无论选哪一种,渲染都要是纯函数:同一个模板版本加同一组变量取值,任何时候渲染出的文本必须一致。渲染里读当前时间、读随机数、读全局配置开关,会让事后复现变得不可能,而复现是回答「这条消息为什么长这样」的唯一手段。需要时间的地方,把时间当成一个显式变量传进来。
版本、灰度与留痕
模板一改立刻全量生效,是这套机制里最容易出的事故 —— 因为改模板的人通常不觉得自己在改代码。三件事能挡住绝大部分问题:模板有版本、版本能只对部分接收方生效、发出的每条消息记录用的是哪个模板的哪个版本。
- 1版本不可变。改模板产生新版本,不覆盖旧的。已经排队等待发送的消息绑定提交时的版本,不会因为中途有人改了一次而变形。
- 2灰度按接收方切,不按时间切。先让少数几个群用新版本跑一轮再放开。按时间切换的问题是出事时旧版本已经没有对照组,你只能凭印象判断是不是这次改动引起的。
- 3留痕记版本号和变量取值,不记渲染全文。存全文既占空间又容易带上敏感内容;记模板 ID 加版本号加取值,需要时重新渲染一遍就能复现 —— 前提是渲染确实是纯函数。这条记录还要和发送侧对得上:把 wecomapi 返回的请求标识存进同一行,「这条消息为什么长这样」和「它到底发出去没有」才是一次查询能同时回答的两个问题。
改模板的权限也要拆开。变量目录由工程维护 —— 有哪些变量可用、各自从哪来、缺失时怎么办;文案由运营维护。两边用这张显式清单对接,运营在编辑器里只能选到清单里存在的变量。少了这条约束,模板里迟早会出现拼错的变量名,而它在渲染时表现为一个空字符串,不报错,也不会有人发现。
从代码里搬出来的顺序
已经跑了一段时间的系统,文案通常散在代码常量、配置文件和几条自动化流程的输入框里。一次性收拢做不完,也没必要。按下面的顺序切,每一步都能单独上线、单独产生收益。
- 1先建变量目录和统一的渲染函数,不动任何文案。这一步只是把「字符串怎么变成消息」收到一处,收完之后才有地方挂校验、留痕和缺值统计。出口也顺手收窄:文案只经这一个函数交给 wecomapi 的发送接口,散在各处的直调点在这一步一并清掉,否则后面挂上去的校验和留痕会被绕过去。
- 2把改动最频繁的那几条搬进模板库。频繁改的通常也是出问题最多的,先搬它们,收益一周内就看得到。
- 3补版本和留痕。到这一步,「上周三那条通知是谁改的、当时长什么样」才回答得了。
- 4多群覆盖放到末尾。它依赖前面三步都做完,早做只会把差异固化成一堆副本,反而更难收。
有一类文案不要搬:只有工程师会改、而且只在一个地方用的。它留在代码里走 code review 和发版流程,那已经是比模板库更严格的管控。判断标准始终是谁在改、被几处复用,而不是它是不是一段带变量的文本。
本文讲的是模板的组织方式与取舍。精确的字段名、消息类型与端点定义以 wecomapi 线上接口文档为准,示意代码里的结构只用于说明形状,不要照抄上生产。
常见问题
- 模板要不要做成可视化编辑器?
- 看改的人是谁。全是工程师改就不需要,一份带注释的配置加 code review 比任何编辑器都可靠。运营要自己改才值得做,而且真正必须做的不是富文本排版,是变量插入器和渲染预览:让运营只能选到工程声明过的变量、提交前能看见渲染结果。缺了这两样,编辑器只是一个更漂亮的出错方式。
- 同一条通知要发到十几个群,模板要复制十几份吗?
- 不要。基础模板定义结构和变量集,按群维度加一层稀疏覆盖,只覆盖文案片段和显示开关。复制出来的副本会各自漂移,半年后没人说得清哪一份是基准。如果某几个群的差异多到覆盖层放不下,说明它们本来就该用不同的基础模板。
- 模板里的变量取不到值,该拦住还是照发?
- 写在模板定义里,别在渲染时临时判断。称呼、金额、单号这类缺了就明显不对的声明为必填,缺失即拦住整条;补充说明类的声明为可空并给回退值。调用 wecomapi 的发送接口之前把这次渲染的缺值情况一并记下来,按模板维度统计缺值率,比等运营来反馈快得多。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
