我把 App Store Connect 搬进了 Mac 菜单栏(零后端)

30 阅读8分钟

摘要

App 上架后,独立开发者的一天是什么样的?刷 App Store Connect、刷 App Store Connect、还是刷 App Store Connect。 销量、评论、审核状态,三个答案都藏在一个网页后面,一天重复几十次。 我花了大半年做了一个常驻 Mac 菜单栏的工具,把这三种焦虑全部变成通知: 每日早报、差评提醒、审核状态追踪——并且没有一行后端代码。

本文记录这个产品的完整思路:痛点、架构、几个值得聊的技术细节(ASC API 接入、限流、幂等通知、多端数据共享),以及踩过的坑。


一、痛点:三个问题,一个网页,无数次点击

对独立开发者来说,App 上架之后最关心三件事:

  1. 今天卖了多少?(下载、收入)
  2. 有没有差评?(要不要赶紧去回复 / 修 bug)
  3. 审核过了没?(提审之后什么时候能上)

这三个答案都在 App Store Connect(ASC)网页里。看一次的成本是:开浏览器 → 登录 → 翻菜单 → 找数据。 重度用户一天重复几十次。更致命的是坏消息发现得晚:

  • 差评晚回复一天,就发酵一天;
  • 审核被拒晚发现一天,就晚上线一天。

苹果官方有 ASC App,但它是 iOS App——没有 Mac 菜单栏常驻形态,没有差评定向警报,审核状态通知也不可靠、没有历史记录。这就是我做这个工具的出发点。

二、产品形态:早报 + 警报 + 驾驶舱

CoderPulse 是一个常驻 Mac 菜单栏的小工具(免费 + Pro 订阅),核心是四件事:

  • 🌅 每日早报:每天早上一条本地通知,汇总昨日下载、收入、新评论,10 秒了解大盘;
  • ⚠️ 差评提醒:新评论按你设置的周期自动检查,可以只关注 ≤3 星;
  • 🔍 审核状态追踪:每次状态变化(In Review / Rejected / Ready…)都有通知,并保留完整历史时间线;
  • 📊 菜单栏驾驶舱:点图标 5 秒扫完昨日概览、30 天趋势、最新评论流、各 App 版本状态。

dashboard-en-light-2880x1800.png

iPhone / Apple Watch 提供只读伴侣体验,桌面和锁屏还有小组件:

iOS_cn_1.png

三、架构决策:为什么「无后端」是认真设计过的

整个产品最重要的一条架构铁律是:无自建服务器。

┌─ Mac App (SwiftUI, macOS 14+) ─────────────────────┐
│  菜单栏面板(NSPanel) ─ Settings ─ Onboarding ─ Paywall │
│        │                                          │
│  AlertEngine(事件比对 / 通知决策)                    │
│        │                                          │
│  SyncScheduler(轮询调度)                           │
│        │                                          │
│  ASCKit(API 客户端:JWT / 端点 / TSV 解析)           │
│        │                                          │
│  Storage: SwiftData(业务数据)+ Keychain(API Key)  │
└────────┴──────────────────────────────────────────┘

理由有三层:

  1. 隐私即卖点。App 业务数据直接用用户自己的 ASC API Key 直连苹果官方 API, 开发者(我)的服务器根本不存在,谈不上泄露。App Store 隐私标签可以直接声明「不收集任何数据」。
  2. 零运维。没有服务器 = 没有域名、没有备案、没有半夜告警、没有账单。 这对个人开发者是巨大的成本优势。
  3. 上架资格。对大陆区上架而言,「无自有联网功能」的声明是 App 能上架中国大陆的依据之一 (本 App 未走 ICP / App 备案,是与苹果沟通评估、由作者提交声明承担相应责任后获准)。 任何引入自建服务端的改动都会动摇这项资格——这条约束反过来锁死了架构方向。

代价也很明确:Mac 关机、合盖睡眠时没有数据被拉取,通知会延迟到唤醒后补发。 这是「无后端」方案必须接受的取舍,我在文档里如实记录,不吹成「全天候实时推送」。

四、关键技术细节(干货)

4.1 ASC API 接入:JWT ES256 手写,约 40 行

App Store Connect API 用 JWT 认证,需要三要素:Issuer ID、Key ID、.p8 私钥文件。 JWT 的签名算法是 ES256,标准实现:

  • header:{"alg":"ES256","kid":"<Key ID>"}
  • payload:{"iss":"<Issuer ID>","aud":"appstoreconnect-v1","exp":<≤20min>}

用 CryptoKit 的 P256.Signing 实现,约 40 行,不引第三方 JWT 库。 .p8 私钥内容只进 Keychain(kSecAttrAccessibleAfterFirstUnlock),内存中用后即焚,绝不落盘。

4.2 限流与容错:读响应头自适应降频

ASC API 的配额是 3600 次/小时/Key(另有约 300 次/分钟的未公开限制)。 我们的用量(每 15 分钟 ≤ 2+N 个请求,N = App 数)余量巨大,但实现仍然:

  • 读取 x-rate-limit 响应头,余量 <10% 时自动降频;
  • 指数退避重试(1s / 4s / 16s,只对 5xx 与网络错误);
  • 429 直接等下一个周期。

所有请求串行经过一个 actor,避免并发请求打爆限流。

4.3 数据模型:VersionEvent 只追加,白捡一个「审核历史」

核心是 5 个 SwiftData 实体:Account、App、DailySales、Review、VersionEvent。

值得一提的设计是 VersionEvent 只追加、不更新:每次轮询发现审核状态变化就插入一条新记录, 于是「版本审核历史时间线」这个功能是免费的——不需要任何额外的历史表。

另外两个细节:

  • 审核状态字段做过迁移(appStoreState → appVersionState,2025 年起),解码用双字段兼容;
  • ASC 枚举用 RawRepresentable struct 而不是 enum,未知新值不会导致解码失败(前向兼容)。

4.4 通知决策:diff + 幂等,误报为零

AlertEngine 是产品的灵魂,逻辑要求最严谨:

  • 输入新一轮快照,与本地上一状态做 diff:
    • 新评论 = reviewId 不在库中(按设置过滤:仅 ≤3 星 / 全部);
    • 审核状态变化 = (appId, version) 的状态与库中不同 → 必通知(质量红线:漏报为零);
    • 早报 = 昨日销量报表首次解析成功时触发。
  • 防误报:首次接入只入库不通知(避免开机轰炸);同一事件幂等(事件表记录已通知指纹)。

通知用 UNUserNotificationCenter(本机通知,不是推送),附深链:点击直接打开面板定位到对应 App, 或跳转 ASC 网页对应页。

4.5 多端:单一多平台 target + 菜单栏自绘面板

  • 单一多平台 target:macOS / iOS / visionOS 共享 SwiftData 模型和仪表盘视图, 平台差异用 #if os(macOS) 分流。iOS 伴侣不用新建工程。
  • 菜单栏不用 MenuBarExtra:MenuBarExtra 的 popover 在内容变宽时会整体重绘并重定位, 二级页(设置)展开时一、二级页一起闪烁。改为自定义透明无边框 NSPanel (NSStatusItem + AppDelegate),尺寸由 SwiftUI 内容单一驱动,右上角锚定、向左生长。
  • iOS / Watch 只读伴侣:iCloud Keychain 同步凭据(kSecAttrSynchronizable), Watch 通过 WatchConnectivity 收 iPhone 摘要。
  • 小组件:Widget 是独立进程,通过 App Group 的 UserDefaults(suiteName:) 读 KB 级快照 (昨日下载 / 7 日 / 收入 / 最新评论 / 审核状态),WidgetCenter.reloadAllTimelines() 刷新。 小组件渲染直接用产品源码里的 AppPulseWidget.swift 原样编译,保证宣传图 ≠ 实物。

widget-zh-light-2880x1800.png

4.6 质量红线

  • 审核状态变化 100% 捕获(该事件不可被任何规则过滤);
  • 误报为零(事件指纹幂等);
  • 资源占用:CPU < 1%、内存 < 80MB(常驻菜单栏 App 的基本素养)。

五、踩过的坑(真实复盘)

  1. 审核状态字段迁移:appStoreState → appVersionState,2025 年新字段上线后旧字段不可靠。 解法是双字段兼容读取,并订阅 ASC API changelog。
  2. 多平台版本资源混在一起:单一多平台 target 下,一个 App 的各平台版本资源在同一个接口里混着返回, limit=2(纯 macOS 时期的取值)会漏掉平台导致状态卡在旧值——limit 必须覆盖全平台当前版本。
  3. 销量日报「当日不可得」:苹果销量日报次日 ~8am PT 才生成(全行业同此约束), 所以产品定位是「次日早报」而不是实时销量。每天 9:00 尝试拉昨日报表,404 则 30 分钟后重试至多 6 次。
  4. 菜单栏面板性能:柱状图悬停曾出现过明显卡顿,根因是 Swift Charts 的响应式重绘—— 修法涉及惰性截断与重绘范围收窄,这属于常驻菜单栏 App 特有的性能约束(详见项目复盘文档)。
  5. 「买断」改「年订阅」:上线后把一次性买断改成按年订阅,老买断用户的 entitlement 双轨归并、 永久保留,App 内价格一律读 Product.displayPrice、不留硬编码——价格只在 ASC 配置。

六、商业化:免费 + Pro

  • 免费版:接入 1 个 App + 内置 Demo 模式(没有 API Key 也能把完整界面逛一遍,审核员也能体验)。
  • Pro(按年订阅):无限 App、自动监控与全部通知;价格以 App 内为准(美国基准 $11.99/年,中国区 ¥12/年), 随时可在 App Store 账户设置中取消;老买断用户权益永久保留。

connect-zh-light-2880x1800.png

七、链接

结尾

做完这个产品我最大的体会是:独立开发者最缺的不是「看数据」,而是「别被坏消息打个措手不及」。 把网页里的焦虑变成一条早上 9 点的通知,是我觉得最值得的一笔投入。

如果你也是独立开发者,欢迎下载试试——免费版就够用一阵子,Demo 模式不需要任何 Key。 有任何问题、建议,或者想讨论无后端架构的取舍,评论区见,我会认真回复每一条。

本文为产品作者的实践分享,非广告软文模板——所有技术细节均为真实实现(单测 119 项全绿, ASCKit / AlertCore 两包纯逻辑全单测,CI 双平台编译)。