ARTICLE DETAIL

资讯详情

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

微信小程序消息订阅机制与后端接入完整指南

微信小程序消息订阅机制与后端接入完整指南 1. 微信小程序消息订阅先搞清楚它的三种订阅机制很多人一上来就问消息订阅怎么做结果第一步就卡在概念上。微信小程序的消息订阅不是给用户发私信那么简单它有一套自己的授权和下发机制。先花三分钟把机制弄清楚比直接抄代码重要得多。微信小程序消息订阅本质上是微信服务端代发通知的机制用户在你的小程序里点击某个按钮小程序弹出一个订阅授权框用户点了允许之后你的后端服务器才能调用微信的接口向这个用户发送一条模板消息。这条消息会出现在用户的微信服务通知里跟公众号消息是同一入口用户不会错过。目前微信提供三种订阅方式区别非常大订阅类型授权规则发送限制适用场景一次性订阅用户每次授权只能接收一条消息授权一次后台只能成功发送1次下单成功通知、预约确认、审核结果长期订阅用户授权后可长期接收无次数限制但模板需要特殊申请政务、医疗、教育等特定类目永久订阅已下线已全面停止新申请无限制不再考虑一次性订阅是最常见的。用户每次点订阅按钮授权弹窗出现他点了允许你的后端就获得了一次发送机会。这次机会用完就没有了用户下次还要接收还得再点一次。长期订阅看着美好但门槛极高。个人开发者基本拿不到只有政务、医疗、教育、交通等特定行业的小程序在微信公众平台提交对应的资质材料走人工审核流程才可能申请到长期订阅模板。绝大多数做普通商业项目的开发者实际能用到的就是一次性订阅。这里有个常见的误解以为用户授权之后就可以一直给他发消息。一次性订阅的一次性指的是发送机会只能用一次不是订阅关系只存在一次。用户第一次点了允许你发了一条如果还想发第二条用户必须再次点击订阅按钮并再次允许。这个机制天然地防止了小程序骚扰用户。理解了这三种类型后面的开发路径就清晰了。我下面整个教程都围绕一次性订阅展开这是最主流、门槛最低、绝大多数中小程序项目都会用到的方案。2. 完成后台配置模板申请和开发信息准备写代码之前先要去微信公众平台把基础配置搞定。记住一句话后台配置不完成接口天上调不通。这一步看着简单实际是很多人卡住的第一道坎。2.1 申请订阅消息模板的完整路径登录微信公众平台mp.weixin.qq.com进入小程序管理后台左侧菜单找到功能 - 订阅消息。首次进入会让你选择类目这个类目必须跟你的小程序主体一致选错会导致后续模板搜索不到。进入订阅消息页面后可以看到公共模板库和我的模板两个Tab。在公共模板库里搜索你需要的模板关键词比如订单、审核、预约每个模板都有一个对应的ID像这样模板IDTZ9dW9PQo8B-Zzx1OrfpyG2TtGxxxxxxxx点选用之后这个模板会进入我的模板列表。模板库里的模板内容都是微信官方定义好的字段格式固定你不能自己新增字段。比如一个订单通知模板可能包含以下字段订单号商品名称下单时间订单金额备注每个字段在模板里都有一个固定的key这些key在发送消息的时候要用到。点开模板详情能看到字段名称和对应的字段key。还有一个必须记住的步骤在模板详情页面有一个关键词列表你必须选择合适的关键词组合。有的模板提供了10个关键词你只需要选其中3个组成最终的模板内容。关键词的组合决定了用户看到的通知样式也决定了后端要传哪些数据参数。潜在风险选关键词的时候一定要注意语义有些关键词虽然跟业务沾边但发送时你不一定有对应的数据。比如模板提供了订单金额但你的业务里订单金额存在分还是元的单位这个在发送的时候要格外小心后面会详细说。2.2 获取AppID、AppSecret和IP白名单消息订阅的后端接口调用需要用到小程序的AppID和AppSecret。AppID在小程序后台开发 - 开发管理 - 开发设置里可以看到。AppSecret点重置会重新生成记住重置之后旧Secret立即失效如果有多个环境共用重置前一定要确认没有其他服务在用。把这两个参数配置到你的后端服务配置文件中比如Spring Boot的application.ymlwechat: appid: wx1234567890abcdef secret: 1234567890abcdef1234567890abcdef还有一项容易被忽视的配置IP白名单。同一个开发设置页面下面有一个服务器域名/IP白名单的设置。微信要求调用后端接口时服务器的出口IP必须加到白名单里否则调用接口会返回40164错误。这里说的IP是你的后端服务器公网出口IP不是本地开发机的IP。怎么查在服务器上执行curl ifconfig.me如果后端服务搭在腾讯云或阿里云上直接查内网对应公网IP也行。查到的IP填进白名单通常10分钟内生效。本机联调的时候可以把本地公网IP也加进去方便本地调试但上线后一定要把本地IP移除只保留线上服务器的IP。2.3 下发路径域名配置订阅消息的url回调、图片等资源如果page跳转需要携带参数还有一个隐藏配置点——request合法域名。小程序的wx.request请求默认要校验域名但在开发工具里可以勾选不校验合法域名先跑通上线前必须配置到位。订阅消息本身经由微信服务器下发不经过你的服务器所以这一步相对简单但后续联调如果发现前端请求自己被拦截优先检查这里。3. 后端第一关稳定获取access_token并缓存后端所有微信接口的调用都绕不开access_token。access_token是调用微信全局接口的凭证有效期7200秒2小时而且微信官方明确限制了获取频率每天调用次数上限是2000次同时接口本身也有稳定性要求不建议频繁刷新。实际开发中最常犯的错误就是每次发消息都去重新获取access_token一两天就把配额用光了然后接口报45009超出调用频率限制一脸懵。3.1 access_token获取接口接口地址GET https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappidAPPIDsecretAPPSECRET返回{ access_token: ACCESS_TOKEN, expires_in: 7200 }代码实现上用Spring Boot Redis来管理access_token是最常见的方案。Redis的setex命令天然适合做有效期控制缓存2小时过期自动删除。Service public class WxTokenService { Autowired private StringRedisTemplate redisTemplate; Value(${wechat.appid}) private String appid; Value(${wechat.secret}) private String secret; private static final String TOKEN_KEY wx:access_token; public String getAccessToken() { // 先从redis取存在直接返回 String token redisTemplate.opsForValue().get(TOKEN_KEY); if (StringUtils.hasText(token)) { return token; } // 不存在调微信接口获取 String url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid appid secret secret; RestTemplate restTemplate new RestTemplate(); String result restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(result); String accessToken json.getString(access_token); Integer expiresIn json.getInteger(expires_in); // 缓存到redis有效期设置比微信短一点避免边界问题 redisTemplate.opsForValue().set(TOKEN_KEY, accessToken, expiresIn - 200, TimeUnit.SECONDS); return accessToken; } }注意我把缓存有效期设置成了expires_in - 200秒也就是在微信token真正过期前200秒就主动换取新的。这样能避免一个经典问题代码里拿到token中间处理了半分钟业务调用发送接口时token刚好过期返回40001错误。既然微信的expires_in是7200秒提前3分多钟刷新是完全合理的。3.2 处理token失效自动重试有一个情况你一定会遇到token在Redis里还有效但调微信接口时返回了40001invalid credentialaccess_token无效或过期。这时候不能直接返回错误给前端应该主动清掉本地缓存重新获取一次token再重试业务请求。public String sendSubscribeMessage(SendMessageRequest request) { String token getAccessToken(); try { return doSend(token, request); } catch (WechatApiException e) { if (40001.equals(e.getErrCode())) { // token失效清除缓存重新获取 redisTemplate.delete(TOKEN_KEY); token getAccessToken(); return doSend(token, request); } throw e; } }这个重试逻辑在并发场景下也能正常工作——多个请求同时发现token失效都去删除缓存再获取因为getAccessToken里Redis的检查再设置操作虽然存在并发窗口但即便多调一次token接口也无伤大雅最多浪费一次配额。3.3 接口返回码速查表后端接入微信接口必须对返回码有概念。下面是订阅消息相关的重点返回码建议收藏返回码含义处理建议0发送成功正常入库记录40003openid无效检查touser是否正确可能用户没关注/没登录40037template_id不正确检查模板ID是否从后台复制完整43101用户拒绝接收消息用户没点授权或授权次数已用完静默处理47003模板参数不准确检查data里的key和value是否符合模板45009接口调用超过频率限制检查access_token缓存逻辑发送频率过高41030page路径不正确检查page参数不要带.html后缀40001access_token无效按上面的重试逻辑处理把这条规则定成后端开发规范订阅消息发送接口对75000以下的业务错误码都算作预期内失败不能影响主流程。比如用户授权次数用完了后端返回43101你只需要把这个状态记录一下不能为了提高成功率而多次重复发送。4. 后端发送消息参数构造和完整代码实现核心接口调用是标准的HTTP POST通往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: { character_string1: { value: DD20240001 }, thing2: { value: 某某商品 }, time3: { value: 2024-01-15 14:30:00 }, amount4: { value: 99.50元 } } }参数说明touser接收者openid。注意是用户在小程序里的openid不是公众号的openid两者不通用。template_id后台我的模板里的模板ID。page用户点击消息后跳转的小程序页面路径可以带参数。miniprogram_state有formal正式版、trial开发版、developer体验版三个值。后端如果填了developer那么只有开发版的微信才能打开这条消息正式版环境的一定要填formal这是很多人上线后踩的坑测试的时候能用上线后用户点通知没反应检查这里。lang语言类型默认zh_CN。data模板字段每个字段的类型在模板后台已经固定比如character_string表示字符串、thing表示事物名称、amount表示金额、time表示时间。传值的时候类型必须匹配否则报47003错误。data里有一个很隐蔽的规则thing类型的字段value长度不能超过20个字符。如果商品名称有30个字截断后再传不然接口会直接报错。这个坑我遇到过好几次尤其是电商类小程序商品全名动不动就超长。下面是完整的Java后端实现包含参数校验和错误处理RestController RequestMapping(/api/wx) public class SubscribeMessageController { Autowired private WxSubscribeMessageService subscribeMessageService; /** * 发送订阅消息业务系统内部调用或前端触发 */ PostMapping(/subscribe/send) public Result sendSubscribe(RequestBody SendSubscribeMessageDTO dto) { // 校验模板参数 if (!StringUtils.hasText(dto.getOpenId())) { return Result.error(openId不能为空); } if (!StringUtils.hasText(dto.getTemplateId())) { return Result.error(templateId不能为空); } boolean success subscribeMessageService.send(dto); return success ? Result.success() : Result.error(发送失败请检查参数或用户授权状态); } }Service实现Service public class WxSubscribeMessageServiceImpl implements WxSubscribeMessageService { Autowired private WxTokenService wxTokenService; Override public boolean send(SendSubscribeMessageDTO dto) { // 拼装请求参数 JSONObject body new JSONObject(); body.put(touser, dto.getOpenId()); body.put(template_id, dto.getTemplateId()); body.put(page, dto.getPage()); body.put(miniprogram_state, dto.getMiniprogramState()); body.put(lang, zh_CN); JSONObject data new JSONObject(); for (Map.EntryString, String entry : dto.getDataMap().entrySet()) { JSONObject fieldValue new JSONObject(); // thing类型限制20字符 String value entry.getValue(); if (value.length() 20) { value value.substring(0, 20); } fieldValue.put(value, value); data.put(entry.getKey(), fieldValue); } body.put(data, data); // 发送 String url https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token wxTokenService.getAccessToken(); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityString entity new HttpEntity(body.toJSONString(), headers); RestTemplate restTemplate new RestTemplate(); ResponseEntityString response restTemplate.postForEntity(url, entity, String.class); JSONObject result JSON.parseObject(response.getBody()); int errCode result.getIntValue(errcode); if (errCode ! 0) { // 记录错误日志 log.error(发送订阅消息失败errCode{}, errMsg{}, errCode, result.getString(errmsg)); return false; } // 发送成功可以在这里入库记录发送历史 return true; } }开发过程中强烈建议先把所有日志打全openid、template_id、page、errcode、errmsg。订阅消息排错最依赖日志因为用户侧看不到具体错误只能靠服务端日志定位问题。5. 前端接入调起订阅弹窗与参数校验后端就绪后前端要做的事情其实不多但每个细节都关乎用户体验。前端主要负责两件事调起订阅授权弹窗和把openid传给后端。5.1 wx.requestSubscribeMessage基础用法在用户点击某个触发按钮时调用wx.requestSubscribeMessage// pages/order/confirm.js Page({ data: { orderId: }, onLoad(options) { this.setData({ orderId: options.id }); }, // 用户点击提交订单并订阅通知 handleSubmitOrder() { // 第一步调起订阅授权 wx.requestSubscribeMessage({ tmplIds: [TZ9dW9PQo8B-Zzx1OrfpyG2TtGxxxxxxxx], success: (res) { // 判断用户是否点击了允许 if (res[TZ9dW9PQo8B-Zzx1OrfpyG2TtGxxxxxxxx] accept) { // 用户允许了继续下一步 this.submitOrder(); } else if (res[TZ9dW9PQo8B-Zzx1OrfpyG2TtGxxxxxxxx] reject) { // 用户拒绝了也可以继续下单只是没有通知 this.submitOrder(); } else { // 用户点击了取消或者弹窗显示异常 wx.showToast({ title: 订阅失败将无法收到通知, icon: none }); this.submitOrder(); } }, fail: (err) { console.error(requestSubscribeMessage fail, err); // 接口调用失败常见原因是基础库版本过低 wx.showToast({ title: 当前微信版本不支持订阅消息, icon: none }); this.submitOrder(); } }); }, submitOrder() { // 实际提交订单的请求 wx.request({ url: https://yourdomain.com/api/order/create, method: POST, data: { orderId: this.data.orderId }, success: (res) { // 后端在保存订单后通过openid发送订阅消息 } }); } });几个关键点首次弹窗时机要合适。wx.requestSubscribeMessage只能在用户点击行为tap的同步回调里调用不能在异步请求的回调里调用否则会弹窗失败。这是微信的限制目的是防止小程序在非交互场景下随意打扰用户。授权结果的状态值除了accept和reject还有一种情况是返回ban——表示用户之前在系统设置里关闭了订阅消息的授权。这种情况比较麻烦需要引导用户去设置页重新打开。下面会细说。一次可以传多个模板ID最多3个。每个模板会单独弹出确认框让用户选择一次授权多个模板是很常见的需求比如同时订阅发货通知和签收通知。写法上tmplIds传数组即可wx.requestSubscribeMessage({ tmplIds: [TEMPLATE_ID_1, TEMPLATE_ID_2, TEMPLATE_ID_3], success: (res) { // 分别判断每个模板的授权状态 } });注意一次传多个模板用户可以选择接受一部分、拒绝一部分所以一定要逐个模板判断状态不能只判断一个。5.2 openid的获取与传递订阅消息的发送方是后端而后端需要的touser是用户的openid。所以前端还必须具备获取openid的能力。常用做法用户登录时小程序端把wx.login得到的code传给后端后端用code换openid并把openid返回给前端缓存。// 登录 wx.login({ success: (res) { const code res.code; wx.request({ url: https://yourdomain.com/api/wx/login, method: POST, data: { code }, success: (res) { const { openid, sessionKey } res.data.data; wx.setStorageSync(openid, openid); } }); } });后端用code换openid的接口GET https://api.weixin.qq.com/sns/jscode2session?appidAPPIDsecretSECRETjs_codeCODEgrant_typeauthorization_code这个流程属于登录模块的内容消息订阅发送的时候直接用缓存好的openid即可。5.3 订阅授权状态检查与引导打开如果用户点了ban说明他在微信的设置里关闭了接收订阅消息前端任何弹窗都不会再出现。这种情况下需要引导用户手动打开开关。可以弹出自定义确认框wx.showModal({ title: 提示, content: 你已关闭消息接收请在设置中重新开启后才能收到通知, confirmText: 去设置, success: (res) { if (res.confirm) { wx.openSetting({ success: (settingRes) { // 用户从设置页返回后可以再次尝试请求订阅 if (settingRes.authSetting[scope.subscribeMessage]) { // 已打开继续业务流程 } } }); } } });注意wx.openSetting只能打开当前小程序的设置页无法打开微信总体的服务通知设置。如果用户在微信我 - 设置 - 新消息通知里关了服务通知小程序端是引导不过去的只能提示用户去微信设置里打开。6. 端到端实战用户下单到消息送达的完整链路前端的弹窗授权和后端的接口发送都讲完了但把它们串起来才是真正的核心。很多教程只教单个环节结果读者联调时发现消息发不出去。下面用一个完整的订单状态通知案例把整个链路走一遍。6.1 业务场景设定场景用户在小程序里提交一个商品订单后台审核通过后给用户推送一条审核结果通知。整个时序如下用户点击提交订单按钮前端调起订阅授权弹窗模板审核结果通知用户点击允许前端将订单提交到后端后端保存订单拿到订单号后端调用微信订阅消息接口向用户openid发送审核结果通知用户在微信服务通知里收到这条消息用户点击消息跳转小程序对应订单详情页6.2 前端代码整合完成授权和订单提交的整合// pages/order/confirm.js Page({ data: { orderId: }, onLoad(options) { this.setData({ orderId: options.id }); }, // 点击提交订单 onSubmitOrder() { const templateId TZ9dW9PQo8B-Zzx1OrfpyG2TtGxxxxxxxx; wx.requestSubscribeMessage({ tmplIds: [templateId], success: (res) { const authResult res[templateId]; // 不管用户是否允许订阅订单都可以提交 // 但把授权结果传给后端后端决定是否发送订阅消息 this.createOrder(authResult accept); }, fail: (err) { // 弹窗调用失败不阻塞下单 console.error(订阅授权失败, err); this.createOrder(false); } }); }, createOrder(canSubscribe) { wx.request({ url: https://yourdomain.com/api/order/create, method: POST, data: { orderId: this.data.orderId, canSubscribe: canSubscribe }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 提交成功, icon: success }); wx.navigateTo({ url: /pages/order/list }); } } }); } });这里有个产品设计上的小技巧订阅授权按钮最好独立于主操作按钮。让用户先点击订阅通知按钮完成授权再点击提交订单按钮下单。如果两个操作绑在同一个按钮上用户面对一个弹窗一个确认框容易懵也会影响订单转化率。我实际运营中发现订单确认页放两个按钮开启通知和提交订单比提交订单并开启通知这种单一按钮的授权通过率高不少。6.3 后端同步/异步发送策略后端收到createOrder请求后订单保存成功就要发送订阅消息。这里有两种设计思路方案A同步发送。在创建订单接口里直接调微信接口用户提交订单请求后一直等到微信返回再给用户响应。优点是逻辑简单缺点是微信接口耗时不稳定高峰期可能几百毫秒甚至一秒用户的请求被拖慢。方案B异步发送。创建订单接口先返回提交成功订单保存后扔消息队列或者线程池去处理订阅消息发送。优点是响应快用户体验好缺点是需要额外的异步基础设施。我个人的建议上线初期用同步跑顺了再改异步。同步代码少、好定位问题等业务量上来了再引入MQ。改造成异步也不复杂。如果项目里已经有RabbitMQ或者RocketMQ把发送订阅消息封装成一个消息体投递到队列即可。如果没有MQ用简单的线程池也可以Component public class SubscribeMessageAsyncSender { private static final ExecutorService EXECUTOR Executors.newFixedThreadPool(8); public void sendAsync(SendSubscribeMessageDTO dto) { EXECUTOR.submit(() - { try { subscribeMessageService.send(dto); } catch (Exception e) { log.error(异步发送订阅消息失败, e); } }); } }注意线程池一定要用有名字的、可监控的配置别用Executors.newCachedThreadPool不然生产环境排查问题要抓瞎。6.4 记录订阅消息发送历史消息发送不应该是发完不管的。推荐建一张发送历史表方便日后排查和统计CREATE TABLE wx_subscribe_message_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL, template_id VARCHAR(64) NOT NULL, order_id VARCHAR(64), errcode INT, errmsg VARCHAR(255), send_status TINYINT COMMENT 0失败 1成功, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_openid (openid), INDEX idx_order_id (order_id) ) COMMENT微信订阅消息发送日志;每次发送都记录包括失败原因。这样如果用户投诉没收到通知查一下send_status0且errcode43101你就能明确告诉用户是因为订阅授权过期了而不是系统出了bug。7. 高频踩坑实录一次性订阅机制引发的四大坑订阅消息的坑90%集中在一次性订阅这个机制上。下面四个坑是我在多个项目里都真实遇到过的逐个说说解决方案。7.1 用户只授权一次后端却当天发了两条场景用户下单时订阅了订单通知结果订单状态从待审核变已审核发了一条从已审核变已发货又发了一条。第二条明明调接口返回成功了但用户就是收不到。根因一次性订阅的授权次数只能用一次。第一次发送成功后该订阅授权已经消耗掉了第二次虽然接口没有立即报错实际返回43101或0要看时序但消息不会出现在用户的服务通知里。解法发送前先查询本次订阅机会是否还有效。微信没有开放的查询订阅次数接口所以这个状态必须自维护。推荐做法用户授权成功时前端把授权状态同步到后端后端记录subscribe_count 1后端每次发送订阅消息后对应记录的subscribe_count减1减到0时不再发送或者提醒用户订阅已过期请重新开启更简单的方案一个订单只发一条订阅消息。把多个通知合成一条模板消息比如您有新的订单动态已审核、已发货这样只需一次订阅就能承载整个订单生命周期。7.2 用户授权了却没收到消息这个场景非常普遍而且原因多种多样。常见的排查顺序检查openid。用户在开发者工具里预览和真机上跑openid可能不一样。体验版环境的openid跟正式版也不一样要注意别混了。检查miniprogram_state。如果你在正式环境发了developer或trial的消息用正式版微信打开是收不到的服务通知里也不会展示。检查模板ID前后端是否一致。有人改过后台模板但前端代码里还是旧的模板ID这也常见。检查邀请体验成员。如果是开发版/体验版的测试必须在微信后台成员管理里加入测试者非成员收不到体验版消息。7.3 用户上次拒绝过订阅下次弹窗不出现微信的规定同一个模板在用户拒绝之后短时间内再次调起wx.requestSubscribeMessage弹窗会直接不显示。这种场景下你的success回调会执行但返回的结果是reject甚至可能直接是fail。这是微信防打扰的设计。解法不要为了刷授权率而频繁调起弹窗。产品上可以把订阅入口做成可选项——用户不订阅也能正常用只是收不到通知。用户主动点击开启通知时才调起弹窗比每次进入页面都自动弹窗效果好得多。如果确实需要再次引导用户订阅可以走wx.openSetting打开小程序设置页面但小程序的设置页里没有单独的订阅消息开关订阅消息的授权记录在微信底层设置页不展示所以wx.openSetting对恢复订阅弹窗没有直接帮助。真正有效的是等待一段时间再尝试微信的防打扰机制有冷却时间实测一般要过几分钟到几十分钟不等。7.4 消息模板字段长度超限前面提到过thing类型限制20个字符实际操作中还有更多限制字段类型最大长度说明thing20字符名称、商品名等超长截断character_string32字符单号、编号等amount1个字符串位置金额格式为xx.xx 单位time具体时间24小时制phone_number17字符手机号number32字符数字phrase5个汉字短语超长会报47003特别注意phrase类型只有5个汉字比如审核通过订单取消这种超过5个字就报错。另外amount类型传的时候要把元带上或者不带实测两种写法都支持但一定不能传类似99.50元人民币这种多余的内容。另一个容易忽略的规则模板字段的value不能包含换行符。有些业务场景想通过换行做排版微信接口会直接拒绝。要做多行内容可以在模板设计时用多个字段承载而不是塞进一个字段里加\n。8. 进阶实践消息重试、降级策略与数据分析订阅消息调试通了只是第一步真正上线后要考虑稳定性、率控、体验几个维度的问题。这里分享几个进阶的实战思路。8.1 发送结果回执与自动重试微信的订阅消息发送接口是即时返回结果的不像短信有异步回执。所以你要在发送失败的场景自行设计重试策略网络超时超时无响应可以重试因为接口可能已经成功处理也可能没处理。这种场景最好配合发送历史表的幂等键做去重避免用户收到两条重复消息。推荐在发送前生成一个消息唯一ID微信接口的请求体虽然不支持幂等键但你自己在日志和检索层面要有这个标识。业务错误如43101用户拒绝不要重试。重试只会再次失败浪费时间。应该把这个状态透传给业务方提示用户重新订阅。系统错误如-1微信接口偶发系统繁忙这种可以延迟几秒重试一次最多重试3次。public boolean sendWithRetry(SendSubscribeMessageDTO dto, int maxRetry) { int attempt 0; while (attempt maxRetry) { try { return subscribeMessageService.send(dto); } catch (WechatApiException e) { if (-1.equals(e.getErrCode())) { attempt; Thread.sleep(1000 * attempt); continue; } throw e; } } return false; }8.2 万级用户场景下的发送频控如果你的小程序日活有几十万要特别留意消息发送的频率。微信对订阅消息的发送接口虽然没有明确的每日总量限制只要access_token配额够理论上可以一直发但单用户被持续推送通知会引起投诉严重时可能导致小程序被封禁消息能力。建议做两件事用户级频控单个用户一天最多收到3条订阅消息超过就不再发。这需要后端维护一个维度为openid日期的计数器。全局限流在网关层给subscribe/send接口配一个QPS上限比如100 QPS防止异常情况打爆后端。Redis计数器实现用户级频控代码非常简洁public boolean checkFrequency(String openid) { String key wx:sub:count: openid : LocalDate.now(); Long count redisTemplate.opsForValue().increment(key); if (count ! null count 1) { redisTemplate.expire(key, 1, TimeUnit.DAYS); } return count ! null count 3; }8.3 用发送数据反向优化小程序消息订阅的数据不光是技术数据还是产品数据。发送成功率高不高、用户拒绝率高不高直接反映你的产品设计和用户预期管理。我自己在项目里会把发送日志做定时统计关注三个指标授权点击率弹窗弹出后用户点允许的比例。如果低于50%说明弹窗时机不对或诱导文案不够清晰。发送成功率后端调微信接口成功的比例。如果低于95%重点查access_token缓存、模板参数格式。人均接收条数每个用户平均收到的消息条数。超过3条就要警惕消息打扰。这些指标用SQL就能跑出来配合报表展示。用户授权点击率特别值得关注——它直接影响你后续所有通知类功能的效果。实测经验是把订阅通知按钮做在表单提交按钮同一屏、文案直接告诉用户订阅后可收到订单进度能把点击率从30%拉到60%以上。9. 全栈联调自查清单最后给一份我自己每次联调订阅消息时都会过一遍的清单照着检查能省下大量排错时间。后台配置检查[ ] 小程序后台功能 - 订阅消息里能看到已选用的模板[ ] 模板ID已复制到前端和后端配置[ ] 后端服务器出口IP已添加到IP白名单[ ] 正式环境request合法域名已配置后端检查[ ] access_token有Redis缓存不会每次请求都重新获取[ ] 发送接口参数完整touser、template_id、data[ ] data字段类型与后台模板一致[ ] thing类型内容不超过20字符[ ] miniprogram_state正式版填的是formal[ ] 日志记录了发送结果和错误码前端检查[ ] 订阅弹窗在用户点击行为回调中同步调用[ ] 用户拒绝对业务流程无阻塞[ ] openid已从后端获取并持久化[ ] 多模板场景逐个判断授权状态真机验证[ ] 开发者工具模拟器先行验证[ ] 真机上使用体验版或开发版验证[ ] 确认测试账号已在成员管理中添加[ ] 用户打开服务通知能收到消息[ ] 点击消息能正常跳转到小程序页面这套流程走完消息订阅功能基本就稳了。最后再提一句我在实际项目中的体会消息订阅接入本身技术难度不大真正拉开差距的是对一次性订阅机制的深刻理解以及对用户授权时机的产品把控。把这两件事想明白了订阅消息就是小程序里一个稳定可靠、成本极低的通知渠道。现在可以把这篇教程存下来下次开发直接照着做遇到问题回来对着清单排查能省下不少时间。
返回列表