4.2 消息通知:订阅消息与模板
场景
开业的第一个月,朋友每天晚上做一件事:把当天付款的订单挨个截图发微信给顾客,“亲,您的杯子今天发货啦”。我看着觉得又心酸又好笑,问她为什么不用模板消息。她说以前干淘宝习惯了,还问小程序能不能群发短信——短信一条三分钱,一个月几千条,光是通知费就是一笔不小的开支。对一个刚开业的小店,这笔钱花在通知上不如花在茶上。
这就是订阅消息要解决的问题:官方提供的、免费的、能合法触达顾客的通知通道。但它有个绕不开的前提——“订阅”:顾客必须主动点过授权,你才能给他发,而且一次订阅往往只能发一条(长期订阅资质很难拿,小店基本无缘)。市面上还有一条路是短信,算过一笔账:一条三分钱,一个月两千条就是六十块,一年七百多,对一个刚开业的小店不是小数目,而且短信的打开率远低于小程序服务通知。结论很清楚:订阅消息是性价比最高的官方通道,没有之一。这个机制刚接触时我很不适应,做过服务号的人脑子里全是“群发”思维,订阅消息的模型完全不同:它不是喇叭,是回执。还有个差异点:订阅消息的额度是按“用户订阅次数”计算的,顾客授权一次你才能发一条,题目本身是做“额度管理”而不是“文案管理”。早期版本我们没做额度库,发送时一大片报错“43101 用户取消订阅”,就是因为查不到额度瞎发。
真正让我理解这套机制的是一次客服事件:有顾客付完款没收到任何通知,打电话来问“到底有没有成功”。那一刻我意识到通知不是营销,是安全感:付了钱要有回音、发了货要有单号、快到了要有预期。后来我们把顾客对通知的评价整理过一次,出现频率最高的词是“踏实”——原来一个小通知的情绪价值,远大于它的字数。从这个角度设计通知体系,思路就顺了——围绕订单状态机的每个关键节点,都配一条订阅消息。这节讲我们怎么搭这套通知:模板申请、订阅授权的时机、发送时机的编排。
方案
先看通知落在订单生命周期的哪些节点:
| 节点 | 模板 | 何时发送 | 关键字段 |
|---|---|---|---|
| 待付款 | 订单待付款提醒 | 下单后立即 | 订单号、金额、15 分钟提示 |
| 已付款 | 支付成功通知 | 支付回调后 | 订单号、支付时间 |
| 已发货 | 发货通知 | 录入单号后 | 快递公司、单号 |
| 待收货 | 即将自动确认 | 发货后第 6 天 | 自动确认时间 |
| 已完成 | 评价邀约 | 确认收货后 | 订单号、引导语 |
| 售后 | 退款结果通知 | 退款回调后 | 退款金额、到账时间 |
设计上有两个要点。第一是订阅授权的“时机营销”:授权弹窗出现在顾客最愿意点的时刻——下单结算页(“接收订单动态”)、支付成功页(发货提醒最相关)。结账时一次勾选,能覆盖好几个模板的发送额度。授权按钮的文案要诚实:说清发什么、何时发,别打着“优惠提醒”的旗号发广告,订阅消息内容受模板约束,乱来会被投诉封资质。除了结算页,商品详情页底部的“到货通知”也是个高授权场景:售罄商品旁边放一个按钮,“有货时提醒我”,顾客为真需求点授权,授权质量(之后的打开率)比结算页还高。
第二是发送时机的编排。通知不是状态机扭转的副产品想发就发,而是由定时任务统一编排:发货通知实时发、自动确认前一天发、评价邀约隔一天再发(顾客刚收货还没用上,立刻邀评价显得急切)。这些时间间隔都是上线后根据打开率调出来的:最初评价邀约是确认收货后立刻发,打开率只有百分之六;挪到第二天后翻了一倍多——时机本身就是通知内容的一部分,催得太急和催得太晚同样失败。
配置/代码
订阅授权在前端就一行关键代码:
// 下单结算页:一次性订阅多个模板
wx.requestSubscribeMessage({
tmplIds: ['tmpl_pay_ok', 'tmpl_shipped', 'tmpl_auto_confirm'],
success(res) {
// res 里记录每个模板的授权结果:accept / reject
console.log(res);
}
});
发送是云函数侧的事,模板申请在公众平台的订阅消息模块里操作,字段从订单里取:
// 云函数 sendNotify:给某顾客发某模板
const res = await cloud.openapi.subscribeMessage.send({
touser: openid,
templateId: 'tmpl_shipped',
page: `pages/order/detail?id=${orderId}`,
data: {
thing1: { value: '白釉手绘杯' }, // 商品名
character_string2: { value: 'SF1234' }, // 运单号
time3: { value: '10月24日 14:00' } // 发货时间
},
miniprogramState: 'formal' // 正式版发正式通知
});
字段类型(thing、character_string、time)必须和模板定义严格匹配,长度超了会被拒。写一个校验函数在下发前跑一遍,比线上失败了再排查省心。thing 类型最常见的失败是长度超 20 字,character_string 的麻烦是只认数字字母组合、不能带符号,踩过一次运单号里带了横杠被拒的坑。另外模板里的占位符内容要做“内容安全”检查(涉政涉黄词汇zero-tolerance),云函数里调一次内容安全接口再下发,给自己的店上个保险。
编排逻辑放在定时器里,和 3.4 节的订单状态机共用同一次扫描:
// 定时器里:扫发货后第 6 天的订单,发自动确认提醒
const presets = { autoConfirmHours: 24 * 6 };
const candidates = await db.collection('orders').where({
status: 'shipped', shippedAt: _.lt(Date.now() - presets.autoConfirmHours * 36e5)
}).get();
发送记录入库(notify_log 集合),每天对账发了哪些、失败哪些、失败原因,失败的根据错误码决定重试或放弃。发送前先扣额度(原子操作减一),发送失败再补回,和库存扣减是同一个模式——云数据库的原子 inc 在这类“配额”场景里无处不在,想系统补课的可以翻云开发 Python 实战里的数据库命令一章。
讲清楚额度的存储结构:一个文档记录某 openid 对某 template 的剩余次数,顾客每次授权成功就把 tmplIds 里 accept 的模板各加一,发送时减一。结构简单,但它决定了发信系统的心智模型——你不是在“发通知”,你是在“花一笔顾客预付的信任额度”,花光了就没了,得等顾客下次愿意再给。
踩坑
第一个坑是授权时机错位。早期授权弹窗放在首页弹一次,顾客一脸懵地拒绝,后面整条链路的通知都发不出去。挪到结算页后授权率翻了倍——在顾客最关心通知的时候要授权,才是双赢。
第二个坑是模板字段类型不匹配。thing 类型的长度上限是 20 个字符,商品名超长直接发送失败。一个 trunc 函数截断加省略号解决,但更好的做法是上线前跑一遍所有商品做校验。
第三个坑是用户拒收后无感知。订阅授权状态要存库(receiveSubscribe 集合记录每个 openid 对每个模板的可用额度),发之前查额度,没额度直接跳过免得报错刷屏。发送失败错误码里有一类是“用户取消订阅”的,需要在库里标记,以后不再骚扰这位顾客。
第四个坑是开发版误发正式通知。miniprogram_state 参数漏配时,开发环境调试发出的通知会以正式模板形态发给真用户,闹过乌龙。调试期一律用 developer 状态,上线前检查清单里加这一项。
第五个坑是把通知群发当营销。订阅消息没有“群发”能力,它就是点对点的状态回执。想做营销触达(上新、活动),只能靠扫码场景值、朋友圈海报这类手段,别和通知混为一谈——混了容易被封模板。有一次朋友想给所有老客发“上新通知”,被我从发送列表里拦了下来:订阅消息的额度只属于顾客授权过的那几个节点,借它做营销,等于把顾客的信任一次性清零。营销触达想正经做,思路可以参考小程序信息流实战里的内容分发设计,而不是拿通知通道硬凑。
第六个坑是通知文案机械化。最初模板里发货时间精确到秒,顾客来问“14:00:23 是几点”。后来时间格式统一成“10月24日 14:00”这种口语化表达。模板字段内容要当人话来写,不是数据库 dump。模板申请下来后,每个字段把最长的内容(商品名、店铺名)都填一遍真发测试,超长问题暴露在上线前。
小结
订阅消息体系上线后,朋友每天晚上手工截图发微信的活儿彻底消失了,取而代之的是顾客主动反馈“你们通知真及时”。再往后我们做了一次数据盘点:发货通知的打开率稳定在四成以上,是所有模板里最高的——付了钱的顾客,最惦记的就是“我的货到哪了”。她后来说了句很到位的话:通知不是广告,是让顾客知道“这事儿有人在管”。这套体系的精髓就是围绕订单状态机的每个节点给一次回音,不多不少,恰到好处。订阅授权这个机制设计上也教了我一课:好的通知系统,发什么由用户说了算。下一节讲搜索:云数据库索引怎么建,让顾客输入两个字就找到想要的杯子。
延伸阅读
- 订单状态机:待付款到已完成:通知触发的状态节点来源
- 数据分析:自建事件埋点:打开率这类数据的采集
- 回到本书目录:全书五篇结构一览
