PAYATHON 2026

Android In App Billing 定期订阅如何检测续订

支付阿杰

明确结论

Google Play 的自动续订订阅在正式环境中会按周期自动续费,应用无需主动发起续订。这个过程会一直持续,直至用户取消订阅、付款失败且无法恢复,或者开发者撤销订阅等情况发生。

建议按以下方式检测续订:

  1. 用户首次购买时,将 purchaseToken 上传到服务器。
  2. 服务器通过 Google Play Developer API 查询订阅状态。
  3. 配置 Real-time Developer Notifications(RTDN),接收续订、取消、暂停和恢复等事件。
  4. 收到通知后,再调用 Google Play Developer API 查询最终状态,不要直接根据通知内容授予权益。

正常的周期续订通常沿用原来的 purchaseToken,不会在每个账期生成新 token。升级、降级或重新订阅时可能产生新的 purchaseToken,服务器需要结合 linkedPurchaseToken 处理订阅关系。

应用如何知道订阅已经续订

续订成功后,Google Play 会更新服务端的订阅状态。客户端也可以通过 BillingClient.queryPurchasesAsync() 查询当前用户拥有的订阅,但不应将客户端查询作为唯一依据,原因包括:

  • 应用可能长时间没有启动;
  • 网络或缓存可能导致状态没有及时更新;
  • 退款、撤销和付款宽限期等变化可能发生在应用关闭期间;
  • 客户端数据更容易被篡改。

实际项目通常以服务器查询结果为准,客户端查询主要用于用户登录后快速刷新界面。

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 和对应商品信息发送到自己的服务器验证
            uploadPurchaseTokenToServer(purchase.purchaseToken)
        }
    }
}

客户端返回“已购买”,并不表示服务器已经确认用户当前仍拥有完整权益。最终判断应在服务器完成。

使用 purchaseToken 查询订阅状态

服务器保存客户端上传的 purchaseToken 后,可以调用 Android Publisher API 的 purchases.subscriptionsv2.get:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}
Authorization: Bearer {accessToken}

参数含义如下:

  • packageName 是应用包名;
  • token 是 Billing Library 返回的 purchaseToken;
  • accessToken 应通过有权访问 Google Play Developer API 的服务器凭据获取。

简化的服务端请求示例:

async function getSubscription(packageName, purchaseToken, accessToken) {
  const url =
    `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/` +
    `${encodeURIComponent(packageName)}/purchases/subscriptionsv2/tokens/` +
    `${encodeURIComponent(purchaseToken)}`;

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${accessToken}`
    }
  });

  if (!response.ok) {
    throw new Error(`Google Play API returned ${response.status}`);
  }

  return response.json();
}

查询结果中需要重点检查:

  • subscriptionState;
  • lineItems[].expiryTime;
  • lineItems[].autoRenewingPlan.autoRenewEnabled;
  • acknowledgementState;
  • linkedPurchaseToken;
  • 暂停、取消以及付款相关的上下文。

判断用户当前是否拥有权益时,不能只看自动续订是否开启。用户取消自动续订后,通常仍可使用服务,直到当前已付款周期结束。更可靠的方式是结合订阅状态和 expiryTime 判断。

purchaseToken 是否会在续订时变化

普通的自动续订通常会沿用同一个 purchaseToken。服务器可以继续使用该 token 查询最新的到期时间和订阅状态。

以下操作可能产生新 token:

  • 用户升级或降级订阅;
  • 用户在订阅结束后重新购买;
  • 订阅被替换;
  • 其他会建立新购买关系的操作。

新订阅信息可能带有 linkedPurchaseToken,用于指向上一笔相关购买。服务器应保存 token 之间的关联,并避免旧订阅继续重复授予权益。

建议至少保存以下字段:

user_id
package_name
product_id
purchase_token
linked_purchase_token
subscription_state
expiry_time
last_verified_at

一个用户可能对应多个 token,因此不能假设每个用户始终只有一个 token,也不能把 purchaseToken 当作用户身份。它表示的是购买关系,需要与应用自己的用户账号绑定。

使用 RTDN 及时检测续订

轮询 Google Play Developer API 可以确认订阅状态,但不适合频繁调用。较完整的做法是配置 Real-time Developer Notifications,通过 Google Cloud Pub/Sub 接收订阅事件。

处理流程通常如下:

  1. 用户首次购买订阅。
  2. 客户端将 purchaseToken 上传到服务器。
  3. 服务器调用 Google Play Developer API 验证并保存订阅。
  4. Google Play 中发生续订、取消、恢复、暂停或撤销等变化。
  5. RTDN 将事件发送到 Pub/Sub。
  6. 服务器从通知中取得 purchaseToken。
  7. 服务器再次调用 purchases.subscriptionsv2.get。
  8. 根据查询结果更新权益和到期时间。

RTDN 只表示订阅状态发生了变化,不包含最终可信的订阅详情。消息可能重复、延迟或乱序,因此处理程序必须保证幂等,并以 API 查询结果为准。

如何测试自动续订

Android 订阅应通过 Google Play Console 提供的测试轨道和许可测试账号进行测试。不要依靠本地安装的未发布 APK 判断完整的订阅生命周期,因为购买流程需要由 Google Play 环境正确处理。

基本步骤如下:

  1. 在 Play Console 中创建订阅商品和基础方案。
  2. 将测试账号配置为许可测试人员。
  3. 把应用发布到内部测试轨道。
  4. 使用测试账号从 Google Play 安装应用。
  5. 完成测试购买。
  6. 检查客户端查询结果、RTDN 消息和 Developer API 返回的状态。

测试订阅通常使用加速的续订周期,并会在续订有限次数后停止。具体周期和次数可能因 Google Play 的测试规则、基础方案和账号配置而变化,因此不应将“一天”或某个固定次数写进业务逻辑。测试时,应以当前 Play Console 的实际表现和官方测试规则为准。

如果测试订阅在一天后结束,可能是测试环境停止了加速续订,并不表示正式环境中的订阅也只持续一天。正式订阅会按照配置的真实结算周期自动续费。

权益判断示例

下面是一个简化示例。实际项目还需要处理宽限期、账号保留、暂停、撤销和延迟付款等状态。

function hasSubscriptionEntitlement(subscription, now = new Date()) {
  const activeStates = new Set([
    "SUBSCRIPTION_STATE_ACTIVE",
    "SUBSCRIPTION_STATE_IN_GRACE_PERIOD"
  ]);

  if (!activeStates.has(subscription.subscriptionState)) {
    return false;
  }

  return (subscription.lineItems ?? []).some((item) => {
    if (!item.expiryTime) {
      return false;
    }

    return new Date(item.expiryTime) > now;
  });
}

不能只根据 autoRenewEnabled 判断权益:

  • false 可能只表示用户取消了下一周期的续订,当前周期仍然有效;
  • true 也不能保证下一次扣款一定成功;
  • 订阅处于宽限期、暂停或账号保留状态时,需要按照产品规则决定是否继续提供服务。

需要特别注意的事项

  • 首次购买必须按照 Google Play Billing 的要求进行确认,否则购买可能被自动退款。
  • 不要在客户端保存服务账号密钥,也不要让客户端直接调用需要服务器凭据的 Developer API。
  • RTDN 消息可能重复,处理程序需要支持重复消费,并在收到消息后重新查询订阅状态。
  • 服务器应定期校验仍处于有效状态的订阅,用于弥补 RTDN 消息丢失或配置异常。
  • 到期时间应以 Google Play 返回的值为准,不要直接在本地给上次到期时间增加一个月。
  • 发生升级、降级或重新订阅时,需要处理 linkedPurchaseToken,避免新旧购买同时授予权益。
  • 业务数据库应将购买关系绑定到应用自己的登录用户,不要依赖设备、Google 账号显示信息或 purchaseToken 识别用户。

备注:内容仅供参考。