PAYATHON 2026

Google Play In-App Billing 如何实现一次性购买的 7 天试用

数据小许

明确结论

Google Play In-App Billing 不支持为一次性购买的非消耗型商品设置原生免费试用。订阅商品可以配置免费试用,但一次性商品无法直接实现“前 7 天免费,试用结束后恢复受限功能”。

这部分逻辑需要由应用自己管理:

  1. 用户首次使用时记录试用开始时间。
  2. 在 7 天试用期内开放完整功能。
  3. 试用结束后恢复受限功能。
  4. 用户购买一次性商品后永久解锁。

不使用服务端也可以实现,但用户清除应用数据、重新安装、修改系统时间或篡改应用后,可能再次获得试用资格。

推荐的状态设计

“正在试用”和“已经购买”是两种不同的状态,不应混在一起。应用至少需要区分以下三种状态:

PURCHASED     已购买,永久开放完整功能
TRIAL_ACTIVE  仍在 7 天试用期内
LIMITED       未购买且试用已结束

判断时应先检查购买状态,再检查试用状态:

已拥有一次性商品?
├─ 是:PURCHASED
└─ 否:试用是否仍有效?
   ├─ 是:TRIAL_ACTIVE
   └─ 否:LIMITED

购买状态通过 Google Play Billing 查询,试用状态则由应用自己的逻辑计算。

本地实现示例

下面的 Kotlin 示例从用户第一次启动应用时开始计算 7 天试用期:

import android.content.Context

class TrialManager(context: Context) {

    private val preferences = context.getSharedPreferences(
        "trial_preferences",
        Context.MODE_PRIVATE
    )

    companion object {
        private const val KEY_TRIAL_START_TIME = "trial_start_time"
        private const val KEY_LATEST_KNOWN_TIME = "latest_known_time"
        private const val TRIAL_DURATION_MILLIS = 7L * 24 * 60 * 60 * 1000
    }

    fun initializeTrialIfNeeded() {
        if (!preferences.contains(KEY_TRIAL_START_TIME)) {
            val now = System.currentTimeMillis()

            preferences.edit()
                .putLong(KEY_TRIAL_START_TIME, now)
                .putLong(KEY_LATEST_KNOWN_TIME, now)
                .apply()
        }
    }

    fun isTrialActive(): Boolean {
        val startTime = preferences.getLong(KEY_TRIAL_START_TIME, -1L)
        if (startTime < 0L) {
            return false
        }

        val currentTime = System.currentTimeMillis()
        val latestKnownTime = preferences.getLong(
            KEY_LATEST_KNOWN_TIME,
            startTime
        )

        // 检测明显的系统时间回拨。
        if (currentTime < latestKnownTime) {
            return false
        }

        preferences.edit()
            .putLong(KEY_LATEST_KNOWN_TIME, currentTime)
            .apply()

        return currentTime - startTime < TRIAL_DURATION_MILLIS
    }

    fun remainingTrialMillis(): Long {
        val startTime = preferences.getLong(KEY_TRIAL_START_TIME, -1L)
        if (startTime < 0L) {
            return 0L
        }

        val elapsed = System.currentTimeMillis() - startTime
        return (TRIAL_DURATION_MILLIS - elapsed).coerceAtLeast(0L)
    }
}

应用启动时初始化试用:

val trialManager = TrialManager(applicationContext)
trialManager.initializeTrialIfNeeded()

然后结合购买状态决定用户可以使用哪些功能:

enum class AccessState {
    PURCHASED,
    TRIAL_ACTIVE,
    LIMITED
}

fun resolveAccessState(
    ownsFullVersion: Boolean,
    trialManager: TrialManager
): AccessState {
    return when {
        ownsFullVersion -> AccessState.PURCHASED
        trialManager.isTrialActive() -> AccessState.TRIAL_ACTIVE
        else -> AccessState.LIMITED
    }
}

使用功能时可以统一判断:

when (resolveAccessState(ownsFullVersion, trialManager)) {
    AccessState.PURCHASED,
    AccessState.TRIAL_ACTIVE -> enableFullFeatures()

    AccessState.LIMITED -> enableLimitedFeatures()
}

ownsFullVersion 不能只读取本地布尔值,而应通过 Google Play Billing 查询用户当前拥有的商品。

查询一次性商品的购买状态

查询一次性商品的已有购买时,应使用 ProductType.INAPP:

import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.QueryPurchasesParams

fun queryFullVersionPurchase(
    billingClient: BillingClient,
    productId: String,
    onResult: (Boolean) -> Unit
) {
    val params = QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.INAPP)
        .build()

    billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
        if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
            onResult(false)
            return@queryPurchasesAsync
        }

        val ownsProduct = purchases.any { purchase ->
            purchase.purchaseState ==
                com.android.billingclient.api.Purchase.PurchaseState.PURCHASED &&
                productId in purchase.products
        }

        onResult(ownsProduct)
    }
}

实际项目还需要按照 Google Play Billing 的要求确认购买,也就是对尚未确认的购买调用 acknowledgement API。应用不能仅凭购买回调永久解锁功能,也不应只把“已购买”状态写入 SharedPreferences。

Billing Library 的初始化方式和部分接口写法可能随版本变化,具体实现应以项目当前依赖版本的官方文档为准。整体判断逻辑不受影响:购买系统管理永久权益,应用管理试用期限。

不使用服务端的限制

纯本地方案比较容易实现,但无法可靠保证“每个用户只能试用一次”,原因包括:

  • 清除应用数据会删除 SharedPreferences。
  • 卸载后重新安装通常会丢失本地试用记录。
  • 用户可能回拨系统时间。
  • Root、备份恢复或应用篡改可能绕过检查。
  • 同一个 Google 账号在多台设备上的试用状态无法同步。
  • 本地购买状态可能暂时无法查询,因此不能简单地把查询失败当作未购买。

记录 latestKnownTime 只能拦截一部分系统时间回拨行为,无法让纯本地方案变得真正安全。

需要严格限制试用次数时

如果业务要求“同一账号只能试用一次”,应用就需要使用可信的远端状态。常见流程如下:

  1. 用户登录应用账号。
  2. 服务端保存 trialStartedAt 和 trialEndsAt。
  3. 应用从服务端获取当前权益状态。
  4. 服务端验证 Google Play 购买,并把永久购买权益绑定到用户。
  5. 必要时结合 Play Integrity API,提高滥用成本。

服务端只需保存少量权益数据,不一定要实现复杂的订阅系统。试用记录可以使用类似下面的结构:

{
  "userId": "12345",
  "trialStartedAt": "2026-09-13T08:00:00Z",
  "trialEndsAt": "2026-09-20T08:00:00Z",
  "ownsFullVersion": false
}

如果应用不要求用户登录,也可以为设备生成安装标识,但这种方式仍然无法可靠阻止换设备、清除数据或重新安装。

其他实现方式是否合适

Google Play 原生支持订阅商品的免费试用,但试用结束后通常会进入订阅计费流程。这与“试用结束后,由用户自行决定是否购买永久版”的模式不同,因此不应为了使用免费试用功能,把永久解锁商品设计成订阅。

即使目标市场和当前 Play Console 支持一次性商品的优惠、租赁或其他销售配置,它们也不等同于“不发生购买交易的 7 天免费试用”,无法代替应用自己的试用状态管理。

实际建议

如果应用价值不高,也能接受试用被重复使用的风险,本地计时 7 天通常已经足够。界面中最好明确显示剩余试用时间。

如果一次性购买价格较高,或者试用资格会直接影响收入,则应由服务端保存试用状态并验证购买。Google Play Billing 可以可靠管理一次性购买权益,但不能单独管理非消耗型商品的免费试用。

备注:内容仅供参考。