微信小程序订阅消息实战:模板申请、一次性订阅与下发全链路

微信小程序订阅消息实战:模板申请、一次性订阅与下发全链路

admin
2026-05-15 / 0 评论 / 107 阅读

微信小程序订阅消息实战:模板申请、一次性订阅与长期订阅全链路

模板消息下线后,订阅消息成了小程序触达用户的唯一官方通道。但一次性订阅"发一条扣一次"的机制、模板类目的限制、下发条件的坑,让不少后端同学在第一次接入时栽跟头。这篇从模板申请到后端下发,把完整链路和真实踩坑记录下来。

一、订阅消息的两种类型

类型规则适用场景
一次性订阅用户订阅一次,只能下发一条订单发货、审核结果、活动提醒
长期订阅订阅一次可反复下发仅限公共服务类目(政务、医疗、交通等)

残酷现实:长期订阅模板的类目卡得非常严,普通电商/内容类小程序基本申请不到。所以对大多数团队来说,玩转"一次性订阅"才是重点。

一次性订阅还有个经典场景化玩法:每次用户触发关键操作时都弹订阅授权(勾选"总是保持以上选择"后不再弹窗),积攒下发次数。

二、申请模板的正确姿势

进入 mp.weixin.qq.com → 功能 → 订阅消息 → 我的模板,从公共模板库挑选或申请新模板。

关键词选择的原则

  1. 变量字段(thing、time、phrase 等)数量够用就好,越多下发时越容易出错
  2. thing 类型长度限制 20 个字符(以内),超了直接下发失败
  3. 模板类目必须和小程序服务类目匹配,否则审核不过
  4. 一旦模板审核通过,字段不能改,只能重新申请新模板——所以先把业务字段想清楚

三、前端:请求订阅授权

3.1 基础调用

// 请求订阅授权
wx.requestSubscribeMessage({
  tmplIds: ['tmpl_123456789'],  // 最多3个模板
  success(res) {
    console.log(res['tmpl_123456789'])
    // 'accept'   用户同意
    // 'reject'   用户拒绝
    // 'ban'      已被后台封禁/关闭
    // 'filter'   该模板在 async 调用下被过滤
  },
  fail(err) {
    console.error(err.errMsg) // 例如 "requestSubscribeMessage:fail can only be invoked by user TAP gesture"
  }
})

核心限制:必须由用户点击行为直接触发。你不能在 onLoad 里静默调用,也不能 setTimeout 延迟调用——否则报 fail can only be invoked by user TAP gesture

3.2 引导订阅的时机设计

一次性订阅的本质是"攒次数",所以要在用户动机最强的时刻请求:

  • 下单成功页 → 订单进度通知
  • 提交表单后 → 审核结果通知
  • 预约成功后 → 预约提醒
// 提交订单成功后的订阅引导
async function submitOrder() {
  const order = await createOrder()

  // 订单创建成功后,在用户点击"提交"的同一个手势链路里请求订阅
  try {
    const res = await wx.requestSubscribeMessage({
      tmplIds: ['tmpl_order_status']
    })
    if (res['tmpl_order_status'] === 'accept') {
      // 上报后端:这个用户此订单的通知已授权
      api.reportSubscribe({
        orderId: order.id,
        templateId: 'tmpl_order_status',
        status: 'accept'
      })
    }
  } catch (e) {
    // 用户拒绝或勾选了不再询问,不影响主流程
  }
}

3.3 检测"总是保持以上选择"

用户勾选"总是保持以上选择,不再询问"后,后续调用 wx.requestSubscribeMessage 不再弹窗,直接静默返回结果。可以调用 wx.getSetting 里的 subscriptionsSetting 判断:

wx.getSetting({
  withSubscriptions: true,
  success(res) {
    const mainSwitch = res.subscriptionsSetting.mainSwitch       // 订阅消息总开关
    const itemSettings = res.subscriptionsSetting.itemSettings   // 各模板的勾选状态
    if (mainSwitch && itemSettings?.['tmpl_123'] === 'accept') {
      // 用户已选"总是允许",直接攒次数成功
    }
  }
})

如果用户关了总开关,只能引导去设置页:wx.openSetting 或提示文案引导。

四、后端:下发订阅消息

4.1 获取 access_token

// Node.js 示例:access_token 有效期 7200 秒,务必缓存
const redis = require('redis')
const client = redis.createClient()

async function getAccessToken() {
  const cached = await client.get('wx:access_token')
  if (cached) return cached

  const res = await axios.get(
    'https://api.weixin.qq.com/cgi-bin/token', {
      params: {
        grant_type: 'client_credential',
        appid: process.env.WX_APPID,
        secret: process.env.WX_SECRET
      }
    })
  // {"access_token":"xxx","expires_in":7200}
  await client.set('wx:access_token', res.data.access_token, 'EX', 7000)
  return res.data.access_token
}

高频坑access_token 有每日调用限额,且重复获取会让旧 token 失效。多实例部署时一定要用集中式缓存(Redis),不要每个实例自己刷。

4.2 下发消息

async function sendSubscribeMessage(openid, orderId) {
  const token = await getAccessToken()
  const res = await axios.post(
    `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${token}`,
    {
      touser: openid,
      template_id: 'tmpl_order_status',
      page: `pages/order/detail?id=${orderId}`,  // 点击消息跳转的页面
      miniprogram_state: 'formal',               // developer/trial/formal
      lang: 'zh_CN',
      data: {
        character_string1: { value: 'DD202601150042' },
        thing2: { value: '您的订单已发货' },       // thing 最长20字
        time3: { value: '2026-01-15 14:30' }
      }
    })

  // 常见错误码
  // 43101: 用户拒绝接收消息(订阅次数用完了)
  // 47003: 模板参数不准确(字段类型/长度不符)
  // 40003: openid 不属于这个 appid
  return res.data
}

4.3 错误码 43101 的处理策略

43101 是日常最高频的错误——下发时用户没有剩余订阅次数。这不是异常,是业务常态,处理策略:

  1. 下发前先查剩余次数(自己记账):前端每次 accept 时上报,后端维护 openid + template_id 的剩余次数
  2. 次数为 0 时跳过下发,不要反复重试浪费调用额度
  3. 在用户下次打开小程序时,用运营位引导再次订阅
CREATE TABLE subscribe_quota (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  openid VARCHAR(64) NOT NULL,
  template_id VARCHAR(64) NOT NULL,
  quota INT NOT NULL DEFAULT 0,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_openid_tmpl (openid, template_id)
);

前端 accept 一次 quota + 1,下发成功 quota - 1,43101 时归零。

五、测试环境与体验版

miniprogram_state 参数决定用户点击消息后跳转的版本:

  • developer:跳开发版(需要 wx.openSetting 相关的开发者权限)
  • trial:跳体验版
  • formal:跳正式版

本地联调流程:开发者工具里模拟订阅 → 后端用 trial 状态下发 → 真机(体验版二维码)收消息点击验证跳转。注意:开发者工具收不到订阅消息,只能真机收。

六、完整时序图

用户                小程序                 后端                  微信服务器
 │ 点击"提交订单"    │                      │                      │
 │────────────────>│                      │                      │
 │                  │ wx.requestSubscribe  │                      │
 │                  │──────────────────────────────────────────> │
 │  弹出授权弹窗     │                      │                      │
 │<─────────────────────────────────────────────────────────────│
 │  点击"允许"       │                      │                      │
 │────────────────>│  accept 回调          │                      │
 │                  │──上报订阅成功────────>│ quota+1              │
 │                  │                      │                      │
 │      ......订单状态变更......             │                      │
 │                  │                      │  sendSubscribeMsg    │
 │                  │                      │────────────────────>│
 │  收到订阅消息     │                      │                      │
 │<──────────────────────────────────────────────────────────────│
 │  点击消息跳详情页 │                      │                      │

七、避坑清单

现象解法
非用户手势调用fail: can only be invoked by user TAP gesture只能在 tap 回调链路里调用
thing 字段超长47003 模板参数不准确下发前做长度裁剪(≤20字)
43101 频繁出现用户没攒够订阅次数记账管理 quota,关键动作多攒
access_token 互相顶掉线上偶发 40001Redis 集中缓存,提前 200s 刷新
开发工具收不到消息联调两眼一抹黑用真机体验版 + trial 状态
模板字段想改无解,模板不可改申请新模板,旧模板下线
time 字段格式47003yyyy年M月d日 HH:mmyyyy-MM-dd HH:mm

写在最后

订阅消息的设计哲学是"用户授权一次,你触达一次",这在产品层面倒逼你把通知做得更有价值——没有价值的消息,用户不会给你攒次数。技术上记住三件事:手势触发、quota 记账、token 集中缓存,剩下的都是模板字段的体力活。

0

评论 (0)

取消
0:00