数据库上传失败时如何取消StoreKit应用内购?
内购点数交易的错误处理与离线适配方案
核心逻辑说明
Apple StoreKit的机制是先完成支付流程,再向开发者返回交易凭证,无法直接实现「数据库更新成功才扣费」的逻辑。我们需要通过「交易暂存+重试机制」+「失败补偿」来达成等效效果:
- 验证交易后,不立即调用
transaction.finish() - 尝试同步数据库,成功后再告知Apple交易完成
- 若同步失败(含离线场景),缓存交易信息,后续自动重试;多次失败则触发退款流程
具体实现步骤
1. 本地缓存未完成交易
用UserDefaults或Core Data存储未成功同步数据库的交易ID,确保App重启后能恢复重试流程。
2. 重构交易处理逻辑
- 验证交易后,优先执行数据库同步操作
- 仅当同步成功时,才调用
transaction.finish() - 同步失败时,将交易ID存入缓存,等待后续重试
3. 离线自动重试
- 监听App联网状态,联网后自动触发缓存交易的重试
- App启动时主动检查缓存,处理未完成的交易
4. 失败退款处理
若交易重试超过阈值(如3次)仍失败,可通过App Store Connect API发起自动退款,或在App内引导用户联系客服申请退款。
修改后的StoreKitManager代码
import Foundation import StoreKit import Network public enum StoreError: Error { case failedVerification case dbUpdateFailed } class StoreKitManager: ObservableObject { private let userService = UserService() @Published var storeProducts: [Product] = [] var updateListenerTask: Task<Void, Error>? = nil private var productDict: [String] = { var keys: [String] = [] for i in stride(from: 10, through: 900, by: 10) { keys.append("\(i)ELO") } keys.append("OneDay") keys.append("ThreeDay") return keys }() // 本地缓存未完成交易的Key private let unfinishedTransactionsKey = "UnfinishedTransactions" private var networkMonitor: NWPathMonitor? init() { updateListenerTask = listenForTransactions() Task { await requestProducts() } // 初始化网络监听,联网后重试缓存交易 setupNetworkMonitor() // App启动时自动重试未完成交易 retryUnfinishedTransactions() } deinit { updateListenerTask?.cancel() networkMonitor?.cancel() } private func setupNetworkMonitor() { networkMonitor = NWPathMonitor() let queue = DispatchQueue(label: "NetworkMonitor") networkMonitor?.start(queue: queue) networkMonitor?.pathUpdateHandler = { [weak self] path in if path.status == .satisfied { DispatchQueue.main.async { self?.retryUnfinishedTransactions() } } } } func listenForTransactions() -> Task<Void, Error> { return Task.detached { for await result in Transaction.updates { do { let transaction = try self.checkVerified(result) // 先同步数据库,成功后再完成交易 try await self.syncDatabaseAndFinish(transaction: transaction) } catch { print("交易处理失败: \(error)") // 验证失败或同步失败时,缓存未完成的交易ID if case .verified(let transaction) = result { self.cacheUnfinishedTransaction(transactionID: transaction.id.uuidString) } } } } } @MainActor func requestProducts() async { do { storeProducts = try await Product.products(for: productDict) } catch { print("获取商品列表失败: \(error)") } } func checkVerified<T>(_ result: VerificationResult<T>) throws -> T { switch result { case .unverified: throw StoreError.failedVerification case .verified(let signedType): return signedType } } // 统一处理数据库同步与交易完成逻辑 private func syncDatabaseAndFinish(transaction: Transaction) async throws { guard let amount = Int(transaction.productID.dropLast(3)) else { throw StoreError.dbUpdateFailed } // 异步等待数据库更新结果(需确保userService的editElo方法能返回成功状态) try await withCheckedThrowingContinuation { continuation in userService.editElo(withUid: nil, withAmount: amount) { success in if success { continuation.resume() } else { continuation.resume(throwing: StoreError.dbUpdateFailed) } } } // 数据库更新成功,标记交易完成 await transaction.finish() // 从缓存中移除该交易ID removeCachedTransaction(transactionID: transaction.id.uuidString) } func purchase(_ product: Product) async throws -> Bool { let result = try await product.purchase() switch result { case .success(let verificationResult): let transaction = try checkVerified(verificationResult) try await syncDatabaseAndFinish(transaction: transaction) return true case .userCancelled, .pending: return false default: return false } } // MARK: - 交易缓存相关方法 private func cacheUnfinishedTransaction(transactionID: String) { var cachedIDs = UserDefaults.standard.stringArray(forKey: unfinishedTransactionsKey) ?? [] guard !cachedIDs.contains(transactionID) else { return } cachedIDs.append(transactionID) UserDefaults.standard.set(cachedIDs, forKey: unfinishedTransactionsKey) } private func removeCachedTransaction(transactionID: String) { var cachedIDs = UserDefaults.standard.stringArray(forKey: unfinishedTransactionsKey) ?? [] cachedIDs.removeAll { $0 == transactionID } UserDefaults.standard.set(cachedIDs, forKey: unfinishedTransactionsKey) } private func retryUnfinishedTransactions() { guard let cachedIDs = UserDefaults.standard.stringArray(forKey: unfinishedTransactionsKey), !cachedIDs.isEmpty else { return } Task { for transactionID in cachedIDs { guard let uuid = UUID(uuidString: transactionID), let transaction = await Transaction.lookup(uuid: uuid) else { // 交易不存在,移除无效缓存 removeCachedTransaction(transactionID: transactionID) continue } do { let verifiedTransaction = try checkVerified(transaction) try await syncDatabaseAndFinish(transaction: verifiedTransaction) } catch { print("重试交易失败: \(transactionID), 错误: \(error)") // 可在此添加重试次数统计,超过阈值后触发退款流程 } } } } }
补充注意事项
- 数据库回调改造:需确保
userService.editElo方法能返回更新是否成功的状态,若现有方法无此逻辑,需先修改服务层代码。 - 重试阈值控制:可在缓存中增加交易重试次数记录,超过阈值后将交易标记为异常,触发人工退款流程。
- 待处理交易:若购买返回
.pending状态(如需要家长批准),需等待Transaction.updates回调再处理,不要直接执行数据库同步。
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

