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

数据库上传失败时如何取消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)")
                    // 可在此添加重试次数统计,超过阈值后触发退款流程
                }
            }
        }
    }
}

补充注意事项

  1. 数据库回调改造:需确保userService.editElo方法能返回更新是否成功的状态,若现有方法无此逻辑,需先修改服务层代码。
  2. 重试阈值控制:可在缓存中增加交易重试次数记录,超过阈值后将交易标记为异常,触发人工退款流程。
  3. 待处理交易:若购买返回.pending状态(如需要家长批准),需等待Transaction.updates回调再处理,不要直接执行数据库同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 06:37:04