被 App Store 打回的那几次,和我改的东西
ITMS-90683、bundle id 撞车、恢复购买、外链风险、导出合规——逐条贴报错码和 Info.plist 键名,附一份可复制的提审前 checklist。
ITMS-90683。
看到这串东西的时候,我以为我完了。
那时候我已经把包传上去了,正准备去洗澡,邮件弹出来,标题是「App Store Connect: Your app has one or more issues」。我站在原地把邮件读了三遍。
后来才知道这玩意儿其实很小。但当时不知道,那种「我是不是做错了什么根本性的事」的感觉,我想很多第一次提审的人都懂。
这篇把我踩到的坑逐条写下来,贴真实报错码和真实键名。写得越具体越有用,所以你会看到一堆代码片段,忍一下。
先声明一件事
我这篇里的「被打回」,严格说分两种:
- 自动校验打回:包传上去,苹果的机器扫一遍,不合规直接退回来。根本走不到人。ITMS-9xxxx 系列都是这类。
- 人工审核意见:走到审核员那里,被挑出问题。
区分这个很重要——自动校验的打回,秒级到分钟级就来了,改完重传就行,不占审核队列。看到 ITMS 别慌,那不是「被拒」。
我这次的首审实际用时:【待填:首审实际耗时】。
一、ITMS-90683:缺 NSLocationWhenInUseUsageDescription
现象:包上传后自动校验失败,邮件退回。
报错原文(大意):ITMS-90683: Missing purpose string in Info.plist —— 你的 App 引用了 NSLocationWhenInUseUsageDescription 相关的能力,但 Info.plist 里没有对应的用途描述字符串。
我的第一反应:不可能。我这 App 是管冰箱的,我根本没写过一行定位代码。
真相:我确实没写。但我用的相册/文件选择组件,链进来的系统接口里带了定位能力。苹果的自动校验只看「你的包里链进了什么」,不看「你的 Dart 代码有没有调」。
我改了什么:在 ios/Runner/Info.plist 里补上,并且如实说明我不用它:
<key>NSLocationWhenInUseUsageDescription</key>
<string>本 App 不使用定位功能。此项仅因所用的相册/文件选择组件依赖系统接口而被要求声明,我们不会获取或上传你的位置信息。</string>
然后版本号 bump 到 1.0.0+3,重新构建上传。过了。
教训:
凡是链接进包的能力都要有用途描述,哪怕你的代码从没主动调用过。
第三方 SDK 带进来的权限也算。你以为自己没用,链接器不这么认为。
顺带一句:用途描述别写敷衍话。我这条把「为什么明明不用还要写」解释清楚了,人工审核那关也少一次问询。同一个文件里我还有:
<key>NSPhotoLibraryUsageDescription</key>
<string>用于从相册选择购物小票或食材照片,自动识别并入库到冰箱</string>
具体到哪一句话对应哪个功能,比「用于提供更好的服务」强一百倍。
二、bundle id 撞车,我改了两次包名
这条不是被打回,是我自己给自己挖的坑,但坑得很典型。
现象:在开发者后台注册 App ID,填 com.chihaofan.app,提示不可用。
原因:这串被我此前另一个免费账号在 Apple 侧占用了。我换成 com.chihaofan.ice——还是不可用。
这里是关键认知:App ID 在 Apple 全局命名空间唯一,跨所有开发者账号。不是「你的账号里没有就能用」,是「全世界没人用过才能用」。
我改了什么:换成自己域名的反写——top.chihaofan.app。域名 chihaofan.top 是我的,这串只有我能注册,杜绝再撞。
连带要改的东西:
- iOS/macOS Bundle ID →
top.chihaofan.app - iCloud 容器 →
iCloud.top.chihaofan.app - Android applicationId 也统一改成
top.chihaofan.app(跟 iOS、跟华为 AGC 里agconnect-services.json的package_name对齐) - 内购商品 ID 前缀跟着变
最疼的一个坑:安卓那边,旧包名和新包名在系统眼里是两个不同的应用。手机上装过旧包的,装不上新包,必须先卸载。我自己的测试机就这么中招过一次,卡在那儿研究了半天为什么「安装失败」。
教训:
包名在注册前先去后台试着注册一次,别等到写完代码才发现要改。 反写自有域名是最省事的防撞方案——注意,苹果和谷歌都不校验你是否真的拥有该域名,但拥有它能保证别人不会跟你想到一块去。
补一句:包名用户看不见,防不了山寨。真要防山寨得靠名称占位 + 注册商标(第 9/42 类),有商标才能走 App Store Content Dispute 通道。
三、iPhone-only:TARGETED_DEVICE_FAMILY
现象:一开始设备支持范围是通用(iPhone + iPad)。结果在 iPad 上,手机 UI 居中、两边补大片灰边,非常难看。
我改了什么:ios/Runner.xcodeproj 里把 TARGETED_DEVICE_FAMILY 从 "1,2" 改成 1,SUPPORTED_PLATFORMS = iphoneos。
改完有两个好处:
- ASC 不再要求 iPad 截图——少一整套素材要做。
- iPad 上系统把它当手机应用跑(画布 < 600pt,我
lib/app.dart里那段补灰边逻辑压根不触发),反而没有灰边;台前调度下还能变成一个可拖拽的手机窗口,体验更好。
这是我这次最蠢的一次返工:我先花时间做完了一整套 iPad 截图(还专门写了个排版脚本),做完才决定改 iPhone-only。那批图现在躺在 design-spec/appstore_shots/ipad-13/ 里,一张也没用上。
教训:
先定设备支持范围,再做截图。 顺序反了,返工的是最费时间的美术活。
四、内购必须有「恢复购买」入口
这条是苹果必查项,没有会被拒,属于「不需要有人告诉你、你就应该知道」的那类。
我改了什么:设置页放一张「完整版」状态卡,卡里有「恢复购买」按钮。付费墙半屏底部也有一个。
技术上就是调一次 restorePurchases(),把商店里这个 Apple 账号名下的非消耗型商品捞回来、重新落解锁态。
顺带说我的解锁态设计:存在 Hive(proUnlocked + ownedPaidSkins),不进导出同步码。
原因很实际:吃好饭有个「导出同步码」功能给家人同步冰箱和账本用。如果解锁态跟着同步码走,那这串码就是破解码,一个人买了全家甚至全网都能用。换机恢复解锁,走「恢复购买」,那是商店的活。
教训:
内购三件事一起做:购买 → 恢复购买入口 → 卸载重装后能恢复。 少任何一个都会被打回,第三个还得真机验过才算数。
五、外链风险:我把协议搬进了 App 里
现象:一开始「功能介绍 / 用户协议 / 隐私政策」我打算全部外链到官网,App 里放几个跳转按钮。
为什么改:苹果对 App 内跳外部网页这件事一直敏感(尤其涉及支付引导时,3.1.1 那条)。我的协议页本身无害,但我不想为了省事去赌审核员当天怎么理解。
我改了什么:把三份文档做成 App 内置的 Markdown 常量,放在 lib/features/game/about_docs.dart,App 内直接渲染,不跳浏览器。ASC 那边要填的隐私政策 URL 照旧指向官网(那是硬要求,必须有公网 URL)。
代价,说实话:同一份内容两处维护。官网 HTML 一份,App 内 Markdown 一份。改任何一处都要同步另一处,包括底部的更新日期。
我现在还没有好办法,靠的是每次改的时候自己记得。这是我留给未来自己的一颗定时炸弹,写在这里也算立个字据。
教训:
法务文本在 App 内可读、不依赖网络,是最稳的做法。 但要提前想好两处怎么同步——最好一开始就从同一份源文件生成两份产物,别像我一样手动抄。
六、导出合规:ITSAppUsesNonExemptEncryption
现象:每次提交都被问「你的 App 是否使用加密」,答一次问卷。
我改了什么:Info.plist 里直接声明掉:
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
我只用系统 HTTPS,属于豁免范围。声明一次,以后每次提审不再答题。
教训:能在 Info.plist 里一次性声明的,就别留给每次提交的手工操作。手工操作总有一天会点错。
七、几件顺手做了、但没做会挨打的事
onboarding 与购买流的边界。我修过一批边界情况,典型的是「引导流程还没走完就能摸到付费墙」。这种路径审核员很爱走,因为它就是「乱点」。改法很土:付费墙入口统一加前置判断,引导没完不给进。
启动屏。做了品牌着色的启动屏。这个不改也能过,但一个纯白闪屏会让整个 App 显得没做完。
评分引导。接了 RatingService(lib/services/rating_service.dart),在「高兴时刻」触发系统评分请求——采购全部完成之后、用餐打卡成功之后。
这里有个硬约束要记住:苹果的评分 API 一年最多真正弹 3 次,你申请了不弹是正常的。所以时机必须挑最爽的那一下,不能上来就问。我自己还加了一层节流:累计打卡 ≥ 5 次才开始问,两次间隔 ≥ 60 天。
沙盒测试。提审前必须在 ASC 建沙盒测试员账号,真机登录,走完整四步:
购买 → 解锁 → 卸载重装 → 恢复购买。
这四步任何一步断了,上线之后就是真金白银的差评。模拟器测不了,必须真机。
八、钱的部分
Apple Developer 个人账号 ¥688 / 年。到期不续,所有已上架 App 会下架(数据保留),记得设自动续费。
还有一件必须主动去点的事:Small Business Program。上一自然年全部 App 收入 ≤ 100 万美元就符合条件,申请通过后苹果抽成从 30% 降到 15%。
不申请就默认 30%。这不是自动的。
而且要注意顺序:签《付费应用协议》→ 填 W-8BEN 税表 → 绑银行卡 → 申请 SBP,这一整套要在第一笔销售之前办完,否则前面卖的那些就按 30% 抽了。
提审前 checklist(可以直接抄走)
Info.plist
- 所有用到的权限都有用途描述,包括第三方 SDK 链进来的(相册、相机、麦克风、定位…)
- 用途描述写具体功能,别写「用于提供更好的服务」
-
ITSAppUsesNonExemptEncryption已声明 - 版本号 / 构建号已递增(改完必须 bump,否则传不上去)
标识符
- Bundle ID 在开发者后台已注册(不注册,ASC 建 App 时下拉框是空的)
- iCloud 容器标识与 Bundle ID 对齐
- 内购商品 ID 与代码常量一字不差(建后永久不可改)
内购
- ASC 里商品已创建、状态正常、价格档已选
- 商品本地化的显示名与描述已填(必填)
- 审核信息里的截屏已上传(放付费墙半屏,要能看到价格和购买入口)
- App 内有「恢复购买」入口,且在显眼位置
- 沙盒真机跑通:购买 → 解锁 → 卸载重装 → 恢复购买
显示与素材
-
TARGETED_DEVICE_FAMILY已定,截图按这个来做(顺序别反) - 启动屏不是纯白
- 6.9" 截图齐了
合规文本
- 隐私政策、用户协议有公网 URL(ASC 硬要求)
- 技术支持网址是专门的 support 页,别拿隐私政策页顶替
- App 内也能读到协议全文(减少外链风险)
- App 隐私问卷如实填
账号与钱
- 付费应用协议已签
- W-8BEN 已提交(提交后不可改,填之前逐项核对)
- 银行卡已绑
- Small Business Program 已申请(不点就是 30%)
流程边界
- onboarding 没走完时,付费墙、权限申请等敏感路径都进不去
- 首次启动的隐私弹窗在同意前不申请任何权限
最后说句实在的。
被打回不丢人,ITMS 那一串数字看着吓人,本质上只是苹果的机器在提醒你少填了一个字段。真正费时间的从来不是改这些,是顺序搞错导致的返工——比如我那批白做的 iPad 截图。
所以这份 checklist 的价值不在于「有哪些项」,在于「哪些项必须先做」。
吃好饭 iOS 已上架,App Store 搜「吃好饭 AI冰箱」。这个系列会一直写下去,欢迎在留言区骂我。