如何使用 Google Market Billing API v2 添加应用内订阅?
结论
不要再参考 Market Billing API v2 或 Dungeons 示例来实现订阅。这些内容对应的是早期计费方案,已经淘汰,无法反映现在 Google Play 的订阅模型和测试流程。
目前应使用 Google Play Billing Library,并按以下流程管理订阅权益:
- 在 Play Console 中创建订阅、基础方案和优惠。
- 通过
BillingClient查询商品并发起购买。 - 持久化
Purchase中的purchaseToken。 - 在服务端通过 Google Play Developer API 验证订阅。
- 验证成功后调用
acknowledgePurchase()。 - 每次启动应用或恢复登录时调用
queryPurchasesAsync(),恢复仍然有效的订阅。
即使客户端当时没有保存 token,也不代表它一定无法找回。只要订阅仍属于当前 Google 账号,通常可以通过查询已有购买,重新取得 Purchase 和 purchaseToken。
旧文档为什么不能直接套用
Market Billing API v2 使用早期的 Android Market/Google Play 计费模型。Dungeons 示例主要讲解一次性商品,没有覆盖后来逐步完善的订阅功能。
现在的订阅还需要处理:
- 基础方案和优惠
- 自动续订与取消
- 宽限期和账号保留
- 暂停、恢复及重新订阅
- 升级或降级
- 服务端验证
- 实时开发者通知
- 购买确认
- 测试续订周期
所以,即使旧示例还能编译,也不适合用作新应用的实现基础。
推荐的完整实现流程
1. 在 Play Console 配置订阅
先创建订阅商品,再为它配置可购买的基础方案。应用代码通常使用产品 ID 查询订阅,但发起购买时需要传入 Google Play 返回的 offerToken。
发布测试版本前,还需要确认:
- 应用包名与 Play Console 中的应用一致。
- APK 或 App Bundle 使用了正确的签名。
- 订阅及其基础方案处于可用状态。
- 测试账号已经加入许可测试人员。
- 测试账号可以访问对应的内部测试或封闭测试版本。
- 设备上的 Google Play 登录账号就是测试账号。
Play Console 的界面和订阅配置项可能会调整,请以当前官方控制台显示的内容为准。
2. 建立 BillingClient
下面的示例省略了 ViewModel、协程作用域和依赖注入等项目结构代码:
private lateinit var billingClient: BillingClient
fun initializeBilling(context: Context) {
billingClient = BillingClient.newBuilder(context)
.setListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
purchases.orEmpty().forEach(::handlePurchase)
}
}
.enablePendingPurchases()
.build()
billingClient.startConnection(
object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
querySubscriptionProducts()
restoreSubscriptions()
}
}
override fun onBillingServiceDisconnected() {
// 稍后重连,实际项目中应使用带退避策略的重试机制。
}
}
)
}
项目应使用 Google Play Billing Library 当前受官方支持的稳定版本,不要直接照搬旧文章中的依赖版本号。
3. 查询订阅及可用优惠
private fun querySubscriptionProducts() {
val product = QueryProductDetailsParams.Product.newBuilder()
.setProductId("annual_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 offer = productDetails.subscriptionOfferDetails
?.firstOrNull()
?: return@queryProductDetailsAsync
// 保存 productDetails 和 offer.offerToken,
// 在用户点击订阅按钮时用于发起购买。
}
}
不要默认第一个优惠就是业务所需的优惠。正式代码应根据基础方案 ID、优惠 ID、标签或价格阶段,选择正确的 offerToken。
4. 发起订阅购买
fun launchSubscriptionPurchase(
activity: Activity,
productDetails: ProductDetails,
offerToken: String
) {
val productParams =
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
val flowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productParams))
.build()
val result = billingClient.launchBillingFlow(activity, flowParams)
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
// 记录错误并向用户显示合适的提示。
}
}
launchBillingFlow() 不会直接返回最终的购买结果。购买完成后,结果会传入创建 BillingClient 时设置的 PurchasesUpdatedListener。
5. 处理并保存 purchase token
private fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
return
}
val purchaseToken = purchase.purchaseToken
// 将 token 发送到自己的服务端进行验证。
verifyPurchaseOnServer(
productId = purchase.products.firstOrNull().orEmpty(),
purchaseToken = purchaseToken
)
// 只有服务端确认订阅有效后,才应授予长期订阅权益。
}
服务端至少要关联以下信息:
- 应用用户 ID
purchaseToken- 产品 ID
- Google Play 返回的订阅状态
- 到期时间
- 是否已确认购买
- 最近一次验证时间
不要只在客户端保存一个”已订阅”布尔值。客户端数据可能被清除或篡改,也无法可靠处理退款、撤销和续订失败。
6. 服务端验证订阅
服务端应使用 Google Play Developer API 验证 purchaseToken。当前订阅接口与早期 Market Billing API v2 使用的不是同一套模型。新系统通常使用订阅购买状态接口,例如 purchases.subscriptionsv2.get。
请求参数主要包括:
packageName: 应用包名
token: 客户端上传的 purchaseToken
服务端需要根据 Google Play 返回的数据判断订阅目前是否仍应提供权益,不能只检查用户是否”曾经购买过”。判断时还要处理到期、取消、宽限期、账号保留和撤销等状态。
访问该 API 需要正确配置 Google Cloud 项目、服务账号和 Play Console 权限。具体授权步骤可能随着控制台更新,请以当前官方文档为准。
7. 确认购买
验证购买并交付权益后,还需要确认购买:
private fun acknowledgeIfNeeded(purchase: Purchase) {
if (purchase.isAcknowledged) {
return
}
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { result ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
// 确认成功。
}
}
}
确认操作必须具有幂等性。网络中断后,可以再次检查 isAcknowledged 并重试,但不能因为收到重复回调而重复创建用户权益。
如果没有及时确认,Google Play 可能会自动退款。具体时限请以当前 Google Play Billing 官方文档为准。
没有保存 token 时如何恢复
对于仍然有效的订阅,可以重新查询当前账号拥有的购买:
private fun restoreSubscriptions() {
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
billingClient.queryPurchasesAsync(params) { result, purchases ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
purchases.forEach(::handlePurchase)
}
}
}
可以在以下时机执行恢复查询:
- BillingClient 连接成功后
- 应用启动或用户登录后
- 用户点击”恢复购买”时
- 应用从后台回到前台时
恢复查询只能返回当前 Google Play 账号能够识别的相关购买。如果设备更换了账号、订阅已经失效,或者购买记录不属于当前应用包名,就可能无法恢复。
如果应用之前从未保存 token,客户端查询也无法返回该购买,那么仅凭信用卡账单或订单截图,通常无法自动为某个应用账号恢复权益。遇到这种情况,需要通过 Google Play 订单记录和开发者后台人工核对。
稳定、可重复的测试流程
使用许可测试账号
不要使用普通生产账号反复购买真实的年度订阅。建议按以下流程测试:
- 将测试 Google 账号加入 Play Console 的许可测试人员。
- 将构建上传到内部测试轨道。
- 让该账号加入测试,并从 Google Play 安装应用。
- 确认购买界面显示的是测试付款方式。
- 完成购买并记录客户端回调。
- 检查服务端是否收到并验证 token。
- 检查购买是否已经确认。
- 重启应用,验证能否通过
queryPurchasesAsync()恢复订阅。 - 清除应用数据并重新登录,再次验证恢复流程。
- 测试取消、续订、到期、支付失败和撤销等状态。
Google Play 测试订阅通常采用缩短的生命周期,但具体周期和重复续订次数可能发生变化,不要在代码中硬编码这些测试时间。
保留可追踪日志
测试时可以记录以下字段,但不要把完整的敏感 token 写入公开日志:
applicationUserId
productId
purchaseState
isAcknowledged
purchaseTokenHash
billingResponseCode
serverVerificationStatus
subscriptionExpiryTime
eventTimestamp
可以对 purchaseToken 进行哈希处理,或者只记录末尾少量字符。这样既方便排查问题,也能避免泄露完整凭证。
每轮测试前检查状态
退款、取消订阅和撤销购买是三种不同的操作:
- 取消订阅通常只是停止后续续费,不一定会立即终止当前权益。
- 退款可能会退回款项,但是否立即撤销权益取决于具体操作方式和订单状态。
- 撤销通常会使订阅权益失效。
- 测试账号中的订阅状态也可能需要等待一段时间才能同步。
因此,不能把”取消”当成”重置测试”。开始每轮测试前,都应通过客户端和服务端确认当前订阅状态。
关于已经发生的真实扣款
如果购买界面使用的是真实付款方式,这就是实际交易,不是沙盒模拟。应用无法通过本地代码自行撤销信用卡扣款。
可以采取以下措施:
- 在 Google Play 订单记录中确认订单状态。
- 在 Play Console 的订单管理页面查找对应订单。
- 根据可用操作执行退款,或退款并撤销。
- 如果控制台显示退款成功,但付款状态仍然异常,请联系 Google Play 支持或支付渠道核查。
- 不要为了重新测试而连续重复购买同一个长期订阅。
早期的 Google Checkout、Market Billing API v2 与现在 Play Console 的退款流程差异很大。旧订单能否退款、是否已经结算,以及能否恢复 token,都要以该订单在后台显示的实际状态为准。
容易遗漏的问题
purchaseToken应立即发送到服务端,不能只保存在设备中。- 购买成功回调可能重复到达,服务端必须进行幂等处理。
- 不能只依赖购买回调,应用启动时还要主动查询已有购买。
- 不要根据客户端时间计算订阅是否到期,应以 Google Play 服务端状态为准。
- 处于
PENDING状态时,不能立即交付完整权益。 - 用户取消自动续订后,在当前已付款周期内,订阅可能仍然有效。
- 退款、撤销或续费失败可能发生在用户没有打开应用的时候。生产环境应结合实时开发者通知和定期服务端校验来处理这些变化。
purchaseToken是重要凭证,不能写入分析平台、崩溃报告或可公开访问的日志。
判断一个订阅系统是否可靠,不能只看购买按钮能否成功扣款。购买、重装、换设备、续订、取消、退款或网络失败后,系统都应该能够根据 Google Play 的权威状态重新建立正确的用户权益。
备注:内容仅供参考。
