PAYATHON 2026

Android Market subscription payment model 如何实现?

支付阿杰

明确结论

不建议通过“每月发布一个 licensing app,让用户重复下载”的方式收取订阅费用。更合适的做法是保持主应用不变,通过 Google Play Billing 提供自动续订订阅,并在服务端验证订阅状态。

如果继续更新同一个 licensing app:

  • applicationId(包名)和签名证书均相同时,Android 会把新版本作为更新安装,用户不必先卸载上个月的版本。
  • applicationId 不同时,系统会将其视为另一个应用,新旧版本可以同时安装。
  • 普通 Android 应用无法静默卸载自己,只能打开系统卸载确认页面,交由用户确认。

为什么不适合按月发布 licensing app

将许可功能拆成独立应用会增加不少问题:

  • 用户必须定期手动下载或更新,续费体验很差。
  • 主应用与 licensing app 之间的校验可能被伪造、Hook 或绕过。
  • licensing app 被卸载、停用或限制后台运行后,主应用可能误判许可状态。
  • 每月更换包名会让设备上残留多个应用。
  • 如果只是每月提高 versionCode,本质上仍是应用更新,不能代替订阅收费。
  • 客户端保存的“已付费”标记很容易被修改,不能作为可靠的授权依据。

订阅是否有效,应该由支付平台和服务端状态决定,不能取决于某个 APK 是否已安装。

推荐实现方式

1. 在主应用中接入订阅商品

在 Google Play Console 中创建订阅商品,通过 Google Play Billing Library 发起购买。依赖版本以接入时的官方文档为准,例如:

dependencies {
    implementation("com.android.billingclient:billing-ktx:<version>")
}

初始化 BillingClient:

private val billingClient = BillingClient.newBuilder(context)
    .setListener { billingResult, purchases ->
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            purchases.orEmpty().forEach(::handlePurchase)
        }
    }
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder()
            .enableOneTimeProducts()
            .build()
    )
    .build()

不同版本的 Billing Library 对 enablePendingPurchases 的调用形式可能不同,需要按照项目实际采用的版本调整。

2. 查询订阅商品并发起购买

val product = QueryProductDetailsParams.Product.newBuilder()
    .setProductId("monthly_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 offerToken = productDetails.subscriptionOfferDetails
        ?.firstOrNull()
        ?.offerToken
        ?: return@queryProductDetailsAsync

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

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

    billingClient.launchBillingFlow(activity, flowParams)
}

实际项目中不能直接选取第一个 offer,而应根据 base plan、地区、优惠资格和业务规则选择正确的 offerToken。

3. 将购买令牌发送到服务端验证

收到购买结果后,将 purchaseToken、商品 ID 和应用包名发送到自己的服务端:

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

    api.verifySubscription(
        productId = purchase.products.first(),
        purchaseToken = purchase.purchaseToken
    )
}

服务端需要通过 Google Play Developer API 验证订阅,并至少保存以下信息:

  • 用户账号
  • purchaseToken
  • 商品及 base plan
  • 当前订阅状态
  • 到期时间
  • 自动续订状态
  • 取消、暂停、宽限期或账号保留状态

主应用应向业务服务端请求授权结果,不能只相信客户端传来的布尔值。

购买成功后还要及时 acknowledge。可以在客户端完成,也可以在服务端完成,具体取决于整体架构,但不能遗漏,否则购买可能被自动退款。

如果仍要保留独立 licensing app

每月发布时,应保持相同的 applicationId,提高 versionCode,并始终使用同一签名证书:

android {
    defaultConfig {
        applicationId = "com.example.licensing"
        versionCode = 13
        versionName = "2026.08"
    }
}

新 APK 或 App Bundle 满足以下条件时,可以覆盖更新旧版本:

  1. applicationId 相同。
  2. 签名身份一致。
  3. versionCode 高于已安装版本。
  4. 安装包与设备及系统版本兼容。

满足这些条件后,用户通常不需要先卸载旧版本。也不应要求用户卸载,因为卸载会清除该应用的本地数据,重新安装时还可能因签名或数据迁移问题导致授权异常。

不过,这种设计只能解决“版本更新”,不能可靠地验证订阅。主应用通过 PackageManager 检查另一个应用是否安装,只能证明某个包存在:

fun isLicensingAppInstalled(context: Context): Boolean {
    return try {
        context.packageManager.getPackageInfo(
            "com.example.licensing",
            PackageManager.PackageInfoFlags.of(0)
        )
        true
    } catch (_: PackageManager.NameNotFoundException) {
        false
    }
}

在 Android 11 及以上版本中,查询其他应用时还可能需要声明包可见性:

<queries>
    <package android:name="com.example.licensing" />
</queries>

只检查包名并不够,至少还需要校验签名证书,并通过签名权限、绑定服务或服务端签发的短期令牌进行通信。即便加入这些措施,可靠性仍不如标准订阅体系。

应用能否卸载自己

普通应用不能在用户不知情的情况下自行卸载,但可以请求系统打开卸载界面:

val intent = Intent(Intent.ACTION_DELETE).apply {
    data = Uri.parse("package:$packageName")
}
startActivity(intent)

系统会显示确认页面,由用户作出最终决定。

“清除数据”“停止服务”和“禁用功能”都不等于卸载。订阅失效后,更合适的处理方式是让应用进入未授权状态:

if (!subscriptionState.isEntitled) {
    showSubscriptionRequiredScreen()
    disablePremiumFeatures()
}

只有设备所有者、企业设备管理程序、系统应用或具备特殊管理权限的安装器,才可能通过受管接口执行静默卸载。这类能力不适用于面向普通 Google Play 用户发布的常规应用。

注意事项

  • 不要只根据本地日期判断订阅是否过期,因为用户可以修改设备时间。
  • 不要将永久密钥或全部许可判断逻辑放在 APK 中。
  • 需要处理取消续订、退款、撤销、暂停、宽限期、待处理交易和离线状态。
  • 网络暂时不可用时,可以采用服务端最近一次验证结果,并设置有限的宽限时间,避免影响已付费用户。
  • 将业务账号与购买记录绑定,同时设计好换机、重装和恢复购买流程。
  • Billing Library 和 Google Play Developer API 会持续更新,具体类名、接口参数和接入要求应以项目采用版本的官方文档为准。

备注:内容仅供参考。