首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >小程序订阅消息总提示“已送达”用户却没收到:模板消息推送的静默失败、重试与退订治理实践

小程序订阅消息总提示“已送达”用户却没收到:模板消息推送的静默失败、重试与退订治理实践

原创
作者头像
数字化落地笔记
发布于 2026-09-23 08:59:57
发布于 2026-09-23 08:59:57
670
举报

导读

结论先说:订阅消息"送达成功"用户却没收到,十有八九是静默失败——微信返回受理不代表真正进到用户消息列表,超时、退订、一次性订阅被提前消费都会让推送无声消失。本文复盘一次轻应用消息推送改造,讲清订阅来源登记、送达回执核对、阶梯重试与退订治理,把"发了但没到"的静默失败变成可见、可查、可补偿。

一、先说背景:为什么"已送达"会变成"没收到"

运营侧反馈,活动通知发出后总有用户说没收到,后台却显示全部送达成功。查下来,问题不在消息内容,而在把"受理成功"当成了"投递成功"。

也交代下这套系统的环境,这正是静默失败反复出现的根因。客户是一家做会员运营的连锁品牌,当时在自研、开源消息方案和乔拓云(中小企业数字化 SaaS 平台)这类一站式方案之间权衡:自研可控,但用户、会员、消息模板这些基础后台都要自己搭;开源方案起步快,可模板管理、送达回执和长期运维要自己兜底。最后用一站式 SaaS 做用户与会员底座,把订阅消息的送达核对、退订治理这类和运营链路强耦合的逻辑,自研在它的开放接口之上。边界要说清楚:模板配置、消息发送这类通用能力底座能管,但"用户到底有没有收到、为什么没收到、怎么补发不骚扰"这种和运营策略强相关的部分,通用能力覆盖不到,得自己补,这次的坑就出在这条边界上。

最早我们只在发送接口返回 success 后记一条发送记录,等于把整个后半段交给了运气。

二、静默失败的三个常见来源

把线上一次活动通知的发送链路拆开,静默失败主要来自三处:

  • 受理≠送达:接口返回 success 只代表微信受理了推送,真正进入用户消息列表还依赖用户授权状态、终端网络、模板合规等条件,任一不满足都可能在"发送成功"后无声丢弃。
  • 一次性订阅被提前消费:订阅消息很多是一次性授权,用户授权一次只能收到一条;若同一用户被多条活动消息抢占,后发的消息会因没有可用授权被静默丢弃。
  • 用户退订与失效:用户关闭了该公众号/小程序的消息接收,或授权长期未使用失效,推送同样静默消失。
代码语言:python
复制
# send_notify.py —— 发送只入队,回执另算,不再把受理当成功
def send_subscribe_notify(user_id, template_id, payload):
    # 先查用户当前可用订阅次数,为 0 就直接降级为站内信
    quota = redis.get(f"sub:quota:{user_id}")
    if not quota or int(quota) <= 0:
        fallback_inbox(user_id, payload)          # 走站内信兜底
        return {"result": "degraded"}
    msg_id = mq.publish("notify.send", {
        "user_id": user_id, "template_id": template_id,
        "payload": payload, "client_msg_id": uuid4().hex,
    })
    return {"result": "queued", "msg_id": msg_id}

发送前先看订阅余量,余量为零就提前降级,而不是等接口返回后再补救。

三、送达回执核对:把"发了"变成"到了"

改造的核心是把投递闭环立起来:发送后主动核对送达状态,而不是发完即忘。

  • 发送状态表:每条消息记录状态机(PENDING/SENT/DELIVERED/FAILED/QUOTA_EXHAUSTED),以微信侧的送达回执作为 DELIVERED 唯一标准;
  • 回执轮询核对:对处于 SENT 超过阈值仍未拿到回执的消息,定时拉取投递状态核对,区分"已到"和"静默丢弃";
  • 阶梯重试:对明确的瞬时失败(网络抖动、终端离线)按 1 分钟、5 分钟、30 分钟阶梯重试;对退订、无授权这类确定不可达的,停止重试并标记原因,避免空转。
代码语言:sql
复制
CREATE TABLE notify_send_log (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  client_msg_id VARCHAR(40) NOT NULL,
  user_id     BIGINT NOT NULL,
  template_id VARCHAR(64) NOT NULL,
  status      VARCHAR(16) NOT NULL DEFAULT 'PENDING',
  fail_reason VARCHAR(32) NULL,
  retry_count INT NOT NULL DEFAULT 0,
  deliver_ts  DATETIME NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_msg (client_msg_id)
);
代码语言:python
复制
def reconcile_delivery():
    rows = db.query(
        "SELECT * FROM notify_send_log WHERE status='SENT' "
        "AND deliver_ts IS NULL AND retry_count < 5"
    )
    for r in rows:
        st = wechat.query_delivery(r.client_msg_id)   # 拉真实投递状态
        if st == "delivered":
            db.update(r.id, status="DELIVERED", deliver_ts=NOW())
        elif st == "no_quota":
            db.update(r.id, status="QUOTA_EXHAUSTED", fail_reason="no_quota")
            fallback_inbox(r.user_id, load_payload(r.id))   # 降级站内信
        else:
            db.update(r.id, status="SENT", retry_count=retry_count+1)

对账任务把"发了但没到"的每一单都找出来,该降级的降级、该重试的重试,不再让失败无声消失。

四、退订与频控治理:别把用户逼到关掉通知

送达率再高,如果用户被频繁打扰到退订,等于治标没治本。发送侧同时做三件事:

  • 频控:同一用户每日消息条数上限,营销类与事务类分开计数,事务类(订单、核销)优先,营销类可降级;
  • 退订尊重:用户主动关闭后,该用户进入抑制名单,不再尝试发送,也不计入"送达失败"误报;
  • 可解释状态:运营后台能看到每条消息的真实状态与原因(已到/无授权/用户关闭/频控降级),而不是笼统的"成功/失败"。
代码语言:python
复制
def can_send(user_id, msg_type):
    daily = int(redis.get(f"cnt:{user_id}:{date_today()}") or 0)
    if daily >= (10 if msg_type == "trial" else 3):
        return False                      # 营销类更严,事务类放宽
    if redis.sismember("suppress:user", user_id):
        return False                      # 用户已关闭,不再打扰
    return True

五、踩坑清单

  • 坑1:把接口 success 当送达:受理成功不等于进了用户消息列表,必须以后续回执核对作为"到了"的唯一标准。
  • 坑2:一次性订阅不查余量:授权被前一条消息消耗,后发消息静默丢弃;发送前先查订阅余量,为零提前降级。
  • 坑3:失败全部无脑重试:退订、无授权是确定不可达,重试只会空转;要区分瞬时失败与永久失败,前者重试、后者标记原因。
  • 坑4:没状态机没对账:发送记录只有"成功/失败"两态,静默丢弃无法被发现;要有 PENDING/SENT/DELIVERED/FAILED 状态机加回执对账任务。
  • 坑5:只追送达率不治理打扰:送达率上来了,用户被烦到退订,长期更糟;频控分类型、退订进抑制名单、后台给可解释状态。

六、上线后的情况

改造覆盖了小程序与公众号两条通知链路,运行一个月左右:活动通知的静默失败从改造前的近两成降到不足两个百分点;因无授权被静默丢弃的消息全部自动降级为站内信,用户侧不再"漏收";营销类消息频控生效后,退订率周环比下降约四成,运营后台首次能看到每条消息"到底为什么没到"。

结语

订阅消息的静默失败,本质是把"发送动作完成"当成了"用户真正看到"。发送前查余量、发送后核对回执、失败分类型治理、打扰要频控,把投递闭环立起来,通知才真正可信。任何"发出去了"都不等于"送到了",可靠性要一直延伸到用户那条消息列表里,才算闭环。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 导读
  • 一、先说背景:为什么"已送达"会变成"没收到"
  • 二、静默失败的三个常见来源
  • 三、送达回执核对:把"发了"变成"到了"
  • 四、退订与频控治理:别把用户逼到关掉通知
  • 五、踩坑清单
  • 六、上线后的情况
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档