APP消息推送体系设计:通道选型、权限时机与到达率的工程落地
写在前面:本文面向正在自建推送服务、或正在把推送逻辑从业务代码里拆出来的移动端开发者。核心立场是——推送的成败在「按下发送」之前就已经决定了:通道选对了,运营才有优化的空间;权限要错了时机,后面所有数据都是在一个缩小的分母上波动。全文给出五段损耗模型、四条通道的选型依据、两张核心表设计、通道路由与频控代码、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 | 小米开放平台账号、应用审核 | 营销类有频次与内容规范 |
| OPPO | OPPO PUSH | 开放平台账号,通常要求已上架 | 通道权限需单独申请 |
| vivo | vivo 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 未清理 | 运营一到两个月后 | 低 | 按回执自动失效 |
| 无回执与点击埋点 | 首次做数据分析时 | 中 | 一期就接回执 |
| 交易与营销共用频控 | 用户批量关闭通知后 | 高 | 分类别独立配置 |
参考资料
- 《中华人民共和国个人信息保护法》全文(国家网信办):https://www.cac.gov.cn/2021-08/20/c_1631050028355286.htm
- 《互联网弹窗信息推送服务管理规定》全文(国家网信办,2022-09-30 施行):https://www.cac.gov.cn/2022-09/08/c_1664260384702890.htm
- 三部门联合发布《互联网弹窗信息推送服务管理规定》答记者问:https://www.cac.gov.cn/2022-09/08/c_1664260384479226.htm
- Android Developers · 通知运行时权限:https://developer.android.com/develop/ui/views/notifications/notification-permission
- Android Developers · 创建与管理通知渠道:https://developer.android.com/develop/ui/views/notifications/channels
- Android Developers · 省电模式与 Doze:https://developer.android.com/training/monitoring-device-state/doze-standby
- Apple Developer · 请求通知授权:https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications
- Apple Developer · 通知的时效等级:https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel
- 华为开发者联盟 · Push Kit 文档:https://developer.huawei.com/consumer/cn/hms/huawei-pushkit/
- 小米开放平台 · 消息推送服务:https://dev.mi.com/console/appservice/push.html
更多推荐


所有评论(0)