PAYATHON 2026

Play Store Billing 如何获取最后一次订阅续订日期

支付小周

结论

只使用客户端的 Google Play Billing Library,无法可靠获取订阅最后一次成功续订的时间、续订次数,也无法准确判断当前计费周期何时结束。

queryPurchases() 和 queryPurchasesAsync() 返回的是当前有效的购买权益,不是每个续订周期的交易流水。对于自动续订订阅,客户端获取的 purchaseTime 通常是这条订阅购买链的首次购买时间,orderId 也不能作为统计续订次数的可靠依据。

可靠的处理方式如下:

  1. 客户端取得 purchaseToken。
  2. 客户端将 purchaseToken 发送给可信服务端。
  3. 服务端通过 Google Play Developer API 查询订阅状态。
  4. 如果需要及时跟踪续订、取消、暂停和宽限期,再接入 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。

可以采用以下流程:

  1. 首次验证购买时创建订阅记录;
  2. 接收 Real-time Developer Notifications;
  3. 收到通知后,再次调用 Developer API 查询真实状态;
  4. 根据唯一交易标识进行幂等处理;
  5. 每确认一个新的成功计费周期,就记录一次续订事件;
  6. 根据事件记录统计续订次数。

不能直接用当前时间减去首次购买时间,再除以订阅周期。订阅期间可能发生暂停、延期、宽限期、套餐变更或支付失败,这些情况都会使计算结果不准确。

如果没有从订阅开始时保存事件,也不查询服务端 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 是订阅状态的权威数据来源。

备注:内容仅供参考。