PAYATHON 2026

Android InApp Billing 订阅续费与取消测试

支付阿杰

明确结论

  • 用户在 Google Play 之外取消订阅时,应用通常收不到 Intent,PurchasesUpdatedListener 也不一定会触发回调。
  • 自动续费成功或失败时,同样不能依赖客户端 Intent。生产环境应使用 Real-time Developer Notifications(RTDN)通知服务端,再由服务端调用 Google Play Developer API 查询最终状态。
  • 正常自动续费通常会继续使用原 purchase token,但每次续订都会产生新的订单记录。重新订阅、升级、降级或替换订阅时,则可能获得新的 token。
  • 测试时不用真的等待一个月。将测试账号配置为 License Tester 后,Google Play 会启用加速的测试订阅周期。具体周期和续费次数可能随 Play Billing 测试规则变化,应以 Play Console 当前说明和实际返回值为准。
  • purchase token 只是查询订阅的标识,不代表订阅当前有效。是否继续向用户提供权益,应根据服务端查询到的到期时间、订阅状态和撤销状态判断。

取消订阅时会发生什么

用户可以在 Google Play 的订阅管理页面关闭自动续费。取消后,已经支付的订阅权益一般不会立即失效,而是持续到当前计费周期结束。

问题中使用的是旧版响应:

{
  "kind": "androidpublisher#subscriptionPurchase",
  "initiationTimestampMsec": "1378389347847",
  "validUntilTimestampMsec": "1379037347847",
  "autoRenewing": true
}

取消后,响应通常会变成:

{
  "validUntilTimestampMsec": "1379037347847",
  "autoRenewing": false
}

这表示订阅不会继续自动续费,但在 validUntilTimestampMsec 之前通常仍然有效。判断实际权益时,还需要考虑退款、撤销、暂停、宽限期和账号保留等状态。

这些字段来自较早的订阅接口。当前 Google Play Developer API 通常使用 purchases.subscriptionsv2.get,返回结构也有所不同,需要通过订阅状态、lineItems[].expiryTime 和自动续费计划等信息判断状态。维护旧系统时,应先确认项目实际调用的是哪个接口版本。

自动续费时会发生什么

自动续费不需要用户重新打开应用,因此客户端回调不能作为确认续费的依据。

正常续费成功后,一般会有以下变化:

  • 到期时间延长;
  • 订阅状态仍然有效;
  • 自动续费标志保持开启;
  • 生成新的续费订单记录;
  • 连续正常续费时,通常继续使用原 purchase token。

如果续费扣款失败,订阅可能进入宽限期或账号保留状态,之后也可能最终过期。此时只检查 autoRenewing 或本地缓存无法准确判断订阅状态。

purchase token 是否会变化

同一条连续自动续费的订阅通常会一直使用相同的 purchase token。各次续费交易一般通过新的订单 ID 区分,而不是每个月分配一个新 token。

以下情况可能产生新 token:

  • 订阅过期后重新购买;
  • 取消后重新订阅;
  • 升级或降级至其他订阅方案;
  • 替换基础方案或优惠;
  • 购买流程建立了新的订阅关系。

因此,服务端不应将 token 永久绑定为“一个用户只能有一个”。可以按以下方式处理:

  1. 保存 token、应用包名、订阅商品或基础方案,以及应用用户 ID。
  2. 收到新 token 后,调用 Google Play Developer API 验证。
  3. 根据接口返回的关联购买信息处理新旧 token 之间的关系。
  4. 根据当前查询结果决定是否提供权益,不要通过 token 是否变化推断续费状态。

purchase token 属于敏感凭据,不应写入日志或分析事件,也不能暴露给其他用户。

推荐的状态同步方式

订阅系统可以通过三层检查同步状态:

  1. 客户端在购买完成后取得 purchase token,并将其发送给服务端。
  2. 服务端调用 Google Play Developer API 验证订阅并保存状态。
  3. 服务端接收 RTDN,在订阅续费、取消、暂停、恢复或撤销后重新查询 API。

RTDN 只表示订阅发生了变化,不能直接作为发放或撤销权益的唯一依据。收到通知后,服务端仍需调用 Developer API 获取最终状态。

客户端启动或回到前台时,也可以查询当前购买记录:

val params = QueryPurchasesParams.newBuilder()
    .setProductType(BillingClient.ProductType.SUBS)
    .build()

billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        purchases.forEach { purchase ->
            // 将 purchase.purchaseToken 发送到服务端验证。
            // 不要仅凭客户端返回结果直接授予长期订阅权益。
            sendPurchaseTokenToBackend(purchase.purchaseToken)
        }
    }
}

PurchasesUpdatedListener 主要用于处理当前客户端购买流程产生的更新,不能作为监听外部取消和自动续费事件的通用方式。

服务端验证示例

下面的结构化示例主要说明验证流程。认证方式、异常处理和字段映射,需要根据项目使用的 Google API 客户端版本补充:

async function refreshSubscription(packageName, purchaseToken) {
  const response = await androidPublisher.purchases.subscriptionsv2.get({
    packageName,
    token: purchaseToken
  });

  const subscription = response.data;

  // 应根据 subscriptionState、lineItems[].expiryTime、
  // acknowledgementState 以及撤销、暂停等信息计算权益。
  await saveSubscriptionState({
    packageName,
    purchaseToken,
    rawState: subscription
  });

  return subscription;
}

不要只用“当前时间小于到期时间”判断所有状态。退款或撤销可能要求立即终止权益;宽限期内是否继续提供权益,则取决于产品策略。

完整测试流程

1. 配置测试环境

在 Play Console 中完成以下配置:

  • 将测试邮箱加入 License Testing;
  • 确保测试账号可以访问对应测试轨道;
  • 使用从 Google Play 安装的测试版本;
  • 使用测试支付方式,避免误用真实付款账号;
  • 确认订阅商品、基础方案和优惠均处于可测试状态。

测试账号、设备上 Play Store 当前登录的账号,以及加入测试轨道的账号应保持一致。

2. 测试首次购买

完成购买后,检查以下内容:

  • 客户端是否取得 purchase token;
  • 是否按要求完成 acknowledgement;
  • token 是否成功上传至服务端;
  • Developer API 是否返回有效订阅;
  • 应用是否只在服务端验证通过后开通权益。

如果没有及时确认购买,交易可能被自动退款,因此不能遗漏 acknowledgement。

3. 测试取消

购买后进入 Google Play 的订阅管理页面取消订阅,并验证:

  • 应用没有收到 Intent 时能否正常处理;
  • RTDN 是否到达服务端;
  • 服务端重新查询后能否识别订阅已停止自动续费;
  • 当前周期结束前是否继续保留权益;
  • 到期后是否自动关闭权益。

4. 测试自动续费

保持订阅的自动续费状态,等待加速测试周期结束,然后比较续费前后的服务端响应:

  • purchase token 是否保持不变;
  • 到期时间是否延长;
  • 是否生成新的续费订单;
  • 是否收到与续费相关的 RTDN;
  • 应用未运行时,服务端能否正常更新订阅状态。

不要在代码中写死测试周期。周期长度可能因订阅周期和 Google Play 当前的测试规则而异。

5. 测试扣款失败和恢复

使用 Google Play 提供的测试支付场景,覆盖以下状态:

  • 续费付款成功;
  • 付款失败;
  • 宽限期;
  • 账号保留;
  • 用户修复付款方式后恢复;
  • 最终过期;
  • 退款或撤销。

每种状态都要验证对应的服务端权益策略,并检查客户端重新进入应用后能否正确同步状态。

6. 测试重新订阅和方案变更

取消订阅并等待其过期后重新购买,同时测试升级、降级和替换方案。重点检查:

  • 是否返回新的 purchase token;
  • 新旧 token 能否正确关联;
  • 生效时间和按比例计费行为是否符合配置;
  • 数据库是否会继续将旧订阅误判为有效。

注意事项

  • 不要依赖广播、Intent 或单次客户端回调维护订阅状态。
  • 不要看到 autoRenewing: false 就立即关闭权益。它通常表示当前周期结束后不再续费。
  • 不要仅凭 token 存在就认定订阅有效。
  • RTDN 可能重复、延迟或乱序,因此处理逻辑需要具备幂等性。
  • 服务端应保存最近一次查询时间、到期时间和原始状态,以便审计和排查故障。
  • 测试环境可以验证大部分状态流转,但正式上线前仍需监控少量真实订阅的续费、退款和异常支付流程。

备注:内容仅供参考。