PAYATHON 2026

如何获取 subscription 的最后续订时间和结束时间?

数据小陈

结论

Purchase.getPurchaseTime() 不能提供订阅最近一次续订的时间。对于订阅,这个值表示订阅关系最初建立的时间,不能用于计算当前订阅周期何时结束。

正确的处理流程如下:

  1. Android 客户端通过 queryPurchasesAsync() 获取 purchaseToken。
  2. 将 purchaseToken 发送到自己的服务端。
  3. 服务端调用 Google Play Developer API 的 purchases.subscriptionsv2.get。
  4. 从响应中的 lineItems[].expiryTime 读取当前订阅周期的结束时间。
  5. 如果业务必须记录续订发生时间,应接入 Real-time Developer Notifications(RTDN)。收到 SUBSCRIPTION_RENEWED 后,立即重新查询订阅状态并保存事件记录。

判断用户是否仍有订阅权益时,通常只需要可信的 expiryTime 和订阅状态,无需自行推算“最后续订时间”。

为什么不能在客户端直接获取

queryPurchasesAsync() 返回的是用户当前拥有的购买项目,并非完整的订阅交易流水。

同一个订阅可能自动续订多次,但 purchaseToken 和最初购买时间不会像普通一次性商品那样,在每次续订后重新生成。因此:

purchase.purchaseTime

不能表示最近一次扣款或续订的时间。

也不要使用下面的公式计算结束时间:

结束时间 = purchaseTime + 订阅周期

这种算法无法正确处理多次续订、升级或降级、延期、暂停、宽限期、账号保留期和预付费订阅等情况。

获取订阅结束时间

客户端只需获取并上传 purchaseToken:

billingClient.queryPurchasesAsync(
    QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.SUBS)
        .build()
) { billingResult, purchases ->
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        purchases.forEach { purchase ->
            uploadPurchaseTokenToServer(
                productIds = purchase.products,
                purchaseToken = purchase.purchaseToken
            )
        }
    }
}

服务端使用自己的 Google Play API 凭据发起请求:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{purchaseToken}
Authorization: Bearer {accessToken}

收到响应后,主要检查订阅状态和 lineItems:

{
  "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
  "latestOrderId": "GPA.1234-5678-9012-34567..0",
  "lineItems": [
    {
      "productId": "premium_monthly",
      "expiryTime": "2026-09-30T12:34:56Z"
    }
  ]
}

expiryTime 是 RFC 3339 格式的时间戳,表示对应订阅项目当前权益的截止时间。服务端应按 UTC 时间解析并保存,例如:

const expiryTime = new Date(subscription.lineItems[0].expiryTime);

if (Number.isNaN(expiryTime.getTime())) {
  throw new Error("Invalid subscription expiryTime");
}

const entitlementActive = expiryTime.getTime() > Date.now();

实际业务中,不能假定 lineItems 永远只有一个。套餐变更或其他订阅状态转换可能产生多个项目,需要结合 productId、订阅状态和各项目的 expiryTime 判断具体权益。

如何记录最近一次续订时间

purchases.subscriptionsv2.get 主要用于返回当前订阅状态和到期时间,并不保证包含可以直接使用的“最后续订时间”字段。

如果业务确实需要记录续订时间,应在服务端按以下方式处理:

  1. 配置 Google Play RTDN。
  2. 接收 SUBSCRIPTION_RENEWED 通知。
  3. 使用通知中的 purchaseToken 调用 purchases.subscriptionsv2.get。
  4. 保存新的 expiryTime、latestOrderId、订阅状态和通知事件时间。
  5. 根据 purchaseToken、订单号或事件进行幂等处理,避免重复通知导致重复记账。

RTDN 中的 eventTimeMillis 更适合用作通知事件时间。在要求严格财务精度的场景中,不应直接将它视为扣款完成时间。如果需要精确的交易入账时间,应通过 latestOrderId 对应的订单信息或 Google Play 财务报表进行核对。具体可用字段取决于当前启用的 Google Play API 和报表能力。

需要注意的状态

仅比较 expiryTime 无法覆盖所有业务规则,还需要检查 subscriptionState。例如:

  • 用户取消自动续订后,在 expiryTime 之前通常仍有权益。
  • 宽限期内是否继续提供权益,由产品策略决定。
  • 账号保留期可能表示付款失败,且权益已经暂停。
  • 暂停、撤销和过期状态不能只根据客户端缓存判断。
  • 预付费订阅可能不会自动续订,延期方式也有所不同。
  • 套餐升级或降级后,不能按固定周期倒推开始时间。

订阅权益应由服务端最终判定,并以 Google Play Developer API 返回的最新状态和 expiryTime 为准。客户端数据只能用于发起验证,不能作为可信的订阅到期依据。

备注:内容仅供参考。