You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Google Play Billing Library识别订阅时长(月付/年付)

判断Google Play Billing订阅的时长(月付/年付)

我在Kotlin应用中使用Google Play Billing Library实现订阅功能,同一个产品提供月付和年付两种订阅方案。用户完成购买后,通过BillingClient.queryPurchasesAsync()能获取购买详情,但无法直接得知订阅时长,希望判断用户购买的是月付还是年付订阅。

已尝试操作

  • 通过BillingClient.queryProductDetailsAsync()查询ProductDetails以获取SubscriptionOfferDetails,尝试对比Purchase对象与SubscriptionOfferDetails中的offerToken,但二者不匹配。

问题

如何正确判断已完成购买的订阅时长?有没有合适的方法将购买详情与具体优惠详情进行映射?恳请提供相关指导或示例!

现有相关代码

fun startBillingConnection() {
    billingClient.startConnection(object : BillingClientStateListener {
        override fun onBillingSetupFinished(billingResult: BillingResult) {
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                Log.d(TAG, "Billing response OK")
                queryPurchases()
                queryProductDetails()
                _billingState.update { it.copy(isClientConnected = true) }
            } else {
                Log.e(TAG, billingResult.debugMessage)
                handleBillingError(billingResult, ErrorContext.CONNECTION)
                _billingState.update { it.copy(isClientConnected = false) }
            }
        }

        override fun onBillingServiceDisconnected() {
            Log.i(TAG, "Billing connection disconnected")
            _billingState.update { it.copy(isClientConnected = false) }
        }
    })
}

private fun querySubscriptionProducts() {
    val params = QueryProductDetailsParams.newBuilder()
    val productList = SUBS_PRODUCTS.map { productId ->
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId(productId)
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
    }

    params.setProductList(productList).let { productDetailsParams ->
        billingClient.queryProductDetailsAsync(productDetailsParams.build(), this)
    }
}

override fun onProductDetailsResponse(
    billingResult: BillingResult,
    productDetailsList: MutableList<ProductDetails>
) {
    when (billingResult.responseCode) {
        BillingClient.BillingResponseCode.OK -> {
            if (productDetailsList.isEmpty()) {
                Log.e(TAG, "onProductDetailsResponse: Found null or empty ProductDetails.")
            } else {
                val newProducts = productDetailsList.associateBy { it.productId }
                _billingState.update {
                    it.copy(products = it.products + newProducts)
                }
            }
        }

        else -> {
            handleBillingError(billingResult, ErrorContext.PRODUCT_DETAILS)
        }
    }
}

解决方案

1. 核心思路:通过Purchase的offerToken匹配对应SubscriptionOfferDetails

你之前的问题可能出在对比逻辑或购买发起时的参数传递——只要发起购买时正确传入了目标SubscriptionOfferDetails的offerToken,Purchase对象中的offerToken就会和该优惠详情的offerToken完全匹配。

匹配后,可从SubscriptionOfferDetails的pricingPhases中提取订阅周期信息,该字段采用ISO 8601格式(如P1M代表1个月,P1Y代表1年)。

2. 示例代码:匹配并判断订阅时长

假设你已经维护了products: Map<String, ProductDetails>的全局/局部映射,可添加以下逻辑:

// 根据Purchase判断订阅时长
fun getSubscriptionDuration(purchase: Purchase): String? {
    // 通过productId找到对应的ProductDetails
    val productDetails = products[purchase.productId] ?: return null
    
    // 匹配Purchase的offerToken与SubscriptionOfferDetails的offerToken
    val matchedOffer = productDetails.subscriptionOfferDetails?.find { offer ->
        offer.offerToken == purchase.offerToken
    } ?: return null
    
    // 获取订阅周期:如果有试用/优惠阶段,取最后一个阶段(常规订阅周期),否则取第一个
    val targetPhase = if (matchedOffer.pricingPhases.pricingPhaseList.size > 1) {
        matchedOffer.pricingPhases.pricingPhaseList.last()
    } else {
        matchedOffer.pricingPhases.pricingPhaseList.firstOrNull()
    } ?: return null
    
    // 解析ISO 8601格式的周期字符串
    return when (targetPhase.billingPeriod) {
        "P1M" -> "月付"
        "P1Y" -> "年付"
        "P3M" -> "季付"
        "P6M" -> "半年付"
        else -> "未知周期(${targetPhase.billingPeriod})"
    }
}

// 在查询购买记录的回调中调用
private fun queryPurchases() {
    billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.SUBS)
        .build()) { billingResult, purchases ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                purchases.forEach { purchase ->
                    val duration = getSubscriptionDuration(purchase)
                    Log.d(TAG, "用户订阅类型:$duration")
                }
            }
        }
}

3. 关键注意事项

  • 发起购买时,必须传入对应SubscriptionOfferDetails的offerToken,否则Purchase中的offerToken无法匹配:
    // 发起购买的示例代码片段
    val offerToken = matchedSubscriptionOffer.offerToken
    val billingFlowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(listOf(
            BillingFlowParams.ProductDetailsParams.newBuilder()
                .setProductDetails(productDetails)
                .setOfferToken(offerToken)
                .build()
        ))
        .build()
    billingClient.launchBillingFlow(activity, billingFlowParams)
    
  • 若订阅包含试用/ introductory price,pricingPhaseList会有多个阶段,优先取最后一个阶段作为常规订阅周期。

内容的提问来源于stack exchange,提问作者TheDragoner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 20:55:15