微信小程序的订阅消息,几乎是每个做小程序的人都会碰到的模块。不管是外卖的出餐提醒、预约挂号的结果通知,还是商城里的发货提醒、活动开奖通知,都绕不开“什么时候让用户点订阅”“后端什么时候能把消息真正推出去”这两个问题。这篇文章围绕标题里的两个核心方向展开:一次性订阅和长期订阅。我会把开发中容易踩的坑、需要提前想清楚的设计点,以及后端发送链路上的关键参数,全部展开讲清楚。如果你正在做小程序,或者打算给已有项目加消息触达能力,这篇内容可以帮你省下不少摸索时间。
1. 先分清一次性订阅和长期订阅,别选错方向
订阅消息最容易被误解的地方,就是很多人以为“用户授权一次,以后就能随便发”。真实情况远没有这么美好。微信从模板消息改版成订阅消息之后,核心逻辑变成了“用户授权次数决定你可以发几条”。一次性订阅和长期订阅在授权方式、可用范围上有本质区别,搞混了轻则消息发不出去,重则整个消息方案要推倒重来。
1.1 一次性订阅:用户点一次允许,你只拿到一条下发机会
一次性订阅是这个功能最基础的形态。用户每点击一次“允许”,开发者就获得一次向该用户发送订阅消息的机会。这里的关键词是“一次”。你可以把这个理解成一张一次性餐券,用完就没有了。就算用户在同一个页面上点了三次允许,你也只是拿到了三条下发额度,发一条少一条,而不是“永远可以发”。
实际开发中很容易出现这样的场景:用户下单后点了订阅,商家发货时你推送一条提醒,用户点进去看到消息了。过两天你希望再推一条“请及时确认收货”的消息,发现推送失败,返回码 43101。原因很简单,订阅额度已经被上一次发货通知消耗掉了。所以一次性订阅只适合做低频、单次强相关的结果通知,比如“审核结果已出”“快递已签收”“预约成功”。
还有一点要注意,用户授权的一次性订阅额度并不是永久保留的,超过一定时间没下发会自动失效。所以拿到授权后要尽快规划发送时机,不要囤额度。这也是很多项目在“订阅后第二周才推送”时会莫名失败的原因之一。
1.2 长期订阅:一次授权多次下发,但行业门槛不低
长期订阅是在一次性订阅基础上推出的进阶能力,核心价值是“用户授权一次,开发者可以在后续一段时间内多次推送”。看起来解决了所有触达痛点,但它有一个非常硬的门槛:目前长期订阅只对政务民生、医疗、交通、金融、教育等公共服务属性明显的类目开放。个人主体小程序基本不用想,普通电商、工具类小程序也大概率没有长期订阅的入口。
这一点我在不少技术群里见过有人反复问:“为什么我在后台找不到长期订阅模板?”答案往往不是操作问题,而是主体类目不在白名单里。如果你的类目真的符合要求,在小程序管理后台的“订阅消息”页面新增模板时,会看到长期订阅模板的选项。如果看不到,说明当前主体或类目暂未开放。
长期订阅的模板和一次性订阅在发送接口调用上没有本质区别,依然是调用订阅消息发送接口,只是授权次数和业务含义不同。这就引出一个实践上的坑:后台模板列表里既有一次性模板也有长期模板时,一定要在业务代码里做好区分,否则很容易出现“用户明明订阅过,但发送时依然失败”的情况。
1.3 两种订阅的使用场景对比
| 对比维度 | 一次性订阅 | 长期订阅 |
|---|---|---|
| 授权行为 | 每次发送前需用户点击允许 | 用户一次授权,后续可多次发送 |
| 下发次数 | 授权一次只能发一条 | 一次授权可获得多个下发次数 |
| 开放范围 | 所有已认证小程序 | 仅公共服务类目,有白名单限制 |
| 适用场景 | 订单结果、审核通知、核销通知 | 政策变更、账单提醒、周期报告 |
| 后端设计 | 关注剩余额度,用完再引导订阅 | 关注防打扰,避免次数被一次刷完 |
简单总结就是:能用一次性订阅解决的,不要硬上长期订阅;能用长期订阅的类目,也别把一次性订阅做得太复杂。方案选型不是越高级越好,而是越匹配业务越好。
2. 前端订阅触发设计:弹窗时机和授权率直接挂钩
选好订阅类型之后,下一个问题就是“怎么让用户愿意点允许”。订阅消息的授权弹窗不是想弹就能弹的,微信对调用时机有明确要求:必须由用户的点击行为直接触发。你在onLoad里弹、在定时器里弹、在网络回调里弹,都会导致弹窗失败或者授权率断崖式下降。
2.1 弹出时机:把订阅按钮嵌入真实操作流程
我是这样做的:把订阅动作自然融入用户原本就要做的操作里,单独搞一个“开启通知”的入口页面,效果一定不好。比如用户在下单后点击“提交订单”,支付成功的回调里弹订阅请求,用户当时的心态是“我要完成交易”,顺手点允许的意愿会高很多。如果搞一个“设置页”让用户主动来开通知,绝大多数人根本不会点进来。
代码层面,直接调用wx.requestSubscribeMessage就行。模板 ID 可以传一个数组,但同时弹多个授权框的体验很差,我实际测试下来的授权转化率会跌到单独弹窗的一半以下。建议一次只弹一个最核心的模板,其他通知通过合并文案处理。
Page({ handleOrderSuccess() { // 用户完成支付后进入这个回调,此时弹订阅授权 wx.requestSubscribeMessage({ tmplIds: ['模板ID,例如:模板消息 id 的字符串'], success(res) { // res 会返回一个对象,key 是模板ID,value 是 accept/reject/ban if (res['模板ID'] === 'accept') { // 授权成功,把状态上报给后端,由后端发送订阅消息 } else { // 用户拒绝,记录日志但不要反复强弹 } }, fail(err) { console.error('订阅消息调用失败', err) } }) } })如果你用的是 uniapp,写法几乎一致,把wx换成uni,uni.requestSubscribeMessage的参数和回调结构跟微信原生 API 是同一套,迁移成本很低。
2.2 授权结果要认真处理,别只弹不管
wx.requestSubscribeMessage返回的res里,value 有三种可能:accept表示用户接受了;reject表示用户拒绝了;ban表示用户已经被系统判定为过度打扰,后续不会再弹窗。ban这个状态最容易被忽略,一旦出现了,短期内再怎么调接口都不会有弹窗,需要等一段时间或者引导用户去小程序设置页手动打开通知权限。
正确的做法是:拿到accept后把状态同步给后端,后端记录该用户当前有一个可用的订阅额度;拿到reject后不要马上下一次再弹,可以等业务节点,比如用户再次进入订单详情页时,通过一个“开启发货通知”的按钮来二次引导;拿到ban就要把入口弱化,只保留设置页的开启路径,别再骚扰用户。
2.3 按钮文案和页面说明影响授权率
我对比过两种写法。一种是一句话不说,直接弹系统授权框;另一种是先在页面里放一个小提示条,写着“订阅后可以收到审核结果通知,每周最多提醒一次”,然后再引导用户点击按钮触发弹窗。后一种的授权率高出不少。原因也简单:用户不知道订阅之后会收到什么、多久收一次,自然不敢点。你把这两个信息提前告诉用户,他心里的确定性上来了,授权意愿自然就高了。
注意:引导文案别写“永久免费通知”“无限次提醒”这类字眼,一方面跟实际能力不符,另一方面容易被微信判定为误导用户。文案合规这件事,出问题比想象中要严重。
3. 后端发送链路:从 access_token 到订阅消息接口
前端拿到了用户授权,后端才能真正把消息推出去。这个过程里最核心的是两件事:拿到一个有效的access_token,然后按接口规范组装参数发起请求。很多第一次做订阅消息的人,会在这一步卡很久。
3.1 稳定获取 access_token
服务端要调用微信接口,第一步是拿 AppID 和 AppSecret 换access_token。接口地址是:
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRETaccess_token的有效期是 7200 秒(两小时),而且微信对获取频率有限制。所以强烈建议在服务端做缓存,用一个全局变量或者 Redis 存起来,快过期了再重新获取,不要每次发消息都去换一次。我见过有团队直接把getAccessToken写在每次发送前调用,结果一天下来接口频繁报45009之类的频率限制错误。
这里顺便把热搜词里“code 换 token”这个说法理清楚。小程序登录时,前端通过wx.login拿到code,后端拿code换的是openid和session_key,这是登录态的一部分。而订阅消息发送需要的access_token是“接口调用凭证”,跟用户身份没有直接关系。两条链路完全不同,不要搞混。
3.2 组装订阅消息发送请求
获取access_token之后,调用:
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN请求体结构如下:
{ "touser": "用户openid", "template_id": "订阅消息模板ID", "page": "pages/order/detail?id=123", "data": { "thing1": { "value": "订单已发货" }, "character_string2": { "value": "SF1234567890" }, "time3": { "value": "2025-06-01 10:00" } }, "miniprogram_state": "formal", "lang": "zh_CN" }data里的键必须和模板中定义的字段名一致,不能随便起名。比如模板里是thing1,你就只能写thing1。不同字段类型有长度限制:thing类型最多 20 个中文字符;number类型最多 32 位数字;character_string类型最多 32 个字符;time必须是标准时间格式;phrase类型一般不超过 5 个汉字。超长会直接报47003参数错误。
page字段是用户点击订阅消息后跳转到的小程序页面路径,这里要注意必须是已经存在的页面,否则也会报错。miniprogram_state有三个值:formal正式版、trial体验版、developer开发版。如果小程序还没发布,你要在开发环境自测,应填developer或trial,填formal在未上线版本上是无法正确跳转的。
如果后端用的是 PHP,可以用 curl 实现,下面是一个简化示例:
function sendSubscribeMessage($accessToken, $openId, $templateId, $page, $data) { $url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" . $accessToken; $data = [ 'touser' => $openId, 'template_id' => $templateId, 'page' => $page, 'data' => $data, 'miniprogram_state' => 'formal', 'lang' => 'zh_CN' ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE)); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true); }3.3 返回码:正确理解 0 和 43101
微信接口返回errcode和errmsg。errcode为 0 表示发送成功。常见的非 0 状态码和原因如下:
| 返回码 | 含义 | 处理建议 |
|---|---|---|
| 0 | 发送成功 | 无需处理 |
| 40003 | openid 不正确 | 检查是否传错用户标识 |
| 40037 | 模板ID不正确 | 检查后台模板是否与前端授权模板一致 |
| 41030 | page 路径不正确 | 检查跳转页面路径是否存在 |
| 43101 | 用户拒收或订阅额度不足 | 一次性订阅最常见,需重新引导订阅 |
| 47003 | data 参数类型或格式错误 | 检查字段类型和长度 |
| 45009 | 接口调用频率超限 | 检查 access_token 缓存和发送频率控制 |
这里重点说 43101。用户没点过授权、授权被用户关闭、一次性订阅额度已经用完,都会返回这个码。很多项目第一次上线时,用户授权后发送失败,查来查去发现是后端把用户的 openid 拿错了,或者前后端用了不同的模板 ID。排查顺序建议是:先确认 openid 正确,再确认模板 ID 是用户授权时传的那个,最后看返回码的完整信息。
4. 长期订阅的申请路径与次数管理
长期订阅虽然诱人,但实际落地时会遇到比一次性订阅更繁琐的审核和管理工作。这里把申请路径和次数管理的关键点讲清楚。
4.1 先确认类目有没有资格
打开小程序管理后台,进入“功能 - 订阅消息”页面,新增模板时如果能搜到长期订阅类别,说明你的主体类目有资格;搜不到,后面就不用继续了。长期订阅目前主要覆盖政务、医疗、交通、金融、教育等公共服务领域。个人主体、普通商贸主体基本不在此列。
如果你的业务确实属于公共服务范畴,但后台看不到长期订阅入口,可以先检查小程序认证主体和当前服务类目是否匹配,必要时调整服务类目并等待审核。这里要有一点心理准备,类目审核和长期订阅模板的审核都需要时间,不要把整个项目的时间线卡在这上面。
4.2 模板内容审核要注意什么
长期订阅模板的关键词和内容结构一旦确定,推送时不能随意改动。模板里每个字段的语义要和实际推送内容强相关,比如“政策名称”“生效时间”“办理进度”这种。关键词不要带营销词汇,不要出现“优惠”“折扣”“点击领取”等字眼。长期订阅的优点是可多次触达,但这也意味着微信对内容合规的审查会更严格。一旦被用户投诉或者被系统判定为骚扰,影响的不只是模板权限,严重时整个小程序的消息能力都会被限制。
4.3 订阅次数管理与防打扰策略
用户对长期订阅模板授权后,会获得一批可下发次数。这个次数是有限的,不是无限发。所以后端最好维护一张用户订阅状态表,记录每个用户对每个长期模板的剩余可用次数。每次推送前检查一下剩余次数,避免把额度一次性刷完。
真正做的时候还要想清楚防打扰策略。长期订阅的额度虽然比一次性订阅宽裕,但用户容忍度是有限的。同一周内推送超过两三条通知,用户的关闭率会明显上升。比较好的做法是:把用户可能关心的通知合并成一条,比如每周一次的汇总报告,而不是每条业务动作都单独推一条。我在数据上看过,高频消息的关闭率是低频消息的三倍以上,这个数字很能说明问题。
5. 常见问题与排查心得
最后把实际开发中最常踩的几个坑集中说一下,都是真实项目里反复出现过的。
5.1 用户明明点了允许,后端还是发送失败
先别急着找微信客服。大概率是两种情况:一是模板 ID 对不上,前端授权时传的模板 ID 和后端发送时用的模板 ID 不是同一个;二是用户 openid 取错了,尤其是多端登录的场景,一个用户可能在不同小程序账号下有不同 openid。排查方法很简单,在后端发送接口的请求日志里把touser、template_id、errcode全部打出来,逐个核对。
5.2 一次性订阅额度什么时候会被消耗
很多团队以为“发送失败不消耗额度,下次还能重试”,实际不是这样。只要调用了发送接口且返回0成功,额度就会扣掉。如果返回43101,则说明原本就没有可用额度。还有一种情况要注意:发送接口返回成功之后,消息也不一定必然进入用户会话,如果订阅消息的page跳转路径失效,用户点不进去,体验上也算“发了一条废消息”。所以每次发送前要确保跳转页面可用。
5.3 长期订阅什么时候会用不了
除了类目白名单限制外,长期订阅还有一个容易被忽略的问题:用户首次授权后,如果长期不打开小程序,订阅次数用完之后,你在后台再发就会收到43101。这时候需要想办法再次触达用户,比如在用户进入小程序处理关键业务时,增加一个“重新订阅”的操作入口。注意这里不能用一次性订阅的逻辑来替代长期订阅,因为一次性订阅在根本没点过授权的情况下不会产生可用额度。
5.4 调试阶段的几个实用技巧
- 在微信开发者工具里,可以手动模拟订阅结果:在“模拟操作”面板中把订阅操作的返回值设成“接受”或“拒绝”,不用每次都真机扫码。
- 真机调试时,用测试号或者把
miniprogram_state设成developer,配合开发版小程序使用,能看到完整的消息链路。 - 如果要用抓包工具查看后端请求,注意区分小程序前端请求和服务器后端请求。订阅消息发送是服务端发起的,跟小程序前端的网络请求不在同一条链路里。
提示:正式上线前,一定要用真实账号做一次端到端测试,从用户点击订阅、后端发送、用户点击消息跳转页面,全流程走一遍。很多问题都是到了这一步才暴露的,建议提前规划测试时间。
在实操中的体会
我在实际项目里最顺的一次消息触达,是把订阅弹窗完全嵌进用户交易流程里。用户下单后,页面上放一个“订阅订单状态通知”的按钮,用户点击后弹授权,接受率能做到七成以上。而在冷启动或者首页直接弹窗,接受率往往只有两成。这个差别不是接口层面的问题,而是产品设计层面的问题。订阅消息能不能成为有用的触达工具,很大程度上取决于你愿不愿意花心思把订阅时机和用户当下的需求结合起来。
另外想说,模板和前后端联调顺序也很重要。我的习惯是先在小程序后台把模板配好,再写前端订阅引导,最后做后端发送逻辑。如果顺序反了,会出现前端已经弹了授权、后端模板还没配好的尴尬情况,联调时浪费不少时间。希望这篇记录能帮正在做订阅消息的你省下一些排查时间。