PAYATHON 2026

如何通过 Purchase ID 检查 Google 订阅状态?

支付阿杰

结论

不能只凭 purchaseID 或 transactionId 查询 Google Play 订阅状态。Google Play Developer API 使用 purchase token 查询订阅。

在 Flutter 的 in_app_purchase 插件中,Android 购买记录的 token 通常可以从以下字段取得:

purchaseDetails.verificationData.serverVerificationData

Flutter 客户端应将 purchase token 和商品 ID 发给自己的服务端,再由服务端调用 Google Play Developer API 验证订阅。不要在 App 内直接调用该 API,否则会暴露服务账号凭据。

为什么不能使用 transactionId

这些字段各有用途:

  • purchaseID / transactionId:通常对应 Google Play 订单号,用于识别订单、客服查询或对账。
  • productID:订阅商品 ID,例如 premium_monthly。
  • purchaseToken:Google Play 为该笔购买签发的令牌,用于查询订阅状态和确认购买。

Google Play 的订阅查询接口不接受订单号作为查询凭据。即使续订产生了新的订单号,也应根据 purchase token,以及接口返回的到期时间和订阅状态等字段判断订阅生命周期。

Flutter 客户端获取 purchase token

监听购买更新后,可以从 PurchaseDetails 中取得服务端验证数据:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:in_app_purchase/in_app_purchase.dart';

Future<void> handlePurchase(PurchaseDetails purchaseDetails) async {
  if (purchaseDetails.status == PurchaseStatus.purchased ||
      purchaseDetails.status == PurchaseStatus.restored) {
    final purchaseToken =
        purchaseDetails.verificationData.serverVerificationData;

    final response = await http.post(
      Uri.parse('https://api.example.com/google-play/verify-subscription'),
      headers: {
        'Content-Type': 'application/json',
      },
      body: jsonEncode({
        'productId': purchaseDetails.productID,
        'purchaseToken': purchaseToken,
      }),
    );

    if (response.statusCode != 200) {
      throw Exception('Subscription verification failed');
    }

    final result = jsonDecode(response.body) as Map<String, dynamic>;

    if (result['entitled'] == true) {
      // 在应用中解锁订阅权益
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await InAppPurchase.instance.completePurchase(purchaseDetails);
    }
  }
}

packageName 最好配置在服务端,不要信任客户端传入的包名。服务端还应检查商品 ID 是否属于当前应用允许销售的商品。

可以按照插件提供的 purchaseStream 监听购买:

final Stream<List<PurchaseDetails>> purchaseUpdated =
    InAppPurchase.instance.purchaseStream;

late final StreamSubscription<List<PurchaseDetails>> subscription;

void listenToPurchases() {
  subscription = purchaseUpdated.listen(
    (purchases) async {
      for (final purchase in purchases) {
        await handlePurchase(purchase);
      }
    },
    onError: (Object error) {
      // 记录并处理购买流错误
    },
  );
}

客户端显示“购买成功”只表示购买流程已经完成,不能代替服务端验证。

服务端查询订阅状态

如果继续使用问题中提到的 purchases.subscriptions.get,请求格式如下:

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

各参数对应如下:

packageName     = Android 应用包名
subscriptionId  = purchaseDetails.productID
token           = purchaseDetails.verificationData.serverVerificationData

调用方需要使用有权访问该应用订单数据的服务账号,并取得包含以下 OAuth scope 的 access token:

https://www.googleapis.com/auth/androidpublisher

较新的集成可以优先评估 purchases.subscriptionsv2.get。该接口通过包名和 purchase token 查询,并返回更明确的订阅生命周期状态:

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

Google Play Developer API 的推荐接口和字段可能调整,具体选择应以项目当前使用的 API 文档及响应结构为准。

Node.js 服务端示例

以下示例使用 googleapis 调用 purchases.subscriptionsv2.get:

import { google } from 'googleapis';

const auth = new google.auth.GoogleAuth({
  scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});

const androidPublisher = google.androidpublisher({
  version: 'v3',
  auth,
});

export async function verifySubscription(req, res) {
  const { productId, purchaseToken } = req.body;

  if (!productId || !purchaseToken) {
    return res.status(400).json({
      error: 'Missing productId or purchaseToken',
    });
  }

  try {
    const packageName = 'com.example.app';

    const response =
      await androidPublisher.purchases.subscriptionsv2.get({
        packageName,
        token: purchaseToken,
      });

    const subscription = response.data;
    const lineItem = subscription.lineItems?.find(
      (item) => item.productId === productId,
    );

    const expiryTime = lineItem?.expiryTime
      ? Date.parse(lineItem.expiryTime)
      : 0;

    const entitled =
      expiryTime > Date.now() &&
      subscription.subscriptionState !==
        'SUBSCRIPTION_STATE_EXPIRED';

    return res.json({
      entitled,
      subscriptionState: subscription.subscriptionState,
      expiryTime: lineItem?.expiryTime ?? null,
    });
  } catch (error) {
    console.error('Google Play verification failed', error);

    return res.status(502).json({
      entitled: false,
      error: 'Unable to verify subscription',
    });
  }
}

这段代码只演示了基本的状态判断。生产环境还需要按照业务规则处理宽限期、账号保留、暂停、撤销和待处理交易等状态。

如果继续使用旧版 subscriptions.get

旧版接口通常会返回以下字段:

{
  "expiryTimeMillis": "1735689600000",
  "autoRenewing": true,
  "paymentState": 1,
  "acknowledgementState": 1
}

判断用户当前是否仍有权益时,至少要检查:

const expiryTime = Number(subscription.expiryTimeMillis);
const entitled = Number.isFinite(expiryTime) && expiryTime > Date.now();

不能只看 autoRenewing:

  • autoRenewing: false 可能表示用户已经取消续订,但当前计费周期还没有结束。
  • 在 expiryTimeMillis 之前,用户通常仍享有已支付周期内的权益。
  • 退款、撤销、宽限期和账号保留还需要结合其他状态字段判断。

恢复购买后的处理

应用重新安装、用户更换设备或重新登录后,可以调用:

await InAppPurchase.instance.restorePurchases();

恢复结果同样会通过 purchaseStream 返回。收到恢复记录后,仍应将 purchase token 发给服务端重新验证,不能因为状态是 PurchaseStatus.restored 就直接永久解锁权益。

服务端最好保存以下信息:

userId
packageName
productId
purchaseToken
subscriptionState
expiryTime
lastVerifiedAt

还要确保一个 purchase token 不会与多个业务账号重复绑定,除非产品明确允许共享订阅。

注意事项

  • 不要把服务账号 JSON、私钥或 OAuth 凭据放入 Flutter 应用。
  • 不要只根据 PurchaseStatus.purchased、订单号或客户端缓存授予长期权益。
  • purchase token 是敏感的购买凭据,不要将完整内容写入普通日志或错误上报。
  • 服务端应验证 token 对应的包名、商品 ID 和当前登录用户。
  • 验证完成后应及时确认购买。使用插件时通常通过 completePurchase() 完成。如果改为服务端确认,不要在两端设置相互冲突的确认流程。
  • 只在用户打开 App 时查询并不可靠。如果需要及时处理续订、退款和撤销,应在服务端接入 Google Play Real-time Developer Notifications,并在收到通知后再次调用 Developer API 获取最终状态。
  • 网络或 Google API 暂时失败不代表订阅已经失效。服务端应区分“明确无权益”和“暂时无法验证”,避免因短暂故障错误回收用户权益。

备注:内容仅供参考。