iOS 15下iAP自动续期订阅恢复购买旧交易重复及新购异常问题
问题根因
你遇到的所有问题本质上都是IAP交易处理逻辑不符合StoreKit的设计规范,核心错误点如下:
- 自动续期订阅的有效性验证依赖整包App Store收据,而非单个交易,你逐个上报历史恢复交易属于冗余操作,也是造成后端压力、重复弹窗的核心原因
- 未按照要求在交易处理完成后第一时间调用
finishTransaction,导致历史交易长期堆积在支付队列中,每次触发恢复购买都会重复返回累积的历史交易 - 在
paymentQueueRestoreCompletedTransactionsFinished中无差别批量结束所有交易,会导致未处理的新交易被提前结束,是你后续购买新订阅后收据找不到对应交易的直接原因 - 按productID去重交易的操作会过滤新增的购买交易,进一步导致新订阅的交易被忽略,收据无法正常更新
正确处理流程
1. 基础规则前置
- App启动时第一时间设置
SKPaymentQueue.default().add(self)注册支付代理,同时遍历当前队列中所有未完成的历史交易,全部调用finishTransaction清空堆积 - 所有交易无论最终结果是成功、失败、验证不通过,都必须在处理完成后立刻调用
finishTransaction,不允许遗留任何未结束的交易 - 自动续期订阅的所有有效性判断,统一通过
Bundle.main.appStoreReceiptURL读取本地整包收据,base64编码后传给后端做一次性苹果验证,不需要处理任何单个交易的凭证信息
2. 首次购买流程
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch transaction.transactionState { case .purchased: // 1. 读取整包收据传给后端验证 guard let receiptUrl = Bundle.main.appStoreReceiptURL, let receiptData = try? Data(contentsOf: receiptUrl) else { // 收据读取失败,结束交易并提示用户 queue.finishTransaction(transaction) showToast("购买失败,请重试") return } let base64Receipt = receiptData.base64EncodedString() // 2. 调用后端验证接口 IAPManager.shared.verifyReceipt(base64Receipt) { [weak self] isValid, expireDate in guard let self = self else { return } // 3. 无论验证是否通过,都先结束交易 queue.finishTransaction(transaction) if isValid { // 验证通过,更新用户订阅状态,提示购买成功 UserDefaults.standard.set(expireDate, forKey: "subscribe_expire_date") showToast("订阅购买成功") } else { // 验证失败,提示用户 showToast("订阅验证失败,请重试") } } case .failed: // 失败直接结束交易,根据错误码提示用户 queue.finishTransaction(transaction) if let error = transaction.error as? SKError { switch error.code { case .paymentCancelled: showToast("已取消购买") default: showToast("购买失败,请重试") } } case .restored: // 恢复状态的交易在这里不需要做任何处理,等恢复流程结束后统一处理 break default: break } } }
3. 恢复购买流程
- 用户点击恢复购买按钮时调用:
SKPaymentQueue.default().restoreCompletedTransactions() - 恢复流程完成回调处理:
// 恢复成功完成 func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { // 1. 先结束所有restore状态的交易 queue.transactions.filter { $0.transactionState == .restored }.forEach { queue.finishTransaction($0) } // 2. 读取整包收据传给后端验证 guard let receiptUrl = Bundle.main.appStoreReceiptURL, let receiptData = try? Data(contentsOf: receiptUrl) else { showToast("恢复失败,请重试") return } let base64Receipt = receiptData.base64EncodedString() IAPManager.shared.verifyReceipt(base64Receipt) { isValid, expireDate in if isValid, let expireDate = expireDate, expireDate > Date() { // 有有效订阅,更新用户状态,提示恢复成功 UserDefaults.standard.set(expireDate, forKey: "subscribe_expire_date") showToast("订阅恢复成功") } else { // 无有效订阅,提示用户 showToast("未找到有效订阅记录") } } } // 恢复失败回调 func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { // 先结束所有restore状态的交易 queue.transactions.filter { $0.transactionState == .restored }.forEach { queue.finishTransaction($0) } showToast("恢复失败,请重试") }
4. 弹窗优化说明
- 整个流程中只在最终验证完成、交易失败两个节点弹出用户提示,不要在每个交易回调中弹出提示,即可避免多次弹窗的问题
- 系统自带的购买/恢复成功弹窗是苹果系统控制的,无法取消,属于正常现象
遗留问题修复
针对你现在已经出现的新购买订阅找不到的问题,先在测试环境执行一次清空操作:
- 启动App时先遍历所有队列中的交易,全部finish,清空历史堆积
- 退出当前测试的AppleID,重新登录沙箱测试账号
- 删除App重新安装,即可恢复正常的购买流程
内容的提问来源于stack exchange,提问作者Ali
相关产品推荐
相关产品推荐

