如何在新版 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}
服务端需要重点检查:
subscriptionStatelineItems[].expiryTimelineItems[].productId- 订阅是否已撤销、暂停或进入其他异常状态
伪代码如下:
subscription = subscriptionsv2.get(packageName, purchaseToken)
expiryTime = 找到目标 lineItem 的 expiryTime
notExpired = expiryTime > 当前时间
active =
notExpired
AND subscription.subscriptionState 属于业务允许授予权益的状态
仅比较 expiryTime 仍然不够。例如,订阅可能还没到原定的到期时间,却已因撤销等原因不再适合授予权益。
哪些 subscriptionState 可以继续访问,应由具体业务规则决定。宽限期、账号保留和暂停状态尤其需要单独处理,不同应用可以采用不同策略。
推荐的完整流程
- 初始化
BillingClient并连接 Google Play。 - 调用
queryPurchasesAsync()恢复当前订阅。 - 只处理
PurchaseState.PURCHASED的购买。对于PENDING状态,不要发放权益。 - 将
purchaseToken发送到业务服务端。 - 由服务端调用
purchases.subscriptionsv2.get验证订阅。 - 根据
subscriptionState、expiryTime和业务策略决定是否授权。 - 按要求确认新购买,避免购买因未确认而被自动退款。
- 服务端接入 Real-time Developer Notifications。收到续费、取消、暂停或撤销等通知后,重新查询 Google Play API。
- 用户打开应用、登录或进入付费功能时,按需再次同步状态,不要只依赖通知。
在客户端确认新购买的示例:
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 完成。
备注:内容仅供参考。
