如何从 backend server 获取 Google Play 订阅到期日期
结论
backend server 应通过 Google Play Developer API(Android Publisher API) 查询订阅状态,建议使用:
purchases.subscriptionsv2.get
查询需要两个主要参数:
packageName:应用包名,例如com.example.apptoken:Google Play 返回的 Purchase Token
服务器不应依赖客户端传入的 OAuth Authorization Token。生产环境通常由服务器使用 Service Account 完成身份认证,并为该账号授予目标应用的订阅查询权限。
setApplicationName() 只是在 Google API 客户端中设置一个便于识别的名称,与 Play Store 应用名称、包名和商品 ID 无关,填写自定义名称即可。
调用哪个 API
建议发送以下 REST 请求:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}
Authorization: Bearer {serverAccessToken}
参数说明:
{packageName}是 Android 应用的包名。{token}是购买成功后从 Google Play Billing Library 获取的 Purchase Token。{serverAccessToken}由服务器使用 Service Account 凭据获取,不是 App 用户登录时得到的 OAuth Token。
订阅到期时间位于响应的:
lineItems[].expiryTime
expiryTime 使用 RFC 3339 时间格式,例如:
2026-09-30T08:15:30Z
还需要检查:
subscriptionState
响应中存在到期时间,不代表订阅当前一定可以正常使用。订阅可能处于宽限期、暂停、取消待到期或已过期等状态。
后台配置
先在对应的 Google Cloud 项目中启用:
Google Play Android Developer API
然后创建 Service Account,并在 Google Play Console 中授予该账号查询目标应用订阅或订单的权限。
Google Play Console 中的权限名称可能会随界面调整。只要确保该账号可以访问目标应用,并有权查看所需的订单、订阅或财务数据即可。
服务器需要妥善保存 Service Account JSON 凭据。不要:
- 将 JSON 凭据打包进 APK。
- 将私钥发送到客户端。
- 直接信任客户端提供的用户身份。
- 在日志中完整记录 Purchase Token 或 Service Account 私钥。
PHP 示例:使用 Google APIs Client Library
安装 PHP 客户端:
composer require google/apiclient
创建 Android Publisher 客户端并查询订阅:
<?php
require __DIR__ . '/vendor/autoload.php';
use Google\Client;
use Google\Service\AndroidPublisher;
$packageName = 'com.example.app';
$purchaseToken = 'PURCHASE_TOKEN_FROM_GOOGLE_PLAY';
$client = new Client();
$client->setApplicationName('Subscription Verification Backend');
$client->setAuthConfig('/secure/path/service-account.json');
$client->setScopes([
AndroidPublisher::ANDROIDPUBLISHER,
]);
$publisher = new AndroidPublisher($client);
try {
$purchase = $publisher->purchases_subscriptionsv2->get(
$packageName,
$purchaseToken
);
$subscriptionState = $purchase->getSubscriptionState();
$lineItems = $purchase->getLineItems();
$expiryTimes = [];
foreach ($lineItems as $lineItem) {
$expiryTime = $lineItem->getExpiryTime();
if ($expiryTime !== null) {
$expiryTimes[] = new DateTimeImmutable($expiryTime);
}
}
if ($expiryTimes === []) {
throw new RuntimeException('Google Play response contains no expiryTime');
}
// 多个 line item 存在时,应结合商品和业务规则判断。
// 如果目标是取得当前订阅覆盖的最晚时间,可取最大值。
$expiresAt = max($expiryTimes);
echo 'Subscription state: ' . $subscriptionState . PHP_EOL;
echo 'Expires at: ' . $expiresAt->format(DateTimeInterface::RFC3339) . PHP_EOL;
} catch (Google\Service\Exception $e) {
error_log('Google Play API request failed: ' . $e->getMessage());
throw $e;
}
不同版本的 PHP 服务类,其生成的属性或类型名称可能有所不同。如果当前安装版本中没有:
$publisher->purchases_subscriptionsv2
请检查 google/apiclient-services 中的 AndroidPublisher 服务定义。也可以直接调用 REST API,避开生成类的命名差异。
直接调用 REST API
如果只调用这一个接口,也可以通过 Google Auth 库生成服务器 Access Token,再使用 HTTP 客户端发送请求。
composer require google/auth guzzlehttp/guzzle
示例:
<?php
require __DIR__ . '/vendor/autoload.php';
use Google\Auth\Credentials\ServiceAccountCredentials;
use GuzzleHttp\Client as HttpClient;
$packageName = 'com.example.app';
$purchaseToken = 'PURCHASE_TOKEN_FROM_GOOGLE_PLAY';
$credentials = new ServiceAccountCredentials(
['https://www.googleapis.com/auth/androidpublisher'],
'/secure/path/service-account.json'
);
$authToken = $credentials->fetchAuthToken();
if (!isset($authToken['access_token'])) {
throw new RuntimeException('Unable to obtain Google access token');
}
$url = sprintf(
'https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptionsv2/tokens/%s',
rawurlencode($packageName),
rawurlencode($purchaseToken)
);
$http = new HttpClient();
$response = $http->get($url, [
'headers' => [
'Authorization' => 'Bearer ' . $authToken['access_token'],
'Accept' => 'application/json',
],
]);
$data = json_decode(
(string) $response->getBody(),
true,
512,
JSON_THROW_ON_ERROR
);
$subscriptionState = $data['subscriptionState'] ?? null;
$expiryTimes = [];
foreach ($data['lineItems'] ?? [] as $lineItem) {
if (isset($lineItem['expiryTime'])) {
$expiryTimes[] = new DateTimeImmutable($lineItem['expiryTime']);
}
}
if ($expiryTimes === []) {
throw new RuntimeException('Subscription response contains no expiryTime');
}
$expiresAt = max($expiryTimes);
echo $subscriptionState . PHP_EOL;
echo $expiresAt->format(DateTimeInterface::RFC3339) . PHP_EOL;
旧版接口
旧代码中常见的是:
purchases.subscriptions.get
对应的 PHP 调用通常写成:
$purchase = $publisher->purchases_subscriptions->get(
$packageName,
$subscriptionId,
$purchaseToken
);
$expiryTimeMillis = $purchase->getExpiryTimeMillis();
$expirySeconds = intdiv((int) $expiryTimeMillis, 1000);
$expiresAt = (new DateTimeImmutable('@' . $expirySeconds))
->setTimezone(new DateTimeZone('UTC'));
旧接口需要额外传入 subscriptionId,响应中的:
expiryTimeMillis
是 Unix Epoch 毫秒数。新实现应优先使用 purchases.subscriptionsv2.get,因为它能表示更完整的订阅生命周期和新型订阅结构。
为什么客户端 Authorization Token 不应参与查询
这里涉及两类容易混淆的 Token:
- Purchase Token:标识一笔 Google Play 购买或订阅,是查询订阅状态时使用的业务参数。
- OAuth Access Token:证明 API 调用方有权访问 Google Play Developer API,是请求的认证凭据。
客户端获取的 OAuth Token 通常代表用户身份,并不会自动获得查询开发者后台订阅数据的权限。正确的处理流程是:
App 获取 Purchase Token
↓
发送到自己的 backend server
↓
服务器使用 Service Account 认证
↓
调用 purchases.subscriptionsv2.get
↓
校验状态、商品和到期时间
↓
更新服务器中的用户权益
setApplicationName() 的作用
这行代码:
$client->setApplicationName('Subscription Verification Backend');
只用于设置 API 客户端的应用标识,可能出现在请求标识、日志或客户端信息中。它不会:
- 选择 Google Play API。
- 绑定 Play Console 应用。
- 替代
packageName。 - 指定订阅商品。
- 决定 API 权限。
调用哪个服务由以下代码决定:
new Google\Service\AndroidPublisher($client);
应用及其订阅购买记录则由以下参数定位:
packageName + purchaseToken
实际使用时的注意事项
服务器不能只比较到期时间,至少还要验证:
packageName是否属于自己的应用。- 返回的订阅商品是否与用户购买的套餐一致。
subscriptionState是否允许继续提供权益。expiryTime是否晚于当前服务器时间。- Purchase Token 是否已经绑定到其他业务账号。
- 是否存在
linkedPurchaseToken,以处理升级、降级或重新订阅后的 Token 更替。
数据库中的时间建议统一保存为 UTC,同时保留原始 Purchase Token、商品标识、订阅状态,以及最后一次向 Google Play 验证的时间。
系统可以通过轮询更新状态,但生产环境更适合配合 Google Play 实时开发者通知,处理续订、取消、退款、暂停和过期事件。收到通知后,仍需再次调用 Google Play Developer API 获取权威状态,不能只根据通知内容修改用户权益。
备注:内容仅供参考。
