模板消息与订阅消息:区别、授权与下发

admin 微信QQ技巧 2026-10-29 4 0

先说一个判决性的结论:你要下发的通知能不能发出去,八成不取决于接口怎么调,而取决于用户在什么时候点过哪个按钮。公众号侧的模板消息和小程序侧的订阅消息,本质都是「先拿到额度,再在额度内下发」,而额度来自用户的主动点击。很多人接口联调通过,上线第一天就发现发十条成功三条,转头怀疑 access_token 有并发问题——其实问题在产品设计阶段就埋下了。

先分清你要下发的到底是哪一种

两者经常被混为一谈,因为场景长得很像:都是「用户做了某件事,过一会儿你给他推一条结构化通知」。但载体不同,能力也不同。

  • 公众号模板消息:走公众号会话或服务通知,用户必须关注,模板要在后台申请通过,字段是「关键字 + 值」。
  • 小程序订阅消息:走小程序内的服务通知,不要求关注,模板从公共库里选,字段是 thing1、time2、amount3 这样的编号占位符。
  • 两者都受授权约束,但形态不同:前者受发送频次与账号能力限制,后者受授权次数与模板类型限制。

选型先问一句:用户在哪里?只在公众号活动就走模板消息;核心场景在小程序、用户可能压根没关注号,就走订阅消息。两者可以并行,但字段命名规则完全不同,别指望一套模板兼容两边。

一次性订阅与长期订阅

小程序的授权分两种形态,差别直接决定你的产品能不能做「持续提醒」。

一次性订阅

用户点一次授权,你拿到一次下发额度,发完就消耗掉了,下次要提醒必须再让他点一次。同一模板下可以连续多次授权,额度会累积,但每次都对应一次真实点击。

长期订阅

用户授权一次,在较长时间内可以多次下发,但只对特定类目开放,普通工具类、电商类小程序通常申请不到。别一开始就把方案压在长期订阅上,先确认自己的类目有没有这个能力。

// 前端请求授权(一次性订阅)
wx.requestSubscribeMessage({
  tmplIds: ['模板ID1', '模板ID2'],
  success(res) {
    // res['模板ID1'] === 'accept' 才算拿到额度
  }
})

res 里的值只有三种:accept、reject、ban。accept 才是拿到额度,reject 是这次点了拒绝,ban 是被后台封禁。很多人只看到接口没报错就开始下发,结果自然是空跑。

授权必须由用户点击触发

这是整条链路上最硬的约束:requestSubscribeMessage 只能在用户点击的同步调用栈里发起。你在 setTimeout 里调、在接口回调里调、在页面 onLoad 里调,全部失败或被静默忽略。

正确做法是把授权和业务动作绑在一起:用户点「提交预约」,处理函数里先请求授权,拿到 accept 再提交表单。用户对我点了预约、所以要一条提醒这件事是有预期的,转化率远高于莫名弹一个授权框。反例是进页面就弹,用户不知道你要提醒什么,拒绝率极高,且拒绝后短期内很难再拉起来。另外弹窗每次最多带三个模板 ID,超出的会被忽略,要批量授权就分批,别在一次点击里连弹两次。

模板申请与字段命名规范

模板得先在后台申请或从公共库里选。申请时的关键词列表决定了你能下发什么,事后想加字段只能重新申请一个新模板。所以设计要一次想全,宁可多留一个字段。

字段命名

小程序的字段是「类型前缀 + 序号」,前缀决定了格式约束:thing 是短文本且有长度上限,time 是时间,amount 是金额(一般带币种单位),character_string 适合订单号,phrase 更短,number 是纯数字。超长度会直接导致下发失败或内容被截断,联调时用最长的真实样例压一遍。

公众号模板

公众号模板用 first、keyword1、keyword2、remark 这套结构,字段名固定,靠位置语义对齐。keyword 的顺序不能乱,用户在消息里看到的顺序就是你在请求体里提交的顺序。

下发接口的请求结构

两边都是 POST JSON,都要带 access_token,都不接受表单编码。

POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
{
  "touser": "OPENID",
  "template_id": "TEMPLATE_ID",
  "url": "https://your.domain/detail?id=123",
  "miniprogram": { "appid": "MINI_APPID", "pagepath": "pages/detail/index?id=123" },
  "data": {
    "first":    { "value": "您的预约已确认" },
    "keyword1": { "value": "18:30" },
    "keyword2": { "value": "三楼会议室" },
    "remark":   { "value": "请提前十分钟到场" }
  }
}

小程序侧结构类似,但 data 的键就是占位符名字,值同样包一层 value。跳转参数有两个坑:url 只能跳公众号内的网页;pagepath 不能以斜杠开头,页面还必须已发布,否则点进去是白屏。

access_token 的缓存与刷新

全局 access_token 有效期 7200 秒,一天有调用次数上限,必须缓存。但缓存本身有个更隐蔽的陷阱:并发刷新会让旧 token 失效。多个实例同时判断过期、同时刷新,微信侧只保留最新生成的那一个,先拿到的立刻作废,于是你手上出现一批看着新鲜实则已死的 token,接口开始随机报错。

  1. 集中刷新:所有实例共用一个带锁的入口,只有一个线程真正发起请求。
  2. 提前刷新:剩余有效期低于安全水位就开始刷新,不必等它真过期。
  3. 双活兜底:同时保留新旧两个值,旧值再留一小段时间,避免飞行中的请求失败。
  4. 失败重试一次:凭证类错误强制刷新并重试,但只重试一次,别把额度打爆。

素材、二维码等接口同样吃这个 token,所以刷新策略必须全局唯一。

跳转参数与常见失败原因

失败绝大部分不是网络问题,而是权限或额度问题。下面按发生频率排。

错误码原因处理
43004用户未关注公众号引导关注,或改用小程序订阅消息
43101用户拒绝或未授权该模板在业务动作里重新引导一次授权
40003openid 不正确确认 openid 与当前 appid 属同一账号体系
40001凭证失效,多为并发刷新导致统一缓存入口,强制刷新后重试一次
47003模板参数不合法,如超长或空值按字段类型校验长度,空值也给占位文案

场景对应与迁移注意

场景用哪种理由
订单状态变更、发货提醒小程序一次性订阅下单天然是点击时机,额度可控
预约到店、课程开课小程序一次性订阅提交预约时授权,提醒一次即够
存量粉丝的活动通知公众号模板消息用户已在公众号内,触达路径最短
用户没关注号的小程序业务小程序订阅消息不依赖关注关系
周期性提醒、多次触达先确认类目能否长期订阅做不到就只能每次攒额度

迁移时先做一件事:把授权和下发解耦。授权发生在用户端,你只记录「某用户对某模板还剩几次额度」;下发发生在服务端,只消费额度。这样后端换哪种接口,前端都不用改。同时提前把字段长度上限做成校验规则,别等上线后被 47003 一条条打回来。