PAYATHON 2026

如何确定 Google Play Store 订阅的到期时间?

支付阿杰

结论

不要在应用端用“购买时间 + 1 个月”计算订阅到期时间。Google Play Billing 客户端返回的购买记录没有可靠的到期时间,续订后也不一定会产生一条新的客户端 Transaction。

正确的处理方式是:

  1. 应用通过 Google Play Billing 获取 purchaseToken。
  2. 将 purchaseToken 和订阅商品 ID 发送到自己的服务器。
  3. 服务器调用 Google Play Developer API 的 purchases.subscriptionsv2.get。
  4. 从返回结果的 lineItems[].expiryTime 中读取当前订阅周期的结束时间。
  5. 通过 Real-time developer notifications(RTDN)或定期重新查询,处理续订、取消、宽限期、暂停等状态变化。

旧系统可能仍在使用 purchases.subscriptions.get,其响应中包含 expiryTimeMillis。新实现应优先使用 subscriptionsv2.get。

为什么不能自行计算

“月度订阅”并不等于固定的 30 天。实际到期时间会受多种情况影响:

  • 每个月的天数不同;
  • 自动续订的扣款时间可能发生变化;
  • 用户取消订阅后,通常仍可使用到当前周期结束;
  • 付款失败后,订阅可能进入宽限期或账号保留期;
  • 用户可能升级、降级或切换订阅方案;
  • 订阅可能被暂停、撤销或退款;
  • Google Play 可能调整续订时间;
  • 测试订阅的周期与正式环境不同。

首次购买时算出的时间即使暂时准确,也可能在续订或状态变化后失效。

续订时是否会返回新的 Transaction

不能根据“是否出现新的 Transaction”判断续订是否成功。

自动续订通常由 Google Play 在后台完成。此时应用可能没有运行,也不保证立即收到一条可以按新交易处理的客户端回调。续订后,订阅可能继续使用原来的 purchaseToken。在重新订阅、替换订阅、升级或降级等场景中,令牌也可能变化。

客户端查询到的 Purchase 主要用于确认购买权利并取得 purchaseToken,不能视为完整的续订历史。每次续订后的准确状态和到期时间,都应由服务器重新向 Google Play 查询。

推荐实现流程

1. 客户端取得购买令牌

购买成功或通过 queryPurchasesAsync() 恢复购买后,将令牌发送到业务服务器:

val token = purchase.purchaseToken

subscriptionApi.verifySubscription(
    productId = productId,
    purchaseToken = token
)

不要把 Google Play Developer API 的服务账号凭据放入 APK,也不要直接从 Android 客户端调用该 API。

2. 服务器查询订阅状态

服务器通过有权访问对应 Play Console 应用的身份获取 OAuth 2.0 访问令牌,然后调用:

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

响应中会包含订阅状态和一个或多个 lineItems,例如:

{
  "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
  "lineItems": [
    {
      "productId": "monthly_subscription",
      "expiryTime": "2026-09-29T08:30:00Z",
      "autoRenewingPlan": {
        "autoRenewEnabled": true
      }
    }
  ]
}

expiryTime 是 Google Play 当前记录的周期结束时间,采用 RFC 3339 格式,通常以 UTC 表示。

如果响应中有多个 lineItems,应按照业务对应的 productId、订阅方案和状态选择正确的项目,不能直接读取数组第一项。

3. 由服务器返回适合展示的数据

服务器可以解析响应,并向客户端返回统一格式的数据:

function buildSubscriptionView(subscription, expectedProductId) {
  const item = subscription.lineItems?.find(
    lineItem => lineItem.productId === expectedProductId
  );

  if (!item?.expiryTime) {
    throw new Error("Subscription expiry time is unavailable");
  }

  return {
    state: subscription.subscriptionState,
    expiryTime: item.expiryTime,
    autoRenewEnabled: item.autoRenewingPlan?.autoRenewEnabled ?? false
  };
}

Android 客户端收到时间后,再按用户所在时区格式化:

val expiryInstant = Instant.parse(response.expiryTime)

val displayTime = DateTimeFormatter
    .ofPattern("yyyy年M月d日 HH:mm")
    .withZone(ZoneId.systemDefault())
    .format(expiryInstant)

页面文案应区分“到期”和“下次续订”

expiryTime 表示用户当前已获得权益的周期结束时间。页面如何展示,还要结合自动续订状态:

  • autoRenewEnabled == true:适合显示“下次续订时间”或“当前周期至……”
  • autoRenewEnabled == false:适合显示“订阅将于……到期”
  • 订阅处于宽限期、暂停、账号保留或撤销状态:应根据 subscriptionState 显示相应提示

自动续订开启时,expiryTime 不代表用户一定会在该时间失去权益。续订扣款成功后,这个时间会更新到下一个周期。

如何保持时间最新

只在首次购买后查询一次并长期缓存,无法保证数据准确。可以采用以下方式:

  • 首次购买后,立即由服务器校验;
  • 应用启动或用户进入订阅页面时,从业务服务器获取最新状态;
  • 配置 RTDN,在续订、取消、暂停或恢复等事件发生后重新查询 Developer API;
  • 定期校验,以处理通知丢失或处理失败的情况;
  • 根据服务器保存的最新查询结果决定用户权益,客户端缓存只用于短暂展示。

RTDN 只负责通知服务器“订阅状态发生了变化”。不要直接根据通知内容推算新的到期时间。收到通知后,应再次调用 subscriptionsv2.get 获取权威状态。

旧接口说明

旧版服务端实现可能调用:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{purchaseToken}

响应中的 expiryTimeMillis 是以 Unix Epoch 毫秒表示的到期时间:

const expiryDate = new Date(Number(response.expiryTimeMillis));

这个接口与客户端使用的 Google Play Billing API 版本不是同一个概念。即使应用使用较旧的 Billing 客户端,也不需要自行计算到期时间。到期信息仍应通过服务端 Google Play Developer API 获取。

新代码应优先使用 purchases.subscriptionsv2.get,并根据实际返回的订阅状态和 lineItems 进行处理。

备注:内容仅供参考。