In-App Billing 中 ProductID 和 SubscriptionID 是否相同?
明确结论
是的。对于订阅商品,getPurchases() 返回的 productId 就是旧版 purchases.subscriptions.get API 路径参数中的 subscriptionId。
验证订阅时,各参数的来源如下:
| 服务端参数 | 来源 |
|---|---|
packageName | 应用包名,建议在服务端配置 |
subscriptionId | 客户端购买记录中的 productId |
token | 客户端购买记录中的 purchaseToken |
旧版请求格式如下:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}
这里的 subscriptionId 并不是另一套独立 ID。API 使用这个参数名,只是为了表明它是订阅商品的 ID。
为什么有 products 和 subscriptions 两个 API
Google Play 将可售项目统称为商品,但会根据购买类型使用不同的 API:
products:一次性应用内商品,也就是旧文档中的 managed product,客户端类型通常是inapp。subscriptions:订阅商品,客户端类型是subs,包含续订、取消、宽限期、暂停和到期等生命周期状态。
两类购买使用的都是在 Play Console 中配置的商品 ID。它们的区别不在于 ID 来源,而在于购买模型、状态字段和服务端验证接口。
具体对应关系如下:
| Play 商品类型 | 客户端商品 ID | 旧版服务端验证 API |
|---|---|---|
| 一次性商品 | productId | purchases.products.get |
| 订阅商品 | productId | purchases.subscriptions.get,作为 subscriptionId 传入 |
因此,不能只根据参数名称判断它们是否为同一种 ID。在这个场景中,productId 和 subscriptionId 指向同一个 Play Console 商品标识,只是名称随 API 上下文不同。
服务端验证步骤
客户端完成购买后,至少需要把以下信息发送给自己的服务端:
{
"productId": "premium_monthly",
"purchaseToken": "example-purchase-token",
"productType": "subs"
}
packageName 应由服务端保存或确定,不要依赖客户端提交。验证时,再根据 productType 选择接口:
productType == "subs"
-> purchases.subscriptions.get
-> subscriptionId = productId
productType == "inapp"
-> purchases.products.get
-> productId = productId
例如,订阅商品 premium_monthly 的旧版验证请求可以写成:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.example.app/purchases/subscriptions/premium_monthly/tokens/PURCHASE_TOKEN
Authorization: Bearer ACCESS_TOKEN
一次性商品使用:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.example.app/purchases/products/remove_ads/tokens/PURCHASE_TOKEN
Authorization: Bearer ACCESS_TOKEN
服务端代码示例
以下示例使用 Node.js 和 googleapis。相关方法是否仍适用于新项目,需要以当前 Android Publisher API 文档和实际使用的 SDK 版本为准。
import { google } from "googleapis";
const auth = new google.auth.GoogleAuth({
credentials: {
client_email: process.env.GOOGLE_CLIENT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n"),
},
scopes: ["https://www.googleapis.com/auth/androidpublisher"],
});
const androidPublisher = google.androidpublisher({
version: "v3",
auth,
});
async function verifySubscription({
packageName,
productId,
purchaseToken,
}) {
const response = await androidPublisher.purchases.subscriptions.get({
packageName,
subscriptionId: productId,
token: purchaseToken,
});
return response.data;
}
一次性商品的验证逻辑如下:
async function verifyOneTimeProduct({
packageName,
productId,
purchaseToken,
}) {
const response = await androidPublisher.purchases.products.get({
packageName,
productId,
token: purchaseToken,
});
return response.data;
}
调用订阅验证方法时,直接传入对应字段:
await verifySubscription({
packageName: "com.example.app",
productId: purchase.productId,
purchaseToken: purchase.purchaseToken,
});
不要只保存 purchaseToken
purchaseToken 是服务端验证购买的核心凭据,但通常还需要保存:
productIdpurchaseToken- 商品类型,即
subs或inapp - 用户账户或业务用户 ID
- 服务端首次验证时间
- 服务端最后一次确认的购买状态
- 必要时保存订单号,但不要将订单号用作验证凭据
如果只保存 purchaseToken,服务端调用旧版订阅接口时仍会缺少 subscriptionId。没有保存 productId 的情况下,只能额外恢复购买记录,或者查询自己的业务映射。
服务端也不能只凭客户端提交的数据授予权益。客户端数据的作用是告诉服务端需要验证哪笔购买,最终状态应以 Google Play 服务端的返回结果为准。
订阅取消不等于立即失效
订阅生命周期不能简单地处理成 purchased -> cancelled -> invalid。
用户关闭自动续订后,通常仍然可以使用已经付费的剩余订阅期。服务端需要分别判断:
- 当前是否拥有有效权益
- 是否会自动续订
- 当前订阅周期何时结束
- 是否处于宽限期、账号保留、暂停或撤销等状态
autoRenewing == false 通常只能说明订阅不会自动续订,不能单独证明订阅已经到期。授予或收回权益时,需要结合服务端返回的有效期和具体状态。
状态同步建议
不建议通过高频轮询每一笔订阅来发现所有状态变化。可以采用以下流程:
- 客户端购买后,将
productId、purchaseToken和商品类型提交给服务端。 - 服务端立即调用 Google Play Developer API 验证。
- 服务端保存 token 与业务用户的绑定关系。
- 使用 Real-time Developer Notifications 接收续订、取消、暂停和撤销等事件。
- 收到通知后,再调用 Developer API 获取权威状态。
- 在用户登录、恢复购买或权益即将到期时补充校验。
Google Play 后续增加了新版订阅查询接口,例如 purchases.subscriptionsv2.get。新系统应先查看当前官方文档推荐使用哪个接口。无论使用旧版 subscriptions.get 还是相应的新接口,购买令牌都是验证订阅的关键标识。
对于题目中的旧版 purchases.subscriptions.get,直接将客户端返回的 productId 作为 subscriptionId 即可。
备注:内容仅供参考。
