如何在 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、订阅状态和有效期。
备注:内容仅供参考。
