移动端推送偏好规格:类目模型与频控规则
推送偏好看上去只是一个设置页,实际却是一个分布式系统。我见过太多团队先上了一个"消息通知"总开关,然后在接下来两年里反复排查:为什么一条安全告警会和一条营销推送一起被静掉。这篇就是我希望那些团队在第 1 天就写下的规格。
内容复查说明
复查日期:2026-05-06。本文已作为可索引的专题参考重新放出,并与 API 契约 Hub 形成内链。内容保留了可复用的评审材料、失败模式和下一步阅读路径,适合直接用于实际团队讨论。
类目模型胜过渠道模型,而且差得不是一点点
第一个真正要做的决定,是怎么给通知分组。很多团队习惯按渠道建模偏好——声音、角标、锁屏——因为系统设置页就是这么暴露的。千万别这么做。渠道是渲染层的事,类目才是同意(consent)层的事,而同意才是你的产品真正需要推理的对象。
把类目分类写成一个封闭的枚举。我一般分四类:交易类(订单已发货、收款成功)、安全类(新设备登录、2FA 验证码)、社交类(点赞、评论、@ 提及)和营销类(公告、促销、召回)。任何一条拟议的通知如果塞不进这四类,规格就会强制追问:它到底是一个新类目,还是你在把营销内容伪装成交易通知,好让更多用户看到?
设想一个图片分享 App。有人给我点赞——社交类。有人在一个陌生国家登录我的账号——安全类。一条黑五促销——营销类。我买的照片打印的订单确认——交易类。同一个用户、同一台设备、四个类目、四套相互独立的订阅状态。每个类目的默认值还不一样:安全类默认开启且极难关闭,交易类默认开启,社交类默认开启但每个子类型都能一键静音,营销类在 GDPR 适用地区必须默认关闭、显式 opt-in、留存日志、且可撤回。一个全局推送开关把这些区别全都抹平了。
系统权限的来回动作必须写进规格
在 iOS 上,对于那些可以静默投递到通知中心、不打断用户的通知,我会申请预授权(UNAuthorizationOptionProvisional)——这样我们能在硬提示之前先把价值展示出来。在 Android 13 及以上,POST_NOTIFICATIONS 是一个运行时权限,规格必须写清楚我们什么时候才申请。
我的规矩是:首次启动时绝不弹。只在通知明显能带来价值的那个时机弹,比如用户发完第一张照片之后,或是刚开启双因子认证之后。弹出时记一条 permission_prompt_shown 事件,并且永远不弹第二次。如果用户拒绝,就引导到一个 App 内的说明页,再用深链跳到系统设置,而不是反复触发原生提示。
单类目状态本身就是一个小小的状态机
按类目、按用户、按设备来看,偏好有三种状态:default、user-on、user-off。区分 default-on 和 user-on 决定了以后你改默认值时会发生什么。如果用户把它设成了 user-on,服务端切换默认值就不应该碰它;如果它还是 default-on,那新的默认值就应该应用上去。没有这个区分,每一次默认值调整都会悄悄覆盖掉用户的真实选择。
要记录状态迁移的时间戳和来源(in_app_settings、os_settings_sync、unsubscribe_link、admin_override)。等到有一天法务问你"这个用户到底有没有在 2025-11-03 同意过营销推送",你会庆幸当初留下了这份审计轨迹。
静音时段:存时区,不要存偏移
"晚上 9 点到早上 8 点"脱离时区是没有意义的,而时区又不等于一个 UTC 偏移。如果我在纽约设了静音时段,然后飞到东京,我要的是东京的 9 点还是纽约的 9 点?我的答案是:静音时段跟着设备当前的 IANA 时区走。把起止时间存成本地挂钟时间,再加一个 IANA 时区 id,每次 App 进入前台都刷新一次。
规格还必须写明"静音"到底意味着什么。我的默认规则是:营销类和社交类直接抑制;交易类延迟到窗口结束再投递;安全类完全绕过静音。每个类目都要有一条明确规则,而不是一句笼统的"遵守静音时段"。
设备级 vs 账号级:最严格的那个赢
一个用户有账号级偏好,而每台设备又有各自的系统权限。冲突规则是:"最严格的那个赢,而且用户永远能看到通知为什么没到。"如果账号层面关了社交类,但 iPhone 上权限是开的,社交类依然会被抑制;如果账号层面开了营销,但 Android 上 POST_NOTIFICATIONS 被拒了,设置页应该显示"被系统层屏蔽",而不是一个会误导人的绿色开关。
关键告警需要守门人,不该由 PM 自己决定
iOS 的 critical alerts 会绕过 DND,Android 的 time-sensitive 和 full-screen-intent 也有类似能力。规格必须写清楚谁有资格把一条通知标成"关键",而答案几乎永远不是那个功能的 PM。我会这么写:只有安全团队可以申请 critical-alert 投递,且只有白名单上的事件(suspicious_login、account_takeover_blocked、2fa_code)才够资格。其他请求在组装层直接拒绝,而不是留到评审时才发现。
投递回执会撒谎,规格应该坦白这一点
APNs 和 FCM 只能告诉你消息被它们的服务器接收了。它们没法可靠地告诉你消息真的上了锁屏。省电模式、Android 定制系统的通知节流、停留时长限制——这些都会悄悄丢弃或延迟消息。规格要写明"已投递"到底衡量的是什么(被推送服务商接收),再定义一个独立的 user_observed 事件,在用户点击或清除角标时触发。不要让 Dashboard 承诺平台根本兜不住的保证。
GDPR、TCPA 和 SMS 兜底的陷阱
向欧盟用户发营销推送需要合法依据(lawful basis),"他装了你家 App"不是合法依据。对于最后已知区域在 EEA 或英国的设备,任何营销推送之前都必须有带时间戳的显式 opt-in 记录。交易类和安全类可以走 legitimate interest 和 contractual necessity,但规格必须把这句话写出来,方便法务审阅。
最容易被忽视的是 SMS 兜底。推送失败的时候,团队本能地会想到短信。可这一步就跨进了 TCPA 的地盘,而推送的 consent 并不能覆盖 SMS。规格要么完全禁止对营销使用 SMS 兜底,要么要求一套独立的、带专属审计轨迹的 SMS opt-in。
验收标准
- Given a user in Germany with no prior marketing opt-in When the marketing service attempts to send a promo push Then the send is rejected at the compose layer with reason "no_gdpr_consent" And no delivery attempt is made to APNs or FCM - Given a user with quiet hours 21:00 to 08:00 in America/New_York When a social notification is generated at 23:30 New York time Then the notification is suppressed and logged as "quiet_hours_dropped" And a security notification in the same window delivers with critical-alert flag - Given a user who toggled marketing to user-off on device A When the server changes the marketing category default from off to on Then device A remains user-off and no delivery occurs And the preference audit log retains the original user-off transition - Given an Android 13 device where POST_NOTIFICATIONS is denied When the in-app settings screen renders Then all category toggles display a "blocked at OS level" state And tapping any toggle deep-links to the system app settings
那条推送 App 总是忘记做的退订流程
CAN-SPAM 不适用于推送,但一个没办法轻松静掉某个类目的用户,会直接在系统层把你整个 App 静掉,然后你再也找不回他。规格要求每一条营销和社交推送内部都带一个一键退订的跳转——点一下,落到一个页面确认该类目已被静音,给一个"撤回"按钮和一个跳到完整偏好设置的链接。不要登录墙,不要"舍不得你走"那种挽留摩擦。在我经手过的产品里,这一个流程对留存的贡献,比任何召回活动都大。
偏好设置要区分“用户想要”和“系统能发”
移动推送规格不能只写一个 enabled 字段。用户偏好、系统权限、设备 token、安静时段和服务端抑制规则是五件事。混在一起,support 很难解释“为什么我关了还收到”或“为什么我开了却没收到”。
Preference fields: - user_category_opt_in: marketing=false, security=true - os_permission: granted | denied | provisional - device_token_status: active | expired | revoked - quiet_hours: 22:00-08:00, timezone=Asia/Shanghai - suppression_reason: bounced | abuse_hold | legal_hold - evidence: notification_attempt log includes all five values
边界:安全通知不要和营销通知走同一套关闭逻辑。密码重置、登录告警和付款失败可能必须绕过普通偏好,但要在 spec 里写明。
测试证据要覆盖设备状态
偏好测试不要只调 API。要造三种设备状态:token active、token expired、OS permission denied。然后验证服务端 suppression_reason、通知尝试日志、客户端显示状态和用户偏好字段是否一致。这样 support 才能按证据解释一次未送达。
偏好字段还要进入 API 文档和客服后台。用户问为什么没收到推送时,客服应该看到 category、device status、suppression_reason、provider response 和最后一次测试证据,而不是让工程去查日志。
专题阅读路径
继续阅读
编辑说明与免责声明
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面