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

多商品IAP沙盒正常但生产环境商品匹配异常问题排查

问题

我开发的iOS应用包含8个消耗型与非消耗型IAP商品,均已在App Store Connect审核通过。沙盒测试时,获取的商品顺序正确,可与购买按钮一一对应,测试全部通过后提交应用审核并上线。但上线后生产环境中,商品与购买按钮出现匹配混乱的情况,而使用相同代码再次进行沙盒测试仍完全正常。请问可能的问题是什么?

原代码展示

var productsRequest = SKProductsRequest()
var validProducts = [SKProduct]()
var productIndex = 0

override func viewDidLoad() {
    super.viewDidLoad()
    

func fetchAvailableProducts()  {
    let productIdentifiers = NSSet(objects:
                                    "com...WL.Purchase12LessonsSet","com...WL.Purchase3LessonsSet","com...WL.Purchase6LessonsSet","com...WL.PurchaseAllLessons"
    )
    
    productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
    productsRequest.delegate = self
    productsRequest.start()
    
}

// This function will get IAP products info from the App Store Connect 

func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
    if (response.products.count > 0) {
        validProducts = response.products
        
        
        let prod12Set = response.products[0] as SKProduct
        let prod3Set = response.products[1] as SKProduct
        let prod6Set = response.products[2] as SKProduct
        let prodFullSet = response.products[3] as SKProduct
        
        //let prodUnlockPro = response.products[0] as SKProduct
        //print("Products found in AppStoreConnect! " + prodUnlockPro.localizedDescription)
        
        print("1st rpoduct: " + prod12Set.localizedDescription)
        print("2nd rpoduct: " + prod3Set.localizedDescription)
        print("3rd rpoduct: " + prod6Set.localizedDescription)
        print("4th rpoduct: " + prodFullSet.localizedDescription)
        
        productFoundInAppStoreConnect = true
    }
}


func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
    return true
}


func canMakePurchases() -> Bool {  return SKPaymentQueue.canMakePayments()  }


func purchaseMyProduct(_ product: SKProduct) {
    if self.canMakePurchases() {
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().add(payment)
    } else { print("Purchases are disabled in your device!") }
}


func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction:AnyObject in transactions {
        if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction {
            switch trans.transactionState {
                
            case .purchased:
                SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
                if productIndex == 0 {
                    print("Set of 12 lessons purchased!")
                    // code to unblock 12 lessons here  
                  
                }
                
                if productIndex == 1 {
                    print("Set of 3 lessons purchased!")
                    // code to unblock 3 lessons here  
                   
                }
                
                if productIndex == 2 {
                    print("Set of 6 lessons purchased!")
                    // code to unblock 6 lessons here  
                 
                }
                
                if productIndex == 3 {
                    print("All lessons purchased!")
                    // code to unblock all lessons here  
                    
                }
                
            case .failed:
                SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
                print("Payment has failed.")
                
                selectedPurchase = ""
                UserDefaults.standard.set(0, forKey: "remainingBonusLessons")
                
                
                break
            case .restored:
                SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
                print("Purchase has been successfully restored!")
                
                //bundle4Btn.setTitle("10000 words loaded!", for: .normal)
                
                break
                
            default: break
            }}}
}

@IBAction func auto12Pressed(_ sender: Any) {
    
    productIndex = 0
    purchaseMyProduct(validProducts[productIndex])
}

@IBAction func auto3Pressed(_ sender: Any) {
    
    productIndex = 1
    purchaseMyProduct(validProducts[productIndex])
}

@IBAction func auto6Pressed(_ sender: Any) {
    
    productIndex = 2
    purchaseMyProduct(validProducts[productIndex])
}


@IBAction func fullSetPressed(_ sender: Any) {
    
    productIndex = 3
    purchaseMyProduct(validProducts[productIndex])
}

问题根源分析

核心问题是依赖SKProductsResponse.products数组的固定索引匹配商品,但苹果官方从未承诺过返回的商品顺序会和请求时传入的productIdentifiers顺序一致:

  • 沙盒环境中返回顺序碰巧和请求顺序吻合,导致测试阶段未暴露问题;
  • 生产环境中,苹果服务器返回的商品顺序可能因缓存策略、地区差异、商品状态更新等因素发生变化,直接按索引取商品就会出现匹配混乱;
  • 另外,用全局变量productIndex记录购买索引的方式存在并发风险——如果用户快速点击多个购买按钮,productIndex会被覆盖,导致后续交易的逻辑判断完全错误。

修复方案及代码说明

你修改后的代码通过商品唯一标识productIdentifier匹配索引,彻底摆脱了对返回顺序的依赖,这是IAP开发的标准正确做法:

func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
    if (response.products.count > 0) {
        validProducts = response.products
        
        for i in 0...response.products.count-1 {
            switch response.products[i].productIdentifier {
            case "com...Purchase3LessonsSet":
                productIndexFor3Set = i
            case "com...Purchase6LessonsSet":
                productIndexFor6Set = i
            case "com...Purchase12LessonsSet":
                productIndexFor12Set = i
            case "com...PurchaseAllLessons":
                productIndexForFullSet = i
            default:
                print("none!")
            }
        }
        productFoundInAppStoreConnect = true
    }
}

额外优化建议

为了进一步避免潜在问题,还可以做以下调整:

  1. 放弃用索引关联商品和按钮,直接通过productIdentifier查找对应商品,比如在购买按钮点击事件中:
@IBAction func auto3Pressed(_ sender: Any) {
    if let product = validProducts.first(where: { $0.productIdentifier == "com...Purchase3LessonsSet" }) {
        purchaseMyProduct(product)
    }
}
  1. 移除全局productIndex变量,在交易回调中直接通过transaction.payment.productIdentifier判断购买的商品,彻底消除并发风险:
case .purchased:
    SKPaymentQueue.default().finishTransaction(trans)
    let productId = trans.payment.productIdentifier
    switch productId {
    case "com...Purchase12LessonsSet":
        print("Set of 12 lessons purchased!")
        // 解锁12节课的逻辑
    case "com...Purchase3LessonsSet":
        print("Set of 3 lessons purchased!")
        // 解锁3节课的逻辑
    case "com...Purchase6LessonsSet":
        print("Set of 6 lessons purchased!")
        // 解锁6节课的逻辑
    case "com...PurchaseAllLessons":
        print("All lessons purchased!")
        // 解锁全部课程的逻辑
    default: break
    }

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 15:08:10