微信小程序订阅消息实战:模板申请、一次性订阅与长期订阅全链路
模板消息下线后,订阅消息成了小程序触达用户的唯一官方通道。但一次性订阅"发一条扣一次"的机制、模板类目的限制、下发条件的坑,让不少后端同学在第一次接入时栽跟头。这篇从模板申请到后端下发,把完整链路和真实踩坑记录下来。
一、订阅消息的两种类型
| 类型 | 规则 | 适用场景 |
|---|---|---|
| 一次性订阅 | 用户订阅一次,只能下发一条 | 订单发货、审核结果、活动提醒 |
| 长期订阅 | 订阅一次可反复下发 | 仅限公共服务类目(政务、医疗、交通等) |
残酷现实:长期订阅模板的类目卡得非常严,普通电商/内容类小程序基本申请不到。所以对大多数团队来说,玩转"一次性订阅"才是重点。
一次性订阅还有个经典场景化玩法:每次用户触发关键操作时都弹订阅授权(勾选"总是保持以上选择"后不再弹窗),积攒下发次数。
二、申请模板的正确姿势
进入 mp.weixin.qq.com → 功能 → 订阅消息 → 我的模板,从公共模板库挑选或申请新模板。
关键词选择的原则:
- 变量字段(thing、time、phrase 等)数量够用就好,越多下发时越容易出错
thing类型长度限制 20 个字符(以内),超了直接下发失败- 模板类目必须和小程序服务类目匹配,否则审核不过
- 一旦模板审核通过,字段不能改,只能重新申请新模板——所以先把业务字段想清楚
三、前端:请求订阅授权
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 是日常最高频的错误——下发时用户没有剩余订阅次数。这不是异常,是业务常态,处理策略:
- 下发前先查剩余次数(自己记账):前端每次 accept 时上报,后端维护
openid + template_id的剩余次数 - 次数为 0 时跳过下发,不要反复重试浪费调用额度
- 在用户下次打开小程序时,用运营位引导再次订阅
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 互相顶掉 | 线上偶发 40001 | Redis 集中缓存,提前 200s 刷新 |
| 开发工具收不到消息 | 联调两眼一抹黑 | 用真机体验版 + trial 状态 |
| 模板字段想改 | 无解,模板不可改 | 申请新模板,旧模板下线 |
| time 字段格式 | 47003 | 用 yyyy年M月d日 HH:mm 或 yyyy-MM-dd HH:mm |
写在最后
订阅消息的设计哲学是"用户授权一次,你触达一次",这在产品层面倒逼你把通知做得更有价值——没有价值的消息,用户不会给你攒次数。技术上记住三件事:手势触发、quota 记账、token 集中缓存,剩下的都是模板字段的体力活。
评论 (0)