PAYATHON 2026

如何在多个 Android apps 间实现分级订阅?

支付阿杰

明确结论

不能只靠各个 App 内的 Google Play Billing Library 读取其他 App 的订阅记录。

Billing Library 通常只能查询当前应用 packageName 下、已在 Google Play Console 中配置的商品。即使 A、B、C 由同一个开发者账号发布,B 也无法通过 queryPurchasesAsync() 枚举用户在 A 中购买的订阅。

要实现跨应用分级订阅,需要由 Google Play 和自有后端共同完成:

  1. Google Play 负责收款及订阅生命周期管理。
  2. 自有后端验证购买并维护统一权益。
  3. A、B、C 共用一套用户账号体系。
  4. 用户登录任一 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
AA
BA、B
CA、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
  • packageName
  • productId
  • purchaseToken
  • 当前订阅状态
  • 到期时间
  • 对应的权益等级
  • 最近一次与 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 返回统一的授权结果。

备注:内容仅供参考。