Android In-App Billing 如何获取订阅产品的全部付款记录?
结论
不能通过 Android 端的 In-App Billing API 获取某个订阅产品的全部历史付款记录。
BillingClient.queryPurchasesAsync()主要查询 Google Play 当前识别为有效的购买,不是完整的账单或历史订单接口。订阅取消、过期、退款或设备重置后,仅靠客户端返回的数据,无法还原用户过去购买过哪些月份。
建议采用以下流程:
- 要求用户登录应用自己的业务账号。
- 每次购买后,将
purchaseToken和产品信息发送到服务端。 - 服务端通过 Google Play Developer API 验证订阅。
- 服务端持续处理续订、取消、过期和退款等状态变化。
- 将每个已结算且确认有效的订阅周期转换为永久内容权益。
- 应用重装后,根据业务账号从服务端恢复权益,不再尝试从客户端数据反推历史付款。
为什么客户端无法完成历史恢复
客户端通常可以获得以下信息:
productIdpurchaseToken- 当前购买状态
- 购买是否已确认
- 部分订单标识和购买时间
这些信息适合处理当前购买和订阅状态,但不能保证包含:
- 所有已经过期的续订周期
- 每次续订的完整付款记录
- 已取消订阅对应的历史月份
- 退款、撤销或拒付后的最终状态
- 用户格式化设备前保存在本地的权益
即使某些 API 或控制台能够显示历史订单,也不应让移动端直接把账单历史作为永久权益数据库。客户端数据可能被清除,也无法可靠处理异步发生的购买状态变化。
设备、Google Play 账号或邮箱也不能单独作为业务权益标识。应用通常无法获得可供服务端稳定关联购买者的 Google Play 邮箱。用户需要登录应用自己的账号,或者使用应用能够长期恢复的账号体系。
推荐的数据模型
服务端至少需要保存两类数据。
订阅事件记录:
subscription_events
- app_user_id
- product_id
- purchase_token
- order_id
- event_type
- period_start
- period_end
- payment_state
- raw_response
- created_at
内容权益记录:
content_entitlements
- app_user_id
- content_id
- source_product_id
- source_purchase_token
- entitlement_start
- entitlement_end
- granted_at
- revoked_at
例如,用户的订阅周期为:
2025-08-12T10:00:00Z ~ 2025-09-12T10:00:00Z
服务端可以按照业务规则,为用户永久授予八月刊或该周期内发布内容的访问权。下一次成功续订后,再授予九月刊。
需要把”当前订阅状态”和”已经获得的内容权益”分开管理:
- 取消订阅只表示以后不再自动续订。
- 订阅过期后,用户不能继续获得新内容。
- 已经授予的历史月份权益仍可保留。
- 如果发生退款、撤销或拒付,是否撤销对应内容,由业务规则决定。
购买时的客户端处理
以下示例使用 Kotlin,省略了 BillingClient 的初始化和连接管理:
private fun handlePurchases(purchases: List<Purchase>) {
purchases.forEach { purchase ->
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
return@forEach
}
val request = VerifyPurchaseRequest(
productIds = purchase.products,
purchaseToken = purchase.purchaseToken
)
purchaseApi.verifySubscription(request) { verified ->
if (verified && !purchase.isAcknowledged) {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { billingResult ->
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
// 安排重试,不能只忽略失败
}
}
}
}
}
}
客户端请求的数据结构可以保持简单:
data class VerifyPurchaseRequest(
val productIds: List<String>,
val purchaseToken: String
)
客户端不能自行声明”这次购买对应八月刊”。它只负责上传购买凭据,订阅状态和有效周期必须由服务端验证后确定。
服务端验证成功后才能确认购买,同时要满足 Google Play 对购买确认时限的要求。生产环境还需要采用幂等处理,避免网络重试导致权益被重复授予。
服务端验证和授予权益
服务端收到 purchaseToken 后,应调用 Google Play Developer API 查询订阅状态。订阅 API 的具体接口和返回字段可能随版本变化,调用时应以当前官方文档为准。
核心逻辑可以写成:
function verifySubscription(appUserId, productId, purchaseToken):
subscription = googlePlay.getSubscription(productId, purchaseToken)
if subscription is not valid:
reject purchase
begin transaction
saveOrUpdateSubscription(
appUserId,
productId,
purchaseToken,
subscription
)
for each verified billing period in subscription:
contentIds = resolveContentForPeriod(
period.startTime,
period.endTime
)
for each contentId in contentIds:
insertEntitlementIfAbsent(
appUserId,
contentId,
purchaseToken
)
commit transaction
insertEntitlementIfAbsent()必须有唯一约束,例如:
UNIQUE (app_user_id, content_id, source_purchase_token)
这样,无论客户端重复上传购买、服务端重复收到通知,还是任务重复执行,都不会重复发放权益。
持续跟踪续订和退款
只在用户打开应用时验证一次并不够。续订、取消、暂停、宽限期、账号保留、退款和撤销,都可能在应用未运行时发生。
通常需要配合使用:
- Google Play Developer API:查询权威的订阅状态。
- Real-time Developer Notifications:接收订阅状态变化通知。
- 定时对账任务:处理通知丢失、失败或延迟。
- 服务端事件表:记录已经处理的通知和状态变化。
实时通知通常只表明”某项购买发生了变化”。服务端收到通知后,仍应使用对应的 purchaseToken 查询 Google Play,不能只根据通知类型直接修改权益。
通知处理同样需要保证幂等:
receive notification
-> deduplicate notification
-> query Google Play
-> update subscription state
-> grant or revoke entitlements according to business rules
应用重装后的恢复流程
用户重装应用后,可以按以下流程恢复:
- 用户登录原来的应用账号。
- 应用向业务服务端请求内容权益。
- 服务端返回已经授予的八月刊、九月刊等内容。
- 应用查询当前的 Google Play 购买,补传尚未同步的购买。
- 服务端验证补传的
purchaseToken,再更新订阅和权益。
客户端查询当前购买的示例:
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
handlePurchases(purchases)
}
}
这一步只能补同步当前可查询的购买,不能代替服务端保存的历史权益库。如果用户没有登录原来的业务账号,应用通常无法安全判断应该恢复哪个用户的历史月份。
订阅产品是否适合这个场景
如果用户获得每一期杂志后都能永久阅读,业务上需要明确区分:
- 订阅:用户在有效周期内持续获得新刊。
- 单期购买:永久解锁指定的一期。
- 历史权益:用户在订阅有效期间已经获得,并在订阅结束后继续保留的刊物。
可以继续用订阅销售连续的新刊访问权,但服务端必须把每个有效订阅周期转换为具体的刊物权益。如果用户需要自由购买任意历史单期,更适合把单期设计为一次性商品,或者使用服务端商品系统,并采用符合 Google Play 支付政策的结算方式。
不能因为用户曾经有过一次订阅购买,就直接开放全部历史杂志;也不能只根据当前订阅是否有效,决定历史月份能否阅读。订阅状态用于判断用户现在是否还能获得新内容,权益记录则用于确定用户过去已经取得了哪些内容。
备注:内容仅供参考。
