有效 receipt 中的 iOS purchase transaction 未出现在 payment queue
结论
Receipt 和 SKPaymentQueue 不是同一份交易列表。
Receipt 中有某笔 purchase,只代表 Apple 已将它写入购买凭证,不代表对应的 SKPaymentTransaction 仍未完成,也不意味着它会再次出现在 payment queue 中。调用 finishTransaction: 会从队列中移除交易,但通常不会删除 receipt 中的购买记录。
因此,如果旧交易存在于 receipt 中,却不在 SKPaymentQueue 中:
- 不要尝试根据 transaction identifier 构造 transaction,或对其调用 finish。
- 根据验证后的 receipt 恢复用户权益。
- 仅对 queue 回调中实际收到的 transaction 调用
finishTransaction:。 - 如需重新获取可恢复的购买,可使用 StoreKit 的恢复购买流程。该流程不适用于消耗型商品,也不保证将 receipt 中的每条历史记录重新放入队列。
为什么 receipt 中仍然有这些交易
调用 finishTransaction: 是在通知 StoreKit,应用已经完成这笔交易的处理,可以将其从 payment queue 中移除。这个接口不会删除购买历史。
所以下列情况可以同时存在:
- Receipt 中包含某笔历史购买。
- 该交易以前已经被 finish。
- 当前
SKPaymentQueue中没有对应的SKPaymentTransaction。
自动续期订阅尤其如此。Receipt 可能包含多次续期记录,这些记录用于判断订阅状态和有效期,并不表示每条记录都要作为待处理 transaction 再次进入 queue。
Sandbox 环境还可能积累大量测试购买记录。只看 receipt 中是否有历史记录,无法判断当时是否确实漏掉了 finishTransaction:。
正确的处理方式
1. 启动时尽早注册 observer
在应用启动阶段注册 observer,并让它保持足够长的生命周期:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
StoreKit 只会重新投递仍由队列管理的未完成交易。已经离开队列的历史交易,不会因为注册 observer 而重新出现。
2. 处理实际收到的 transaction
购买成功后,先完成服务端验证和权益发放,再调用 finishTransaction::
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
break;
case SKPaymentTransactionStatePurchased:
[self validateAndDeliverTransaction:transaction
completion:^(BOOL success) {
if (success) {
[[SKPaymentQueue defaultQueue]
finishTransaction:transaction];
}
}];
break;
case SKPaymentTransactionStateRestored:
[self restoreEntitlementForTransaction:transaction
completion:^(BOOL success) {
if (success) {
[[SKPaymentQueue defaultQueue]
finishTransaction:transaction];
}
}];
break;
case SKPaymentTransactionStateFailed:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateDeferred:
break;
}
}
}
成功交易的权益尚未可靠保存时,不要调用 finish。否则,应用崩溃或网络请求失败后,可能无法再次处理该 transaction。
3. 使用 receipt 恢复权益
如果服务端验证发现 receipt 中有旧购买,但本地或服务端尚未记录相应权益,应直接进行对账,不要等待 queue 返回历史 transaction。
常见流程如下:
- 验证 receipt 的真实性。
- 读取与商品类型相关的购买记录。
- 使用
transaction_id、original_transaction_id等字段进行幂等去重。 - 根据商品类型、取消状态和有效期计算当前权益。
- 将结果写入服务端账户。
处理自动续期订阅时,通常应围绕 original_transaction_id 建立订阅链,再根据有效期和相关状态判断当前权益。不要将 receipt 中的每条记录都视为需要再次交付的独立队列事件。
4. 必要时发起恢复购买
对于非消耗型商品和支持恢复的订阅,可以让用户触发:
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
恢复的交易会以 SKPaymentTransactionStateRestored 状态返回。权益恢复完成后,仍要调用 finishTransaction:。
需要注意:
- 消耗型商品不能通过
restoreCompletedTransactions恢复。 - 恢复购买不会把 receipt 中的所有历史记录重新加入 queue。
- 如果有效 receipt 已经足以确定权益,通常可以直接通过服务端对账完成恢复。
如果只需要更新本地 receipt,可以使用 SKReceiptRefreshRequest。刷新 receipt 同样不会让历史交易重新进入 payment queue。
无法直接执行的操作
下面的遍历只能处理当前仍在 queue 中的交易:
for (SKPaymentTransaction *transaction
in [[SKPaymentQueue defaultQueue] transactions]) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
它无法处理 receipt 中的历史条目。公开 API 也不支持根据 receipt 里的 transaction identifier 创建 SKPaymentTransaction,再将其传给 finishTransaction:。应用不应保存旧的 transaction 对象,以后再用它伪造完成流程。
实现时的注意事项
- Receipt 是判断购买和权益的数据来源之一,payment queue 则是待处理事件队列,两者的条目数量不能直接对应。
- 服务端发放权益时必须保证幂等,避免重复处理同一个
transaction_id。 - 应分别为订阅、非消耗型商品和消耗型商品设计恢复策略。
SKPaymentTransactionStatePurchasing表示交易仍在进行,不能发放权益,也不能 finish。SKPaymentTransactionStateDeferred可能还在等待批准,不能按成功或失败处理。- 删除应用、切换 sandbox tester、更换设备或刷新 receipt,都可能让测试现象变得更复杂。
如果能确认某笔交易从未调用过 finishTransaction:,但 StoreKit 长期不再投递它,可以先换用新的 sandbox tester 或重置测试账户环境进行复现。同时记录商品 ID、transaction identifier、时间、设备系统版本和完整的 StoreKit 日志,再向 Apple Developer Technical Support 提交问题。业务处理仍应通过 receipt 对账恢复权益,不能依赖历史 transaction 一定会重新进入 queue。
备注:内容仅供参考。
