微信小程序订阅消息总发不出去:我把授权、模板和 Node.js 重试队列补齐
一次支付成功后的订阅消息没送达,我把问题拆成授权、模板、错误码和重试队列四段,补上可观测与延迟重试。

作者
ZHOUYI
周五晚上收到反馈:支付成功了,但小程序订阅消息没提醒。第一反应是接口挂了,看日志才发现发送任务有时根本没进发送函数,有时进了却被微信拒掉。订阅消息不是单个接口问题,而是一条链路:前端授权、模板配置、后端发送、失败重试,哪一段断了都会表现为“发不出去”。
先别急着重试:把失败分成三类
- 授权失败:用户没点允许、一次性授权次数不足,或者授权记录没落库。
- 模板失败:模板 ID 换了、字段映射错了、模板状态不可用。
- 发送失败:网络超时、接口限流、参数不合法、用户拒收。
我把前两类当成配置和状态问题,不做盲目重试。授权不足就引导用户重新订阅;模板变更就在配置表里统一切换,避免代码里散落旧 ID。第三类才进入重试,但也要看错误类型:网络、超时、限流可以延迟重试;用户拒收、模板不可用、参数错误就记录原因,重到天涯海角也没用。具体错误码含义以官方文档为准。
用 Redis 延迟队列补上偶发失败
后端收到支付事件后,先生成发送任务,写入 MongoDB,再投递到 Redis ZSET。score 是下次执行时间,消费者只取到期的任务。发送前检查任务状态和幂等键,避免同一笔订单提醒发两遍。失败后按退避时间重新入队,超过最大次数就转人工排查。
async function sendWithRetry(task) {
try {
await sendSubscribeMessage(task);
await markDone(task.id);
} catch (err) {
if (err.retryable && task.attempts < 5) {
const delay = Math.min(60_000 * 2 ** task.attempts, 10 * 60_000);
await redis.zadd('subscribe:retry', Date.now() + delay, JSON.stringify({ ...task, attempts: task.attempts + 1 }));
} else {
await markFailed(task.id, err.message);
}
}
}前端授权也要有回执
小程序端不要只在支付按钮里顺手调 wx.requestSubscribeMessage。我会把授权结果、模板 ID、场景值一起回传给后端,后端按用户和模板记录可用次数。用户拒绝时给出明确提示,而不是默默失败。这样排查时能回答“是没授权、没次数,还是发送时参数错了”。
“这次最大的感受是:订阅消息的稳定性不靠运气,靠链路。授权有记录、模板有配置、错误有分类、失败有重试,问题才不会变成玄学。”