如何在Android Billing Library 6中准确获取有效订阅列表并解决状态不一致
解决Google Play订阅状态不一致与服务器端校验方案
一、客户端问题排查与优化
你遇到的跨设备状态不一致,核心原因是queryPurchasesAsync默认返回本地缓存的购买记录,而非实时从Google Play服务器拉取,导致不同设备缓存不同步。另外,仅靠isAutoRenewing()和PurchaseState判断状态也不够严谨,需要补充更多维度校验。
客户端代码优化
- 强制从服务器拉取最新数据:在
QueryPurchasesParams中添加setForceRefresh(true),跳过本地缓存 - 综合判断订阅有效性:结合过期时间、购买状态、自动续费标识,覆盖「取消自动续费但仍在订阅期」「订阅过期但缓存未更新」等场景
优化后的代码示例:
billingClient.queryPurchasesAsync( QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) .setForceRefresh(true) // 强制从服务器拉取最新数据 .build(), new PurchasesResponseListener() { public void onQueryPurchasesResponse( @NonNull BillingResult billingResult, @NonNull List<Purchase> purchases) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { long currentTime = System.currentTimeMillis(); for (Purchase purchase : purchases) { boolean isSubscriptionActive = false; // 1. 购买状态为已购买,且未过期 boolean isPurchasedAndNotExpired = purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.getExpirationTime() > currentTime; // 2. 处理自动续费状态:即使取消自动续费,只要在订阅期内仍有效 boolean isAutoRenewing = purchase.isAutoRenewing(); if (isPurchasedAndNotExpired) { isSubscriptionActive = true; // 可区分「自动续费中」和「已取消自动续费但仍在有效期」 if (isAutoRenewing) { // 活跃订阅,自动续费开启 } else { // 订阅仍有效,但已取消自动续费,到期后将终止 } } else { // 订阅已过期或未完成购买 } } } else { // 处理查询失败逻辑(比如网络问题) } } } );
二、为什么必须做服务器端校验?
客户端校验存在天然缺陷:
- 本地缓存延迟或篡改风险
- 跨设备状态无法实时同步
- 部分订阅状态变更(比如Google Play后台退款、账号封禁)无法通过客户端API感知
服务器端校验是获取订阅真实状态的唯一可靠方式,直接对接Google Play Developer API获取官方数据,不受客户端环境影响。
三、Node.js服务器端实现步骤
1. 前置准备
- 在Google Cloud Console创建项目,启用「Google Play Developer API」
- 创建服务账号,下载JSON格式的密钥文件
- 在Google Play Console的「用户权限」页面,给该服务账号授予「查看应用权限」(仅查询订阅状态足够)
2. 项目初始化与依赖安装
创建Node.js项目,安装必要依赖:
npm init -y npm install googleapis express
3. 核心代码实现
以下是基础的订阅状态查询服务示例:
const { google } = require('googleapis'); const express = require('express'); const app = express(); app.use(express.json()); // 初始化Google Play Developer API客户端 const auth = new google.auth.GoogleAuth({ keyFile: './service-account-key.json', // 替换为你的服务账号密钥文件路径 scopes: ['https://www.googleapis.com/auth/androidpublisher'] }); const androidpublisher = google.androidpublisher({ version: 'v3', auth: auth }); // 查询单个订阅状态接口 app.post('/api/subscription/status', async (req, res) => { const { packageName, subscriptionId, purchaseToken } = req.body; if (!packageName || !subscriptionId || !purchaseToken) { return res.status(400).json({ error: '缺少必要参数' }); } try { const response = await androidpublisher.purchases.subscriptions.get({ packageName: packageName, subscriptionId: subscriptionId, token: purchaseToken }); const subscription = response.data; // 解析关键状态字段 const status = { isActive: subscription.expiryTimeMillis > Date.now(), autoRenewing: subscription.autoRenewing, expiryTime: subscription.expiryTimeMillis, cancelReason: subscription.cancelReason, // 0=用户取消,1=账单问题,2=服务取消 cancelSurveyResult: subscription.cancelSurveyResult // 用户取消时的反馈(可选) }; res.json(status); } catch (error) { console.error('查询订阅状态失败:', error); res.status(500).json({ error: '查询失败', details: error.message }); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`服务器运行在端口 ${PORT}`); });
4. 客户端与服务器同步策略
- 实时同步:客户端在启动、订阅完成、订阅变更(比如用户取消自动续费)时,调用服务器接口获取最新状态
- 定期同步:服务器每天定时拉取所有用户的订阅状态(通过
purchases.subscriptions.list接口),更新本地数据库,减少实时查询的API调用量 - 本地缓存:客户端保存服务器返回的状态,设置合理的过期时间(比如1小时),避免频繁请求服务器
四、最佳实践总结
- 客户端始终强制拉取最新数据,但仅作为临时状态展示,最终以服务器校验结果为准
- 服务器端校验必须覆盖所有订阅状态判断逻辑,客户端不做核心权限控制(比如解锁付费功能必须依赖服务器返回的有效状态)
- 定期清理服务器中过期的订阅记录,避免无效数据堆积
内容的提问来源于stack exchange,提问作者Sujith S Manjavana
相关产品推荐
相关产品推荐

