ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小程序消息推送迁移指南:从模板消息到订阅消息全解析

小程序消息推送迁移指南:从模板消息到订阅消息全解析 小程序消息推送这个东西说简单也简单说坑也坑。早几年用模板消息的时候很多团队把它当免费营销通道用用户在小程序里点了一下某个按钮后面就能被推好几条模板消息甚至有的项目靠这个做召回。后来微信明显收紧了这扇门模板消息下线订阅消息上位规则变成了“用户主动订阅一次你只能给他发一条”。这个变化从源头上掐断了骚扰式推送也让很多开发者在适配过程中踩了一脚又一脚的坑。我这两年陆续接手过好几个小程序项目都是老代码里用模板消息新版本必须改成订阅消息。改造过程中踩过的坑从申请模板 ID 到服务端推送再到审核被拒基本上各个层面都遇到过。这篇就按我实际动手的顺序把从模板消息到订阅消息这条迁移链路讲清楚里面涉及的机制、参数、代码、排查方法都是验证过的。如果你是刚开始接触小程序消息推送或者正在做老项目改造这篇应该能帮你省下不少翻文档和试错的时间。1. 模板消息的“野蛮生长”与订阅消息的接棒逻辑要理解订阅消息为什么是现在这套规则得先回头看模板消息时代发生了什么。1.1 模板消息时代一个 formId 就能发一条模板消息的核心机制大家应该还有印象用户在小程序里触发某个动作后前端能拿到一个 formId开发者把 formId 传到服务端服务端调用接口就能给用户推送一条模板消息。关键是这个 formId 的有效期是七天七天内的发送次数不受严格限制。一些运营思路“灵活”的团队会通过隐藏表单、模拟点击、支付回调等手段囤积大量 formId然后把用户当成短信群发对象每周准时推送营销内容。结果就是用户被骚扰得厉害小程序生态的体验口碑直线下降。微信后来对模板消息做了一系列限制比如要求用户必须主动点击按钮、限制模板消息的使用场景最终在 2020 年前后完成了向订阅消息的切换。1.2 订阅消息的规则变化本质订阅消息最核心的变化是把“推送权”从开发者手里交还给了用户。用户每次通过wx.requestSubscribeMessage授权某个模板后开发者只能向该用户下发一条对应模板的消息。想再发一条就必须让用户再次点击授权。这意味着开发者的思维必须彻底转变模板消息时代先让用户授权一次然后想办法用足额度。订阅消息时代用户授权一次只换来一次推送机会每一条推送都要精打细算。很多老项目改造的难点不在代码本身而在于产品逻辑要跟着改。以前那种“先收集授权、后批量下发”的玩法彻底行不通了。1.3 模板消息与订阅消息的核心差异为了看得更清楚我列个对比表维度模板消息已下线订阅消息现行用户授权方式表单提交或支付触发可批量获取 formId用户主动点击授权按钮每次授权对应一条下发额度可推送次数7 天内理论上可多次推送授权一次推一条想再推需再授权消息跳转固定页面可指定 page但须为已发布页面行业限制基本所有类目可用长期订阅消息仅特定行业可用开发者体验formId 收集繁琐存在滥用空间接口简洁但授权与下发次数强绑定这个表不是文档抄过来的是我踩完坑之后总结的。理解了这个差异后面代码怎么写业务引导怎么设计才有真正的方向。2. 先把订阅消息的机制吃透一次性、长期与授权时机订阅消息不是简单调一个接口它的几种形态和触发边界很容易搞混。我在这块吃过亏起初以为长期订阅消息能申请就能用结果审核直接被驳回。2.1 一次性订阅消息最常见的接入方式一次性订阅消息是绝大多数小程序用的方案。流程上小程序端在用户点击按钮的回调里调用wx.requestSubscribeMessage传入一个或多个模板 IDwx.requestSubscribeMessage({ tmplIds: [模板ID1, 模板ID2], success(res) { // res[模板ID1] 返回 accept、reject 或 ban if (res[模板ID1] accept) { // 用户同意了一次性订阅可向该用户下发一条该模板消息 } }, fail(err) { console.error(订阅消息授权调用失败, err); } });这里有几个容易误判的点必须由用户点击触发。wx.requestSubscribeMessage必须在用户 tap 事件回调里调用。如果你把它放到onLoad里或者放到某个异步网络请求的回调里弹窗根本不会出现。这是平台硬性限制没有变通办法。用户拒绝后再次调用弹窗是否能再次出现能。用户拒绝reject后下次点击仍然可以再次弹窗。但如果用户在弹窗里勾选了“总是保持以上选择”那后续该模板的授权请求会被静默拒绝这时候res[模板ID]会返回ban弹窗不会再出现。一次可以请求多个模板。弹窗会列出所有这些模板用户可以分别选择允许或拒绝。但实际业务里我建议最多一次传两个模板超过两个用户往往会不耐烦反而降低整体授权率。2.2 长期订阅消息行业资格是最大门槛长期订阅消息的接入条件和一次性订阅完全不同。它只对特定行业开放比如医疗、政务、金融、教育等民生服务类目个人主体小程序基本没有申请资格。开发者在后台申请模板的时候就能看到当前主体可选的模板列表如果根本没有“长期订阅”相关选项就别指望了。如果你所在的主体属于开放行业接入流程和一次性订阅类似用户授权一次之后开发者可以多次下发。但要注意长期订阅消息同样不能用于营销内容发送内容必须和用户订阅时的场景强相关。这一点平台审核非常严格。2.3 授权时机的选择在业务动作发生前弹出一次性订阅消息的授权时机直接决定了用户愿不愿意点“允许”。我踩过最经典的坑是尝试把授权弹窗放到用户刚进入小程序的时候就弹出来用户还不知道这个小程序能干嘛自然大部分人都选拒绝。后来改成了在业务动作自然发生前弹授权率大幅提升。举个实际例子一个预约挂号小程序用户完成挂号支付后页面出现“接收挂号结果通知”的按钮点击后调用订阅接口这时候用户对“接下来会收到一条消息”是有预期的接受度远高于冷启动时的强制弹窗。这里有个隐藏的联动点如果订阅接口调用成功但用户随后没有完成业务动作这一次授权额度就被白费了。所以顺序上最好是核心业务动作已完成再引导订阅这样订阅额度能真正用在下一条有效消息上。2.4 授权结果的状态机res里返回的状态除了accept和reject还有ban。把这三个状态理清楚能避免不少误判状态含义处理策略accept用户同意订阅可下发一条记录该用户的授权状态触发服务端下发reject用户本次拒绝可在合适场景再次引导不视为永久拒绝ban用户选择“总是保持以上选择”且为拒绝后续弹窗不会再出现建议在业务上不再强推ban这个状态尤其重要。有些用户一开始拒绝了两三次微信会进一步降低弹窗出现的优先级。如果业务很依赖订阅消息触达最好在用户同意过一次后做好后续的触达价值维护别让第一条消息就让用户后悔。3. 完整推送链路申请模板 ID 到服务端 send 接口订阅消息从触发授权到最终推送成功是一条完整链路卡在任何一个环节消息都发不出去。我把整条链路拆成四步每一步都有对应的关键细节。3.1 第一步小程序后台申请模板 ID先登录微信公众平台在“功能-订阅消息”里选择类目并选用现有模板或者申请自定义模板。模板审核一般需要 1 到 7 个工作日建议提前申请。选择模板时注意看字段类型。比如“订单发货通知”模板里可能有“商品名称”“发货时间”“快递公司”“物流单号”这些字段每个字段都有类型约束thing、number、letter、time 等后续调用 send 接口时字段类型必须完全匹配不能把数字填到 thing 里否则接口直接报 47003。这里有个我犯过的低级错误申请的模板类目和小程序实际提供的服务不一致比如做的是一个工具类小程序模板却是电商售后类的提交审核时被以“模板与小程序服务类目不符”为由驳回。所以申请前先确认小程序的服务类目别急着选模板。3.2 第二步小程序端获取用户的 openid服务端推送订阅消息时接口参数里需要传touser也就是用户的 openid。很多新手会在这里绕弯路以为订阅接口返回的数据里直接带着 openid其实没有。openid 的获取链路是传统的小程序登录流程前端调用wx.login()获取临时 code。前端把 code 传给自己的服务端。服务端调用微信的code2Session接口用 code 换取 openid 和 session_key。wx.login({ success: async (res) { if (res.code) { // 将 res.code 发送到服务端 const response await fetch(https://your-server.com/login, { method: POST, body: JSON.stringify({ code: res.code }) }); const data await response.json(); // data.openid 即用户在小程序内的唯一标识 } } });这里要注意code是一次性的5 分钟有效只能使用一次用完立即失效。服务端如果因为网络问题重复调用第二次调用会报40029code 无效。所以服务端拿到 code 后马上调 code2Session不要把 code 存库。3.3 第三步服务端获取并缓存 access_token调subscribeMessage.send接口必须携带access_token这是调用微信服务端接口的通行证。access_token有效期为 7200 秒为了避免频繁刷新触发平台限流必须在服务端做缓存。我见过不少项目的坑是多个服务实例同时启动每个实例都认为 access_token 过期了同时调用刷新接口导致后获取的 access_token 把前面先获取的顶掉出现“一小时内大量 access_token 失效”的诡异问题。解决思路是引入一个统一的缓存服务Redis 或数据库所有实例从同一个地方读取和刷新 access_token并加分布式锁防止并发刷新。3.4 第四步组装推送参数调用 send 接口服务端最终调用的是POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_tokenACCESS_TOKEN请求体示例{ touser: OPENID, template_id: TEMPLATE_ID, page: pages/order/detail?id123, miniprogram_state: formal, lang: zh_CN, data: { thing1: { value: 蓝牙耳机 }, time2: { value: 2024-06-01 14:30:00 }, phrase3: { value: 已发货 } } }关键字段说明touser接收者的 openid。template_id后台申请到的模板 ID。page点击消息后跳转的小程序页面路径不能带域名否则点击后会提示页面不存在。miniprogram_stateformal代表正式版trial代表开发版developer代表体验版。线上环境必须用formal。我在测试阶段就吃过教训忘了把trial改成formal正式环境里用户收到的消息点开后在真机上竟然跳到了一个不存在的开发版页面。data模板字段填充内容字段名必须是模板里的字段名thing1、time2 这些。3.5 一个可以跑通的 Node.js 示例下面是我的一个项目里实际用过的发送函数经过线上验证你可以直接参考// notify.js const axios require(axios); const APPID your_appid; const SECRET your_secret; const TOKEN_URL https://api.weixin.qq.com/cgi-bin/token; const SEND_URL https://api.weixin.qq.com/cgi-bin/message/subscribe/send; let cachedToken null; let tokenExpireAt 0; async function getAccessToken() { // 如果当前 token 未过期且已存在直接返回缓存 if (cachedToken Date.now() tokenExpireAt - 60000) { return cachedToken; } const res await axios.get(TOKEN_URL, { params: { grant_type: client_credential, appid: APPID, secret: SECRET } }); if (res.data.errcode) { throw new Error(获取 access_token 失败: ${JSON.stringify(res.data)}); } cachedToken res.data.access_token; tokenExpireAt Date.now() res.data.expires_in * 1000; return cachedToken; } async function sendSubscribeMessage(openid, templateId, data, page ) { const accessToken await getAccessToken(); const body { touser: openid, template_id: templateId, page, miniprogram_state: formal, lang: zh_CN, data }; const res await axios.post(${SEND_URL}?access_token${accessToken}, body); return res.data; }这段代码在并发比较低的场景足够用。如果服务是多实例部署cachedToken这种进程内缓存会出现各自维护一份 token 的情况还是要上 Redis 做全局共享缓存。4. 真实踩坑排查弹窗不出现、接口报错、字段截断这部分我把过去踩过的、以及帮别人排查过的高频问题按“现象-原因-解法”的结构整理出来。遇到消息发不出去的时候按这个顺序检查基本能覆盖 80% 的情况。4.1 弹窗不出现或只出现一次现象用户点击按钮后订阅授权弹窗没有弹出或者第一次弹了之后之后再也不弹了。排查思路确认调用时机wx.requestSubscribeMessage必须在用户点击回调的同步链路里调用。如果点击回调里先发了一个异步请求等请求返回后再调用订阅接口弹窗也会被拦截。微信对这里的判断很严格必须在 tap 的同步作用域内调用。检查用户是否已 ban用户选择“总是保持以上选择”并拒绝后后续不会再弹窗。此时res[templateId]会返回ban。这种状态基本无解只能引导用户在“设置-订阅消息”里重新开启。检查基础库版本低版本基础库对订阅消息支持不完整。调试时可以在小程序开发者工具右上角切换基础库版本生产环境建议要求库版本不低于 2.4.0实际现代版本基本都 2.20了。4.2 send 接口报错从 errcode 反推问题服务端调用 send 接口微信会返回一个errcode。遇到errcode为 40001、40003、40037、43101、47003 这几类时排查口径完全不同。我按自己的经验做个汇总errcode可能原因排查方法40001access_token 无效或过期检查缓存中的 token 是否被其他实例覆盖重新获取并保证全局唯一缓存40003openid 不正确确认 openid 是否属于当前小程序主体注意同一用户在不同小程序下的 openid 不同40037template_id 不正确检查模板 ID 是否为本小程序已申请的模板是否被微信下架43101用户拒绝或未订阅该用户并没有同意过这条模板的一次性订阅需确认授权记录47003参数格式错误逐字段核对模板字段类型特别是 thing 类型的长度限制和 number 格式45009接口调用超过限额小程序订阅消息接口有频控检查是否存在给同一用户重复发送41030page 路径不正确确认 page 不需要带域名且页面路径为已发布版本中存在的路径其中 43101 是日常最常遇到的。以前项目从旧的模板消息改过来时服务端数据库里存了一批老用户的 openid但用户实际没有完成过订阅授权一调用 send 就报 43101。这类报错是正常的需要在业务层把订阅授权记录和下发记录做起来不能每次无脑调用接口。4.3 thing 类型字段的截断问题这是特别隐蔽的一个坑。模板里的 thing 类型字段内容长度不能超过 20 个字符这里的“字符”对中文来说就是一个字算一个。如果你填了一串超过 20 字的商品描述微信不会报错而是直接静默截断。结果就是用户收到的消息里商品名称断在一半体验很差。解决方法是服务端发送前主动截断function truncate(value, maxLen 20) { if (value.length maxLen) return value; return value.slice(0, maxLen); } const data { thing1: { value: truncate(order.productName) } };类似的number类型字段传字符串“123”还可以兼容但传小数时要注意是否有位数限制比如金额类字段要求保留一位小数写成999.0不能传999。这些细节文档里都写了但真遇到接口报 47003 的时候很多人第一反应是 openid 错了很少会去逐字核对字段类型。4.4 审核被拒诱导订阅和类目不符订阅消息相关的审核拒绝我见到的两类最多。第一类是“诱导订阅”比如小程序启动时强制弹窗让用户订阅或者用“不订阅无法继续使用”的文案。微信明确要求订阅授权必须是用户自愿的且不能在页面加载时被动弹出。更合规的做法是用户点击一个明确的功能按钮后弹出授权按钮文案就写“接收通知”之类不要写“同意并领取奖励”这种诱导式文案。第二类是“模板与类目不符”。你自己申请的模板或者自定义模板的标题、字段必须和页面提供的服务强相关。如果小程序实际没有“订单”功能却申请了“订单发货提醒”模板审核大概率被拒。4.5 老用户的订阅数据怎么处理从模板消息迁移到订阅消息老用户的数据天然是“孤儿”状态服务端有 openid在旧接口里有历史痕迹但在新的订阅体系里用户从来没有授权过。要让他们能收到订阅消息只能通过一次新的业务动作引导重新订阅。这块没有捷径。比较顺滑的做法是老用户下次使用小程序核心功能比如下单、预约时在结果页展示一个“开启消息通知”的引导按钮并说明通知的类型和用途。这时候用户正处于业务完成状态授权意愿会比冷启动弹窗高很多。5. 订阅消息的运营分寸模板设计、引导时机与效果度量订阅消息把推送次数变得稀缺之后每一次推送机会都值得仔细规划。做得好的项目和做得差的项目差距往往不在代码而在对订阅机会的分配和内容质量的控制。5.1 不要一次性把所有模板全部抛出有些团队在用户首次进入时一口气弹五六个模板授权想的是“这次让用户多授权几个后面就能多推几条”。实测下来这种策略的授权率极低用户面对一大批不理解的授权请求更容易全部拒绝而且会对小程序产生“又要骚扰我”的心理。更好的做法是一次只弹一个模板并在用户即将用到该通知能力的业务节点弹出。比如用户提交预约弹“预约成功通知”模板。用户支付完成弹“支付成功通知”模板。用户申请退款后弹“退款进度通知”模板。这样每次授权的动机都很具体用户能理解这个授权是用来干嘛的接受度大幅提升。5.2 模板字段设计的两个技巧第一字段顺序要和用户关心的信息顺序一致。比如物流通知用户最关心的是“快递公司”和“物流单号”这两个字段应该放在前面。第二文案尽量精简thing 类型字段控制在 20 字以内不仅是因为平台限制也是因为用户扫一眼就能读完的消息点击率才高。我在一个项目里对比过同样是发货通知模板字段改动后商品名从 18 字缩减到 10 字并增加取件码字段消息点击率提升了近 20%。这个数据说明推送效果和模板字段的打磨关系很大。5.3 发送策略在用户最需要的时间点触达一次性订阅消息没有“定时发送”的能力它是业务事件驱动的。你要做的是在事件发生的那一刻立刻调用 send 接口。比如支付成功消息如果用户下单后 5 分钟还没收到支付结果通知用户会困惑甚至投诉。但也有一个反常规的点非紧急的通知不一定要立刻发。比如用户授权了“每周报表提醒”模板周一产生的数据在周二早上 9 点发送可能比数据生成时立刻发送的效果更好。这时你可以在服务端做一个轻量的延迟任务队列到时间点再触发 send 接口。注意这个延迟不能超过 7 天因为订阅消息的授权额度虽然不像 formId 那样有 7 天有效期但长期不发送用户的预期也会下降。5.4 效果度量与调优订阅消息的效果可以用三个数据来衡量授权率、送达率、点击率。授权率看每次引导授权弹窗的同意比例用来衡量引导时机和文案是否恰当。送达率看 send 接口返回成功的比例排除 43101、47003 等异常。点击率从微信后台“订阅消息”数据页面能看到点击数据点击率低意味着模板字段或跳转页面需要优化。我建议把这三个指标分成三条线来观察别只看一个。授权率低说明引导时机不对送达率低说明代码或数据链路有 bug点击率低说明消息内容和用户预期有偏差。三条线独立优化整个推送系统的表现才会向上走。6. 一个完整的接入流程参考最后把整条接入流程串起来给准备动手的团队一个检查清单。这里的顺序也是我实践中调整过的能减少返工。先确认小程序服务类目再去后台申请订阅消息模板避免类目不符被拒。设计订阅引导时机确定哪些业务动作后需要弹窗、对应哪些模板。服务端实现 access_token 的全局缓存与刷新并实现 code2Session 接口。小程序端在需要订阅的按钮回调里调用wx.requestSubscribeMessage授权成功后把 openid 和模板 ID 绑定记录入库。服务端在对应业务事件发生时组装 data 参数调用subscribeMessage.send接口下发。监控后台数据重点看 43101 报错占比反推是用户未授权还是业务引导失效。这套流程走下来基本能把订阅消息链路里的大多数问题避免掉。跨过模板消息和订阅消息这道坎后面再做小程序相关的消息能力就会顺很多。我自己的感受是订阅消息这套规则虽然在短期限制了开发者的自由度但长期看它逼着开发者更好地理解用户需求、更克制地使用触达能力这对产品本身反而是好事。
返回列表