PAYATHON 2026

如何在新版 Play Billing Library 中检查订阅状态?

支付阿杰

结论

Purchase 中的 purchaseState 只表示交易状态,取值包括 PURCHASED、PENDING 和 UNSPECIFIED_STATE。它不能直接说明订阅是否已经过期。

判断订阅是否有效,应采用以下方式:

  • 客户端调用 queryPurchasesAsync(ProductType.SUBS) 查询当前仍归属于用户的订阅,并确认 purchaseState == Purchase.PurchaseState.PURCHASED。
  • 服务端使用 purchaseToken 调用 Google Play Developer API 的 purchases.subscriptionsv2.get,结合 subscriptionState 和 lineItems[].expiryTime 判断订阅是否有效。
  • 不要用 isAutoRenewing 判断订阅是否过期。用户取消自动续订后,在当前计费周期结束前通常仍可使用订阅权益。

客户端检查当前订阅

新版 Play Billing Library 可以这样查询订阅:

val params = QueryPurchasesParams.newBuilder()
    .setProductType(BillingClient.ProductType.SUBS)
    .build()

billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
    if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
        // 查询失败,应按业务需要重试或暂时保留上一次可信状态
        return@queryPurchasesAsync
    }

    val purchase = purchases.firstOrNull { item ->
        item.products.contains("your_subscription_product_id")
    }

    when (purchase?.purchaseState) {
        Purchase.PurchaseState.PURCHASED -> {
            // 交易已经完成,但生产环境仍建议交给服务端验证 purchaseToken
            val purchaseToken = purchase.purchaseToken
        }

        Purchase.PurchaseState.PENDING -> {
            // 付款仍在处理中,暂时不要授予订阅权益
        }

        Purchase.PurchaseState.UNSPECIFIED_STATE, null -> {
            // 没有查到可用订阅,或交易状态未知
        }
    }
}

较旧版本的 Billing Library 可能使用同步方法 queryPurchases(),商品列表字段也可能是 sku 或 skus。具体应以项目所用版本的 API 为准。

客户端返回的 Purchase 只是设备侧的查询结果,适合用来刷新界面和恢复购买,不应成为服务端长期授权的唯一依据。

为什么 purchaseState 不能代表 active 或 expired

购买状态与订阅生命周期是两套不同的概念。

purchaseState 回答的是”这笔交易是否已经完成”:

PURCHASED
PENDING
UNSPECIFIED_STATE

订阅是否有效还取决于:

  • 当前计费周期是否结束;
  • 用户是否取消自动续订;
  • 续费是否进入宽限期;
  • 账号是否处于保留状态;
  • 订阅是否暂停;
  • 购买是否被撤销;
  • 用户是否升级或降级订阅;
  • 是否发生退款或撤单。

所以,即使 purchaseState 为 PURCHASED,也不能仅凭这个字段一直认定订阅有效。

下面这种判断同样不正确:

val active = purchase.isAutoRenewing

isAutoRenewing == false 只表示下一个周期不会自动续费。用户取消订阅后,在 expiryTime 到来前通常仍可继续使用当前周期的权益。

服务端验证订阅状态

客户端取得 purchaseToken 后,应通过 HTTPS 将它发送到自己的服务端:

val purchaseToken = purchase.purchaseToken
val productId = purchase.products.firstOrNull()

// 将 purchaseToken、productId 和 packageName 发送给业务服务端。
// 不要把 Google Play API 的服务账号凭据放进 Android 客户端。

然后由服务端调用 Google Play Developer API:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}

服务端需要重点检查:

  • subscriptionState
  • lineItems[].expiryTime
  • lineItems[].productId
  • 订阅是否已撤销、暂停或进入其他异常状态

伪代码如下:

subscription = subscriptionsv2.get(packageName, purchaseToken)

expiryTime = 找到目标 lineItem 的 expiryTime
notExpired = expiryTime > 当前时间

active =
    notExpired
    AND subscription.subscriptionState 属于业务允许授予权益的状态

仅比较 expiryTime 仍然不够。例如,订阅可能还没到原定的到期时间,却已因撤销等原因不再适合授予权益。

哪些 subscriptionState 可以继续访问,应由具体业务规则决定。宽限期、账号保留和暂停状态尤其需要单独处理,不同应用可以采用不同策略。

推荐的完整流程

  1. 初始化 BillingClient 并连接 Google Play。
  2. 调用 queryPurchasesAsync() 恢复当前订阅。
  3. 只处理 PurchaseState.PURCHASED 的购买。对于 PENDING 状态,不要发放权益。
  4. 将 purchaseToken 发送到业务服务端。
  5. 由服务端调用 purchases.subscriptionsv2.get 验证订阅。
  6. 根据 subscriptionState、expiryTime 和业务策略决定是否授权。
  7. 按要求确认新购买,避免购买因未确认而被自动退款。
  8. 服务端接入 Real-time Developer Notifications。收到续费、取消、暂停或撤销等通知后,重新查询 Google Play API。
  9. 用户打开应用、登录或进入付费功能时,按需再次同步状态,不要只依赖通知。

在客户端确认新购买的示例:

if (
    purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
    !purchase.isAcknowledged
) {
    val params = AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(purchase.purchaseToken)
        .build()

    billingClient.acknowledgePurchase(params) { billingResult ->
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            // 购买已确认
        }
    }
}

如果确认操作由服务端完成,客户端就不要重复处理。保持一套明确的确认流程即可。

注意事项

  • purchaseTime 表示初始购买或相关交易的发生时间,不是订阅到期时间。
  • 查询结果中没有某个订阅时,客户端未必能区分”从未购买”、“已经过期”、“退款”和”查询暂时失败”。业务服务端需要保存订阅记录。
  • 不要将服务账号 JSON、访问令牌或 Google Play Developer API 凭据放入 APK。
  • purchaseToken 是服务端验证购买的重要标识,不能只依赖 productId。
  • 测试订阅的周期和状态变化可能与正式环境不同,不要把测试周期写死在生产环境的判断逻辑中。
  • Billing Library 和 Google Play Developer API 会持续演进。旧版文档中的 “purchase state” 通常指交易状态,并不等同于完整的订阅有效期状态。

简而言之,客户端的 Purchase 用于发现购买并取得 purchaseToken。可靠的 active/expired 判断应由服务端通过 Google Play Developer API 完成。

备注:内容仅供参考。