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 安全连接两者。
备注:内容仅供参考。
