如何确定 Google Play Store 订阅的到期时间?
结论
不要在应用端用“购买时间 + 1 个月”计算订阅到期时间。Google Play Billing 客户端返回的购买记录没有可靠的到期时间,续订后也不一定会产生一条新的客户端 Transaction。
正确的处理方式是:
- 应用通过 Google Play Billing 获取
purchaseToken。 - 将
purchaseToken和订阅商品 ID 发送到自己的服务器。 - 服务器调用 Google Play Developer API 的
purchases.subscriptionsv2.get。 - 从返回结果的
lineItems[].expiryTime中读取当前订阅周期的结束时间。 - 通过 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 进行处理。
备注:内容仅供参考。
