后台推订阅消息返回 success 但用户全没收到——微信授权 one-shot 机制踩坑
做了一个「后台手动发放礼品通知」功能,逻辑很简单:运营选完用户 → 点发送 → 后端调微信订阅消息 API。接口返回 errcode: 0,日志也漂亮,UI 显示「已发送站内信 + 订阅消息」,结果上线后运营反馈:「用户说根本没收到微信通知」。翻遍代码、参数、模板 ID,一切看起来都对——直到意识到问题根本不在代码,而在平台规则。
现象
- 后端批量调用订阅消息接口,HTTP 返回
{"errcode": 0, "errmsg": "ok"} - 微信公众平台「订阅消息使用记录」:送达次数 = 0
- 少数用户能收到(刚在小程序内点过「允许接收通知」),大部分 0 条
- 前端标签写「已发订阅消息」,实际触达率约 0%
- 开发说「API 都成功了」,运营说「用户没收到」,互相指责,谁都没错
根因
微信订阅消息的授权是 one-shot 且只能在小程序内触发:
| 步骤 | 谁触发 | 何时 |
|------|--------|------|
| 1. 弹窗「允许接收 XXX 通知」 | 用户在小程序内 | 调用 wx.requestSubscribeMessage |
| 2. 服务端获得 openid + tmplId 的授权令牌 | 微信通道 | 用户点「允许」瞬间 |
| 3. 服务端发一次订阅消息 | 服务端任意时刻 | 必须在授权后有限次数内用掉 |
三条硬约束:
- 授权是 one-shot:用户点一次「允许」,服务端只能发一次,发完再调就被平台静默丢弃
- 授权来源只能是小程序内的用户交互:后台没有用户操作上下文,
wx.requestSubscribeMessage是小程序端 API,服务端根本调不到 - API 返回成功 ≠ 用户收到:微信通道遇到「无授权 / 授权用尽 / 模板不匹配 / 用户关闭通知」会静默丢弃,HTTP 层依然 200 OK
errcode: 0 只代表 HTTP 请求格式合法,不代表消息被投递。后台批量推送时,用户没有在小程序内点过「允许」,平台会静默丢弃,不返回任何错误。
排查时去微信公众平台「订阅消息」→「使用记录」看实际送达数,而不是看自己接口的返回值。
正解
后台主动推送场景:只发站内信
// ❌ 错误:后台发放路径也调订阅消息,触达 0%
public function dispatch($userId, $gift) {
InboxService::sendToUser($userId, $title, $content); // OK
WechatSubscribeService::sendGiftNotice($userId, ...); // 静默 0 触达
}
// ✅ 正确:后台路径只走站内信
public function dispatch($userId, $gift) {
InboxService::sendToUser($userId, $title, $content); // 应用内消息中心,100% 可控
// 订阅消息:后台无授权来源,不调用。
// 如需微信通知,必须挂在小程序端用户交互瞬间。
}UI 文案不能承诺平台做不到的事
<!-- ❌ 误导运营和用户 -->
<el-checkbox>同时发送站内信 + 微信订阅消息</el-checkbox>
<el-tag>已发送订阅消息</el-tag>
<!-- ✅ 准确,并解释限制 -->
<el-checkbox>发送站内信通知</el-checkbox>
<div class="form-tip">
微信订阅消息须由用户在小程序内点击「允许」才可投递,
后台手动推送不支持订阅消息通知。
</div>真要推订阅消息:挂在用户交互瞬间
// 小程序端:用户点击操作(如兑换)时同步收集授权
async onExchangeClick() {
// 1. 先弹窗请求订阅授权
const res = await wx.requestSubscribeMessage({
tmplIds: ['TMPL_ID_EXCHANGE_SUCCESS']
})
// 2. 调后端接口,附带授权状态
await api.exchange({
giftId,
subscribeAuthorized: res['TMPL_ID_EXCHANGE_SUCCESS'] === 'accept'
})
// 3. 后端收到 subscribeAuthorized=true 后,可发一次订阅消息
// 授权令牌 7 天有效,发完即失效
}wx.requestSubscribeMessage 必须由小程序端用户手势触发,后端拿到授权信号后才能调一次发送 API。两者之间不能有异步等待(会话窗口关闭)。后台主动发放场景彻底放弃订阅消息,改用站内信。
通道选择速查
| 场景 | 推荐通道 | 订阅消息可用? | |------|---------|--------------| | 后台运营手动发放 / cron 定时推送 | 站内信、短信 | 否 | | 用户在小程序内完成某操作(兑换 / 下单)| 订阅消息 + 站内信 | 是(one-shot) | | 48h 内有互动的用户 | 公众号客服消息 | 不适用 |
一句话外卖
「后台主动推送」和「订阅消息」在微信生态里天然矛盾——前者没有用户授权上下文,平台静默丢弃但不报错;设计阶段就该把两条路锁死:后台路径只走站内信,订阅消息只挂用户交互瞬间。