PAYATHON 2026

如何在 backend 验证 Google Subscription 的 base plan

支付老李

结论

调用 purchases.subscriptionsv2.get 获取订阅详情后,可以从以下字段读取用户购买的 Base Plan ID:

lineItems[].offerDetails.basePlanId

Base Plan ID 不在响应的顶层字段中,也不会拼接在 productId 里。productId 表示 Subscription ID,而 Base Plan 和 Offer 属于具体的订阅条目,所以相关信息放在 lineItems 中。

请求方式

调用以下接口:

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

参数和权限要求如下:

  • packageName:应用包名。
  • token:客户端购买成功后取得的 purchase token。
  • 请求需要使用有权访问该应用的 service account。
  • OAuth scope 为:
https://www.googleapis.com/auth/androidpublisher

响应中的相关结构如下:

{
  "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
  "lineItems": [
    {
      "productId": "premium_subscription",
      "expiryTime": "2026-10-14T08:30:00Z",
      "offerDetails": {
        "basePlanId": "monthly",
        "offerId": "intro-offer",
        "offerTags": [
          "new-user"
        ]
      },
      "autoRenewingPlan": {
        "autoRenewEnabled": true
      }
    }
  ]
}

各字段的对应关系如下:

Subscription ID = lineItems[].productId
Base Plan ID   = lineItems[].offerDetails.basePlanId
Offer ID       = lineItems[].offerDetails.offerId

例如,用户购买了 premium_subscription 下的月付方案,返回结果可能是:

productId: premium_subscription
basePlanId: monthly

Node.js 示例

下面的示例使用 googleapis 和 service account:

import { google } from "googleapis";

const auth = new google.auth.GoogleAuth({
  credentials: {
    client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
    private_key: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(
      /\\n/g,
      "\n"
    ),
  },
  scopes: ["https://www.googleapis.com/auth/androidpublisher"],
});

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

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

  const purchase = response.data;

  const items = (purchase.lineItems ?? []).map((item) => ({
    productId: item.productId,
    basePlanId: item.offerDetails?.basePlanId ?? null,
    offerId: item.offerDetails?.offerId ?? null,
    expiryTime: item.expiryTime,
    autoRenewEnabled:
      item.autoRenewingPlan?.autoRenewEnabled ?? false,
  }));

  return {
    subscriptionState: purchase.subscriptionState,
    items,
  };
}

调用方式:

const result = await getSubscription(
  "com.example.app",
  purchaseToken
);

for (const item of result.items) {
  console.log("Subscription ID:", item.productId);
  console.log("Base Plan ID:", item.basePlanId);
  console.log("Offer ID:", item.offerId);
}

原始 HTTP 示例

取得 service account 的 OAuth 2.0 access token 后,也可以直接发送请求:

curl \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.example.app/purchases/subscriptionsv2/tokens/PURCHASE_TOKEN"

不要把 Base Plan ID 作为旧接口的 subscriptionId 传入。Base Plan 不是独立的 Google Play Product,而是从属于 Subscription。

为什么旧接口看不到 Base Plan

旧版接口:

purchases.subscriptions.get

使用旧订阅模型,调用时需要同时传入 subscriptionId 和 purchase token。它的响应结构没有针对 Base Plan 和 Offer 设计,因此不适合处理新的订阅目录模型。

新版接口:

purchases.subscriptionsv2.get

主要根据 purchase token 查询,并使用 lineItems 描述用户购买的订阅项目。每个条目的 Base Plan 信息都在 offerDetails 中,不在响应顶层。

注意事项

不要固定读取第一个 lineItems

订阅发生升级、降级、替换或延期时,响应中可能有多个条目,因此不要无条件读取:

purchase.lineItems[0]

应该遍历全部 lineItems,再结合以下字段确定需要处理的条目:

productId
expiryTime
autoRenewingPlan
prepaidPlan
deferredItemReplacement

如何选择取决于具体业务需求,例如查看当前权益、续订方案或尚未生效的替换方案。

offerId 和 basePlanId 不是同一个字段

Base Plan 定义计费周期、续订类型等基础规则,Offer 则是附加在 Base Plan 上的优惠配置。用户未使用 Offer 时,offerId 可能不存在,但仍需检查 basePlanId。

代码中应该使用可选访问:

const basePlanId = item.offerDetails?.basePlanId ?? null;

不能因为 offerId 为空,就认定 Base Plan 无效。

兼容旧购买记录

如果 purchase token 来自旧订阅体系,或者返回记录中没有 offerDetails,后端应该允许 basePlanId 为 null。不要根据价格或到期时间强行推断 Base Plan,因为促销、税费、价格迁移和区域定价都可能造成误判。

Service account 需要 Play Console 权限

创建 service account 并下载密钥后,还需要在 Google Play Console 中授予它访问对应应用和订阅订单所需的权限,否则接口可能返回 401 或 403。

不要只依赖客户端上报的 Base Plan ID

客户端可以将用户选择的 Base Plan ID 一并发送给后端,供业务处理时参考,但不能将其视为可信的支付凭据。最终应以 Google Play Developer API 返回的以下字段为准:

lineItems[].offerDetails.basePlanId

后端还应同时验证 purchase token、应用包名、productId、订阅状态和有效期。

备注:内容仅供参考。