PAYATHON 2026

如何使用 Google Market Billing API v2 添加应用内订阅?

支付阿杰

结论

不要再参考 Market Billing API v2 或 Dungeons 示例来实现订阅。这些内容对应的是早期计费方案,已经淘汰,无法反映现在 Google Play 的订阅模型和测试流程。

目前应使用 Google Play Billing Library,并按以下流程管理订阅权益:

  1. 在 Play Console 中创建订阅、基础方案和优惠。
  2. 通过 BillingClient 查询商品并发起购买。
  3. 持久化 Purchase 中的 purchaseToken。
  4. 在服务端通过 Google Play Developer API 验证订阅。
  5. 验证成功后调用 acknowledgePurchase()。
  6. 每次启动应用或恢复登录时调用 queryPurchasesAsync(),恢复仍然有效的订阅。

即使客户端当时没有保存 token,也不代表它一定无法找回。只要订阅仍属于当前 Google 账号,通常可以通过查询已有购买,重新取得 Purchase 和 purchaseToken。

旧文档为什么不能直接套用

Market Billing API v2 使用早期的 Android Market/Google Play 计费模型。Dungeons 示例主要讲解一次性商品,没有覆盖后来逐步完善的订阅功能。

现在的订阅还需要处理:

  • 基础方案和优惠
  • 自动续订与取消
  • 宽限期和账号保留
  • 暂停、恢复及重新订阅
  • 升级或降级
  • 服务端验证
  • 实时开发者通知
  • 购买确认
  • 测试续订周期

所以,即使旧示例还能编译,也不适合用作新应用的实现基础。

推荐的完整实现流程

1. 在 Play Console 配置订阅

先创建订阅商品,再为它配置可购买的基础方案。应用代码通常使用产品 ID 查询订阅,但发起购买时需要传入 Google Play 返回的 offerToken。

发布测试版本前,还需要确认:

  • 应用包名与 Play Console 中的应用一致。
  • APK 或 App Bundle 使用了正确的签名。
  • 订阅及其基础方案处于可用状态。
  • 测试账号已经加入许可测试人员。
  • 测试账号可以访问对应的内部测试或封闭测试版本。
  • 设备上的 Google Play 登录账号就是测试账号。

Play Console 的界面和订阅配置项可能会调整,请以当前官方控制台显示的内容为准。

2. 建立 BillingClient

下面的示例省略了 ViewModel、协程作用域和依赖注入等项目结构代码:

private lateinit var billingClient: BillingClient

fun initializeBilling(context: Context) {
    billingClient = BillingClient.newBuilder(context)
        .setListener { billingResult, purchases ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                purchases.orEmpty().forEach(::handlePurchase)
            }
        }
        .enablePendingPurchases()
        .build()

    billingClient.startConnection(
        object : BillingClientStateListener {
            override fun onBillingSetupFinished(result: BillingResult) {
                if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                    querySubscriptionProducts()
                    restoreSubscriptions()
                }
            }

            override fun onBillingServiceDisconnected() {
                // 稍后重连,实际项目中应使用带退避策略的重试机制。
            }
        }
    )
}

项目应使用 Google Play Billing Library 当前受官方支持的稳定版本,不要直接照搬旧文章中的依赖版本号。

3. 查询订阅及可用优惠

private fun querySubscriptionProducts() {
    val product = QueryProductDetailsParams.Product.newBuilder()
        .setProductId("annual_subscription")
        .setProductType(BillingClient.ProductType.SUBS)
        .build()

    val params = QueryProductDetailsParams.newBuilder()
        .setProductList(listOf(product))
        .build()

    billingClient.queryProductDetailsAsync(params) { result, queryResult ->
        if (result.responseCode != BillingClient.BillingResponseCode.OK) {
            return@queryProductDetailsAsync
        }

        val productDetails = queryResult.productDetailsList.firstOrNull()
            ?: return@queryProductDetailsAsync

        val offer = productDetails.subscriptionOfferDetails
            ?.firstOrNull()
            ?: return@queryProductDetailsAsync

        // 保存 productDetails 和 offer.offerToken,
        // 在用户点击订阅按钮时用于发起购买。
    }
}

不要默认第一个优惠就是业务所需的优惠。正式代码应根据基础方案 ID、优惠 ID、标签或价格阶段,选择正确的 offerToken。

4. 发起订阅购买

fun launchSubscriptionPurchase(
    activity: Activity,
    productDetails: ProductDetails,
    offerToken: String
) {
    val productParams =
        BillingFlowParams.ProductDetailsParams.newBuilder()
            .setProductDetails(productDetails)
            .setOfferToken(offerToken)
            .build()

    val flowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(listOf(productParams))
        .build()

    val result = billingClient.launchBillingFlow(activity, flowParams)

    if (result.responseCode != BillingClient.BillingResponseCode.OK) {
        // 记录错误并向用户显示合适的提示。
    }
}

launchBillingFlow() 不会直接返回最终的购买结果。购买完成后,结果会传入创建 BillingClient 时设置的 PurchasesUpdatedListener。

5. 处理并保存 purchase token

private fun handlePurchase(purchase: Purchase) {
    if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
        return
    }

    val purchaseToken = purchase.purchaseToken

    // 将 token 发送到自己的服务端进行验证。
    verifyPurchaseOnServer(
        productId = purchase.products.firstOrNull().orEmpty(),
        purchaseToken = purchaseToken
    )

    // 只有服务端确认订阅有效后,才应授予长期订阅权益。
}

服务端至少要关联以下信息:

  • 应用用户 ID
  • purchaseToken
  • 产品 ID
  • Google Play 返回的订阅状态
  • 到期时间
  • 是否已确认购买
  • 最近一次验证时间

不要只在客户端保存一个”已订阅”布尔值。客户端数据可能被清除或篡改,也无法可靠处理退款、撤销和续订失败。

6. 服务端验证订阅

服务端应使用 Google Play Developer API 验证 purchaseToken。当前订阅接口与早期 Market Billing API v2 使用的不是同一套模型。新系统通常使用订阅购买状态接口,例如 purchases.subscriptionsv2.get。

请求参数主要包括:

packageName: 应用包名
token: 客户端上传的 purchaseToken

服务端需要根据 Google Play 返回的数据判断订阅目前是否仍应提供权益,不能只检查用户是否”曾经购买过”。判断时还要处理到期、取消、宽限期、账号保留和撤销等状态。

访问该 API 需要正确配置 Google Cloud 项目、服务账号和 Play Console 权限。具体授权步骤可能随着控制台更新,请以当前官方文档为准。

7. 确认购买

验证购买并交付权益后,还需要确认购买:

private fun acknowledgeIfNeeded(purchase: Purchase) {
    if (purchase.isAcknowledged) {
        return
    }

    val params = AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(purchase.purchaseToken)
        .build()

    billingClient.acknowledgePurchase(params) { result ->
        if (result.responseCode == BillingClient.BillingResponseCode.OK) {
            // 确认成功。
        }
    }
}

确认操作必须具有幂等性。网络中断后,可以再次检查 isAcknowledged 并重试,但不能因为收到重复回调而重复创建用户权益。

如果没有及时确认,Google Play 可能会自动退款。具体时限请以当前 Google Play Billing 官方文档为准。

没有保存 token 时如何恢复

对于仍然有效的订阅,可以重新查询当前账号拥有的购买:

private fun restoreSubscriptions() {
    val params = QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.SUBS)
        .build()

    billingClient.queryPurchasesAsync(params) { result, purchases ->
        if (result.responseCode == BillingClient.BillingResponseCode.OK) {
            purchases.forEach(::handlePurchase)
        }
    }
}

可以在以下时机执行恢复查询:

  • BillingClient 连接成功后
  • 应用启动或用户登录后
  • 用户点击”恢复购买”时
  • 应用从后台回到前台时

恢复查询只能返回当前 Google Play 账号能够识别的相关购买。如果设备更换了账号、订阅已经失效,或者购买记录不属于当前应用包名,就可能无法恢复。

如果应用之前从未保存 token,客户端查询也无法返回该购买,那么仅凭信用卡账单或订单截图,通常无法自动为某个应用账号恢复权益。遇到这种情况,需要通过 Google Play 订单记录和开发者后台人工核对。

稳定、可重复的测试流程

使用许可测试账号

不要使用普通生产账号反复购买真实的年度订阅。建议按以下流程测试:

  1. 将测试 Google 账号加入 Play Console 的许可测试人员。
  2. 将构建上传到内部测试轨道。
  3. 让该账号加入测试,并从 Google Play 安装应用。
  4. 确认购买界面显示的是测试付款方式。
  5. 完成购买并记录客户端回调。
  6. 检查服务端是否收到并验证 token。
  7. 检查购买是否已经确认。
  8. 重启应用,验证能否通过 queryPurchasesAsync() 恢复订阅。
  9. 清除应用数据并重新登录,再次验证恢复流程。
  10. 测试取消、续订、到期、支付失败和撤销等状态。

Google Play 测试订阅通常采用缩短的生命周期,但具体周期和重复续订次数可能发生变化,不要在代码中硬编码这些测试时间。

保留可追踪日志

测试时可以记录以下字段,但不要把完整的敏感 token 写入公开日志:

applicationUserId
productId
purchaseState
isAcknowledged
purchaseTokenHash
billingResponseCode
serverVerificationStatus
subscriptionExpiryTime
eventTimestamp

可以对 purchaseToken 进行哈希处理,或者只记录末尾少量字符。这样既方便排查问题,也能避免泄露完整凭证。

每轮测试前检查状态

退款、取消订阅和撤销购买是三种不同的操作:

  • 取消订阅通常只是停止后续续费,不一定会立即终止当前权益。
  • 退款可能会退回款项,但是否立即撤销权益取决于具体操作方式和订单状态。
  • 撤销通常会使订阅权益失效。
  • 测试账号中的订阅状态也可能需要等待一段时间才能同步。

因此,不能把”取消”当成”重置测试”。开始每轮测试前,都应通过客户端和服务端确认当前订阅状态。

关于已经发生的真实扣款

如果购买界面使用的是真实付款方式,这就是实际交易,不是沙盒模拟。应用无法通过本地代码自行撤销信用卡扣款。

可以采取以下措施:

  • 在 Google Play 订单记录中确认订单状态。
  • 在 Play Console 的订单管理页面查找对应订单。
  • 根据可用操作执行退款,或退款并撤销。
  • 如果控制台显示退款成功,但付款状态仍然异常,请联系 Google Play 支持或支付渠道核查。
  • 不要为了重新测试而连续重复购买同一个长期订阅。

早期的 Google Checkout、Market Billing API v2 与现在 Play Console 的退款流程差异很大。旧订单能否退款、是否已经结算,以及能否恢复 token,都要以该订单在后台显示的实际状态为准。

容易遗漏的问题

  • purchaseToken 应立即发送到服务端,不能只保存在设备中。
  • 购买成功回调可能重复到达,服务端必须进行幂等处理。
  • 不能只依赖购买回调,应用启动时还要主动查询已有购买。
  • 不要根据客户端时间计算订阅是否到期,应以 Google Play 服务端状态为准。
  • 处于 PENDING 状态时,不能立即交付完整权益。
  • 用户取消自动续订后,在当前已付款周期内,订阅可能仍然有效。
  • 退款、撤销或续费失败可能发生在用户没有打开应用的时候。生产环境应结合实时开发者通知和定期服务端校验来处理这些变化。
  • purchaseToken 是重要凭证,不能写入分析平台、崩溃报告或可公开访问的日志。

判断一个订阅系统是否可靠,不能只看购买按钮能否成功扣款。购买、重装、换设备、续订、取消、退款或网络失败后,系统都应该能够根据 Google Play 的权威状态重新建立正确的用户权益。

备注:内容仅供参考。