写在前面:本文面向正在自建推送服务、或正在把推送逻辑从业务代码里拆出来的移动端开发者。核心立场是——推送的成败在「按下发送」之前就已经决定了:通道选对了,运营才有优化的空间;权限要错了时机,后面所有数据都是在一个缩小的分母上波动。全文给出五段损耗模型、四条通道的选型依据、两张核心表设计、通道路由与频控代码、Android 13 权限实现,以及合规要求的落地字段,法规引用均标注条文来源。

写在前面:核心立场与读者对象

本文适合的读者:

  • 正在设计或重构推送服务的客户端与服务端开发者
  • 需要评估「自建推送 vs 接第三方推送平台」的技术负责人
  • 做多端产品(iOS + 国内安卓 + 鸿蒙)的架构人员
  • 需要处理推送合规、频次告知、个性化开关的产品与研发

先纠正的三个认知:

常见认知实际情况影响
推送就是把消息发给推送SDK消息要经过网关、通道、系统、权限、展示五段,每段都有损耗排查方向错,只看文案
到达率低是通道的问题一半是token失效与权限未授予,通道只占一部分换服务商解决不了
频次越高,总点击越高超过用户容忍阈值后,通知被关闭,整体池子缩小短期数据涨,长期触达崩

目录

一、推送链路的五段损耗

把推送拆成链路看,比拆成「功能」看更有用。一条消息从服务端到用户锁屏,经过五段,每段的失败原因完全不同。

段位经过的环节典型损耗原因责任方
第 1 段业务服务 → 推送网关批量发送触发限流、任务队列积压、幂等缺失导致重复发后端
第 2 段网关 → 通道通道选错(该走厂商通道却走长连接)、SDK 未初始化后端 + 客户端
第 3 段通道 → 设备token 失效、厂商配额耗尽、消息体超长或含敏感字段被拦后端
第 4 段设备 → 用户可见通知权限未授予、通知类别被单独关闭、省电策略延迟、iOS 定时摘要客户端 + 产品
第 5 段展示 → 点击标题无信息量、落地页错位、发送时段不合适运营

三个指标必须分开定义,否则团队会拿一个数字讨论三件事:

指标定义计算方式健康区间参考
提交成功率网关提交给通道成功的比例提交成功数 / 应发数应接近 100%
到达率通道回执确认送达设备的比例到达回执数 / 提交数厂商通道显著高于长连接
打开率点击通知的比例点击数 / 到达数与文案、时段强相关

关键推论:打开率的分母是到达数。用「点击 / 发送」算,会把通道和权限的问题一并记到文案头上。

二、通道选型:四条路各自的天花板

2.1 四类通道的对比

通道类型送达机制优势天花板适用场景
系统级厂商通道由系统进程代收并展示应用被清理后仍可送达,功耗低需逐家接入,有配额与内容规范国内安卓的主力通道
自建长连接客户端维持 WebSocket/MQTT延迟最低,格式与优先级完全自主应用退后台被清理即断,耗电前台实时同步、IM 在线消息
APNs苹果统一系统通道一次接入覆盖全部 iOS 设备受权限、定时摘要、专注模式影响iOS 唯一选择
短信运营商网络不依赖任何应用权限按条计费,内容极短,无富媒体关键节点的兜底

2.2 国内安卓的厂商通道清单

国内安卓的特殊性在于:FCM 在大多数设备上不可用,而系统允许厂商深度定制,于是形成「一家一套」的格局。

厂商通道接入前置常见限制
华为HMS Push华为开发者账号、应用信息审核部分消息类型需申请权益
小米MiPush小米开放平台账号、应用审核营销类有频次与内容规范
OPPOOPPO PUSH开放平台账号,通常要求已上架通道权限需单独申请
vivovivo Push开放平台账号、应用审核分级管理,营销类受限
荣耀Honor Push独立开发者账号与华为体系已分离,需单独接
魅族Flyme Push开放平台账号覆盖量小,按需接

工程上的判断是:**先接覆盖你用户八成的头部两三家,其余用长连接与短信兜底,量起来再补齐。**一次性全接会把上线时间往后推一个月。

2.3 通道抽象层

无论接几家,服务端都不该让 if vendor == "huawei" 散落在业务代码里。分三层:策略层决定「发不发、发给谁、何时发」;适配层按平台、厂商、前后台状态、历史回执选通道;SDK 层只做字段映射。某家配额收紧时,只改适配层配置。

三、服务端:两张表与一个路由层

3.1 表结构

-- 设备表:一台设备一条;token 会变,所以必须独立于用户表
CREATE TABLE `push_device` (
  `id`          BIGINT UNSIGNED AUTO_INCREMENT,
  `uid`         BIGINT UNSIGNED NOT NULL,
  `platform`    VARCHAR(8)   NOT NULL COMMENT 'ios/android',
  `vendor`      VARCHAR(16)  NOT NULL COMMENT 'apns/huawei/xiaomi/oppo/vivo/honor',
  `token`       VARCHAR(256) NOT NULL,
  `app_version` VARCHAR(16)  DEFAULT NULL,
  `enabled`     TINYINT      NOT NULL DEFAULT 1 COMMENT '用户是否开启通知',
  `updated_at`  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_vendor_token` (`vendor`, `token`),
  KEY `idx_uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 任务表:一次发送一条,用于回执归因与频控统计
CREATE TABLE `push_task` (
  `id`       BIGINT UNSIGNED AUTO_INCREMENT,
  `msg_id`   CHAR(32)        NOT NULL COMMENT '业务幂等键',
  `uid`      BIGINT UNSIGNED NOT NULL,
  `category` VARCHAR(16)     NOT NULL COMMENT 'trade/marketing/system',
  `title`    VARCHAR(64)     NOT NULL,
  `sent_at`  DATETIME        NOT NULL,
  `status`   TINYINT         NOT NULL DEFAULT 0 COMMENT '0待发 1已提交 2已到达 3失败',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_msg_uid` (`msg_id`, `uid`),
  KEY `idx_uid_time` (`uid`, `sent_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

uk_vendor_token 防止同一 token 重复注册:重装应用会拿到新 token,旧 token 若不清理会持续写入并失败。

3.2 路由与频控

# router.py —— 通道路由 + 频控;业务层只提交 (uid, category, payload)
from datetime import datetime

DAILY_CAP = {"trade": 50, "system": 20, "marketing": 3}
QUIET_HOURS = (22, 8)   # 营销类夜间静默

def pick_channel(device: dict) -> str:
    """厂商通道优先,无匹配则降级为长连接"""
    if device["platform"] == "ios":
        return "apns"
    return device["vendor"] if device["vendor"] in VENDOR_SDKS else "longlink"

def allow_send(uid: int, category: str, now: datetime):
    if category == "marketing" and (now.hour >= QUIET_HOURS[0] or now.hour < QUIET_HOURS[1]):
        return False, "quiet_hours"
    key = f"push:{uid}:{category}:{now:%Y%m%d}"
    if redis.incr(key) > DAILY_CAP[category]:
        return False, "daily_cap"
    redis.expire(key, 86400)
    return True, "ok"

def dispatch(uid: int, category: str, payload: dict) -> dict:
    ok, reason = allow_send(uid, category, datetime.now())
    if not ok:
        return {"accepted": False, "reason": reason}
    devices = db.query("SELECT * FROM push_device WHERE uid=%s AND enabled=1", (uid,))
    for dev in devices:
        # 统一消息模型,各厂商适配器只负责字段映射
        VENDOR_SDKS[pick_channel(dev)].submit(dev["token"], payload)
    return {"accepted": True, "device_count": len(devices)}

两个细节:频控走 Redis 计数而非查库,发送路径上不允许范围查询;静默判断放服务端,客户端时区不可信。

四、客户端:权限申请的实现与时机

4.1 Android 13 及以上的实现

// 通知权限:在关键动作完成后申请,而不是冷启动第一屏
private val notifPerm = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted -> prefs.edit().putBoolean("notif_asked", true).apply() }

fun askNotificationPermission() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
    val granted = ContextCompat.checkSelfPermission(
        this, Manifest.permission.POST_NOTIFICATIONS
    ) == PackageManager.PERMISSION_GRANTED
    if (!granted) notifPerm.launch(Manifest.permission.POST_NOTIFICATIONS)
}

4.2 两个版本行为差异

targetSdkVersion弹窗触发时机用户点「不允许」之后
≤ 32(12L 及以下)创建首个通知渠道或启动 Activity 时自动弹不再提示,直到卸载重装或升级 targetSdk
≥ 33完全由应用代码控制可再次申请,但系统弹窗次数受系统策略约束

这条差异直接决定授权率:targetSdk ≥ 33,才能把弹窗挪到用户完成关键动作之后。

4.3 权限时机的三档选择

时机用户心理授权率方向适用产品
冷启动第一屏「还没开始用」最低通知即核心功能(如闹钟、待办)
注册登录完成后「刚有一点投入」中等通用型产品
首个关键动作完成后「刚拿到结果」最高电商、票务、工具、内容

关键动作因产品而异:支付成功、出票完成、首次生成结果、首次收藏。共同特征是「刚获得价值」。

另一个低成本动作:调系统弹窗之前先展示一屏自建说明,讲清推送内容与频率,用户确认后再申请。这不改变系统逻辑,但能抬高一截授权率。

4.4 iOS 的两个隐性开关

iOS 的通道统一,问题出在展示环节。除通知权限外,还有两处会影响到达可见性:定时摘要会把非紧急通知攒成一批定时投递;专注模式会静音指定类别。时效性强的消息应申请时效等级;营销类则老实接受被摘要,别为绕过摘要滥用高优先级。

五、频控:把频次当预算管

5.1 频控的四个维度

维度作用实现方式典型阈值
单用户单日上限防止单点刷屏Redis 计数,按自然日过期按类别分别设置
单用户单周上限控制长期打扰周维度计数营销类更低
夜间静默避免休息时段打扰服务端时间判断22:00–08:00
同类合并减少并排通知时间窗内聚合10 分钟窗口

5.2 消息分级

类别典型内容是否可关频控强度通道选择
交易类订单、支付、物流、票务不可关(业务必需)宽松厂商通道 + 短信兜底
系统类安全提醒、账号异常不可关中等厂商通道
营销类促销、活动、召回必须可关严格厂商通道,受配额限制

三类必须分开频控,否则会出现「营销发多了,用户关掉全部通知,交易通知一起收不到」。

六、合规:三类要求与三个字段

依据核心要求落地动作
《个人信息保护法》第二十四条第二款通过自动化决策方式进行信息推送、商业营销,应同时提供不针对个人特征的选项,或提供便捷的拒绝方式设置页增加「个性化推荐」开关,关闭后仍可收到非个性化推送
《互联网弹窗信息推送服务管理规定》(2022-09-30 施行)第五条以服务协议等明确告知推送形式、内容频次、取消渠道;科学规划频次;不得对普通用户与会员用户不合理差别推送;不得干扰或影响用户关闭隐私政策与推送设置页写明频率与关闭路径;频次策略可配置
同上,第五条关于广告推送广告信息应具有可识别性,显著标明「广告」和关闭标志,确保一键关闭消息模型增加 is_ad 字段,客户端固定渲染广告标识与关闭按钮

落到数据结构上是三个字段:用户维度的 personalized_enabled、消息维度的 is_ad、配置维度的 frequency_policy。

七、监控:四个必须落库的指标

指标采集方式用途异常信号
提交成功率网关返回码发现限流与鉴权问题某通道骤降
到达回执率厂商回执回调评估通道质量与 token 健康度单厂商低于基线
token 失效率回执中的无效 token 比例触发清理任务持续上升说明清理缺失
分渠道打开率客户端点击埋点判断文案与时段某通道点击异常低

没有回执,只能知道「发了多少」,无法回答「到了多少」,优化就退化为猜测。

八、返工点清单

返工点出现阶段修复成本预防措施
只做长连接,未接厂商通道上线首月高立项即按厂商通道为主设计
权限弹窗放在冷启动第一屏上线首周低挪到关键动作之后
推送调用散落在业务代码加频控需求时中收口到统一推送服务
无效 token 未清理运营一到两个月后低按回执自动失效
无回执与点击埋点首次做数据分析时中一期就接回执
交易与营销共用频控用户批量关闭通知后高分类别独立配置

参考资料

  1. 《中华人民共和国个人信息保护法》全文(国家网信办):https://www.cac.gov.cn/2021-08/20/c_1631050028355286.htm
  2. 《互联网弹窗信息推送服务管理规定》全文(国家网信办,2022-09-30 施行):https://www.cac.gov.cn/2022-09/08/c_1664260384702890.htm
  3. 三部门联合发布《互联网弹窗信息推送服务管理规定》答记者问:https://www.cac.gov.cn/2022-09/08/c_1664260384479226.htm
  4. Android Developers · 通知运行时权限:https://developer.android.com/develop/ui/views/notifications/notification-permission
  5. Android Developers · 创建与管理通知渠道:https://developer.android.com/develop/ui/views/notifications/channels
  6. Android Developers · 省电模式与 Doze:https://developer.android.com/training/monitoring-device-state/doze-standby
  7. Apple Developer · 请求通知授权:https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications
  8. Apple Developer · 通知的时效等级:https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel
  9. 华为开发者联盟 · Push Kit 文档:https://developer.huawei.com/consumer/cn/hms/huawei-pushkit/
  10. 小米开放平台 · 消息推送服务:https://dev.mi.com/console/appservice/push.html
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐