Google Play 强制截止,内购应用必须升级 Billing 8,不改无法更新

从 2026 年 8 月 31 日开始,Google Play 不再接受使用 Billing Library 7 或更低版本的新应用和应用更新。可以申请延期到 2026 年 11 月 1 日。

Billing 8 不是只改依赖版本。它改了商品模型、查询结果、失败原因、连接管理和 pending purchase 初始化方式。

如果项目里有内购、订阅、会员、虚拟商品,这次迁移应该提前做。

先看版本和时间线

如果项目直接接入 BillingClient,现在需要把依赖升到 8.x:

bash 复制代码
dependencies {
    implementation("com.android.billingclient:billing:8.3.0")
}

真正麻烦的不是这一行。

Billing 8 删除了一批旧 API,也把一些原来"查不到就没有"的结果变成了更明确的状态返回。

迁移前先确认项目属于哪种情况:

  • • 完全通过 RevenueCat 管内购

  • • RevenueCat 和自研 BillingClient 并存

  • • 完全自研 BillingClient

如果完全通过 RevenueCat,主要看 RevenueCat SDK 版本和 Dashboard 配置。

如果项目里还直接依赖 com.android.billingclient:billing,那就不能只升级 RevenueCat SDK。

一次性商品也有购买选项

Billing 8 里,一次性商品从 "in-app products" 转向 "one-time products" 的表达。

这个变化不只是命名。

一次性商品也开始支持多个 purchase options 和 offers。

以前很多项目会把一次性商品理解成:

bash 复制代码
productId -> price -> purchase

Billing 8 之后,更接近:

bash 复制代码
productId -> purchase option / offer -> price -> purchase

这会影响商品展示和购买参数。

如果一个一次性商品未来有普通购买、限时优惠、预购、特殊活动价,UI 层不能再默认一个 productId 只有一个价格。

更稳的做法是把 Billing 返回结果收敛成业务自己的商品模型:

bash 复制代码
data class PaywallProduct(
    val productId: String,
    val title: String,
    val priceText: String,
    val productDetails: ProductDetails,
    val offerToken: String?
)

UI 只关心 PaywallProduct

Billing 层负责处理 ProductDetails、offer、价格阶段和 token。

这样以后商品配置变复杂,影响范围不会扩散到页面代码里。

商品查询结果变细了

Billing 8 改了 queryProductDetailsAsync() 的返回结果。

旧版本里,查不到的商品通常只是不出现在返回列表里。开发者只能看到"少了几个商品",但不知道为什么少。

Billing 8 会把未成功获取的商品也返回出来,并带上商品级状态。

可能原因包括:商品不存在、商品未激活、用户没有可用 offer、地区或账号条件不满足。

代码上要适配新的结果对象:

bash 复制代码
val params = QueryProductDetailsParams.newBuilder()
    .setProductList(productList)
    .build()

billingClient.queryProductDetailsAsync(params) { billingResult, queryResult ->
    if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
        reportBillingError(billingResult)
        return@queryProductDetailsAsync
    }

    val products = queryResult.productDetailsList
    val unfetchedProducts = queryResult.unfetchedProductList

    products.forEach { productDetails ->
        renderProduct(productDetails)
    }

    unfetchedProducts.forEach { unfetched ->
        reportUnfetchedProduct(unfetched)
    }
}

这类变化对线上排查很有用。

以前商品查不到,常见排查路径是 Play Console 配置、测试账号、商品状态、地区、版本轨道。

现在客户端可以把失败商品和状态码一起打到日志里。

不要只记录:

bash 复制代码
query product failed

至少要记录:

bash 复制代码
productId
productType
BillingResult.responseCode
BillingResult.debugMessage
unfetched product status code

很多商品问题不是代码 bug,而是配置和账号条件问题。

日志不够细,排查成本会很高。

购买流程要选对 offer

订阅从 Billing 5 开始已经进入 base plan / offer 模型。

Billing 8 又把一次性商品的购买选项能力往前推了一步。

购买参数不能只塞一个商品对象就结束。

对订阅来说,通常还要带上 offer token:

bash 复制代码
val offerToken = productDetails.subscriptionOfferDetails
    ?.firstOrNull { offer ->
        offer.basePlanId == "monthly"
    }
    ?.offerToken

val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
    .setProductDetails(productDetails)
    .apply {
        if (offerToken != null) {
            setOfferToken(offerToken)
        }
    }
    .build()

val billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(listOf(productDetailsParams))
    .build()

真实项目里不要随手 firstOrNull()

offer 选择应该来自明确规则:用户地区、订阅档位、实验分组、是否符合优惠条件、是否老用户、是否升级或降级。

否则代码可以跑,但用户看到的价格可能和产品策略不一致。

支付失败原因更具体

Billing 8 给购买流程相关的 BillingResult 增加了 sub-response code。

它不是每次都有值,但在部分失败场景里可以表达更具体原因。

RevenueCat 文章里提到的典型值包括:

  • PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS

  • USER_INELIGIBLE

  • NO_APPLICABLE_SUB_RESPONSE_CODE

这对支付页体验有影响。

以前很多项目只区分成功、用户取消、失败:

bash 复制代码
val result = billingClient.launchBillingFlow(activity, billingFlowParams)

when (result.responseCode) {
    BillingClient.BillingResponseCode.OK -> {
        // 等待 PurchasesUpdatedListener 回调
    }
    BillingClient.BillingResponseCode.USER_CANCELED -> {
        // 用户取消
    }
    else -> showGenericBillingError()
}

Billing 8 之后,可以把部分失败原因拆出来:

bash 复制代码
val result = billingClient.launchBillingFlow(activity, billingFlowParams)

if (result.responseCode != BillingClient.BillingResponseCode.OK) {
    when (result.onPurchasesUpdatedSubResponseCode) {
        BillingClient.OnPurchasesUpdatedSubResponseCode
            .PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS -> {
            showInsufficientFundsMessage()
        }

        BillingClient.OnPurchasesUpdatedSubResponseCode.USER_INELIGIBLE -> {
            showOfferUnavailableMessage()
        }

        else -> showGenericBillingError()
    }
}

这里的重点是:不要把所有失败都塞进一个"支付失败,请重试"。

余额不足、优惠资格不满足、商品不可用,用户下一步动作不一样。

自动重连减少样板代码

BillingClient 需要连接 Google Play 服务。

旧实现里经常有一套手写重连:

bash 复制代码
billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(result: BillingResult) {
        if (result.responseCode == BillingClient.BillingResponseCode.OK) {
            queryProducts()
        }
    }

    override fun onBillingServiceDisconnected() {
        retryConnectLater()
    }
})

Billing 8 提供了自动重连:

bash 复制代码
val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder()
            .enableOneTimeProducts()
            .build()
    )
    .enableAutoServiceReconnection()
    .build()

打开后,如果 API 调用发生时服务已经断开,Billing Library 会尝试重新建立连接。

这不是说业务不用处理失败。

BillingResult 的错误码仍然要处理,尤其是服务不可用、网络异常、用户取消、开发者参数错误。

pending purchase 初始化要改

Billing 8 删除了无参数版本的 enablePendingPurchases()

旧代码:

bash 复制代码
val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases()
    .build()

新版要传 PendingPurchasesParams

bash 复制代码
val pendingPurchasesParams = PendingPurchasesParams.newBuilder()
    .enableOneTimeProducts()
    .build()

val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(pendingPurchasesParams)
    .enableAutoServiceReconnection()
    .build()

如果要支持预付费订阅的 pending 状态,还要启用:

bash 复制代码
val pendingPurchasesParams = PendingPurchasesParams.newBuilder()
    .enableOneTimeProducts()
    .enablePrepaidPlans()
    .build()

pending purchase 不能当成购买成功。

处理购买时要看 purchaseState

bash 复制代码
private fun handlePurchase(purchase: Purchase) {
    when (purchase.purchaseState) {
        Purchase.PurchaseState.PURCHASED -> {
            verifyOnServer(purchase)
            acknowledgeIfNeeded(purchase)
        }

        Purchase.PurchaseState.PENDING -> {
            showPendingState()
        }

        else -> refreshEntitlement()
    }
}

pending 状态下发放权益,会造成未付款解锁。

pending 状态下提示失败,也会让用户误以为支付流程已经结束。

旧 API 需要清掉

Billing 8 删除了多项此前废弃的 API。

比较常见的有:

  • querySkuDetailsAsync()

  • queryPurchaseHistory() / queryPurchaseHistoryAsync()

  • • 无参数 enablePendingPurchases()

  • queryPurchasesAsync(String skuType, PurchasesResponseListener listener)

  • • 旧的订阅替换参数 API,例如 setOldSkuPurchaseToken

商品查询要迁到 queryProductDetailsAsync()

查询当前购买要使用带参数的 queryPurchasesAsync()

bash 复制代码
val params = QueryPurchasesParams.newBuilder()
    .setProductType(BillingClient.ProductType.SUBS)
    .build()

billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        purchases.forEach { purchase ->
            handlePurchase(purchase)
        }
    }
}

购买历史这块要单独看。

RevenueCat 文章里明确提到,Billing 8 不再支持通过旧 API 查询过期订阅和已消耗的一次性商品。

如果项目依赖这些历史交易做恢复、画像或补数据,不能再指望客户端 SDK 查回来。

替代方案通常是:

  • • 当前有效、待确认、pending 的购买:用 queryPurchasesAsync

  • • 已消耗的一次性商品:自己服务端记录

  • • 取消或作废购买:看 Voided Purchases API

  • • 历史数据迁移:做服务端导入或后台补数

这也是 Billing 8 迁移里最容易被低估的地方。

代码能编译,不代表历史权益恢复还正确。

迁移顺序

Billing 8 的迁移不要从"哪里编译报错"开始。

更稳的顺序是:

  • • 先确认项目是否直接依赖 BillingClient

  • • 清理 SkuDetailsquerySkuDetailsAsync

  • • 统一商品模型到 ProductDetails

  • • 适配 queryProductDetailsAsync 新结果,记录 unfetched products

  • • 购买流程里明确 offer 选择规则

  • • 改造 enablePendingPurchases(PendingPurchasesParams)

  • • 打开 enableAutoServiceReconnection

  • • 细分 BillingResult 和 sub-response code

  • • 检查历史购买、恢复购买、已消耗商品的服务端逻辑

内购迁移不要只测成功支付。

至少要覆盖商品查询为空、商品部分失败、多个 offer、用户不符合 offer 条件、用户取消、余额不足、pending purchase、未 acknowledge、服务断开后再次查询、订阅升级降级、已消耗商品恢复策略。

最后

Billing 8 的核心不是 API 名字变化,而是商品模型和购买状态更细。

一次性商品有了更复杂的购买选项,商品查询能返回失败明细,支付失败也能表达更具体原因。

项目越早把 Billing 逻辑收敛到单独模块,后续迁移越轻。

#Android #GooglePlay #Billing #RevenueCat #内购 #订阅

相关推荐
zhangphil1 小时前
Android RecyclerView+Coil解码Bitmap设置进View,RenderThread上屏显示Graphics
android
idingzhi1 小时前
A股量化策略日报(2026年05月11日)
android·开发语言·python·kotlin
我命由我123451 小时前
Jetpack Compose - 设置 Compose 编译器、设置 Compose 依赖项
android·java·java-ee·kotlin·android jetpack·android-studio·android runtime
Kapaseker1 小时前
reified 如何骗过 JVM 类型擦除
android·kotlin
硬件学长森哥2 小时前
成像技术系列-3A算法基础
android·图像处理·计算机视觉
唔662 小时前
Android在局域网中搭建 MQTT服务器 协议V3.1.1
android·运维·服务器
2601_957418803 小时前
Android 手机如何通过 PTP / MTP 连接单反相机?源码级方案分享
android·数码相机·智能手机
阿巴斯甜11 小时前
ARouter
android
Andya_net13 小时前
MySQL | MySQL 8.0 权限管理实践-精确赋予库、表只读等权限
android·数据库·mysql