ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

微信小程序消息订阅实战:一次性与长期订阅解析与避坑指南

微信小程序消息订阅实战:一次性与长期订阅解析与避坑指南 做小程序开发这几年被问得最多的功能里“微信小程序消息订阅”绝对排前三。不是因为它难而是官方文档把一次性订阅、长期订阅、订阅授权、模板配置这些概念拆得太散新手按文档走一遍很容易卡在“用户点了订阅却收不到消息”“按钮点了没反应”“明明授权成功却报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_tokenACCESS_TOKEN调用之前需要拿到小程序的access_token。老接口是https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappidAPPIDsecretAPPSECRET但这个接口有每天的调用次数限制而且官方后来推出了稳定版接口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?id123, 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 后台核对一遍模板配置很多时候答案就在那里。
返回列表