You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

StoreKit 2中消耗型内购是否需保留Transaction监听器?

消耗型内购的Transaction.updates监听器作用与金币发放逻辑解析

1. 监听器的核心作用

这个监听器是用来兜底处理未被即时捕获的交易的,常见触发场景包括:

  • 用户发起购买后,App在交易完成前崩溃/被杀死,此时purchase方法的回调根本没机会执行;
  • 交易需要额外验证(比如SCA强认证、家庭共享的Ask to Buy审批),用户切到其他App完成验证后再返回,purchase的回调已经失效;
  • App重启后,还有之前未标记为完成的交易(比如上次处理时没调用transaction.finish())。

如果没有这个监听器,这些场景下用户付了钱却拿不到金币,会直接引发用户信任危机——这也是Xcode弹出警告的核心原因。

2. 两处"Grant 200 coins"会不会重复发放?

处理不当会,但正确操作可完全避免:

  • 每个交易的transaction.id是唯一标识,且Transaction.updates只会返回**未调用过finish()**的交易。只要你在发放金币后立刻调用transaction.finish(),这个交易就不会再出现在监听器的数据流里。
  • 但如果没记录已处理的交易ID,极端情况(比如finish()调用失败、App在发放后立刻崩溃)下,监听器可能会再次捕获到同一个交易,导致重复发放。所以必须额外记录已处理的交易ID(比如存在UserDefaults或后端服务器),处理前先检查该ID是否已被处理过。

你当前的代码里,purchase和监听器都有发放逻辑,确实存在重复风险:比如用户购买成功后,purchase里发了金币并调用finish(),但如果App在finish()执行前崩溃,监听器重启后会再次处理这个交易,导致重复发币。

3. 应该在哪里发放金币?

最佳实践是统一在Transaction.updates监听器里处理发放逻辑,原因如下:

  • 所有交易场景(即时成功、后台完成、重启后遗留)都能被统一处理,不会遗漏;
  • 避免purchase方法和监听器的逻辑重复,减少出错概率;
  • purchase方法只负责UI层面的反馈:比如显示加载状态、提示用户交易pending/取消/失败,不用碰金币发放的核心逻辑。

修改后的代码示例:

调整purchase方法(仅处理UI反馈)

func purchase(_ product: Product) async {
    do {
        let result = try await product.purchase()
        switch result {
        case .success(.verified), .success(.unverified):
            print("购买请求已提交,请等待金币到账")
        case .pending:
            print("交易等待验证,请完成身份验证或等待家长审批")
        case .userCancelled:
            print("你取消了购买")
        @unknown default:
            print("购买失败,请重试")
        }
    } catch {
        print("购买失败:\(error.localizedDescription)")
    }
}

完善监听器(处理发放+去重逻辑)

extension AppDelegate {
    func observeTransactionUpdates() {
        Task {
            for await result in Transaction.updates {
                switch result {
                case .verified(let transaction):
                    let transactionID = transaction.id.uuidString
                    // 检查是否已处理过该交易(示例用UserDefaults,建议后端存储更可靠)
                    let processedTransactions = UserDefaults.standard.stringArray(forKey: "ProcessedTransactionIDs") ?? []
                    guard !processedTransactions.contains(transactionID) else {
                        await transaction.finish()
                        continue
                    }
                    
                    if transaction.productID == ProductID.coins_200_price_1_95.rawValue {
                        print("发放200金币")
                        print("交易ID:\(transactionID)")
                        // 记录已处理的交易ID
                        var updatedTransactions = processedTransactions
                        updatedTransactions.append(transactionID)
                        UserDefaults.standard.set(updatedTransactions, forKey: "ProcessedTransactionIDs")
                    }
                    await transaction.finish()
                case .unverified(_, let error):
                    print("交易验证失败:\(error.localizedDescription)")
                }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者Pratik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 21:07:17