内购商品拉回来是空数组,查了两天才发现协议没签
一个 API 返回空数组,不抛错,不打日志。我把 Product ID 拼写、沙盒账号、App ID 能力挨个查了一遍,两天后才发现问题在 App Store Connect 的一个协议页面。
这事让我对"静默失败"有了新的认识。
现象:什么都没报,就是空的
给一个 PDF 扫描 App 加订阅,用的是 StoreKit 2。代码不长:
let ids = ["com.ia.scanner.pro.monthly", "com.ia.scanner.pro.yearly"]
let products = try await Product.products(for: ids)
print(products.count) // 0
不抛异常,不崩溃,控制台干干净净,就是 count 是 0。我第一反应是自己 await 写错了,把 do-catch 拆开重写了一遍,还是 0。
然后在 catch 里加了个 print,从来没执行过。这说明调用是成功的,只是结果为空。
先把最蠢的可能一个个划掉
我列了个清单,从上往下排:
Product ID 是不是和后台一字不差。这个查得最久,因为后台的商品标识符旁边没有复制按钮,只能手打,我确实打错过一次,把 monthly 打成了 monthy。改回来之后还是 0。
App ID 的 In-App Purchase 能力有没有勾。去 Certificates, Identifiers & Profiles 里看,勾着的。
Bundle ID 和 Provisioning Profile 对不对得上。对的,因为其他功能都正常跑。
沙盒账号建得对不对。这个我怀疑了很久,因为沙盒账号和普通测试员账号是两套体系,很容易搞混。重新建了一个,退出登录重进,还是 0。
产品状态是不是"准备提交"。也确认了,两个商品都是「准备提交」,不是「缺少元数据」。
四个都排除完,我开始怀疑是工程配置问题,去翻 capability、翻 entitlements、翻 StoreKit Configuration 文件。这一圈花了大半天,方向完全错了。
中间我还试过一个笨办法:把两个商品 ID 分开发请求,看是不是某一个的问题。结果是两个都空。这个实验其实已经暗示问题不在单个商品上,而在更上层的配置——但我当时只想着是不是 ID 又打错了,压根没往这个方向想。
转折点在一个跟代码无关的页面
第二天下午,我登 App Store Connect 想改个描述,首页顶部有条黄色提示,平时我从来不看的那个区域。点进去是「协议、税务和银行业务」。
「付费应用」那一行的状态是:待接受。
我点「同意」,填了几个必填项(联系人、银行账户之前已经填过),提交。页面刷新,状态变「有效」。
回去重新跑,商品列表出来了,两个 ID 都在,价格、本地化描述、订阅周期全对。前后不到十分钟。
两天时间,卡在一个我没以为是问题的地方。因为这个协议是我注册开发者账号时签过的,我以为签一次就永久有效。实际上它会在某些情况下回到待接受状态,比如修改了价格档位、变更了收款信息。
为什么这个错误是静默的
查完我去翻文档,才明白 StoreKit 的行为是有意设计的。
商品 ID 无效或者查不到时,Apple 不会逐个告诉你哪个 ID 有问题,只会把能查到的返回,查不到的直接丢掉。这是为了防止有人拿这个接口枚举别人的商品 ID。所以哪怕你传 10 个 ID、其中 9 个是错的,返回的也只是那 1 个正确的,不会有任何提示。
设计上说得通,但对开发者极不友好:它把「配置错误」和「商品不存在」合并成了同一个结果——空数组。你没法从返回值上区分这两种情况,只能靠外部信息。
我后来在代码里加了一段兜底:如果返回数量小于请求数量,把差集打出来,并提示去检查协议状态和商品状态。虽然还是不知道具体原因,至少不会像这次一样连方向都没有。
let missing = Set(ids).subtracting(products.map(\.id))
if !missing.isEmpty {
print("未返回的商品:\(missing) — 检查协议、商品状态、App ID 能力")
}
顺带踩到的几个相关坑
沙盒购买不扣钱,但流程和真的一样。第一次测的时候我以为要绑卡,其实沙盒账号不需要,弹窗点确认就完成了。订阅会立刻生效,续期周期被压缩到几分钟,方便测试。
TestFlight 和正式环境的商品不是实时同步的。新建的商品在沙盒里能看到,TestFlight 里可能要等一会儿。我遇到过一次沙盒正常、TestFlight 为空,等了大概二十分钟才出现。
价格调整会让协议重新变回待接受。这个是我后来才知道的,改了一次年订阅的价格,第二天又变成空数组,这次五分钟就定位到了,因为我知道该看哪里。
真机连的是沙盒还是正式环境,取决于包的签名方式,不是代码里能指定的。Xcode 直接跑是真机沙盒,TestFlight 也是沙盒,只有从 App Store 下载的才是正式。这一点搞混,很容易得出「线上没商品」的错误结论,然后跑去改一堆不需要改的东西。
StoreKit Configuration 文件会掩盖配置问题。本地测试用 .storekit 文件,商品是假的,协议状态完全不参与。所以本地一切正常,真机一跑就空。这个坑我建议只在纯 UI 调试时用,别拿它验证购买链路。
现在我的顺序反过来了:新建 App 的内购,先去协议页确认状态是「有效」,再去写代码。顺序调一下,能省两天。
- 点赞
- 收藏
- 关注作者
评论(0)