PAYATHON 2026

Google In-App Subscription 重新订阅状态异常

支付小周

结论

在旧版 Android In-App Billing V3 的测试环境中,取消订阅后再次订阅,可能会遇到 Purchase token 没有变化、autoRenewing 和 expiryTimeMillis 更新较慢的情况。测试订阅的续订周期经过压缩,而 Google Play 客户端、Play 管理后台和 Android Publisher API 之间并非强一致,因此短时间内显示不同状态是可能的。

但如果重新订阅已经成功,API 却长期返回过去的到期时间和 autoRenewing: false,就不能把它当作正确的最终状态。常见原因包括:

  • 用户在原订阅到期前恢复了自动续订,因此仍沿用原 Purchase token。
  • Google Play 的测试状态还没有同步到 Android Publisher API。
  • 服务端、代理层或 HTTP 客户端缓存了旧的查询结果。
  • 客户端上传给服务端的仍是旧 Purchase token。
  • 测试订阅已经达到测试环境允许的续订次数,随后进入终止状态。

为什么 Purchase token 可能不变

用户取消订阅时,通常只是关闭自动续订,当前订阅权益不会立即终止。在 expiryTimeMillis 到达之前,订阅仍然有效。

如果用户在有效期内重新打开自动续订,这更接近“恢复订阅”,并不是创建一笔全新的订阅交易。Google Play 可以继续使用原 Purchase token,发生变化的是订阅资源中的续订状态和到期时间。

原订阅真正到期后再次购买,通常才会形成新的购买链路并返回新的 Purchase token。即便如此,也不能把“token 必须变化”当作唯一判断标准。测试环境、订阅替换和恢复购买等流程的表现可能不同。

为什么状态没有立即更新

Google Play 的订阅状态来自多个位置:

  • Play 商店客户端返回的购买对象
  • Google Play 管理后台显示的订阅状态
  • Android Publisher API 返回的订阅资源
  • 实时开发者通知(RTDN)
  • 应用服务端保存的订阅记录

这些数据的更新时间可能不同。管理后台已经显示“将在 X 续订”,不代表 API 查询结果会在同一时刻变为 autoRenewing: true。

测试订阅的周期被大幅压缩,同步延迟也会因此显得格外明显。在生产环境中,几分钟的延迟可能没有明显影响;但在短周期测试中,几分钟可能已经占去一次续订周期的大部分时间。

还要确认服务端读取的是 expiryTimeMillis,而不是业务代码自行命名的 expiryMills。如果字段名写错或映射有误,程序也可能一直读到旧值。

建议的排查步骤

1. 每次购买后重新获取 Purchase token

重新订阅时,不要直接复用服务端数据库中保存的 token。应从购买成功回调或重新查询购买记录的结果中读取 token,再上传到服务端。

@Override
public void onPurchasesUpdated(
        BillingResult billingResult,
        List<Purchase> purchases) {

    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
            && purchases != null) {
        for (Purchase purchase : purchases) {
            String purchaseToken = purchase.getPurchaseToken();
            sendPurchaseTokenToServer(purchaseToken);
        }
    }
}

如果使用更早期的 Billing V3 接口,也应从本次购买返回的数据中读取 purchaseToken,不要假定数据库里的旧 token 仍然代表当前购买。

2. 使用本次返回的 token 查询 Google API

旧版订阅查询接口的形式如下:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{purchaseToken}
Authorization: Bearer {accessToken}

服务端应记录以下查询信息,以便确认是否查错了对象:

packageName
subscriptionId
purchaseToken
requestTime
expiryTimeMillis
autoRenewing
responseStatus

日志中不要输出完整的 Purchase token。生产环境至少应对它进行遮盖或哈希处理。

3. 排除缓存

订阅验证结果不应进入普通 HTTP 缓存、反向代理缓存或应用内永久缓存。可以在服务端请求中明确禁用缓存,同时检查程序是否错误地按用户或商品 ID 缓存了查询结果。

HttpRequest request = HttpRequest.newBuilder()
        .uri(subscriptionStatusUri)
        .header("Authorization", "Bearer " + accessToken)
        .header("Cache-Control", "no-cache")
        .GET()
        .build();

即使代理没有缓存请求,也要检查数据库的更新逻辑。例如,下面的处理会在 token 已存在时跳过更新,导致第一次查到的状态被永久保留:

// 错误示例:相同 token 不代表订阅状态不会变化。
if (!subscriptionRepository.existsByPurchaseToken(purchaseToken)) {
    subscriptionRepository.insert(subscription);
}

应根据每次查询结果更新会发生变化的字段:

subscriptionRepository.upsert(
        purchaseToken,
        expiryTimeMillis,
        autoRenewing,
        paymentState,
        queryTime
);

4. 延迟后重新查询

购买成功后可以立即查询一次,但第一次结果不一定是最终状态。对于可能仍在同步的结果,可以进行有限次数的退避重试。

Duration[] retryDelays = {
        Duration.ofSeconds(5),
        Duration.ofSeconds(15),
        Duration.ofSeconds(30)
};

for (Duration delay : retryDelays) {
    SubscriptionStatus status = queryGooglePlay(purchaseToken);

    if (status.getExpiryTimeMillis() > System.currentTimeMillis()) {
        save(status);
        break;
    }

    Thread.sleep(delay.toMillis());
}

重试次数必须有限,不能因为测试环境返回异常状态而无限循环。

5. 同时保留旧 token 和新 token

如果重新购买确实返回了新 token,不要马上覆盖或删除旧记录。应保存两者,并建立购买链路关系:

userId
productId
purchaseToken
linkedPurchaseToken
expiryTimeMillis
autoRenewing
lastVerifiedAt

现代订阅 API 可能使用 linkedPurchaseToken 表示订阅替换、重新订阅或其他关联购买。旧版接口提供的信息较少,因此保留历史记录更有必要。

服务端应如何判断权益

不能只根据 autoRenewing 判断用户当前是否拥有订阅权益。

autoRenewing: false 通常表示订阅不会在当前周期结束后自动续费,并不表示订阅已经立即失效。判断权益时,至少还要检查经过 Google 验证的到期时间:

boolean entitlementActive =
        expiryTimeMillis != null
        && expiryTimeMillis > System.currentTimeMillis();

可以按下面的方式理解不同状态:

expiryTimeMillis > now && autoRenewing == true
    当前有效,并计划续订

expiryTimeMillis > now && autoRenewing == false
    当前仍有效,但将在周期结束时失效

expiryTimeMillis <= now
    当前订阅已过期,除非存在尚未同步的新购买

如果 API 返回的时间已经过期,但客户端刚刚报告购买成功,应先把该记录标记为“待重新验证”。此时不应立即授予长期权益,也不应马上认定购买失败。

测试时的注意事项

  • 测试订阅规则会随着 Google Play 测试体系和 Play Billing 版本变化,不能直接把旧资料中的“每天续订”套用到当前环境。
  • 测试环境中的加速续订行为,不代表生产订阅也会按相同的时间尺度运行。
  • 取消订阅、恢复自动续订和到期后重新购买是三个不同场景,需要分别测试。
  • 确认 Play 商店登录账号、License Tester 账号和管理后台查看的账号完全一致。
  • 不要根据 Purchase token 是否变化来判断订阅是否有效。
  • 不要把管理后台页面作为服务端授权依据。后台主要用于观察和管理。
  • Billing V3 已经非常陈旧。新实现应使用受支持的 Google Play Billing Library、当前版本的 Android Publisher API,并通过 RTDN 在服务端同步状态。

因此,“在有效期内取消后恢复订阅,仍然使用原 token”属于合理行为;如果“API 一直返回过去的到期时间和 autoRenewing: false”,则需要检查测试环境的同步延迟、缓存或 token 使用错误等问题,不能把它当作正常且稳定的业务语义。

备注:内容仅供参考。