如何通过 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 暂时失败不代表订阅已经失效。服务端应区分“明确无权益”和“暂时无法验证”,避免因短暂故障错误回收用户权益。
备注:内容仅供参考。