PAYATHON 2026

On-Device Google Billing V3 与 Developer API 数据不一致

支付小周

结论

这是测试订阅中可能出现的正常情况。处理跨平台订阅权限时,应以 Google Play Developer API 返回的服务端状态和 expiryTimeMillis 为准,不能把设备端 Billing V3 的 autoRenewing 当作最终授权依据。

两端虽然都有 autoRenewing 字段,但字段含义和数据刷新机制并不相同:

  • 设备端结果可能来自 Google Play 客户端缓存。autoRenewing: true 只能说明设备最后获取状态时,该订阅仍处于自动续订状态。
  • Developer API 返回 Google Play 后端记录。autoRenewing: false 说明后端当前已经不再安排续订。
  • License tester 的订阅采用加速周期,测试续订次数也有限。测试订阅可能被系统停止续订,所以即使用户没有手动取消,服务端也可能变为 autoRenewing: false。
  • Google Play 的订阅页面仍然显示该项目,不代表它还会续订。尚未到期、已取消但仍在有效期内,以及历史订阅都可能继续显示。

如何判断订阅是否有效

autoRenewing 表示订阅是否计划再次续订,并不等同于“当前是否有权限”。

对于题目中的旧版 SubscriptionPurchase 响应,主要判断条件是:

当前时间 < expiryTimeMillis

例如:

function evaluateSubscription(subscriptionPurchase) {
  const now = Date.now();
  const expiryTime = Number(subscriptionPurchase.expiryTimeMillis);

  return {
    entitled: Number.isFinite(expiryTime) && now < expiryTime,
    willRenew: subscriptionPurchase.autoRenewing === true,
    expiresAt: new Date(expiryTime).toISOString(),
    cancelReason: subscriptionPurchase.cancelReason
  };
}

可能出现以下状态组合:

是否有效是否自动续订含义
truetrue当前有效,并计划续订
truefalse当前仍有效,但到期后不再续订
falsefalse已到期且不会续订
falsetrue通常是缓存、同步延迟或异常状态,需要重新向服务端确认

因此,这笔订阅在 expiryTimeMillis 之前仍可提供权限,但到期后不应继续提供权限,也不应预期它会再次续订。

cancelReason: 0 的含义

在旧版 purchases.subscriptions.get 返回模型中,cancelReason: 0 通常表示用户取消,不能据此解释为“并非用户主动取消”。其他值可能表示系统、替换订阅或开发者操作。具体枚举应以项目所用时期的 Android Publisher API 文档为准。

不过,对于测试购买,即使测试人员没有在界面中明确点击取消,订阅也可能因达到测试续订限制、测试环境清理或相关测试流程而停止续订。只看 cancelReason,通常无法完整还原测试订阅停止续订的操作路径。

cancelReason 存在也不代表订阅立即失效。自动续订取消后,用户通常仍可使用已经支付或由测试生成的剩余有效期。

服务端处理建议

服务端收到 purchaseToken 后,应按以下方式处理:

  1. 校验 packageName、订阅商品 ID 与 purchaseToken 的对应关系。
  2. 调用 Google Play Developer API 查询最新状态。
  3. 根据服务端返回的到期时间判断当前权限。
  4. 只把 autoRenewing 作为判断到期后是否可能继续续订的辅助状态。
  5. 在临近到期时再次刷新状态,避免永久沿用某一次查询结果。
  6. 防止同一个 purchaseToken 绑定到多个应用账户。
  7. 生产环境应使用实时开发者通知,并在收到通知后重新查询 Developer API。

旧版接口的查询形式类似:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}

Node.js 中可以进行类似校验:

const response = await androidPublisher.purchases.subscriptions.get({
  packageName: "com.redacted.package",
  subscriptionId: "quarterly_plan",
  token: purchaseToken
});

const purchase = response.data;
const expiryTimeMillis = Number(purchase.expiryTimeMillis);

const entitled =
  Number.isFinite(expiryTimeMillis) &&
  Date.now() < expiryTimeMillis;

const status = {
  entitled,
  autoRenewing: purchase.autoRenewing === true,
  expiryTimeMillis,
  cancelReason: purchase.cancelReason ?? null
};

如果项目使用较新的 Google Play Billing 和 Android Publisher API,应优先采用当前推荐的订阅查询模型,例如 purchases.subscriptionsv2.get。判断状态时,需要结合 subscriptionState、lineItems、到期时间和关联 token,不能继续围绕旧版单一的 autoRenewing 字段设计权限系统。

为什么设备端可能长期显示旧值

Billing V3 的设备端查询主要用于恢复购买和展示客户端界面,不是跨平台权限系统的权威数据源。以下情况都可能导致两端状态出现短暂或持续的不一致:

  • Google Play 客户端尚未刷新本地购买缓存;
  • 测试订阅刚被后端停止续订;
  • 设备端查询发生在服务端状态变化之前;
  • 测试环境使用加速时间,且续订次数有限;
  • 订阅已取消但尚未到期;
  • Google Play 的订阅管理页面仍保留该订阅记录。

排查时可以尝试重新启动 Google Play,等待状态同步后再次查询,但这些操作不能代替服务端验证。

这两个字段应分别处理:expiryTimeMillis 决定当前是否有权限,autoRenewing 决定当前有效期结束后是否计划续订。在这组数据中,服务端状态表示订阅可能仍在有效期内,但已经不会再次续订。设备端缓存中的 autoRenewing: true 不能作为可靠的授权依据。

备注:内容仅供参考。