billingClient.queryPurchases 返回已取消订阅
结论
可以继续使用 Play Billing Library,无需回退到旧版 In-App Billing API。
对于出现该问题的旧版实现,可以先调用 queryPurchaseHistoryAsync,让客户端与 Google Play 同步,然后再次调用 queryPurchases,读取更新后的本地缓存。不要直接通过购买历史判断用户当前是否享有订阅权益,因为历史记录也会包含已经取消或到期的订阅。
在正式环境中,客户端查询结果只适合用来及时更新界面。订阅权益是否仍然有效,应由服务端使用购买令牌向 Google Play Developer API 验证。
原因
queryPurchases 返回 Google Play 本地缓存中的“已拥有购买项”。在相关版本中,这个调用不会主动发起网络同步。因此,即使订阅已经到期,本地缓存也可能暂时保留旧记录。
“取消订阅”和“订阅到期”也不是同一个状态:
- 用户取消自动续订后,在当前计费周期结束前通常仍然享有权益。
Purchase.getPurchaseState()表示购买交易是否完成,例如PURCHASED或PENDING,并不表示完整的订阅生命周期。queryPurchaseHistoryAsync返回的是购买历史。某个商品出现在历史记录中,不代表对应订阅目前仍然有效。
因此,不能仅凭购买历史中是否存在某个 SKU 来判断订阅是否有效。
客户端处理方式
如果使用的是带有同步 queryPurchases 方法的旧版 Billing Library,可以先刷新购买历史,再查询当前购买项:
billingClient.queryPurchaseHistoryAsync(
BillingClient.SkuType.SUBS,
(billingResult, purchaseHistoryRecordList) -> {
if (billingResult.getResponseCode()
!= BillingClient.BillingResponseCode.OK) {
return;
}
Purchase.PurchasesResult purchasesResult =
billingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (purchasesResult.getResponseCode()
!= BillingClient.BillingResponseCode.OK) {
return;
}
List<Purchase> purchases = purchasesResult.getPurchasesList();
boolean hasSubscription = purchases != null
&& purchases.stream().anyMatch(
purchase -> purchase.getSkus().contains(SUBSCRIPTION_SKU)
&& purchase.getPurchaseState()
== Purchase.PurchaseState.PURCHASED
);
updateSubscriptionUi(hasSubscription);
}
);
这段代码适用于题目涉及的旧接口。新版 Billing Library 的类型和方法名称可能已经变为 ProductType.SUBS、queryPurchasesAsync、QueryPurchasesParams 和 Purchase.getProducts(),具体应以项目所使用版本的 API 为准。
新版接口的基本查询方式如下:
QueryPurchasesParams params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build();
billingClient.queryPurchasesAsync(
params,
(billingResult, purchases) -> {
if (billingResult.getResponseCode()
!= BillingClient.BillingResponseCode.OK) {
return;
}
boolean hasPurchasedItem = purchases.stream().anyMatch(
purchase -> purchase.getProducts().contains(SUBSCRIPTION_PRODUCT_ID)
&& purchase.getPurchaseState()
== Purchase.PurchaseState.PURCHASED
);
updateSubscriptionUi(hasPurchasedItem);
}
);
即使客户端返回 PURCHASED,这个结果也不能作为安全可靠的订阅状态依据,因为客户端数据可能有延迟,也可能被篡改。
推荐的生产方案
应用完成购买后,将以下信息发送到自己的服务端:
purchaseToken- 商品 ID
- 应用包名
- 当前登录用户的内部账号标识
服务端通过 Google Play Developer API 查询该购买令牌对应的订阅状态,再根据返回的到期时间、取消信息和支付状态决定是否授予权益。
客户端可以采用以下流程:
客户端查询购买项
↓
取得 purchaseToken
↓
提交给业务服务端
↓
服务端向 Google Play 验证
↓
服务端返回最终权益状态
如果客户端没有查询到购买项,也不要仅凭这一次结果永久撤销服务端权益。可以让服务端再次验证已有的 purchaseToken,或者根据 Google Play 的订阅状态通知更新权益。
注意事项
queryPurchaseHistoryAsync可以用来触发同步或恢复历史购买,但不适合直接判断当前权益。- 用户取消自动续订后,订阅通常要到当前周期结束才会失效,不能在用户执行“取消”操作时立即视为到期。
- 不要把
PurchaseState.PURCHASED当作SUBSCRIBED。它描述的是购买交易状态,不是完整的订阅状态。 - 重新连接
BillingClient、再次查询或等待缓存更新,可以缓解客户端的显示问题,但不能替代服务端验证。 - 无需通过旧版 In-App Billing API 绕过 Play Billing Library。旧接口同样无法提供可靠的服务端订阅校验,还会增加兼容和维护成本。
- 如果应用完全没有服务端,可以使用刷新后的
queryPurchases做有限的本地判断,但需要接受状态延迟和无法可靠防止篡改的问题。
备注:内容仅供参考。
