PAYATHON 2026

如何通过 Google Android Subscription API 获取订阅到期时间?

技术老齐

结论

如需通过购买令牌查询订阅到期时间,应由可信的服务端调用 Google Play Developer API。新实现建议使用 purchases.subscriptionsv2.get,并从响应的 lineItems[].expiryTime 中读取到期时间。

示例请求:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.kizbit.pairfinder/purchases/subscriptionsv2/tokens/SUBSCRIPTION_TOKEN
Authorization: Bearer ACCESS_TOKEN

旧版 purchases.subscriptions.get 返回的是 expiryTimeMillis,新项目不建议再依赖这个接口。

题目中的 URL 结构与当年的 v1 文档基本一致。由于响应中没有错误正文,仅凭 503 Service Unavailable 无法判断 access token 是否过期,也不能确定是 URL 拼写或配置出了问题。503 表示 Google API 当时未能处理请求,原因可能是临时服务异常,也可能是早期接口的服务端问题。身份凭证无效通常返回 401,权限或关联配置有误通常返回 403。

推荐调用方式

访问令牌应放在 Authorization 请求头中,不要作为查询参数写进 URL:

curl --fail-with-body \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Accept: application/json" \
  "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.kizbit.pairfinder/purchases/subscriptionsv2/tokens/${SUBSCRIPTION_TOKEN}"

典型的响应结构如下。实际字段会随订阅状态和订阅模型而变化:

{
  "kind": "androidpublisher#subscriptionPurchaseV2",
  "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
  "latestOrderId": "GPA.1234-5678-9012-34567",
  "lineItems": [
    {
      "productId": "subscription99",
      "expiryTime": "2026-09-30T12:34:56Z"
    }
  ]
}

expiryTime 使用 RFC 3339 UTC 时间格式。一个订阅可能包含多个 lineItems,不能直接假定数组第一项就是目标项目。应结合 productId、当前时间和业务规则进行选择。例如:

const response = await fetch(
  `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(packageName)}/purchases/subscriptionsv2/tokens/${encodeURIComponent(purchaseToken)}`,
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      Accept: "application/json"
    }
  }
);

const body = await response.json();

if (!response.ok) {
  throw new Error(`Google Play API ${response.status}: ${JSON.stringify(body)}`);
}

const matchingItems = (body.lineItems ?? []).filter(
  item => item.productId === subscriptionId && item.expiryTime
);

const expiryTimes = matchingItems.map(item => new Date(item.expiryTime));
const expiryTime =
  expiryTimes.length > 0
    ? new Date(Math.max(...expiryTimes.map(date => date.getTime())))
    : null;

使用旧接口时

旧接口的调用形式如下:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}
Authorization: Bearer ACCESS_TOKEN

对应的命令是:

curl --fail-with-body \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.kizbit.pairfinder/purchases/subscriptions/subscription99/tokens/${SUBSCRIPTION_TOKEN}"

成功响应中的 expiryTimeMillis 是 Unix Epoch 毫秒值:

{
  "startTimeMillis": "1754000000000",
  "expiryTimeMillis": "1756678400000",
  "autoRenewing": true
}

转换方式:

const expiryTime = new Date(Number(result.expiryTimeMillis));

expiryTimeMillis 已经以毫秒为单位,不要将它误按秒处理:

// 正确:expiryTimeMillis 已经是毫秒
new Date(Number(result.expiryTimeMillis));

调用前需要确认

  • SUBSCRIPTION_TOKEN 必须是 Google Play Billing 返回的 purchaseToken,不能使用订单号、商品 ID、混淆后的账号 ID 或自定义 payload。
  • packageName 必须与生成该购买令牌的应用完全一致。
  • 使用旧接口时,subscriptionId 必须对应此次购买的订阅商品。subscriptionsv2.get 不再要求将商品 ID 写入路径。
  • 调用方需要具备正确的 OAuth scope,通常为:
https://www.googleapis.com/auth/androidpublisher
  • Google Cloud 项目需要启用 Google Play Android Developer API。
  • 服务账号或 OAuth 用户必须在相应的 Play Console 开发者账号中获得查看订单和订阅数据所需的权限。
  • 接口应由服务端调用,不要将服务账号密钥或可刷新的 OAuth 凭证放入 Android 客户端。

为什么示例请求返回 503

这个响应表明请求已经到达 Google API,但服务端当时未能完成处理。它不表示授权令牌已经过期:

  • 401 Unauthorized:令牌缺失、无效或已过期。
  • 403 Forbidden:API 未启用、账号未授权、Play Console 权限不足或配额受限等。
  • 400 或 404:参数、应用、商品或购买令牌不匹配,具体状态取决于接口。
  • 429:请求频率过高或受到配额限制。
  • 500、502、503、504:通常应按临时服务端故障处理。

示例使用的是 2012 年的 v1 接口,响应中又没有 JSON 错误正文,因此现有信息不足以判断 Google 内部失败的具体原因。处理时应记录响应头和请求时间,并采用指数退避重试,例如:

const retryableStatus = new Set([429, 500, 502, 503, 504]);
const delays = [1000, 2000, 4000, 8000, 16000];

for (let attempt = 0; attempt <= delays.length; attempt++) {
  const response = await callGooglePlay();

  if (response.ok) {
    return await response.json();
  }

  const errorBody = await response.text();

  if (!retryableStatus.has(response.status) || attempt === delays.length) {
    throw new Error(`Google Play API ${response.status}: ${errorBody}`);
  }

  const jitter = Math.floor(Math.random() * 500);
  await new Promise(resolve => setTimeout(resolve, delays[attempt] + jitter));
}

如果请求持续返回 503,可以先改用当前的 v3 subscriptionsv2 地址重新测试,然后检查 Google Cloud API 是否已启用、Play Console 权限是否正确,以及令牌与包名是否匹配。如果这些项目都没有问题,且故障能够稳定复现,再将请求时间、HTTP 状态、响应正文和 Google 返回的请求标识一并提供给 Google Play 开发者支持。

示例中还公开了 OAuth access token。即使该令牌很可能早已失效,只要有效凭证曾出现在日志、工单或公开页面中,都应立即撤销并重新签发。

备注:内容仅供参考。