内购商品拉回来是空数组,查了两天才发现协议没签

举报
yd_278798845 发表于 2026/11/09 14:54:29 2026/11/09
【摘要】 StoreKit 2 的 Product.products(for:) 返回空数组,不抛错也不打日志。我把 Product ID、App ID 能力、沙盒账号、商品状态全查了一遍,最后发现是 App Store Connect 的付费应用协议回到了待接受状态。

一个 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 的内购,先去协议页确认状态是「有效」,再去写代码。顺序调一下,能省两天。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。