☰
微信小程序消息订阅实战:一次性与长期订阅解析与避坑指南
2026/10/5 11:31:53 网站建设 项目流程

做小程序开发这几年,被问得最多的功能里,“微信小程序消息订阅”绝对排前三。不是因为它难,而是官方文档把一次性订阅、长期订阅、订阅授权、模板配置这些概念拆得太散,新手按文档走一遍,很容易卡在“用户点了订阅却收不到消息”“按钮点了没反应”“明明授权成功却报43101”这种问题上。

这篇文章我想把消息订阅这件事从头到尾讲透,重点放在一次性订阅和长期订阅这两种模式的差异、前端授权交互的完整写法、服务端下发消息的接口细节,以及我踩过的那些坑。无论你是刚接手小程序的新人,还是想给现有项目补上通知能力的开发者,照着这篇文章走一遍,基本能把订阅消息这条路打通。

1. 消息订阅到底解决了什么问题,一次性与长期订阅的关键差异

1.1 两种订阅模式的本质区别

先给不熟悉的读者补个基础。消息订阅,是微信在小程序生态里提供的一套用户主动授权、开发者被动下发的通知机制。用户在你的小程序里点了“允许”之后,你就能通过服务端接口向他的微信下发一条服务通知,入口在微信聊天列表的“服务通知”里。

一次性订阅,字面意思就是“订阅一次,下发一条”。用户点一次授权按钮,你就攒下了一次下发机会;服务端每成功发出一条消息,就消耗一次机会,发完就没了。如果用户想再次收到消息,就得重新点击订阅按钮。长期订阅则相反,只要用户授权一次,你就能在后续任意时间点向他多次下发消息,没有次数限制。

听起来长期订阅更好用,但微信把它卡得很死。长期订阅消息目前只面向政务民生、医疗、交通、金融、教育等公共服务领域开放,普通电商、工具类、内容类小程序基本申请不到。所以我平时给大多数项目做方案的时候,默认都是走一次性订阅的路线,只在确认客户类目符合条件时,才会去尝试申请长期订阅。

这里可以打个比方:一次性订阅像朋友答应帮你带一次咖啡,带完这次,下次你还得再开口;长期订阅像是签了长期委托协议,只要协议在,对方就会一直帮你办。理解了这个差别,后面所有的代码和逻辑都顺了。

1.2 为什么不能继续用模板消息

老开发者可能还记得,小程序早期有个“模板消息”功能,用户在小程序里有过支付、提交表单等行为后,开发者可以下发一条模板消息。2020年之后,微信逐步下线了模板消息,正式替换成了订阅消息。现在你还能在某些老项目里看到sendTemplateMessage相关的接口,但新项目再往这个方向投入已经没有任何意义。

订阅消息和模板消息最大的不同,在于“授权”这件事被摆到了明面上。模板消息是用户做了某个动作后你默默就能发,订阅消息则必须让用户明确点击“允许”按钮。这个变化劝退了很多想“偷偷发通知”的运营,但从用户角度来说,确实更干净了。

所以结论很明确:新项目一律用订阅消息,不要去翻模板消息的老文档。官方推荐的路径只有一条——wx.requestSubscribeMessage接授权,subscribeMessage.send做下发。

2. 一次性订阅:从授权弹窗到服务端下发

2.1 前端唤起订阅授权的正确姿势

一次性订阅的前端入口就是wx.requestSubscribeMessage。直接上代码:

// 在按钮的 tap 事件里调用 Page({ handleSubscribe() { wx.requestSubscribeMessage({ tmplIds: [ '模板ID_1', // 一次性订阅模板,需在 mp 后台申请 '模板ID_2' ], success(res) { // res[tmplId] 可能的值: 'accept' | 'reject' | 'ban' console.log('订阅结果', res); if (res['模板ID_1'] === 'accept') { wx.showToast({ title: '订阅成功', icon: 'success' }); } }, fail(err) { console.error('订阅失败', err); } }); } });

这里有一个新手最容易踩的坑:wx.requestSubscribeMessage必须在用户点击行为(tap)的同步调用链里触发,不能在onLoad、onShow里调用,也不能在setTimeout回调里延迟调用。官方对这个问题报错是requestSubscribeMessage:fail can only be invoked by user TAP gesture,意思就是“必须由用户点击触发”。

如果你想在支付成功后再弹订阅窗口,必须保证支付成功的回调链路里仍然算作“用户点击产生的事件链”。实际上,在wx.requestPayment的success回调里直接调用wx.requestSubscribeMessage是可行的,因为整个事件链是从支付按钮的 tap 开始的。但如果你在支付回调里做了异步请求,等请求回来再弹,就有概率报错。稳妥的做法是:支付成功后用一个半屏弹窗或按钮,让用户再点一次“接收通知”,在这个按钮的 tap 回调里发起订阅。

如果你是 uniapp 用户,写法几乎没有差别,把wx换成uni就可以了:

uni.requestSubscribeMessage({ tmplIds: ['模板ID_1'], success(res) { // ... } });

tmplIds这个数组并不是让你随便塞一堆模板进去的。一次性订阅消息存在一个“消费计数”机制:用户一次授权,每个模板各加一次计数,发一条就减一条。多个模板混在一起时,服务端必须指定用哪一个模板下发,否则就会消费混乱。所以我在项目里一般都会维护一个模板 ID 的常量表,每个业务场景对应一个模板,避免多个场景共用一个模板导致计数被提前清空。

2.2 用户拒绝或总是保持之后,怎么处理

很多产品经理会问:能不能在用户拒绝后再弹一次?不能,至少不能即时再弹。微信对“重复打扰”管得很严,短时间内重复调用wx.requestSubscribeMessage会直接失败。但你可以通过授权状态来判断用户的意向,从而调整入口文案。

基础库 3.19.0 之后,官方新增了wx.getSubscribeMessageSetting接口,可以直接读取用户对当前小程序的订阅消息设置:

wx.getSubscribeMessageSetting({ success(res) { console.log(res.setting); // res.setting 里包含 authSetting、templateSettings 等字段 } });

如果你还在用老的方式,也可以通过wx.getSetting拿到scope.subscribeMessage的授权状态:

wx.getSetting({ success(res) { const auth = res.authSetting; if (auth['scope.subscribeMessage'] === false) { // 用户之前明确拒绝了订阅 // 这里可以显示“去设置开启”的引导 } } });

这里还有一个细节值得说:用户在弹出的订阅面板里勾选了“总是保持以上选择”之后,下次再调用wx.requestSubscribeMessage时,不会再弹出确认框,而是直接按用户之前的选择返回结果。有些开发者以为这是 bug,其实这是微信的既定行为。用户选了这个选项,你既不能替他取消,也不能主动引导他取消,只能靠用户自己在订阅消息设置里关掉。

所以“总保持选择”对开发者来说其实是利好的,它意味着用户在一次明确同意后,你后续再拿到订阅授权就流畅得多。但也别滥用,频繁让用户授权消息,用户反手一个关闭,后续就真的收不到了。

2.3 服务端下发订阅消息的原理与实现

前端拿到授权之后,真正下发消息的动作发生在你的服务端。核心接口是:

POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN

调用之前需要拿到小程序的access_token。老接口是https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,但这个接口有每天的调用次数限制,而且官方后来推出了稳定版接口:

POST https://api.weixin.qq.com/cgi-bin/stable_token
{ "grant_type": "client_credential", "appid": "你的appid", "secret": "你的secret" }

稳定版接口同样需要缓存 token,不是让你每次都重新拉。access_token的有效期是 7200 秒,你在服务端用一个内存缓存、Redis 或者数据库存一下,提前 5 分钟失效判断就可以了。我见过不少项目图省事每次现取现用,结果一天下来接口就报45009(频率限制),得不偿失。

拿到 token 之后,构造下发请求体:

{ "touser": "OPENID", "template_id": "模板ID", "page": "pages/order/detail?id=123", "miniprogram_state": "formal", "lang": "zh_CN", "data": { "thing1": { "value": "订单已发货" }, "number2": { "value": 9527 }, "amount3": { "value": 99.5 }, "time4": { "value": "2024-06-01 12:00" }, "phrase5": { "value": "已完成" } } }

data 里的字段名(thing1、number2这些)不是随便起的,必须和你在 mp 后台申请模板时看到的关键词占位符一一对应。申请模板的时候,每个模板会列出 1~5 个关键词字段,字段有固定的类型:thing(事物)、number(数字)、amount(金额)、time(时间)、phrase(短语)等等。类型不匹配、数量不对、字段名写错,服务端都会返回47003参数错误。

我整理一下服务端发送的几个关键点:

  • touser是用户的 openid,必须和你的小程序 appid 对应。
  • page是点击消息后跳转的小程序页面路径,可以带参数,但不能带协议头。
  • miniprogram_state有三个取值:formal(正式版)、trial(体验版)、developer(开发版)。很多人测试时忘了改这个,开发版和体验版的消息是可以带上开发标识的,但如果你拿正式环境的 access_token 发给体验版,部分情况下会失败。
  • lang是消息模板语言,默认zh_CN。

服务端示例我用 Node.js 写一下,思路通用,其他语言换汤不换药:

const axios = require('axios'); async function sendSubscribeMessage({ openid, templateId, page, data }) { const token = await getAccessToken(); // 自己实现缓存 const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${token}`; const body = { touser: openid, template_id: templateId, page: page || 'pages/index/index', miniprogram_state: 'formal', lang: 'zh_CN', data }; const res = await axios.post(url, body); if (res.data.errcode !== 0) { console.error('订阅消息发送失败', res.data); throw new Error(`send subscribe message failed: ${res.data.errmsg}`); } return res.data; }

服务端发送成功只代表微信受理了消息,不代表用户一定会看到。如果用户取消了订阅、删除了小程序,或者你发送频率过高,微信都有可能直接丢弃消息。这些场景不会报错,所以别把“发送接口返回成功”等同于“用户收到了”。

3. 长期订阅的现实限制与替代方案

3.1 长期订阅的申请条件

长期订阅消息的申请入口在微信公众平台的小程序后台,路径在“功能 - 订阅消息”页面里,切换到“长期订阅消息”标签页,就能看到当前账号可以申请的行业类目。

微信官方给出的说法是:长期订阅消息仅向特定行业开放,包括但不限于政务民生、医疗、交通、金融、教育等。具体的类目范围会不定期调整,你要做的就是在后台看自己账号的类目列表里有没有可申请的模板。如果你的主体是个人开发者,或者类目是普通的电商、工具、内容资讯,基本是搜不到任何长期订阅模板的。

就算你的类目符合条件,审核也不是自动过的。微信会要求你提供相关资质证明,比如医疗机构执业许可证、办学许可证、交通运营资质等。整体流程跟小程序类目审核类似,周期大概 3~7 个工作日。

所以,如果你不是公共服务类的小程序,长期订阅这条路基本可以放弃。我这两年接触的项目里,能真正申请下长期订阅的只有一家做智慧医疗的客户,其余的都在用一次性订阅的变通方案。

3.2 普通人如何用一次性订阅做出“长期感”

既然拿不到长期订阅权限,那就得在设计上想办法。我的经验是把“一次订阅”嵌入到用户的关键行为节点,让用户在需要的时候自愿订阅。

比如一个电商小程序,用户下单后,你在支付成功页放一个“接收订单通知”的按钮,订阅成功后下发一条“订单已受理”的消息。等商品发货时,再让用户通过消息卡片里的小程序入口回来,此时你又可以在页面里发起一次新的订阅,订阅成功后立即下发“物流已更新”。这样一来,用户每一次都需要点一下,但因为他能立刻获得对等的反馈,所以接受度并不低。

还有一种比较取巧但完全合规的做法:利用一次性订阅消息的“一次性”特性,在用户一个真实需求里,同时让他订阅多个模板。比如用户预约了一个服务,你可以同时弹出订单状态通知、服务开始提醒、反馈邀请三个模板的授权框,三份授权一次拿齐。后续三个时间点各发一条,对用户来说体验是连续的,对开发者来说其实还是三次一次性订阅的叠加。

再有就是结合服务号的模板消息。如果你的小程序还关联了一个服务号,服务号的模板消息机制和小程序订阅消息不同,它允许服务号在用户主动触发后的特定周期内发送模板消息。这种跨生态的组合方案,可以在合规前提下,最大程度弥补小程序订阅消息的下发次数限制。不过要注意,服务号模板消息的申请条件和发送场景同样有严格要求,不能当营销通道用。

另外,开发层面还有一个容易被忽略的细节:小程序端无法主动“检查用户当前还剩几次订阅余量”,只能通过服务端记录每次subscribeMessage.send的调用结果来推断。我的做法是给用户建一张订阅计数表,每次授权成功记一条,每次发送成功扣一条,余量不足时小程序端就通过接口展示“重新订阅”的引导。

4. 常见问题排查与避坑清单

4.1 高频错误码与原因对照

我把项目里踩过的订阅消息错误码整理成了一张表,遇到报错直接对着查,比翻文档快很多:

错误码含义常见原因
40003openid 无效用户 openid 与应用 appid 不匹配,或用户未在小程序内登录
40037template_id 不正确模板 ID 和当前小程序不匹配,或模板已失效
41030page 路径不正确页面路径不存在、未发布,或带上了https://前缀
43101用户拒绝订阅用户未授权或已取消订阅,需要引导重新授权
47003参数 errordata 字段与模板关键词不匹配,类型或数量错误
45009接口调用频率超限access_token 未缓存或整体发送频率过高
40001access_token 无效token 过期或获取时 appid/secret 不匹配

47003是我在联调时见最多的错误,基本每一次都是因为 data 里的字段名写错或值类型不匹配。比如模板里规定这个是thing类型,你却传了数字;模板里规定是amount,你传了"99.5元"这样带了单位的字符串,都会被拒。正确的做法是只传纯数字,单位由模板自动带出。

4.2 几个容易被忽略的细节

第一个细节是模板关键词的字数限制。thing类型的关键词上限是 20 个字符,number类型不能有空格,amount类型的单位是固定的,删也删不掉。我之前在做一个预约通知时,把“请您提前 15 分钟到达现场取号”整句话塞进 thing 字段,结果微信提示超出长度,把文案压缩成“请提前15分钟到场”才通过。

第二个细节是page路径校验很严格。官方要求这个页面必须是已经发布上线的小程序页面。在开发调试阶段,你可以把miniprogram_state设为developer或trial,但如果你在正式环境调用,page对应的页面必须是线上已存在的版本,否则会报41030。

第三个细节是“订阅计数不透明”。一次性订阅消息下发后,并不存在一个接口可以查询用户当前剩余订阅次数。你只能靠自己的服务端记录来维护。我之前接手过一个项目,对方把所有订阅记录都存在本地 Storage,用户换了设备、清了缓存,计数就丢了,结果明明有授权却发不出去。后来改成服务端统一存储,问题才解决。

第四个细节是用户删除小程序或拉黑服务通知后,调用subscribeMessage.send可能仍然返回成功,但实际消息已经无法触达。这种场景没法从接口层面感知,只能通过消息点击率等指标侧面观察。如果你的消息打开率断崖式下跌,先别急着自己是不是代码写错了,很可能就是用户群对你的消息已经免疫了。

第五个细节,也是我特别想提醒的:别试图在用户没有主动操作时“强行”弹订阅框。微信对这类行为的容忍度很低,同一个用户短时间多次触发订阅授权,或者订阅面板频繁弹出,轻则被限制调用,重则小程序被投诉下架。消息订阅的正确打开方式,永远是“用户有明确预期、有即时反馈”的时候出现。

最后再分享一个我一直在用的判断逻辑:所有需要发订阅消息的场景,提前在前端预判用户是否有授权余量。如果服务端返回43101,就说明这个用户当前没有可用的订阅授权,此时不要反复调用下发接口,而是引导他回到小程序里重新走一次订阅授权流程。这样既能保证用户体验,也能避免无谓的接口报错。

消息订阅这个功能,看起来只是“前端弹窗 + 后端发个请求”两件事,实际上牵扯到模板配置、授权状态管理、计数服务、异常兜底,每一环都有隐藏的坑。按照我上面这套流程走一遍,至少能绕开 80% 的开发弯路。如果你正在做类似的功能,或者在联调中遇到了本文没提到的报错,欢迎按着错误码去 mp 后台核对一遍模板配置,很多时候答案就在那里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询