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

Android应用内支付问题:querySkuDetailsAsync返回空列表

Android应用内支付querySkuDetailsAsync返回空列表问题排查

我正尝试在我的Android应用中实现应用内支付功能,用户点击“Remove Ads”菜单按钮后,会初始化BillingClient并处理应用内支付流程。以下是我的实现代码:

public class PlayersActivity extends AppCompatActivity { 
    SharedPreferences prefs; 
    boolean adFree; 
    BillingClient billingClient; 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_list_view); 
        prefs = this.getSharedPreferences("package_name", Context.MODE_PRIVATE); 
        adFree = prefs.getBoolean("adFree", false); 
        if (!adFree) { 
            MobileAds.initialize(this, new OnInitializationCompleteListener() { 
                @Override 
                public void onInitializationComplete(InitializationStatus initializationStatus) { } 
            }); 
            AdView mAdView = findViewById(R.id.listBannerAd); 
            AdRequest adRequest = new AdRequest.Builder().build(); 
            mAdView.loadAd(adRequest); 
        } 
    } 

    @Override 
    public boolean onCreateOptionsMenu(Menu menu) { 
        getMenuInflater().inflate(R.menu.menu_history, menu); 
        MenuItem item6 = menu.findItem(R.id.action_remove_ads); 
        if (!adFree) { 
            item6.setVisible(true); 
        } 
        return true; 
    } 

    @Override 
    public boolean onOptionsItemSelected(MenuItem item) { 
        switch (item.getItemId()) { 
            case R.id.action_remove_ads: 
                setUpBillingClient(); 
                return true; 
        } 
        return super.onOptionsItemSelected(item); 
    } 

    private void handlePurchase(Purchase purchase) { 
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { 
            ConsumeParams consumeParams = ConsumeParams.newBuilder() 
                    .setPurchaseToken(purchase.getPurchaseToken()) 
                    .build(); 
            ConsumeResponseListener listener = (billingResult, purchaseToken) -> { 
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { 
                    prefs.edit().putBoolean("adFree", true).apply(); 
                } 
            }; 
            billingClient.consumeAsync(consumeParams, listener); 
        } 
    } 

    private final PurchasesUpdatedListener purchasesUpdatedListener = (billingResult, purchases) -> { 
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { 
            for (Purchase purchase : purchases) { 
                handlePurchase(purchase); 
            } 
        } 
    }; 

    private void setUpBillingClient() { 
        Log.v("InAppPurchase", "billingclientsetup"); 
        billingClient = BillingClient.newBuilder(this) 
                .setListener(purchasesUpdatedListener) 
                .enablePendingPurchases() 
                .build(); 
        startConnection(); 
    } 

    private void startConnection() { 
        Log.v("InAppPurchase", "startconnection"); 
        billingClient.startConnection(new BillingClientStateListener() { 
            @Override 
            public void onBillingSetupFinished(BillingResult billingResult) { 
                Log.v("InAppPurchase", "billing setup finished"); 
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { 
                    queryAvailableProducts(); 
                } 
            } 

            @Override 
            public void onBillingServiceDisconnected() { 
                // Try to restart the connection on the next request to 
                // Google Play by calling the startConnection() method. 
                Log.v("InAppPurchase", "billing disconnected"); 
            } 
        }); 
    } 

    private void queryAvailableProducts() { 
        Log.v("InAppPurchase", "queryavailableproducts"); 
        List<String> skuList = new ArrayList<>(); 
        skuList.add("remove_ads"); 
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); 
        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP); 
        billingClient.querySkuDetailsAsync(params.build(), (billingResult, list) -> { 
            Log.v("InAppPurchases", billingResult.getResponseCode() + "response"); 
            Log.v("InAppPurchases", list + "list"); 
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null && !list.isEmpty()) { 
                Log.v("InAppPurchase", list.get(0).getDescription().toString()); 
                BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() 
                        .setSkuDetails(list.get(0)) 
                        .build(); 
                billingClient.launchBillingFlow(PlayersActivity.this, billingFlowParams); 
            } 
        }); 
    } 
}

但在日志中,我发现querySkuDetailsAsync返回了空列表,响应码为0。我已经尝试了StackOverflow上的一些解决方案,比如确保应用已发布到Google Play商店,且应用内商品在Play Console中配置正确。请问我的代码实现是否存在问题?


先排查querySkuDetailsAsync返回空列表的问题

响应码为0(BillingResponseCode.OK)说明BillingClient和Play服务的连接是正常的,但返回空列表通常和以下几个细节有关:

  • SKU ID完全匹配问题:你代码里用的SKU是"remove_ads",请务必确认Play Console里配置的应用内商品ID和这个字符串完全一致——包括大小写、下划线/连字符、拼写,哪怕一个字符出错都会导致返回空列表。
  • 测试账号与发布轨道配置:
    • 如果应用已发布到正式轨道,测试支付必须使用Play Console中添加的许可测试账号,普通账号无法看到正式上架的商品;
    • 如果用内部测试/封闭测试轨道,要确保测试账号已经加入对应的测试群组,且应用完成了测试轨道的审核(哪怕是内部测试的轻量审核)。
  • 商品状态检查:去Play Console的应用内商品页面,确认remove_ads的状态是活跃(Active),而不是草稿或未发布状态。
  • 额外调试日志:可以在queryAvailableProducts里加一段日志,当列表为空时明确提示,比如Log.v("InAppPurchase", "SKU list is empty, check Play Console configuration"),方便定位问题。

另外,你的代码存在一个严重的逻辑错误!

注意到你在handlePurchase里对remove_ads调用了consumeAsync——但**“移除广告”属于非消耗品(Non-Consumable)**,这类商品是用户永久拥有的,不需要也绝对不能被消耗!

消耗操作是为一次性商品(比如游戏内金币、道具)设计的,消耗后用户可以再次购买。如果对非消耗品执行消耗,会导致:

  1. 用户购买后,Play商店的购买记录会被标记为已消耗,下次重装应用时queryPurchases查不到这个购买记录;
  2. 你仅靠本地SharedPreferences存储的adFree标记,在用户卸载重装后会丢失,用户需要重新购买。

正确的处理方式应该是:

  1. 应用启动时,主动查询用户已购买的非消耗品,用Play商店的记录覆盖本地状态;
  2. 处理购买时,不需要调用consumeAsync,而是调用acknowledgePurchase确认购买(非消耗品必须确认,否则用户可申请退款),然后更新本地标记。

修改后的核心逻辑示例:

// 在onCreate中添加已购商品查询逻辑
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_list_view);
    prefs = this.getSharedPreferences("package_name", Context.MODE_PRIVATE);
    adFree = prefs.getBoolean("adFree", false);
    // 初始化BillingClient并查询已购商品
    setUpBillingClientForPurchaseCheck();
    // ... 原广告初始化逻辑不变
}

private void setUpBillingClientForPurchaseCheck() {
    billingClient = BillingClient.newBuilder(this)
            .setListener(purchasesUpdatedListener)
            .enablePendingPurchases()
            .build();
    billingClient.startConnection(new BillingClientStateListener() {
        @Override
        public void onBillingSetupFinished(BillingResult billingResult) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                queryPurchasedNonConsumables();
            }
        }

        @Override
        public void onBillingServiceDisconnected() {
            billingClient.startConnection(this);
        }
    });
}

private void queryPurchasedNonConsumables() {
    billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.SkuType.INAPP)
            .build(), (billingResult, purchases) -> {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
                    for (Purchase purchase : purchases) {
                        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
                            handlePurchase(purchase);
                        }
                    }
                }
            });
}

private void handlePurchase(Purchase purchase) {
    if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged()) {
            // 非消耗品必须确认购买,避免用户退款
            AcknowledgePurchaseParams acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.getPurchaseToken())
                    .build();
            billingClient.acknowledgePurchase(acknowledgeParams, billingResult -> {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    updateAdFreeStatus(true);
                }
            });
        } else {
            updateAdFreeStatus(true);
        }
    }
}

private void updateAdFreeStatus(boolean isAdFree) {
    adFree = isAdFree;
    prefs.edit().putBoolean("adFree", isAdFree).apply();
    // 更新广告和菜单UI
    AdView mAdView = findViewById(R.id.listBannerAd);
    mAdView.setVisibility(isAdFree ? View.GONE : View.VISIBLE);
    if (!isAdFree) {
        AdRequest adRequest = new AdRequest.Builder().build();
        mAdView.loadAd(adRequest);
    }
    invalidateOptionsMenu();
}

最后补充几个调试小技巧

  1. 确保测试设备上的Google Play服务是最新版本;
  2. 可以用Play Console的静态响应测试功能,使用官方测试SKU(比如android.test.purchased)验证支付流程是否正常;
  3. 检查日志中list的打印是否准确,有时候list.toString()显示为[]可能是日志格式问题,可以尝试遍历列表打印每个元素。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 22:09:08