Android in-app subscription 如何实现可变月费?
结论
Google Play Billing 不支持这样的订阅模式:月底根据用户实际完成的任务数量计算出任意金额,再按这个金额自动续订。
Google Play 订阅必须关联预先配置好的商品、基础方案和价格。开发者可以设置优惠、试用、分阶段价格,也可以让用户变更订阅档位,但不能在每个结算周期结束时,向 Google Play 提交临时计算的续费金额,更不能在未经用户确认的情况下扣除任意金额。
具体可以采用哪种替代方案,取决于收费对象:
- 如果任务属于 App 内的数字功能、数字内容或云端服务,通常应使用 Google Play Billing。可以设置固定订阅档位、预付任务包,或者采用“基础订阅 + 消耗型商品”的模式。
- 如果收费对象是实体商品或现实世界服务,例如运输、家政、线下培训,通常不属于数字内容交易,可以使用适合该业务的外部支付方式。
- 如果 App 符合特定地区或政策计划的资格,也可能使用替代结算或外部购买方案,但这些方案并非对所有 App 开放。上线前需要根据目标国家、商品类型和当时有效的 Google Play 政策逐项核对。
为什么订阅不能动态续费
Google Play 的订阅价格由 Play Console 中配置的订阅商品决定。客户端发起购买时,选择的是确定的 productId、basePlanId,以及可能存在的 offerToken,不能把本月应付金额直接传给 Google Play。
Google Play Billing 没有下面这种接口:
// Google Play Billing 不支持这种动态金额调用
billingClient.renewSubscription(
purchaseToken = token,
amount = calculatedMonthlyAmount
)
Google Play Developer API 主要用于查询、确认、取消、延期购买,或者管理已经发生的购买及订阅状态,不能把任意金额写入下一次续费。
调整订阅价格也不是用量计费。价格调整通常针对商品或基础方案,并且可能涉及用户通知、用户确认或政策规定的生效流程,不适合每个月为每位用户单独计算账单。
方案一:设置多个固定订阅档位
如果每月费用可以划分为有限的几个区间,可以提前创建多个基础方案或订阅商品,例如:
- 基础版:每月包含 100 个任务
- 标准版:每月包含 500 个任务
- 专业版:每月包含 2,000 个任务
服务器统计用户的实际用量,并建议用户在下一个周期切换档位。订阅变更仍然需要通过 Google Play Billing 发起,服务器不能静默提高价格。
查询订阅商品的示例:
val product = QueryProductDetailsParams.Product.newBuilder()
.setProductId("task_subscription")
.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
}
val productDetails = result.productDetailsList.firstOrNull()
?: return@queryProductDetailsAsync
val selectedOffer = productDetails.subscriptionOfferDetails
?.firstOrNull { offer ->
offer.basePlanId == "standard-monthly"
}
?: return@queryProductDetailsAsync
val productParams =
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(selectedOffer.offerToken)
.build()
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productParams))
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
具体的类名和返回结构可能会随 Google Play Billing Library 的版本变化,实施时应以所用版本的官方文档为准。
这种方案的账单比较稳定,也容易让用户理解,但只能按档位收费,无法精确到每次任务。
方案二:基础订阅加消耗型任务包
如果业务需要按量收费,可以把费用分成两部分:
- 固定月费订阅,用于提供基础服务。
- 用户用完额度后,主动购买额外任务包。
例如,订阅每月包含 100 次任务。额度用完后,用户可以购买包含 50 次、200 次或 1,000 次任务的消耗型商品。
查询一次性商品:
val product = QueryProductDetailsParams.Product.newBuilder()
.setProductId("task_pack_50")
.setProductType(BillingClient.ProductType.INAPP)
.build()
val params = QueryProductDetailsParams.newBuilder()
.setProductList(listOf(product))
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, result ->
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
return@queryProductDetailsAsync
}
val details = result.productDetailsList.firstOrNull()
?: return@queryProductDetailsAsync
val productParams =
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(details)
.build()
val flowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productParams))
.build()
billingClient.launchBillingFlow(activity, flowParams)
}
购买完成后,服务器应先验证购买凭证,再为用户增加额度。不要只依赖客户端回调,否则相关数据容易被篡改。
伪代码如下:
receive purchase token
verify token with Google Play Developer API
check product ID and purchase state
check whether this purchase has already been credited
add 50 tasks to user's account
consume or acknowledge the purchase as required
record transaction atomically
这种模式可以达到接近按量付费的效果,但每次购买都需要用户明确确认,不能在月底自动补扣差额。
方案三:预付余额或任务点数
另一种做法是销售固定面额的点数包,并在用户执行任务时扣除相应点数:
purchase 100 credits
perform task A -> deduct 2 credits
perform task B -> deduct 5 credits
如果点数属于数字权益,通常仍需通过 Google Play Billing 购买。服务器必须维护可靠的账本,至少记录以下信息:
user_id
transaction_id
purchase_token
credit_delta
balance_after
created_at
reason
扣减余额时,应使用数据库事务或其他原子操作,避免用户并发执行任务时出现重复扣费或负余额。
点数方案适合不同任务成本不一致的场景,但需要向用户清楚说明兑换规则、有效期、退款处理方式,以及余额能否跨月保留。
方案四:月底生成账单,用户手动支付
如果必须精确计算当月金额,可以由服务器生成账单,再由用户主动完成一次 Google Play 购买。由于 Google Play 商品价格必须提前配置,只能准备若干固定面额,并将账单金额拆分或映射到最接近的金额。
例如:
本月费用 = 37 元
可购买商品 = 30 元任务包 + 7 元任务包
这种方式会增加购买步骤和商品管理成本,也不能假定一次结算流程能够任意组合大量商品。实际使用时,设置合理的固定面额或采用点数体系通常更方便。
不要为了模拟每一种可能的金额而创建海量商品。这不仅难以维护,还可能带来商品数量、审核、展示和用户体验方面的问题。
外部支付是否可用
Google Play Billing 不支持动态月费,并不意味着可以直接在 App 中加入网页支付链接。能否使用外部支付,取决于收费对象和 App 适用的政策,而不是交易是否被称为“订阅”。
通常需要区分以下情况:
- App 内数字内容、数字功能、虚拟物品或数字服务:通常需要遵守 Google Play 的结算要求。
- 实体商品或现实世界服务:通常可以使用其他支付系统。
- 只供用户登录并消费已在其他渠道购买内容的 App:可能适用不同规则,但不能因此认定可以在 App 内引导用户购买。
- 特定国家或地区的替代结算、用户选择结算或外部购买计划:只有满足资格、接入和披露要求后才能使用。
Google Play 的支付政策可能调整,也会因地区和业务类型而有所不同。因此,不能只根据早期的订阅条款决定当前的支付方案。发布前应核对目标市场当时有效的 Payments policy、订阅政策,以及适用的替代结算计划。
推荐的落地设计
对于 App 内的数字任务,可以采用下面这种设计:
固定月度订阅
+ 每月包含一定任务额度
+ 超额后购买消耗型任务包
服务器负责:
1. 记录每次任务的计费单位
2. 管理订阅包含的月度额度
3. 验证 Google Play 购买凭证
4. 防止同一订单重复入账
5. 处理取消、退款和撤销购买
6. 在新结算周期重置或结转额度
客户端只负责展示商品信息和启动购买流程。商品价格、币种和格式应使用 ProductDetails 返回的数据,不要在客户端硬编码:
val formattedPrice = productDetails.oneTimePurchaseOfferDetails
?.formattedPrice
注意事项
- 不要尝试在后台直接从用户的银行卡或支付账户中扣除动态差额。
- 不要把优惠阶段、免费试用或延期续订当作用量计费接口。
- 不要只根据客户端上报的数据增加任务额度。购买验证和记账应在服务器端完成。
- 用户升级或降级订阅时,应明确展示新价格、生效时间和额度变化。
- 发生退款、撤销购买或订阅失效后,应同步调整尚未使用的数字权益。
- 确定支付方案前,应先判断任务属于数字服务还是现实世界服务。这个分类会直接影响适用的结算政策。
- Google Play Billing Library 和支付政策会不断变化,具体接入方式及政策资格应以发布时的官方文档和 Play Console 配置为准。
如果目标是实现完全自动的后付费模式,根据用户实际完成的任务数量生成任意金额的月度账单,那么 Google Play 订阅并不适用。对于数字服务,通常需要改用固定档位、预付点数,或者“订阅 + 消耗型商品”。只有收费对象不受 Google Play 数字内容结算要求约束,或者 App 明确符合相应地区计划时,才可以考虑外部结算。
备注:内容仅供参考。
