PAYATHON 2026

是否可以通过 BillingClient 检测 grace period?

支付小周

结论

仅靠 BillingClient 无法准确区分正常有效的订阅和处于 grace period(宽限期)的订阅。

在这两种状态下,BillingClient.queryPurchasesAsync() 都可能返回:

  • purchaseState == Purchase.PurchaseState.PURCHASED
  • isAutoRenewing == true

这是正常现象。宽限期内,用户仍享有订阅权益,客户端返回的 Purchase 对象也没有单独表示”当前处于宽限期”的状态。

如果业务需要明确识别 grace period,应在服务端查询 Google Play Developer API。新接入的系统通常优先使用 purchases.subscriptionsv2.get;仍在使用旧版订阅接口的系统,可以通过 Purchases.subscriptions:get 获取相应的服务端状态。

为什么 BillingClient 无法判断

BillingClient 用于完成购买流程、查询设备当前能够识别的购买记录,以及确认用户是否拥有相应商品。它并不是完整的订阅生命周期状态接口。

进入宽限期意味着续费扣款暂时失败,但 Google Play 仍允许用户继续使用订阅权益。因此,从客户端授权的角度看,这笔订阅仍是有效购买:

purchase.purchaseState == Purchase.PurchaseState.PURCHASED
purchase.isAutoRenewing == true

这两个字段无法说明最近一次续费是否失败,也无法判断订阅是否已进入宽限期。

不要根据 purchaseState 的数字值判断状态。应使用 Purchase.PurchaseState.PURCHASED 等常量,避免代码依赖具体的实现值。

推荐的处理方式

一般可以按以下流程处理:

  1. Android 客户端通过 BillingClient 获取 purchaseToken。
  2. 客户端把 purchaseToken、应用包名和商品信息发送给自己的服务端。
  3. 服务端调用 Google Play Developer API 验证订阅。
  4. 服务端根据订阅状态决定是否授予权益,并把业务所需的状态返回客户端。
  5. 配置 Real-time Developer Notifications。订阅状态发生变化后及时重新查询,不要只依赖客户端轮询。

不要直接在 Android 应用中调用 Google Play Developer API,也不要把服务账号凭据打包进 APK。

使用 purchases.subscriptionsv2.get 判断

对于支持该接口的订阅实现,服务端可以查询:

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

返回结果中的 subscriptionState 会明确表示订阅是否处于宽限期:

SUBSCRIPTION_STATE_IN_GRACE_PERIOD

服务端判断逻辑可以写成:

const response = await androidpublisher.purchases.subscriptionsv2.get({
  packageName,
  token: purchaseToken,
});

const state = response.data.subscriptionState;

const inGracePeriod =
  state === "SUBSCRIPTION_STATE_IN_GRACE_PERIOD";

const entitlementActive =
  state === "SUBSCRIPTION_STATE_ACTIVE" ||
  state === "SUBSCRIPTION_STATE_IN_GRACE_PERIOD";

这里要分别处理两个概念:

  • inGracePeriod:订阅是否处于宽限期。
  • entitlementActive:是否仍应向用户提供订阅权益。

通常情况下,宽限期内应继续提供权益。

使用旧版 Purchases.subscriptions:get

仍在使用旧版接口的系统可以查询:

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

然后结合返回的 paymentState、expiryTimeMillis、autoRenewing 等服务端字段判断订阅状态。字段是否返回及其具体含义,应以当前接口文档和实际响应为准。客户端的 Purchase.purchaseState 与服务端的 paymentState 不是同一个字段,不能混用。

如果可以选择,建议使用带有明确 subscriptionState 的 purchases.subscriptionsv2.get,判断逻辑更直接。

客户端代码仍然应该做什么

客户端仍可使用 BillingClient 完成基础的购买检查:

billingClient.queryPurchasesAsync(
    QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.SUBS)
        .build()
) { billingResult, purchases ->
    if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
        return@queryPurchasesAsync
    }

    purchases.forEach { purchase ->
        if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
            sendPurchaseTokenToBackend(purchase.purchaseToken)
        }
    }
}

这段代码只能说明客户端发现了一笔已购买的订阅,无法据此确定它处于正常续费状态还是宽限期。最终是否授权,应以服务端验证结果为准。

测试时的注意事项

使用测试卡模拟续费失败时,还要确认以下几点:

  • Play Console 中对应订阅是否启用了 grace period。
  • 测试订阅的续费、宽限期和账号保留时间通常会被压缩,不能按照生产环境的自然时间推算。
  • 修改测试付款方式后,客户端状态不一定会立即出现可见变化。
  • queryPurchasesAsync() 可能继续返回这笔购买,因为用户在宽限期内仍有权益。
  • grace period 结束后,订阅可能进入 account hold。account hold 通常不应继续授予权益,不能沿用宽限期的授权策略。
  • Real-time Developer Notifications 只表示订阅发生了变化。收到通知后,仍需调用 Developer API 获取权威状态。

BillingClient 适合发现购买并获取 purchaseToken。grace period 等更细的订阅状态,应由服务端通过 Google Play Developer API 判断。

备注:内容仅供参考。