PAYATHON 2026

如何从 backend server 获取 Google Play 订阅到期日期

支付老李

结论

backend server 应通过 Google Play Developer API(Android Publisher API) 查询订阅状态,建议使用:

purchases.subscriptionsv2.get

查询需要两个主要参数:

  • packageName:应用包名,例如 com.example.app
  • token: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 获取权威状态,不能只根据通知内容修改用户权益。

备注:内容仅供参考。