如何在多个 Android apps 间实现分级订阅?
明确结论
不能只靠各个 App 内的 Google Play Billing Library 读取其他 App 的订阅记录。
Billing Library 通常只能查询当前应用 packageName 下、已在 Google Play Console 中配置的商品。即使 A、B、C 由同一个开发者账号发布,B 也无法通过 queryPurchasesAsync() 枚举用户在 A 中购买的订阅。
要实现跨应用分级订阅,需要由 Google Play 和自有后端共同完成:
- Google Play 负责收款及订阅生命周期管理。
- 自有后端验证购买并维护统一权益。
- A、B、C 共用一套用户账号体系。
- 用户登录任一 App 后,由该 App 向后端查询当前用户的最高订阅等级。
最终是否授权,应以后端返回的权益为准,不能依赖某个 App 在本地查到的购买列表。
为什么 App 之间不能直接查询
下面的调用只能查询当前 App 可见的订阅:
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
) { billingResult, purchases ->
// purchases 属于当前应用上下文
}
它查询的不是当前 Google Play 账号下的全部订阅,也不支持传入另一个 App 的 packageName。
原因包括:
- Play Billing 商品与发布该商品的应用关联。
- App 无权读取用户在其他应用中的完整购买历史。
- Android App 通常无法确认其他 App 中登录的是否为同一个业务用户。
- Google Play 账号不能代替开发者自己的跨应用身份体系。
- 仅使用本地缓存或应用间通信,无法可靠处理退款、撤销、宽限期、账号保留等状态。
这里所说的”共享订阅”,是指开发者通过自己的系统共享订阅对应的业务权益,不是由 Billing Library 跨应用读取购买记录。
推荐架构
可以在后端把三个订阅等级统一映射为以下权益:
| 订阅等级 | 可访问 App |
|---|---|
| A | A |
| B | A、B |
| C | A、B、C |
典型流程如下:
用户登录开发者账号
↓
在 A、B 或 C 中通过 Google Play 完成购买
↓
App 将 purchaseToken、productId、packageName 发给后端
↓
后端调用 Google Play Developer API 验证购买
↓
后端把订阅绑定到开发者账号,并计算最高权益等级
↓
A、B、C 登录后从后端查询 entitlement
↓
各 App 根据 entitlement 决定是否开放功能
例如,后端可以返回:
{
"subscription": {
"tier": "B",
"state": "ACTIVE",
"expiresAt": "2026-10-01T00:00:00Z"
},
"apps": {
"A": true,
"B": true,
"C": false
}
}
A 和 B 根据该结果开放功能,C 显示升级入口。
App 端购买处理
购买成功后,不能直接把本地 Purchase 对象作为永久授权依据。App 应将购买凭证提交给后端验证。
private fun handlePurchase(purchase: Purchase) {
val productId = purchase.products.firstOrNull() ?: return
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
uploadPurchaseToBackend(
packageName = applicationContext.packageName,
productId = productId,
purchaseToken = purchase.purchaseToken
)
}
}
请求内容可以设计为:
{
"packageName": "com.example.appb",
"productId": "subscription_b",
"purchaseToken": "PURCHASE_TOKEN_FROM_GOOGLE_PLAY"
}
请求必须通过 HTTPS 发送,并携带开发者自己的登录凭证。后端不能直接相信客户端声明的订阅等级、过期时间或支付状态。
如果购买尚未确认,可以在服务端验证成功后进行 acknowledge,也可以由客户端完成确认:
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { billingResult ->
// 记录确认结果;失败时应安排重试
}
购买未经验证时,不应长期授予权益。
后端如何验证订阅
后端需要保存以下信息:
- 业务用户 ID
packageNameproductIdpurchaseToken- 当前订阅状态
- 到期时间
- 对应的权益等级
- 最近一次与 Google Play 同步的时间
后端再根据购买来源应用的 packageName 和 purchaseToken,通过 Google Play Developer API 查询订阅状态。概念代码如下:
async function verifySubscription({
userId,
packageName,
productId,
purchaseToken
}) {
const purchase = await googlePlayApi.getSubscription({
packageName,
token: purchaseToken
});
if (!isValidSubscription(purchase)) {
throw new Error("Subscription is not active");
}
const tier = mapProductToTier(packageName, productId);
await saveEntitlement({
userId,
packageName,
productId,
purchaseToken,
tier,
state: normalizeSubscriptionState(purchase),
expiresAt: getExpirationTime(purchase)
});
return calculateEffectiveEntitlement(userId);
}
其中,googlePlayApi.getSubscription()、isValidSubscription() 等函数只是业务层示意,并不是 Google SDK 中可以直接使用的函数名。实际开发时,应调用当前 Google Play Developer API 提供的订阅查询接口,并以官方最新文档中的响应字段为准。
服务端还要检查:
packageName是否属于自己的应用。productId是否在服务端白名单中。- 商品与订阅等级的映射是否正确。
- 该购买是否已绑定到其他业务账号。
- 订阅当前是否处于可以授予权益的状态。
- 到期时间、退款、撤销和取消状态是否有效。
- 同一个
purchaseToken是否被重复提交。
三个 App 如何获得统一权益
A、B、C 都需要用户登录同一个开发者账号,然后调用统一接口:
GET /v1/me/entitlements
Authorization: Bearer <access-token>
Android 端可以根据等级判断访问权限:
enum class SubscriptionTier(val level: Int) {
NONE(0),
A(1),
B(2),
C(3)
}
fun canAccessApp(
userTier: SubscriptionTier,
requiredTier: SubscriptionTier
): Boolean {
return userTier.level >= requiredTier.level
}
在 B 中:
if (canAccessApp(userTier, SubscriptionTier.B)) {
openMainScreen()
} else {
showSubscriptionScreen()
}
B 获取的是后端计算出的统一业务权益,并不是 A 中的 Google Play 购买列表。
订阅状态需要持续同步
首次购买时验证一次并不够。后续还可能出现以下变化:
- 自动续订
- 用户取消续订
- 支付失败
- 宽限期
- 账号保留
- 暂停或恢复
- 退款或撤销
- 订阅升级、降级
- 到期失效
可以通过 Real-time Developer Notifications 接收订阅变更通知,再由后端重新查询 Google Play Developer API。通知只表示订阅状态可能发生了变化,最终状态仍应以服务端查询结果为准。
后端还应定期对账,以应对通知延迟、丢失或处理失败。
如何处理 B、C 的升级关系
如果 A、B、C 分别使用各自应用内的订阅商品,Google Play 通常无法自动识别这些商品之间的业务层级。特别是商品属于不同 packageName 时,不能假定它们可以使用普通的订阅升级或替换流程。
后端可以从用户的所有有效订阅中选取最高等级:
effectiveTier = max(activeSubscriptionTiers)
但这样可能造成重复付费。例如,用户已经订阅 A,之后又在 C 中购买 C,而原有的 A 订阅仍可能继续续费。
因此,需要提前确定产品方案:
- 设立统一的购买入口,由一个订阅授予对应的全部权益。
- 允许各 App 分别销售订阅,但购买前要由后端检查用户现有等级,并向用户明确提示。
- 无法自动替换旧订阅时,引导用户前往 Google Play 的订阅管理页面处理。
- 不能在未明确告知用户的情况下,让多个等级长期同时续费。
如果业务条件允许,集中销售少量订阅商品,再由后端分发跨应用权益,通常更容易避免三个 App 各自维护商品所带来的重复订阅问题。
不能依赖的方案
以下方式不能作为正式的跨应用订阅验证机制:
- 在 B 中调用 Billing Library 查询 A 的订阅。
- 假定同一设备上的三个 App 一定属于同一个用户。
- 通过共享文件、广播、ContentProvider 或 Shared UID 传递永久授权。
- 只在本地保存一个
isPremium=true。 - 仅根据客户端上传的
productId判断等级。 - 将 Google Play 登录账号作为三个 App 共用的用户 ID。
- 只在安装或首次购买时检查一次订阅。
应用间通信最多只能改善同一设备上的使用体验,不能替代服务端购买验证。
账号绑定是核心前提
跨应用共享权益之前,必须先确定用户身份。
用户可能在 A 中使用一个业务账号,在 B 中使用另一个账号,也可能更换设备或切换 Google Play 账号。因此,购买发生时必须将订阅安全地绑定到当前登录的业务账号。
发起购买时,可以设置经过散列或不可逆处理的账号标识,帮助后端关联账号和进行风控。但这个标识不能代替登录认证,也不应包含明文邮箱、手机号等个人信息:
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.setObfuscatedAccountId(obfuscatedAccountId)
.build()
服务端仍应以已认证的会话、购买凭证验证结果及自身保存的绑定记录为准。
最终建议
可靠的实现方式是由 Google Play 管理交易,自有后端管理跨应用权益:
- 每个 App 只能直接查询自己的 Play Billing 购买。
- 发生购买的 App 将
purchaseToken提交给后端。 - 后端根据购买来源的
packageName验证订阅。 - 后端将有效订阅绑定到统一业务账号。
- A、B、C 从后端获取 A、B、C 级权益。
- 通过实时通知和定期对账更新订阅状态。
- 单独制定升级、降级和重复订阅的处理规则。
一个 App 无法直接访问另一个 App 的订阅信息。要实现 C > B > A 的分级订阅,需要由受信任的后端汇总并验证各 App 提交的购买记录,再向所有 App 返回统一的授权结果。
备注:内容仅供参考。
