PAYATHON 2026

Android In-App Billing 如何获取订阅产品的全部付款记录?

支付小周

结论

不能通过 Android 端的 In-App Billing API 获取某个订阅产品的全部历史付款记录。

BillingClient.queryPurchasesAsync()主要查询 Google Play 当前识别为有效的购买,不是完整的账单或历史订单接口。订阅取消、过期、退款或设备重置后,仅靠客户端返回的数据,无法还原用户过去购买过哪些月份。

建议采用以下流程:

  1. 要求用户登录应用自己的业务账号。
  2. 每次购买后,将 purchaseToken 和产品信息发送到服务端。
  3. 服务端通过 Google Play Developer API 验证订阅。
  4. 服务端持续处理续订、取消、过期和退款等状态变化。
  5. 将每个已结算且确认有效的订阅周期转换为永久内容权益。
  6. 应用重装后,根据业务账号从服务端恢复权益,不再尝试从客户端数据反推历史付款。

为什么客户端无法完成历史恢复

客户端通常可以获得以下信息:

  • productId
  • purchaseToken
  • 当前购买状态
  • 购买是否已确认
  • 部分订单标识和购买时间

这些信息适合处理当前购买和订阅状态,但不能保证包含:

  • 所有已经过期的续订周期
  • 每次续订的完整付款记录
  • 已取消订阅对应的历史月份
  • 退款、撤销或拒付后的最终状态
  • 用户格式化设备前保存在本地的权益

即使某些 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

应用重装后的恢复流程

用户重装应用后,可以按以下流程恢复:

  1. 用户登录原来的应用账号。
  2. 应用向业务服务端请求内容权益。
  3. 服务端返回已经授予的八月刊、九月刊等内容。
  4. 应用查询当前的 Google Play 购买,补传尚未同步的购买。
  5. 服务端验证补传的 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 支付政策的结算方式。

不能因为用户曾经有过一次订阅购买,就直接开放全部历史杂志;也不能只根据当前订阅是否有效,决定历史月份能否阅读。订阅状态用于判断用户现在是否还能获得新内容,权益记录则用于确定用户过去已经取得了哪些内容。

备注:内容仅供参考。