Play Store Billing 如何获取最后一次订阅续订日期
结论
只使用客户端的 Google Play Billing Library,无法可靠获取订阅最后一次成功续订的时间、续订次数,也无法准确判断当前计费周期何时结束。
queryPurchases() 和 queryPurchasesAsync() 返回的是当前有效的购买权益,不是每个续订周期的交易流水。对于自动续订订阅,客户端获取的 purchaseTime 通常是这条订阅购买链的首次购买时间,orderId 也不能作为统计续订次数的可靠依据。
可靠的处理方式如下:
- 客户端取得
purchaseToken。 - 客户端将
purchaseToken发送给可信服务端。 - 服务端通过 Google Play Developer API 查询订阅状态。
- 如果需要及时跟踪续订、取消、暂停和宽限期,再接入 Real-time Developer Notifications。
如果完全不使用 Developer API,只能从应用接入后开始记录客户端观察到的状态变化。这种方式无法补齐历史续订记录,也不能保证数据准确。
为什么客户端数据不会随每次续订变化
自动续订不代表客户端每次都会收到一条新的独立购买记录。
同一条订阅购买链在多次自动续订期间,可能一直使用相同的 purchaseToken。这意味着:
purchaseTime通常仍是首次购买时间;- 客户端返回的
orderId不适合用来推算续订次数; queryPurchaseHistoryAsync()返回历史购买记录,但不等于完整的订阅续订交易明细;- 用户取消自动续订后,当前已支付的周期仍然有效,因此查询结果可能暂时没有明显变化;
- Billing Library 的购买状态无法完整表示到期时间、宽限期、账号保留和暂停等服务端订阅状态。
“取消订阅”和”订阅立即失效”并不是一回事。例如,用户在 3 月 15 日取消订阅,当前周期到 4 月 1 日结束,那么用户通常仍可访问到 4 月 1 日。题目中”3 月 15 日取消,但访问到 3 月 1 日”的时间顺序不成立,周期结束时间应晚于取消时间。
客户端应做什么
客户端应负责获取购买凭证并交给服务端验证,不应自行计算续订周期。
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(
purchaseToken = purchase.purchaseToken,
products = purchase.products
)
}
}
}
不要按下面的方式计算订阅到期时间:
val assumedExpiryTime =
purchase.purchaseTime + 30L * 24L * 60L * 60L * 1000L
这种算法并不可靠。订阅周期未必固定为 30 天,还可能受以下情况影响:
- 不同月份的天数不同;
- 价格或套餐发生变更;
- 升级、降级以及延期生效;
- 免费试用或优惠阶段;
- 订阅暂停;
- 宽限期和账号保留;
- 补偿时间或其他 Play 侧状态变化。
服务端查询方式
服务端收到 purchaseToken 后,应通过 Google Play Developer API 的订阅查询接口获取状态。具体请求类型取决于当前采用的 API 版本。对于较新的订阅模型,通常需要查询订阅购买状态,并读取当前订阅项的到期时间和续订状态。
下面只展示应用自身的服务端接口结构,以免将 Google 凭据放入 Android 客户端:
POST /api/subscriptions/verify
Content-Type: application/json
{
"packageName": "com.example.app",
"productId": "premium_monthly",
"purchaseToken": "..."
}
服务端可以返回经过验证的权益信息:
{
"entitled": true,
"expiryTime": "2026-04-01T00:00:00Z",
"autoRenewEnabled": false
}
客户端只根据服务端返回的权益状态开放内容:
data class SubscriptionEntitlement(
val entitled: Boolean,
val expiryTime: String?,
val autoRenewEnabled: Boolean
)
其中:
entitled表示用户当前是否仍有访问权;expiryTime表示当前已确认计费周期的结束时间;autoRenewEnabled = false表示自动续订已经关闭,但当前权益不会因此立即失效。
如何获得续订次数
如果业务必须统计成功续订次数,应在服务端维护交易事件,不要解析客户端的 orderId。
可以采用以下流程:
- 首次验证购买时创建订阅记录;
- 接收 Real-time Developer Notifications;
- 收到通知后,再次调用 Developer API 查询真实状态;
- 根据唯一交易标识进行幂等处理;
- 每确认一个新的成功计费周期,就记录一次续订事件;
- 根据事件记录统计续订次数。
不能直接用当前时间减去首次购买时间,再除以订阅周期。订阅期间可能发生暂停、延期、宽限期、套餐变更或支付失败,这些情况都会使计算结果不准确。
如果没有从订阅开始时保存事件,也不查询服务端 API,就无法只凭当前客户端的 Purchase 对象准确还原过去的续订次数。
完全不使用 Developer API 时的限制
客户端可以保存每次观察到的购买状态,但这些数据只能用作缓存或辅助信息:
data class LocallyObservedPurchase(
val purchaseToken: String,
val firstPurchaseTime: Long,
val observedAt: Long,
val purchaseState: Int
)
这种方案有几个明显的问题:
- 用户可能长期不打开应用,客户端无法观察到续订事件;
- 换设备、清除数据或重装应用后,本地记录可能丢失;
- 客户端时间可以被修改;
- 支付失败、恢复和退款等变化可能发生在应用未运行期间;
- 无法根据相同的
purchaseToken和purchaseTime推导续订次数; - 这些数据不应作为处理高价值权益或服务端资源访问的依据。
如果需求只是判断当前设备上是否存在已购买的订阅,Billing Library 可以满足。如果需要知道订阅何时到期、是否还会续订、最后一次扣款时间以及历史续订次数,就需要由服务端进行验证。Google Play Developer API 是订阅状态的权威数据来源。
备注:内容仅供参考。
