PAYATHON 2026

如何在 Google Play Store 实现整款 Android 应用订阅

技术老齐

结论

Google Play 没有单独的“整款应用订阅”商品类型。用户按周期付费获得应用的数字使用权时,应将这项权益配置为 Google Play Billing 的 subscription,并在应用内发起购买。

你看到的 in-app subscription 文档就是需要实现的流程。即使应用只有一个订阅项目,订阅后会解锁全部功能,在 Google Play 的计费模型中也仍属于应用内订阅。

常见的产品流程如下:

  1. 用户免费下载并安装应用。
  2. 打开应用后,用户会看到登录页、试用内容或订阅页。
  3. 应用通过 Google Play Billing 发起订阅。
  4. 购买成功并通过验证后,应用解锁全部功能。
  5. 订阅失效、退款或被撤销后,应用收回相应权益。

Google Play Console 用于管理商品、价格和订单。当前用户能否使用完整功能,则由应用或服务器判断。

需要实现的内容

按照 Google Play Billing 的 subscription 流程实现即可,无须另找“应用级订阅”接口。旧文档中的 AIDL、SkuDetails、skuId 和 SkuType.SUBS 属于旧接口体系。新项目应使用当前受支持的 Google Play Billing Library,以及 ProductDetails、BillingClient.ProductType.SUBS、base plan 和 offer 等模型。

主要需要完成以下环节:

  • 在 Play Console 中创建 subscription product。
  • 为该商品创建按月自动续订的 base plan。
  • 在应用中查询 ProductDetails。
  • 从 subscriptionOfferDetails 获取 offerToken。
  • 使用 launchBillingFlow() 发起购买。
  • 处理 PurchasesUpdatedListener 返回的购买结果。
  • 验证 purchaseToken。
  • 对首次购买执行 acknowledgement。
  • 在应用启动、用户登录和应用恢复前台时重新检查订阅权益。
  • 处理续订、取消、退款、宽限期、账号保留和撤销等状态。

一次性商品的消费流程和 consumeAsync() 等逻辑不适用于订阅,无须实现。

在 Play Console 中配置订阅

Play Console 的菜单名称可能随界面更新而变化。订阅配置一般位于 Monetize、Products 或 Subscriptions 相关页面。配置步骤通常包括:

  1. 配置开发者付款资料和商家信息。
  2. 创建订阅商品,例如 full_access。
  3. 创建按月自动续订的 base plan。
  4. 设置价格和销售国家或地区,然后激活 base plan。
  5. 上传包含 Billing Library 的应用版本。
  6. 通过 internal testing 或 closed testing 测试订阅。
  7. 配置 license testers,避免测试时生成普通的正式订单。

商品 ID 发布后不宜随意更改。免费试用和首期优惠等内容应通过 offer 配置,无须另建一套本地计费逻辑。

在应用内发起订阅

以下代码展示了如何使用 ProductDetails 查询订阅并启动购买。不同版本 Billing Library 的回调签名可能略有不同,应以项目实际采用版本的官方 API 为准。

private const val PRODUCT_ID = "full_access"

fun querySubscription(
    billingClient: BillingClient,
    onLoaded: (ProductDetails) -> Unit
) {
    val product = QueryProductDetailsParams.Product.newBuilder()
        .setProductId(PRODUCT_ID)
        .setProductType(BillingClient.ProductType.SUBS)
        .build()

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

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

        result.productDetailsList.firstOrNull()?.let(onLoaded)
    }
}

启动购买时,必须使用该用户实际可以购买的 offer 所对应的 offerToken:

fun launchSubscriptionPurchase(
    activity: Activity,
    billingClient: BillingClient,
    productDetails: ProductDetails
) {
    val offer = productDetails.subscriptionOfferDetails
        ?.firstOrNull()
        ?: return

    val productParams =
        BillingFlowParams.ProductDetailsParams.newBuilder()
            .setProductDetails(productDetails)
            .setOfferToken(offer.offerToken)
            .build()

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

    billingClient.launchBillingFlow(activity, billingFlowParams)
}

实际项目中不能无条件选择第一个 offer。配置了免费试用、用户资格限制或多种价格方案时,需要根据 basePlanId、offerId 和定价阶段选择合适的 offerToken。

购买结果由 PurchasesUpdatedListener 接收:

private val purchasesUpdatedListener =
    PurchasesUpdatedListener { billingResult, purchases ->
        if (billingResult.responseCode ==
            BillingClient.BillingResponseCode.OK
        ) {
            purchases.orEmpty().forEach { purchase ->
                when (purchase.purchaseState) {
                    Purchase.PurchaseState.PURCHASED -> {
                        sendPurchaseTokenToServer(
                            productId = PRODUCT_ID,
                            purchaseToken = purchase.purchaseToken
                        )
                    }

                    Purchase.PurchaseState.PENDING -> {
                        showPendingMessage()
                    }

                    else -> Unit
                }
            }
        }
    }

收到 PURCHASED 只表示客户端已取得购买记录,不能仅凭这次回调或本地保存的布尔值永久解锁功能。PENDING 状态也不能授予权益,必须等购买状态变为 PURCHASED。

是否需要搭建后端

开发者可以在 Play Console 中查看商品、订单和财务数据,但 Play Console 不会替应用实时判断某个登录用户是否仍有订阅权益。正式上线的应用通常应配置后端,尤其是带有账号系统、云端数据或防篡改要求的应用。

可以采用以下处理流程:

Android 应用
    -> 将 purchaseToken 发送给业务服务器
    -> 服务器调用 Google Play Developer API 验证订阅
    -> 服务器保存订阅状态和到期时间
    -> 应用向业务服务器查询当前权益

服务端可以使用 Google Play Developer API 的 purchases.subscriptionsv2.get 查询订阅状态。首次购买验证成功后,还要在规定期限内完成 acknowledgement,否则订单可能被自动退款。客户端和服务器都可以执行 acknowledgement,每个订单成功处理一次即可。

同时建议接入 Real-time Developer Notifications。服务器收到续订、取消、退款或撤销通知后,再调用 Developer API 获取完整状态。通知只能说明订阅状态可能发生了变化,不能直接作为授予权益的依据。

对于规模很小的应用,也可以先在客户端通过 queryPurchasesAsync() 查询当前 Google Play 账号拥有的订阅,并在设备端完成 acknowledgement。不过,这种实现容易受到客户端篡改、本地状态过期和账号映射不清等问题影响,不适合长期用于生产环境。

判断权益时需要处理的状态

用户取消订阅后,通常仍可使用到当前计费周期结束。应用需要根据 Google 返回的有效期和订阅状态判断权益,不能在看到取消标志后立即锁定功能。

各状态可按以下方式处理:

  • PENDING:付款尚未完成,不授予权益。
  • 正常续订:继续保留权益。
  • 用户取消:一般保留权益至当前有效期结束。
  • grace period:是否继续提供权益取决于订阅配置和业务规则。
  • account hold:付款失败并进入保留状态,需要根据实际订阅状态处理。
  • paused:暂停期间通常不再授予订阅权益。
  • refunded 或 revoked:及时收回权益。
  • expired:订阅到期后关闭完整功能。

权益检查不能只在购买当天执行。应用冷启动、用户登录、恢复前台或收到服务端状态变更时,都应重新确认权益。

单个订阅项目也要支持恢复

用户可能会重新安装应用、更换设备,或在另一台设备上登录,因此不能只在本地偏好设置中保存“已订阅”状态。

没有业务账号系统时,可以查询当前 Google Play 账号的有效购买记录。有业务账号系统时,应在服务器上将验证通过的 purchaseToken 与业务用户绑定。购买参数中的 obfuscatedAccountId 可以辅助账号关联和风控,但其中不能包含邮箱、手机号等明文个人信息。

上线前检查

发布前至少确认以下事项:

  • 订阅商品和 base plan 已激活。
  • 测试版本的 package name 与 Play Console 一致。
  • 测试账号已加入测试轨道并接受测试邀请。
  • 应用通过 Google Play 安装,不是只用本地 APK 测试。
  • 页面展示的价格来自 ProductDetails,客户端没有写死价格。
  • 购买成功后已验证 purchaseToken。
  • 首次购买已完成 acknowledgement。
  • PENDING 订单不会提前解锁功能。
  • 重装应用或更换设备后可以恢复权益。
  • 取消、到期、退款和撤销后能够正确更新访问权限。
  • 订阅页面清楚说明价格、计费周期、自动续订规则和管理方式。

这个需求可以配置为“一项解锁整款应用的 subscription product”。它使用标准的应用内订阅流程,订阅授予的权益覆盖整个应用。

备注:内容仅供参考。