ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信小程序订阅消息开发全指南:从授权到服务端发送的避坑实战

2026/10/5 3:56:46 拓冰建站 浏览量
微信小程序订阅消息开发全指南:从授权到服务端发送的避坑实战 1. 一次性订阅和长期订阅到底差在哪选型之前必须搞清楚的事做小程序消息推送的人几乎都会被同一个问题卡住到底该用一次性订阅wx.requestSubscribeMessage还是长期订阅这个选择不是拍脑袋定的它直接决定了你的功能边界、开发成本甚至整个产品流程怎么设计。先说一个我在实际项目里踩过的坑。当时客户要做一个活动提醒功能用户报名后临近活动开始要推送一条通知。我一开始想的很简单用户报名时弹订阅授权同意后到点发消息这不就完了吗。结果上线后才发现一次性订阅的规则是一次授权、一条消息用户那次点击授权的额度在首次发送后就已经消耗掉了。第二场活动如果用户没有再次授权消息就发不出去而系统只会返回一个冷冰冰的 43101 错误码。1.1 两种订阅模式的核心差异一次性订阅消息和长期订阅消息最大的区别可以概括成两句话一次性订阅用户每次授权只允许开发者发送一条订阅消息。授权一次用一次用完下次还需要用户再次点击授权。长期订阅消息用户授权一次开发者在长时间内可以多次下发消息不需要反复征求用户同意。所以从业务角度来选型逻辑其实很清楚如果通知频率低、跟用户某个行为强绑定比如支付成功通知预约成功通知一次性订阅够了如果通知是高频的周期性内容比如每日早安每周课程提醒库存预警那理论上应该用长期订阅。但理论上归理论上现实往往很骨感。1.2 长期订阅到底能不能申请别再被网上教程带偏我看到太多教程把长期订阅写成轻描淡写的一句话调用 subscribeMessage.send 即可发送好像模板随随便便就能申请下来。真实情况不是这样。长期订阅消息的模板申请对类目限制非常严格。目前主要面向政务、医疗、交通、金融、教育、公共服务等特定行业开放而且需要提供相应的资质证明。普通电商、工具类、内容类小程序在后台的订阅消息页面里基本看不到长期订阅模板的申请入口看到的只有一次性订阅消息的模板库。我在项目里问过微信官方客服得到的回复也印证了这一点长期订阅属于白名单邀约制/类目审核制不是你想申请就能申请的。如果你所在的小程序类目不在支持范围内这个功能在立项阶段就可以直接划掉了不用在技术方案上浪费精力。所以正确的工作流程应该是先打开小程序后台的订阅消息管理页面看看自己的类目下面有没有长期订阅模板可选有就深入研究没有就直接设计成一次性订阅的方案。别先写代码后去申请模板那是典型的自嗨式开发。1.3 订阅消息的计费与额度机制顺带说一个容易被忽视的点订阅消息本身不按条数直接收费但模版的选用和下发都受微信侧的配额和频控限制。一次性订阅消息同一用户在同一模板下理论上没有全局的每日上限但用户每次授权只能换一条发送额度。长期订阅消息则要看类目审核时约定的使用范围和频次频繁骚扰用户会被限制接口调用甚至封禁模板。在实际开发中我建议你在发送环节做一层频控比如同一个用户一天最多收到 3 条通知。这既是合规要求也是用户体验的底线。2. 前端订阅动作的实现细节请求时机、参数确认与授权引导前端部分的代码量不大核心 API 就一个wx.requestSubscribeMessage。但就是这个 API用法上面有不少细节弄不好弹窗都不出来。2.1 第一步从后台配置模板并拿到模板 ID在写代码之前先去小程序管理后台mp.weixin.qq.com的功能-订阅消息页面。页面上方会展示你的小程序类目对应的可用模板列表你可以从公共模板库里选用模板也可以申请自定义模板。选一个和你业务最贴合的一次性订阅模板审核通过后你会得到一个模板 ID。这个 ID 形如xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx一长串后面前端调用、后端发送都要用到。建议存到配置文件里不要写死在前端代码里到处复制。2.2 触发时机不能在页面加载时直接弹窗我第一次做的时候直接在onLoad里调wx.requestSubscribeMessage结果弹窗压根不出现控制台报错requestSubscribeMessage:fail can only be invoked by user TAP gesture。后来才搞清楚微信要求订阅弹窗必须由用户的点击行为触发不能出现在页面加载、接口回调等非手势场景。也就是说你必须在按钮的tap事件处理函数里发起订阅请求不能在onLoad、onShow或者异步回调里调用。正确的做法是把订阅动作和用户的一个明确操作绑定在一起比如点击报名按钮、点击开启提醒开关、点击提交订单。如果业务上确实需要自动弹窗也得设计一个引导页面让用户先点一下下一步在那次点击里触发订阅。2.3 wx.requestSubscribeMessage 的参数与真实调用场景调用代码很简单但有几个参数值得注意。tmplIds是一个数组可以传多个模板 ID最多三个微信会依次弹出订阅请求。返回值res[tmplId]有三种可能的状态accept用户同意订阅reject用户拒绝ban用户勾选了总是保持以上选择不再询问这里最坑的是ban状态。一旦用户勾选总是保持以上选择不再询问以后你再用wx.requestSubscribeMessage弹窗微信会直接返回ban不会再弹出授权窗口。也就是说用户已经把这个模板对你永久关闭了。遇到这种情况前端要给出提示引导用户在微信的设置页里手动打开订阅授权否则后续发送永远失败。一个比较完整的调用示例如下Page({ onSubscribeTap() { const tmplId 你的模板ID; wx.requestSubscribeMessage({ tmplIds: [tmplId], success: (res) { if (res[tmplId] accept) { // 用户同意订阅此时后端才有权限发送一条消息 this.notifyServer({ action: subscribe, status: accept }); } else if (res[tmplId] reject) { // 用户拒绝了记录一下后面引导用户重新订阅 this.notifyServer({ action: subscribe, status: reject }); } else if (res[tmplId] ban) { // 总是保持以上选择不再询问被勾选需要引导用户去设置页开启 this.showGuideToSettings(); } }, fail: (err) { // 用户关闭了订阅授权总开关或者基础库版本过低 console.error(subscribe fail, err); } }); } });2.4 用户拒绝后的二次引导策略现实中用户直接点拒绝的情况太常见了。你不能因为用户拒绝就放弃整个通知功能需要设计一个温和的二次引导逻辑。我的做法是在首次拒绝后记录用户的拒绝状态。下一次用户再次触发那个核心操作比如再次报名活动时弹一个自定义的引导弹窗文案大概意思是开启活动提醒不错过重要通知下面放一个开启提醒按钮点击后才进入wx.requestSubscribeMessage。这种二次引导比第一次授权通过率通常会高不少。另外要特别提醒一点不要用uni.requestSubscribeMessage的时候忽略平台差异。如果你用的是 uni-appuni.requestSubscribeMessage在微信小程序端底层会直接封装原生 API但返回结构和错误处理略有不同一定要分别测试。3. 服务端下发链路拆解code 换取 openid、access_token 缓存与模板消息发送前端拿到用户授权只是第一步真正的发送逻辑在后端。整个服务端的链路其实就三件事拿到用户的 openid、拿到全局 access_token、调用 subscribeMessage.send 接口。任何一个环节出错消息都发不出去。3.1 登录态里的 openid 是怎么拿到的很多刚入门的朋友在code 换 token这一步就开始晕。这里说的其实是微信小程序登录流程前端调用wx.login()拿到一个临时凭证code把code传给后端后端用code加上小程序的 appid、secret请求微信的jscode2session接口微信返回openid、session_key等信息这里有个常见误区有些教程里说的是code 换 token但小程序登录拿到的是openid不是token。token是你自己的服务器签发的一个会话标识用来给前端做登录态判断的。两者不要混。jscode2session接口示例GET https://api.weixin.qq.com/sns/jscode2session?appidAPPIDsecretAPPSECRETjs_codeCODEgrant_typeauthorization_code响应长这样{ openid: oXXXXXX, session_key: xxxxx, unionid: 可选 }拿到openid后你自己生成一个登录态 token 返回给前端存储即可。后续要发订阅消息时touser字段填的就是这个openid。3.2 access_token 获取与缓存这里坑得最惨access_token 是调用所有微信服务端接口的通行证。获取接口很简单GET https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappidAPPIDsecretAPPSECRET响应里包含access_token和expires_in有效期默认是 7200 秒2小时。开始做的时候很多同学图省事每次发消息前都实时调一次 token 接口。短时间内问题不大但一旦消息量大起来微信对 token 接口的调用频率有限制每天调用上限且 token 获取接口短时间大量重复调用会被限流很快就会被返回45009接口调用超过限额。正确的做法是服务端做缓存。把 access_token 和获取时间存到 Redis 或内存里判断过期时间快过期了才重新获取没过期就直接复用。简单示例Node.js Redisconst redis require(redis); const client redis.createClient(); async function getAccessToken() { const cached await client.getAsync(wx_access_token); if (cached) return cached; const url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid${APPID}secret${APPSECRET}; const res await axios.get(url); if (res.data.access_token) { await client.setAsync(wx_access_token, res.data.access_token, EX, 7000); return res.data.access_token; } throw new Error(res.data.errmsg); }注意我这里缓存时间设置的是 7000 秒而不是 7200 秒留了 200 秒的余量。这个细节很重要因为网络延迟、服务器时钟偏差都可能导致 token 在边界时刻失效。你不想在用户收到消息的关键时刻因为 token 过期被打回40014。3.3 subscribeMessage.send 的参数与真实返回码处理一切就绪后调用发送接口POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_tokenACCESS_TOKEN请求体长这样{ touser: OPENID, template_id: TEMPLATE_ID, page: pages/index/index, data: { thing1: { value: 活动提醒 }, time2: { value: 2024-06-01 10:00 } } }这里有几个关键点touser是用户的 openid不是前端自己生成的用户 IDtemplate_id是你选的订阅消息模板 IDdata里的字段名比如thing1、time2必须和模板定义完全一致每个字段的key确定了字段类型比如thing类型限长 20 个字符time类型必须是标准时间格式很多同学第一次发都死在47003这个错误上提示argument invalid。这个错误绝大多数原因就是 data 里的字段名和模板不一致或者字段值超过了长度限制。排查方法很简单把返回信息里的errdetail打出来它会直接告诉你哪个字段有问题。返回码的完整处理建议做成一张表返回码含义处理建议0发送成功正常流程40003openid 无效检查是否有拼写错误用户是否取关/删除40037template_id 无效检查模板 ID 是否填错40014access_token 无效重新获取 access_token43101用户拒绝/未订阅引导用户重新订阅47003参数错误检查 data 字段名和长度45009接口调用频率超限降低发送频率或接入频控3.4 一次性订阅的一订阅一发送约束如何落地一次性订阅的一次授权一条消息这个约束经常导致开发者的逻辑混乱。我在项目里总结了一套稳妥的做法前端每次wx.requestSubscribeMessage成功后把该用户对这个模板的订阅额度在后端记录为 1。发送一条消息成功后额度减 1。发送前先查额度额度为 0 就不调用发送接口避免白请求。简单来说订阅动作和生产消费是一一对应的。就算用户按了多次订阅后端也严格按几条额度发几条消息来执行。这个逻辑不写清楚用户一多就会乱。4. 从真实项目里捞出来的高频坑做完几个小程序项目之后我发现消息订阅的功能代码不难真正让人头疼的都是一些证书、时序、状态类的小问题。下面这五个坑基本覆盖了我见过的绝大多数线上故障。4.1 坑一长期订阅的模板类目显示灰色后台订阅消息页面里长期订阅的模板列表是灰色、不能选的。很多人以为是自己账号权限不够各种找客服。其实真相就是你的小程序类目不支持长期订阅。判断标准很简单如果你的小程序属于普通企业类目电商、工具、生活服务等基本不用抱期望。政务、医疗、金融类小程序可以尝试申请但也需要提供资质证明审核周期还不短。我建议你在项目需求评审时就问一句这个通知功能是否能接受用户每次操作授权一次、只能发一条这个限制如果能接受就用一次性订阅如果不能接受那就只有两条路换类目或者换通知渠道比如用手机号短信。4.2 坑二订阅了但发送失败返回 4310143101是高频错误码含义是用户未订阅或已拒绝。但在真实场景里有一种情况很容易让人困惑前端明明弹窗授权成功了后端发了两次消息第二次就报 43101。原因就在一次性订阅的用后即焚机制上第一次发送已经把用户那次授权的额度消耗完了第二次自然没有额度。这不是 bug是规则。解决办法是做订阅额度管理发送前查询用户对该模板的剩余额度不够了就提前在前端触发重新订阅引导。跑一次正常流程你就知道这是必须做的省不掉的。4.3 坑三access_token 过期与并发刷新access_token 在多个服务实例同时运行时还有一个经典问题并发刷新。假设你有两台服务器同时收到发送请求同时发现 token 过期同时去获取新 token。微信侧会记录两个 token后获取的会把前面的顶掉导致其中一台服务器手上的 token 失效。解决方案有两个一是用一个全局的任务锁保证同时只有一个实例刷新 token二是获取 token 的操作不要放在业务接口里而是放到定时任务里统一刷新保证全链路用的是同一份缓存。4.4 坑四订阅状态没有持久化导致用户白点很多项目只在前端存订阅状态后端的用户表里完全不记录。用户一旦清缓存、换设备、删除小程序重进前端存的订阅状态就丢了结果就是用户以为订阅过实际后端已经没有额度了。正确的做法是服务端在用户授权成功后把openid template_id 剩余次数 最近授权时间落库。发送消息时以服务端数据为准。这样就算前端状态丢失后端也能准确判断该不该发。4.5 坑五开发版和体验版收不到消息开发调试的时候很多人发现订阅消息在微信开发者工具里能弹窗但真机收不到。两个原因最常见一是你设置的用户 openid 跟你当前登录微信的 openid 不一致工具里可以用测试 openid但真机发送必须用真机登录后的 openid。二是订阅消息发送必须使用用户信任的 openid也就是用户必须真正打开过你的小程序并完成登录你拿到的 openid 才是有效的。开发阶段我建议直接拿自己的微信真机测走完整的登录、授权、发送流程不要依赖开发者工具的模拟环境。5. 模板申请不下来的产品兜底思路最后一章聊点技术之外但同样重要的事。如果你发现自己业务的通知需求确实需要高频触达用户但长期订阅又申请不下来项目不能就这么卡死。这时候有几条可以落地的替代路径。5.1 替代方案一客服消息客服消息customerServiceMessage.send是很多小程序的隐晦后门。用户在客服会话里有交互动作后开发者可以在一定时间窗口内下发一条客服消息。虽然频次和时间窗口有限制但对于某些特定场景例如售后跟进、订单疑问已经够用了。具体来说如果用户主动给你发过消息比如问了一个问题你在 48 小时内是可以主动回复的。对于一些低频但需要模板消息之外触达的场景客服消息是很好的补充。5.2 替代方案二次数叠加的产品设计长期订阅的本质是一次授权、多次发送既然申请不到长期模板那就用一次性订阅做N 次叠加。举一个实际例子我做的一个会议预约小程序用户每报名一场会议就订阅一次会议提醒模板。之后每场会议开始前后端检查该用户剩余额度有额度就发送没有就引导用户再订阅一次。这样虽然每个用户每次只能获得一次发送机会但通过每报名一次就订阅一次的设计我们实际达到了多次通知的效果。体验上也有优化空间报名按钮旁边放一个开启提醒的开关用户手动开关时订阅能有效避免系统消息打扰。5.3 到底什么业务场景适合长期订阅如果你的行业确实能申请长期订阅你也要想清楚它并不是万能药。长期订阅的模板内容审核通常非常严格而且消息内容必须严格对应模板场景不能随意改文案。适合长期订阅的典型场景是政务办事进度通知医疗报告结果通知车次延误变更通知学校教务通知这些场景有一个共同点通知是周期性的、有一定权威性的而且用户对这个通知有明确的高频需求。如果你做的是营销类、促销类、泛内容类的推送建议还是老老实实用一次性订阅别打擦边球否则模板被封了再想申请就难了。在我做过的项目里消息订阅这个功能的坑主要不在技术而在规则理解。把一次性订阅一次授权一条消息这个底层约束吃透把服务端的额度管理、token 缓存做好剩下的就是产品设计的问题了。如果你正在做类似功能建议先花半小时把后台的模板列表翻一遍再动手写代码能少走一大段弯路。