返回探索
指南VibeFix 编辑部更新于 2026年10月16日

上架只是开始:vibe 项目的移动端原生推送(APNs/FCM)实战

上架只是拿到了入场券。本篇讲透原生推送的完整实战:APNs token/证书双模式、FCM HTTP v1、Expo 捷径、设备令牌管线、iOS 权限话术、四种推送形态、服务端发送、到达率与合规,附 12 项上线检查表。

智能手机屏幕上的推送通知横幅与红色角标插画

本篇讲的是原生推送,与站内三篇明确分工:vibe-project-web-push-code-guide(Web Push 代码实战)讲的是"浏览器推送:VAPID 一套走天下",本篇讲的是"原生推送:APNs + FCM 两套体系"——通道不同,合在一起才是"推送全景";vibe-project-mobile-app-store-launch-guide(移动端上架)讲的是"上架审核",本篇是"上架之后的用户召回",续集关系;vibe-project-mobile-experience-checklist(移动端体验清单)讲的是"体验质量",本篇讲的是"触达能力"。

App 上架那天,很多一人团队会开瓶香槟。但香槟开早了:上架只是拿到了入场券,用户第二天还记不记得打开你的 App,全看"上架之后"的召回能力。而移动端召回的第一通道,不是短信、不是邮件,是原生推送——免费、直达锁屏、点击率比邮件高一个数量级。

问题是,原生推送是 vibe coding 项目里最容易"看起来做完了、其实埋了雷"的一块。AI 生成的代码能帮你调通一次发送,但设备令牌的失效清洗、iOS 权限的一次性机会、sandbox 和 production 发错环境、国产 ROM 杀后台——这些坑,AI 不会主动告诉你。本篇把这套东西一次讲透:两套通道体系、令牌管线、权限话术、四种形态、服务端发送、到达率、合规,以及一份 12 项上线检查表。

一、两套体系一句话讲清:APNs vs FCM

原生推送的世界里只有两个通道,没有第三个。APNs(Apple Push Notification service)管 iOS / iPadOS / macOS / watchOS,FCM(Firebase Cloud Messaging)管 Android。你的服务端要同时对接这两套体系——除非你走捷径(后面讲 Expo)。

APNs:向 Apple 证明"你是谁",两种方式

APNs 的服务端认证有 token 模式和证书模式两种,选错等于给自己埋定时炸弹:

  • Token 模式(推荐):在 Apple Developer 后台生成一把 .p8 签名密钥,拿到 Key ID + Team ID,每次请求用 ES256 签一个 JWT。一把钥匙可以给名下多个 App 用,开发环境和生产环境通用,密钥本身永不过期(可撤销)。
  • 证书模式( legacy ):.p12 / .pem 证书,每个 App、每个环境(开发/生产)各一张,每年过期一次,每年都要重新生成、重新部署。

判断很直接:2026 年的新项目无脑选 token 模式。证书模式是历史遗留的技术债,只有接手老项目、对方 infra 还绑着证书时才需要兼容。AI 生成的老教程里还在教你导 .p12,看到直接划过。

另外两个 APNs 硬知识点:服务端地址分 api.push.apple.com(生产)和 api.sandbox.push.apple.com(开发),topic 就是你的 bundle ID;iOS 13 起请求头必须带 apns-push-type(alert / background 等),静默推送漏了这个头会直接收不到——这是后文到达率章节的高频翻车点。

FCM:HTTP v1 是唯一入口,service account 鉴权

FCM 这边简单一些:HTTP v1 API 是当前唯一的发送接口,老版 legacy HTTP API 已于 2024 年 6 月下线。鉴权用 service account(GOOGLE_APPLICATION_CREDENTIALS 环境变量指向的 JSON 密钥),不再是当年那个 server key。

截至 2026-10-16,Firebase 官方文档有一个值得注意的变化:发送目标推荐用 FID(Firebase Installation ID),registration token 被标注为 Deprecated。token 还能用,但官方话术已经转向——AI 生成的代码如果还在用 token,能跑,别慌;新项目建议按 FID 的写法走,省得一年后返工。群发用 multicast,一次最多 500 个目标,返回数组与请求顺序一一对应,方便逐个对账失败。

Expo Push:一人团队的捷径,什么时候够用

如果你用的是 Expo / React Native,还有第三条路:Expo Push Service。客户端调 getExpoPushTokenAsync() 拿一个 ExpoPushToken,服务端只调 Expo 一套 HTTP API(单次最多 100 条),Expo 替你转发给 APNs / FCM。免费,有多语言 server SDK(Node 版是 Expo 官方维护的)。

它什么时候够用?三条:Expo 系项目、日活万级以下、推送是辅助功能(交易通知+轻量召回)。什么时候不够?官方文档自己写了三条劝退:无 SLA、多一跳的延迟与故障面、以及"想要更精细控制请直连 APNs/FCM"。翻译一下:大促营销季、推送是核心召回手段、需要玩转 apns-push-type 这类精细参数的,别省这点事,直接对接双通道。

一句话总结选型:Expo Push 是"先跑起来"的最优解,直连双通道是"跑得稳"的必经之路,两者不是对立,是阶段。

二、设备令牌管线:注册 → 存储 → 失效清洗

推送系统里真正值得你写代码的,不是"发送"——发送只是两个 HTTP 调用。真正决定系统生死的,是设备令牌的管线:拿得到、存得对、死得掉。AI 生成的代码十有八九只做了第一步。

智能手机屏幕上的推送通知横幅与红色角标插画智能手机屏幕上的推送通知横幅与红色角标插画

注册:拿到令牌

iOS 端调用 registerForRemoteNotifications,系统回调给你 deviceToken;Android 端 Firebase SDK 安装时生成 token、变化时走 onNewToken 回调;Expo 一行 getExpoPushTokenAsync() 全搞定。共同点:令牌是会变的——卸载重装、系统大版本升级、恢复备份、清数据都会让它变化。

存储:每次启动上报覆盖

所以服务端表结构里,token 不能当成"注册一次、永久有效"来存。最小可用 schema:user_id, platform(ios/android), token, app_version, updated_at, enabled。铁律:App 每次冷启动都把当前 token 上报,服务端 upsert 覆盖。不做这一步,三个月后你的库里一半是僵尸令牌,送达率的分母全是脏的。

失效清洗:AI 最常漏的一段

用户卸载 App,Apple/Google 会告诉你——以错误码的形式。发送时收到这些响应,立即删除对应 token,不要重试、不要犹豫:

  • APNs:HTTP 410 Gone + reason=Unregistered。这是 Apple 的权威死亡信号。
  • FCM:HTTP 404 + errorCode=UNREGISTERED。同理删除。
  • Expo:查 push receipt,details.error=DeviceNotRegistered 就删。注意官方建议发送后约 15 分钟再查回执,回执只保留 24 小时。

这里有个能救你一命的细节:APNs 的 BadDeviceToken 不等于死令牌。生产环境的 token 发到了 sandbox 地址(或反过来),Apple 也回这个错。曾经有团队把 BadDeviceToken 当死亡信号批量删库,一次环境配错就清空了全量 iOS 设备,且不可恢复。记住:只有 410 / Unregistered 才删,BadDeviceToken 先查环境。重试策略也一样:429 / 5xx 做指数退避,4xx 一律不重试。

# 发送后处理响应的最小模式(伪代码)
resp = apns_send(device_token, payload)
if resp.status == 410 and resp.reason == "Unregistered":
    db.delete_token(device_token)          # 死令牌,删
elif resp.status == 400 and resp.reason == "BadDeviceToken":
    alert("检查 APNs 环境配置,可能是 sandbox/production 发错了")  # 先查环境,不删库
elif resp.status in (429, 500, 502, 503):
    queue.retry_later(job, backoff="exponential")  # 指数退避重试

三、权限申请话术:iOS 只有一次真正的询问机会

通道调通了,用户收不到——最大可能不是技术问题,是用户根本没给权限。而 iOS 的权限机制,对犯错毫无宽容。

iOS:拒绝之后,系统弹窗永远不会再出现

requestAuthorization 的系统弹窗,实质只有一次有效机会。用户点了"不允许",你之后再调这个 API,系统不会弹任何东西,直接返回 denied。唯一的挽回路径是引导用户去"设置 → 通知"里手动打开——转化率可想而知。

翻车实录:某工具类 App 在首屏启动第三秒直接弹系统权限框,用户还没搞明白这个 App 是干嘛的,60% 以上点了拒绝。之后所有的召回推送全废,团队花了两个版本做"设置页引导",挽回回来的不到 5%。权限申请的时机, ROI 比推送文案本身高得多。

Soft ask:先用自己的 UI 问一遍

标准做法是"预授权弹窗"(soft ask):在真正调用系统 API 之前,先弹一个你自己的解释页。用户点"好的,去开启"才调系统弹窗;点"暂不需要"就记下来,过两周在相关场景(比如他第一次下单时)再问。文案三要素,缺一不可:

  • 具体场景:"发货、降价、回复提醒"——不说"及时获取消息"这种空话;
  • 频率承诺:"重要通知才打扰,每周不超过 2 条"——给用户安全感;
  • 关闭方式:"随时可在我的 → 设置里关闭"——降低决策压力。

Android:别拿 iOS 的经验套

Android 这边规则完全不同:Android 13 以下默认就是开启的,13+ 才需要申请 POST_NOTIFICATIONS 运行时权限;Android 8+ 的通知渠道(channel)必填,且渠道的重要性级别创建后代码改不了——渠道名和描述是写给用户看的产品文案,别起 channel_1 这种名字,否则用户在系统设置里看到的是一堆乱码。另外,权限申请的说明文案要和上架审核时提交的材料口径一致,审核员会看。

四、推送四形态:标题/正文/角标/静默,用错形态等于骚扰

同样一条消息,用什么形态发,决定了它是"服务"还是"骚扰"。四种形态各有明确分工:

  • 形态一 · 展示型(标题+正文):横幅/锁屏看得见。APNs 的 alert、FCM 的 notification 字段。用户感知最强,成本也最高——每多发一条,退订风险涨一分。
  • 形态二 · 角标(badge):App 图标上的红点数字。注意 APNs 的 badge 是覆盖语义不是增量:你发 "badge": 5,图标就显示 5,不是 +5。服务端必须自己维护每个用户的未读计数,发错了就是灵异数字。用户打开 App 后记得清零,这是体验清单里的基本项。
  • 形态三 · 静默推送:不打扰用户,只唤醒 App 干活。APNs 用 content-available: 1(必须配 apns-push-type: background + 低优先级),FCM 用纯 data 消息。iOS 给约 30 秒后台执行时间,省电模式下会被节流——别拿它做实时性要求高的事。
  • 形态四 · 可交互/富媒体:带按钮("查看"/"忽略"一键操作)、带图片(iOS 的 UNNotificationAttachment、Android 的 BigPictureStyle)。点击率比纯文本高,但制作成本也高,留给最重要的营销战役。

决策表——交易通知、营销召回、数据同步,三件事用三种打法:

  • 交易通知(订单状态、支付成功、验证码):展示型,高优先级,必达。用户期待收到,这是推送 ROI 最高的一类。
  • 营销召回(新功能、限时优惠、内容更新):展示型普通优先级 + 深链直达相关页面,必须有频率上限(建议每周 ≤2 条)。标题党和"点我有惊喜"是退订加速器。
  • 数据同步(刷新列表、预拉取内容、角标校准):静默推送。用户无感,失败也无感,重试代价低。

一句话红线:把营销伪装成交易通知(比如"您的账户有一条重要消息"点进去是优惠券),是作死最快的方式——退订、投诉、下架风险三连。App Store 审核指南 4.5.4 写得明白:推送不能用于广告/促销/直销,除非用户明确同意。

五、服务端发送实战:队列、偏好、时区、重试

智能手机屏幕上的推送通知横幅与红色角标插画智能手机屏幕上的推送通知横幅与红色角标插画

链路长这样:业务事件 → 推送任务队列 → 推送网关 → APNs / FCM sender → 设备。这张图里唯一值得你认真写代码的,是中间的"推送网关"——偏好过滤、时区排期、分批发送、模板渲染、回执清洗都在这里。通道本身只是两个 HTTP 调用,包一层函数就行。

队列削峰:别在请求线程里循环发 HTTP

营销群发 10 万条,如果在接口请求线程里 for 循环调 FCM,轻则超时,重则把服务打挂。标准姿势:业务事件只往队列里扔一个 job(BullMQ / Redis / 云厂商的队列服务都行),worker 慢慢消费。队列还顺手解决三个问题:延迟发送(定时营销)、失败重试、削峰(Expo 官方都建议限制并发连接)。

偏好过滤 + 时区排期:群发前必须过的两道闸

发送前,网关要查两张表。第一张是用户偏好表:营销类、交易类、系统类三个开关,用户关了营销,你的优惠券 job 到网关就直接丢弃——这是第七节"一键退订"的技术落点。第二张是时区表:按 user.timezone 分批,"早上 9 点推送"指的是用户当地时间的 9 点,不是 UTC 9 点。翻车实录:某出海 App 全球统一 UTC 时间群发,东八区用户凌晨 3 点被"限时优惠"吵醒,当天退订率 spike 了 4 倍,应用商店评论区沦陷。

重试:只对值得重试的错误重试

重试策略三行就能写对:429 / 5xx / 网络超时 → 指数退避(1s、2s、4s……);400 系列 → 直接丢弃并记日志;410 / UNREGISTERED → 删 token(回第二章)。Expo 官方文档的 retry 章节也是这个口径。不要写"失败就重试 3 次"这种无脑逻辑,对死令牌重试 3 次只是浪费 3 次配额。

// BullMQ worker 骨架:网关只做编排,通道只是函数调用
worker("push", async (job) => {
  const { userId, template, data } = job.data;
  if (!(await prefs.allowMarketing(userId, template.kind))) return; // 偏好闸
  const slot = await schedule.byTimezone(userId);                    // 时区排期
  if (slot.isFuture) { await job.moveToDelayed(slot.at); return; }
  const tokens = await db.tokensFor(userId);
  const results = await sender.sendBatch(tokens, render(template, data)); // APNs/FCM
  await receipts.pruneDeadTokens(results);                           // 回执清洗
}, { concurrency: 20 });

六、到达率实战:为什么 Android 到了、iOS 没到

"为什么 Android 收到了 iOS 没收到"(或反过来),是推送系统里最高频的灵魂拷问。别玄学,按这张排查清单从上往下查,90% 的问题落在前三项:

  • 环境发错了:TestFlight / AdHoc 包的 token 发到了 sandbox 地址,或反过来。这是 iOS 端失联的第一大根因,对应第二章的 BadDeviceToken 陷阱。
  • apns-push-type 漏了:iOS 13+ 的静默推送没带 background 类型头,直接石沉大海,有发送日志、无到达。
  • 权限被关了:用户在系统设置里关了通知,通道返回 200(Apple 照单全收),但设备不展示。"发送成功 ≠ 用户看见",Expo 官方文档也强调:receipt 的 ok 只代表通道收到了。
  • Android 省电策略:Doze 模式下普通优先级的 FCM 消息会被延迟到维护窗口,高优先级能直达但有配额。交易通知用高优先级,营销用普通——反过来就是浪费配额。
  • 厂商 ROM 杀后台:国产 ROM 的激进杀后台是公开的秘密,保活不在本篇范围,但"应用被杀后推送延迟"是预期行为,写进 FAQ 比修好它现实。

中国大陆:FCM 覆盖不到的另一套体系

必须单独说一句:在中国大陆发行的版本,无 GMS 的设备上 FCM 基本不可用,指望一套 FCM 打天下是不现实的。现实方案是接厂商通道:小米、华为、OPPO、vivo 各有一套推送 SDK 和控制台。这是 FCM 之外的另一套证书/鉴权体系,排期和预算里要单独留位置——"上架只是开始",这句话在大陆市场尤其真实。

一人团队的送达率观测最小方案

别一上来就买第三方归因,三张表就够:发送日志表(message_id, user_id, platform, sent_at, provider_code)算通道接受率 = 2xx / 总发送;死令牌率 = Unregistered / 总发送,超过 5% 说明第二章的清洗没做好;客户端埋一个"通知实际展示"事件,算展示率。判断:送达率 100% 是伪命题,通道接受率稳定在 95%+、展示率符合产品类型,就是健康状态。能稳定观测,比数字好看重要得多。

七、合规与退订:营销和事务是两条线

推送的合规只有一条纲:营销推送和事务推送是两种东西,混在一起迟早出事。

  • 事务推送(订单、账单、安全验证):用户期待收到,是服务的一部分,默认开启天经地义。
  • 营销推送(优惠、新功能、内容召回):必须有明确的 opt-in,App 内给一键退订("营销通知"总开关),退订后 24 小时内生效并落到偏好表——第五章的网关偏好闸就是为它准备的。

App Store 审核指南 4.5.4 的三条红线,背下来:推送不能是 App 运行所必需的;不能用于广告、促销、直销(除非用户同意);不能发送敏感个人信息。Google Play 侧同理,垃圾营销推送是下架高频原因。另外两个细节:token 是设备标识符,隐私政策里要写清楚采集了什么、用来干嘛;深夜(用户当地时间 22 点后)不发营销推送——这不是法律,是不被拉黑的常识。

八、上线检查表:12 项,发版前逐项打勾

  • 1. 双端真机验证:iOS 和 Android 各至少一台真机(模拟器收不到真实推送),iOS 用正式版系统,Android 用 13+ 测一遍运行时权限。
  • 2. 双环境验证:sandbox 和 production 两套 APNs 配置分别发通,TestFlight 包走 production——环境混用是第一大翻车点。
  • 3. Token 刷新验证:卸载重装后上报覆盖,库里同一 user_id 只有一条最新 token。
  • 4. 拒绝路径走通:点"不允许"后,App 内有设置页引导入口,深链能跳到系统设置。
  • 5. 死令牌删除单测:mock 410/Unregistered 和 404/UNREGISTERED,确认删库逻辑触发;mock BadDeviceToken,确认不删、只告警。
  • 6. 静默推送真机验证:apns-push-type: background + 低优先级,后台能收到且不打扰。
  • 7. 角标清零:打开 App 后角标清零,badge 计数逻辑与服务端一致。
  • 8. 时区排期:准备两个跨时区测试账号,验证"当地时间 9 点"确实落在当地 9 点。
  • 9. 营销灰度:首次群发先 1% 用户,盯退订率和投诉,正常再全量。
  • 10. 重试策略:429/5xx 指数退避、4xx 不重试,队列里可观测到重试记录。
  • 11. 一键退订:关闭营销开关后 24 小时内不再收到营销推送,偏好表有记录。
  • 12. 文案一致性:权限申请文案、隐私政策、上架审核材料三处口径一致。

12 项全绿,再开香槟。推送是上架之后才开始的内功:先把令牌管线和权限话术做对,再谈到达率优化;Expo 够用就别折腾直连,直连之后也别忘了回执清洗。通道只是 HTTP 调用,真正的工程量永远在网关和管线里——这也是 AI 最不会主动帮你补的那部分,亲手写一遍,值。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

错误预算燃烧图:月度预算随故障消耗,标注快速燃烧与缓慢燃烧告警线
指南
慢多少算事故:vibe 项目的错误预算(SLO)实战

AI 生成的代码没有可靠性概念,一切都靠'感觉挺快'。本指南给一人团队补上 SRE 决策层:SLI/SLO/错误预算三概念一人版、两档烧蚀率告警、最小实现(一个 SQL + 一个 cron + 状态页一句话),以及'预算烧完就冻结发版'的纪律。

数据工作流部署上线后端工程
移动端上架全流程图:TestFlight 内测、商店审核(含被拒回环)与发布上线
指南
从 TestFlight 到 App Store 审核:vibe 项目的移动端上架实战

从 TestFlight 到 App Store 审核:vibe 项目移动端上架的一人团队实战指南。14 项上架前自检清单、苹果三大高频被拒理由(5.2.1、4.2、2.1)与典型翻车形状、隐私清单的第三方 SDK 连带责任、App 内账号删除铁律、TestFlight 内测 SOP、Google Play 的 12 人 14 天封闭测试与 API 36 门槛,以及被拒急救包(申诉信模板与加急审核)。

移动应用部署上线项目创作