PAYATHON 2026

Google In-app billing 无法查询订阅详情

架构老王

结论

在 queryInventoryAsync(true, additionalSkus, listener) 中,additionalSkus 会按一次性商品(ITEM_TYPE_INAPP)的 SKU 查询,不会按订阅(ITEM_TYPE_SUBS)处理。

因此,返回的 SkuDetails 中不会包含尚未购买的订阅。购买后能够查到,是因为 IabHelper 在查询用户已有订阅时取得了该 SKU,随后又加载了对应的商品详情。

如果需要查询尚未购买的订阅及其本地化价格,应通过支持订阅列表的重载方法,将订阅 SKU 作为 moreSubsSkus 传入,或者直接按订阅商品类型查询。

原因

Google Play Billing 将商品分为两种类型:

IabHelper.ITEM_TYPE_INAPP
IabHelper.ITEM_TYPE_SUBS

当前调用:

iabHelper.queryInventoryAsync(true, additionalSkus, listener);

通常对应以下方法:

queryInventoryAsync(
    boolean querySkuDetails,
    List<String> moreItemSkus,
    QueryInventoryFinishedListener listener
)

其中,moreItemSkus 只用于补充查询 inapp 商品。即使列表中放的是订阅 SKU,IabHelper 仍会按错误的商品类型发起查询,所以 Google Play 不会返回对应的订阅详情。

已经购买的订阅属于用户的现有库存。IabHelper 查询库存时,会单独查询用户已有的 subscriptions,因此购买过的 SKU 可能会自动进入后续的详情查询。取消订阅甚至重装应用后仍能查到,也与此有关:取消通常只是停止续费,并不会立即删除购买记录或当前订阅权益。Google Play 账号也可能继续保留相关购买信息。

正确查询方式

使用同时接收一次性商品 SKU 和订阅 SKU 的重载方法,并把订阅列表放在第三个参数中:

List<String> subscriptionSkus = new ArrayList<>();

for (Subscription subscription : subscriptions) {
    subscriptionSkus.add(subscription.getSku());
}

iabHelper.queryInventoryAsync(
        true,
        null,
        subscriptionSkus,
        new IabHelper.QueryInventoryFinishedListener() {
            @Override
            public void onQueryInventoryFinished(IabResult result, Inventory inv) {
                if (iabHelper == null) {
                    return;
                }

                if (result.isFailure()) {
                    Log.i("subs", result.getMessage());
                    return;
                }

                for (Subscription subscription : subscriptions) {
                    SkuDetails skuDetails =
                            inv.getSkuDetails(subscription.getSku());

                    if (skuDetails != null) {
                        subscription.setLocalizedPrice(skuDetails.getPrice());
                        Log.i("subs", subscription.getLocalizedPrice());
                    } else {
                        Log.i(
                                "subs",
                                "No SkuDetails returned for "
                                        + subscription.getSku()
                        );
                    }
                }
            }
        }
);

如果还要同时查询 managed products,可以分别准备两个列表:

List<String> inAppSkus = new ArrayList<>();
List<String> subscriptionSkus = new ArrayList<>();

iabHelper.queryInventoryAsync(
        true,
        inAppSkus,
        subscriptionSkus,
        inventoryListener
);

两个参数分别对应:

moreItemSkus        // managed products / inapp
moreSubsSkus        // subscriptions

订阅 SKU 不应放入 moreItemSkus。

如果使用较新的 Google Play Billing Library

IabHelper 是早期示例中的封装。现代 Google Play Billing Library 使用 ProductDetails 查询,并要求明确指定订阅商品类型:

QueryProductDetailsParams.Product product =
        QueryProductDetailsParams.Product.newBuilder()
                .setProductId(subscriptionSku)
                .setProductType(BillingClient.ProductType.SUBS)
                .build();

QueryProductDetailsParams params =
        QueryProductDetailsParams.newBuilder()
                .setProductList(Collections.singletonList(product))
                .build();

billingClient.queryProductDetailsAsync(
        params,
        (billingResult, queryProductDetailsResult) -> {
            if (billingResult.getResponseCode()
                    != BillingClient.BillingResponseCode.OK) {
                Log.i("subs", billingResult.getDebugMessage());
                return;
            }

            for (ProductDetails details
                    : queryProductDetailsResult.getProductDetailsList()) {
                List<ProductDetails.SubscriptionOfferDetails> offers =
                        details.getSubscriptionOfferDetails();

                if (offers == null) {
                    continue;
                }

                for (ProductDetails.SubscriptionOfferDetails offer : offers) {
                    List<ProductDetails.PricingPhase> phases =
                            offer.getPricingPhases().getPricingPhaseList();

                    for (ProductDetails.PricingPhase phase : phases) {
                        Log.i("subs", phase.getFormattedPrice());
                    }
                }
            }
        }
);

具体的类名和返回结构由项目使用的 Billing Library 版本决定。旧项目无需为了修复这个问题立即迁移,但新项目不建议继续使用 IabHelper。

注意事项

  • 本地化价格应直接使用 Google Play 返回的 getPrice() 或 getFormattedPrice(),不要自行拼接货币符号。
  • 查询失败后应立即 return。原代码只记录了错误,之后仍会调用 inv.getSkuDetails(),如果 inv 无效,可能引发异常。
  • 确认当前设备登录的 Google 账号有权访问测试版本,同时检查安装包的 application ID、签名和发布轨道配置是否一致。
  • 订阅必须已在 Google Play Console 中正确激活,并配置可购买的基础方案或报价。具体要求由所用 Billing Library 版本和 Play Console 的订阅模型决定。
  • 取消订阅不等于立即失效。在当前计费周期结束前,订阅通常仍可能处于有效状态。
  • 如果 SkuDetails 为 null,还应检查 SKU 拼写、商品类型、应用包名和商品发布状态。不过在本例中,主要原因是订阅 SKU 被放进了 inapp 查询参数。

备注:内容仅供参考。