You need to enable JavaScript to run this app. Flutter订阅场景in_app_purchase遇Pending Transaction异常及回调重复执行问题
优惠活动
大模型
产品
解决方案
定价
更多

Flutter订阅场景in_app_purchase遇Pending Transaction异常及回调重复执行问题

问题根因
  • storekit_duplicate_product_object异常:iOS StoreKit队列中存在未调用completePurchase收尾的挂起交易,重复发起同商品购买时就会触发该报错,本质是历史交易未做强制收尾逻辑
  • updateUserSubscription多次触发:未对交易做去重标记,且将历史拉取的旧购买记录、Stream推送的全量交易都纳入了后端上报流程,没有区分“用户当前主动发起的购买”和“系统恢复的历史交易”
代码修改方案

1. 新增类成员状态标记

在页面类的成员变量区新增以下状态位,用于去重、状态追踪:

// 已处理过的交易ID集合,避免重复走逻辑
final Set<String> _processedPurchaseIds = {};
// 记录用户当前主动发起购买的商品ID,仅对该ID对应的交易做后端上报、跳会员页逻辑
String? _currentSelectedProductId;
// 标记后端上报是否进行中,避免并发重复请求
bool _isUploadingReceipt = false;

2. 购买发起逻辑调整

在SUBSCRIBE按钮的确认购买回调里,调用平台购买方法前,先记录当前用户选中的商品ID:

buttonClick: () {
  // 记录当前用户主动选择的购买商品ID
  _currentSelectedProductId = selectedProductDetail.id;
  PurchaseParam purchaseParam = PurchaseParam(
    productDetails: selectedProductDetail,
    applicationUserName: null,
    sandboxTesting: true,
  );
  // 原有购买逻辑保留
  if (selectedProductDetail.id == _kConsumableId) {
    _connection.buyConsumable(
        purchaseParam: purchaseParam,
        autoConsume: _kAutoConsume || Platform.isIOS);
  } else {
    _connection.buyNonConsumable(purchaseParam: purchaseParam);
  }
},

3. 历史交易初始化逻辑改造

替换initStoreInfo中处理queryPastPurchases返回结果的片段,对所有历史交易做强制收尾,从根源解决挂起交易导致的重复异常:

final QueryPurchaseDetailsResponse purchaseResponse =
    await _connection.queryPastPurchases();
if (purchaseResponse.error != null) {
  // 原有错误处理逻辑保留
}
final List<PurchaseDetails> verifiedPurchases = [];
for (PurchaseDetails purchase in purchaseResponse.pastPurchases) {
  final purchaseKey = purchase.purchaseID ?? purchase.productID;
  // 历史交易直接标记为已处理,避免后续Stream推送时重复触发逻辑
  _processedPurchaseIds.add(purchaseKey);
  
  if (await _verifyPurchase(purchase)) {
    verifiedPurchases.add(purchase);
  }
  // 不管验证通过与否,只要存在未完成的交易状态,主动调用completePurchase收尾
  if (purchase.pendingCompletePurchase) {
    await InAppPurchaseConnection.instance.completePurchase(purchase);
  }
}

同时在initState方法开头增加状态重置代码,避免页面重建时残留旧状态:

@override
void initState() {
  // 新增:重置临时状态
  _processedPurchaseIds.clear();
  _isUploadingReceipt = false;
  _currentSelectedProductId = null;
  // 原有初始化逻辑保留
  DateTime currentDate = DateTime.now();
  // ... 其余原有代码不变
}

4. 购买流监听逻辑改造

重写_listenToPurchaseUpdated方法,增加交易去重、上报范围限制,从根源解决updateUserSubscription多次触发的问题:

void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
  purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
    final purchaseKey = purchaseDetails.purchaseID ?? purchaseDetails.productID;
    // 已处理过的交易直接跳过,仅做必要的收尾
    if (_processedPurchaseIds.contains(purchaseKey)) {
      if (purchaseDetails.pendingCompletePurchase) {
        await InAppPurchaseConnection.instance.completePurchase(purchaseDetails);
      }
      return;
    }

    if (purchaseDetails.status == PurchaseStatus.pending) {
      showPendingUI();
      return;
    }

    if (purchaseDetails.status == PurchaseStatus.error) {
      handleError(purchaseDetails.error);
      // 错误交易标记为已处理,完成收尾
      _processedPurchaseIds.add(purchaseKey);
      if (purchaseDetails.pendingCompletePurchase) {
        await InAppPurchaseConnection.instance.completePurchase(purchaseDetails);
      }
      return;
    }

    if (purchaseDetails.status == PurchaseStatus.purchased) {
      bool valid = await _verifyPurchase(purchaseDetails);
      // 所有验证完成的交易先标记为已处理
      _processedPurchaseIds.add(purchaseKey);
      
      if (!valid) {
        _handleInvalidPurchase(purchaseDetails);
        // 无效交易完成收尾
        if (purchaseDetails.pendingCompletePurchase) {
          await InAppPurchaseConnection.instance.completePurchase(purchaseDetails);
        }
        return;
      }

      // 交付商品逻辑保留
      await deliverProduct(purchaseDetails);

      // 仅对用户当前主动发起的购买、且无正在进行的上报时,才调用后端上报方法
      if (purchaseDetails.productID == _currentSelectedProductId 
          && !_isUploadingReceipt) {
        _isUploadingReceipt = true;
        // 调用后端校验接口更新订阅状态
        await updateUserSubscription(purchaseDetails);
        // 上报完成后重置临时状态
        _isUploadingReceipt = false;
        _currentSelectedProductId = null;
        // 校验通过后引导用户进入会员权益页面的逻辑写在这里
      }

      // 原有安卓消耗品处理逻辑保留
      if (Platform.isAndroid) {
        if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) {
          await InAppPurchaseConnection.instance.consumePurchase(purchaseDetails);
        }
      }

      // 所有逻辑处理完成后,必须调用completePurchase收尾,杜绝挂起交易
      if (purchaseDetails.pendingCompletePurchase) {
        await InAppPurchaseConnection.instance.completePurchase(purchaseDetails);
      }
    }
  });
}
额外注意事项
  • 所有走完校验、交付、上报流程的交易,必须调用completePurchase完成StoreKit的交易闭环,否则会持续残留到交易队列,触发后续的重复报错
  • 沙箱模式下的交易恢复逻辑不要和用户主动购买逻辑混同,自动续费、跨设备恢复的交易只做权益同步,不重复弹引导窗
  • 生产环境建议将sandboxTesting参数通过编译环境配置控制,不要硬编码为true

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.28 06:15:03