PAYATHON 2026

In-App Billing Library 多用户订阅处理最佳实践

数据小陈

结论

这三类对象需要分开处理:

  • Google Play Account:负责向 Google Play 付款并保存购买记录。
  • 应用用户账号:负责使用应用内权益。
  • Subscription:由 productId、购买交易和唯一的 purchaseToken 表示。

建议将服务端验证通过的 purchaseToken 作为订阅实例的唯一标识,并在服务端把它绑定到一个应用用户。不要让应用用户与 Google Play Account 一一绑定。应用通常无法可靠获取当前用于结算的 Google 账号,也无法控制用户在购买界面选择哪个账号。

setObfuscatedAccountId() 只是向 Google Play 提供经过混淆的应用账号标识,用于归因、风控和异常检测。它不会创建独立的 Google Play 购买槽位,也无法让同一个 Google Play Account 再次购买已经持有的同一订阅。

各场景应该如何处理

场景Google Play 层面的结果应用推荐行为
U1 通过 G1 购买 S1,随后 U2 登录并尝试购买 S1如果设备当前使用的仍是 G1,Google Play 通常会认定该账号已经持有 S1,不会生成第二个有效订阅。具体界面和返回结果由 Play 决定不要自动将 U1 的权益转给 U2。应提示这笔购买已关联其他应用账号,并提供切回原账号或联系客服恢复绑定的入口
U1 已通过 G1 购买 S1,之后尝试通过 G2 再买 S1对 G2 而言,这可能是一笔新的可购买订阅,因为 Play 中的购买所有权属于 G2服务端发现 U1 已有有效的 S1 时,应阻止重复购买,并提示用户管理现有订阅
U1 通过 G1 购买 S1,又通过 G2 购买 S2只要 S1 和 S2 可以同时生效,Google Play 通常允许不同账号分别购买技术上可以允许,但续费、恢复购买和客服处理都会更复杂。可以提示用户尽量使用同一个 Play 账号
同一个 G1 想分别为 U1、U2 购买同一个 S1一般无法同时持有两份完全相同的订阅。更换 obfuscatedAccountId 也不会改变这一点不要将 obfuscatedAccountId 用作重复购买或多人共享机制

购买流程可能返回 BillingResponseCode.ITEM_ALREADY_OWNED,也可能由 Google Play UI 直接提示用户已经订阅。应用不能依赖某一种固定界面,需要在购买前后查询并校验现有购买。

不要直接按设备或 Google 账号授权

设备不是可靠的授权边界。同一个 Google Play Account 可以登录多台设备,一台设备也可能配置多个 Google Play Account。

应用无法通过 Play Billing 获取可用于业务绑定的 Google 邮箱地址,因此不建议设置“一个应用用户永久绑定一个 Google Account”这样的规则。它不仅难以可靠执行,还会影响换机、更换 Play 账号、家庭共用设备和账号迁移。

更稳妥的关系是:

purchaseToken -> appUserId
purchaseToken -> productId
purchaseToken -> subscription state

数据库可以保存如下记录:

purchase_token        UNIQUE
app_user_id
product_id
base_plan_id
order_id
expiry_time
subscription_state
linked_at

其中必须保持唯一的是 purchaseToken。同一个有效的 purchaseToken 不能同时为两个应用用户授权。

推荐的购买流程

1. 发起购买时提供混淆后的应用账号 ID

不要传递邮箱、手机号、用户名等明文个人信息,应使用服务端生成的稳定且不可逆的标识。

val billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(
        listOf(
            BillingFlowParams.ProductDetailsParams.newBuilder()
                .setProductDetails(productDetails)
                .setOfferToken(offerToken)
                .build()
        )
    )
    .setObfuscatedAccountId(obfuscatedAccountId)
    .build()

val result = billingClient.launchBillingFlow(activity, billingFlowParams)

obfuscatedAccountId 可以帮助服务端核对发起购买的应用用户和最终提交购买的用户,但不能替代对 purchaseToken 的验证。

2. 将购买发送到服务端验证

客户端收到 Purchase 后,不要直接永久开通权益。应先将购买令牌发送到服务端:

override fun onPurchasesUpdated(
    billingResult: BillingResult,
    purchases: MutableList<Purchase>?
) {
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        purchases.orEmpty().forEach { purchase ->
            backend.verifyAndLinkPurchase(
                productId = purchase.products.first(),
                purchaseToken = purchase.purchaseToken
            )
        }
    }
}

服务端通过 Google Play Developer API 验证订阅状态,再按以下规则处理:

if purchaseToken 尚未绑定:
    绑定到当前 appUserId
    授予对应权益
else if purchaseToken 已绑定当前 appUserId:
    幂等返回成功
else:
    拒绝自动转移
    提示购买已关联其他应用账号

服务端还要校验产品、订阅状态、到期时间和应用包名等信息,不能只检查客户端传来的 productId。

3. 登录后查询现有购买

应用用户登录后,可以查询当前 Play 环境中可见的有效订阅:

val params = QueryPurchasesParams.newBuilder()
    .setProductType(BillingClient.ProductType.SUBS)
    .build()

billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        purchases.forEach { purchase ->
            backend.verifyAndLinkPurchase(
                productId = purchase.products.first(),
                purchaseToken = purchase.purchaseToken
            )
        }
    }
}

不能因为查询到了订阅,就直接给当前登录用户开通权益。U1 退出、U2 登录后,查询结果中可能仍然包含 G1 持有的购买。服务端必须先确认该 purchaseToken 是否已经绑定 U1。

已绑定其他用户时怎么提示

相比“这个 Google Account 已经为其他用户订阅”,更准确的提示是:

检测到这笔 Google Play 订阅已关联另一个应用账号。请登录原应用账号恢复权益,或联系客服处理账号迁移。

应用未必知道用户实际使用了哪个 Google Account,因此不应声称已经识别出某个 Google 邮箱账号。

如果业务允许迁移订阅,应设计经过身份验证的转移流程,例如要求用户登录原应用账号并确认。即使新用户能够在设备上查询到 purchaseToken,也不能直接覆盖原绑定关系,否则共用设备上的其他人可能窃取订阅权益。

如何避免同一应用用户重复订阅

U1 已经拥有有效的 S1 时,应用应隐藏或禁用再次购买 S1 的入口,并提供“管理订阅”或“恢复购买”选项。

这只是应用层的防重复措施,无法完全阻止用户:

  • 更换 Google Play Account;
  • 更换设备;
  • 从 Google Play 的其他入口订阅;
  • 在客户端状态尚未同步时重复操作。

最终仍要由服务端判断 U1 是否已经拥有同类权益。如果业务不允许多个订阅同时存在,应在产品设计中明确订阅组以及升级、降级和替换规则,不要依赖 Google Account 绑定来限制。

S1 与 S2 是否可以由不同账号购买

可以,但需要先确定这两种订阅能否同时生效:

  • 如果 S1 和 S2 是互斥套餐,例如基础版与高级版,应使用订阅升级、降级或替换流程,避免产生两笔独立续费。
  • 如果它们对应相互独立的服务,可以允许同时订阅,即使付款来自不同的 Google Play Account。
  • 如果业务要求一个应用用户只能拥有一个有效套餐,应由服务端按应用用户检查,而不是按 Google Play Account 检查。

还需要向用户说明,不同 Play 账号购买的订阅必须分别在对应账号中管理和取消,应用无法代替用户合并这些购买记录。

关键注意事项

  • 不要将 orderId 作为长期订阅的唯一标识。关联订阅时应使用 purchaseToken。
  • 不要只凭客户端查询结果授权。服务端必须验证订阅状态。
  • 使用 Real-time Developer Notifications,并定期通过 Google Play Developer API 对账,以处理续费、取消、宽限期、账号保留、暂停和撤销等状态变化。
  • 购买处理必须具备幂等性。重复上报同一个 purchaseToken 时,不能重复发放权益。
  • 只有服务端成功记录购买后,才能按照 Billing Library 的要求确认购买。
  • 不要在 obfuscatedAccountId 中加入可识别个人身份的信息。
  • obfuscatedAccountId 无法让同一个 Play 账号为两个应用用户分别购买一份相同订阅,也不能作为订阅所有权的最终依据。

核心原则是:Google Play Account 持有付款关系,应用账号持有应用权益,服务端使用唯一的 purchaseToken 安全连接两者。

备注:内容仅供参考。