PAYATHON 2026

In-App Billing subscription 的取消与同步问题

支付阿杰

结论

这些现象通常不能简单归因于应用尚未发布,OnPurchaseStateChange 也解决不了所有问题。可以这样理解:

  • 取消订阅页面显示 Item not found,通常是因为应用、订阅商品或测试账号尚未满足 Google Play 的可见性条件。
  • 订阅权益通常绑定 Google Account,而不是某一台设备,但购买状态事件不会保证同步推送到该账号的所有设备。
  • 取消自动续订不代表订阅立即失效。用户一般仍可使用到当前计费周期结束,因此设备没有马上收到”已失效”通知,可能属于正常情况。
  • 客户端应在应用启动、恢复前台、用户登录和进入付费功能时主动刷新购买状态。生产环境最好由服务端验证和维护权益,不要只比较本地时间。
  • Merchant Center 中的 $0 FAILED 记录可能来自支付验证、授权尝试或后台记账。具体含义取决于当时使用的 Google Play Billing 版本和支付渠道,不能仅凭这条记录判断订阅状态。

OnPurchaseStateChange 是早期 In-App Billing API 的机制。新项目应优先使用当前的 Google Play Billing Library 和服务端订阅状态通知。维护旧项目时,也不能把广播事件作为唯一的状态来源。

1. 取消订阅页面为什么显示 Item not found

这个错误通常表示 Google Play 无法为当前账号识别应用或订阅商品。常见原因包括:

  • 安装包不是通过 Google Play 测试轨道安装的;
  • 当前测试账号不在相应的测试名单中;
  • 应用的 packageName、签名或版本与 Play Console 中的构建不一致;
  • 订阅商品尚未激活,或商品 ID 不匹配;
  • 测试账号所在国家或地区无法访问该应用或商品;
  • Google Play 尚未完成应用或商品配置的同步;
  • 取消链接使用了错误的应用包名或订阅商品。

一般不需要把应用正式发布给所有用户。可以先将构建上传到 Internal testing、Closed testing 等测试轨道,把测试账号加入名单,再让该账号通过 Google Play 提供的测试链接安装应用。

还需要检查以下配置:

  1. Play Console 中已经创建并启用订阅和对应的 base plan。
  2. 测试设备登录的是已获得授权的测试账号。
  3. 应用是从 Google Play 安装的,而不是直接通过 Android Studio 或 adb install 安装。
  4. 应用 ID、签名和商品 ID 与 Play Console 配置完全一致。
  5. Google Play Store 和 Google Play services 已更新,并已处理旧测试安装可能造成的账号缓存问题。

如果项目使用的是早期 In-App Billing API,其测试条件可能与当前 Play Console 不同。此时应查阅项目实际版本对应的官方文档,不要直接套用新版测试流程。

2. 订阅是否绑定设备

订阅权益原则上与购买时使用的 Google Account 关联,并非永久绑定某台设备。同一账号在其他设备上安装相同应用后,可以通过查询已有购买记录恢复权益。

不过,账号级权益并不代表所有设备都会实时收到 OnPurchaseStateChange。购买回调或广播主要用于通知当前设备上的购买流程或状态变化,不能作为可靠的跨设备同步通道。

例如,用户在设备 A 上订阅后,设备 B 上正在运行的应用可能收不到任何事件。因此,设备 B 应在这些时机主动查询:

  • 应用启动时;
  • 用户登录后;
  • 应用从后台恢复时;
  • 用户进入需要订阅权益的页面时;
  • 用户点击”恢复购买”时。

伪代码可以这样写:

@Override
protected void onResume() {
    super.onResume();
    refreshSubscriptionStatus();
}

private void refreshSubscriptionStatus() {
    billingClient.queryPurchasesAsync(
        QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.ProductType.SUBS)
            .build(),
        (billingResult, purchases) -> {
            if (billingResult.getResponseCode()
                    != BillingClient.BillingResponseCode.OK) {
                return;
            }

            boolean entitled = purchases.stream()
                .anyMatch(this::isLocallyEligible);

            updateLocalEntitlement(entitled);
            sendPurchaseTokensToBackend(purchases);
        }
    );
}

其中,isLocallyEligible() 只能用于快速更新界面。最终的权益判断最好交给服务端,因为客户端返回的购买记录仍需验证。

3. 后台取消后为什么没有立即通知设备

首先要区分以下两种操作:

  • 取消自动续订:停止下一个周期的扣款,当前周期通常仍然有效;
  • 撤销或退款:权益可能立即或提前终止,具体取决于执行的操作和订单状态。

如果用户只是取消自动续订,那么设备在当前周期结束前仍将订阅判断为有效,这是正常的。“不再续费”和”当前没有权益”是两种不同的状态。

如果商家后台执行的是退款或撤销,客户端也不能假定自己一定会马上收到回调。设备可能处于离线状态,应用可能没有运行,通知本身也可能延迟。较可靠的实现应以服务端状态为准:

Google Play
    ├── 客户端购买查询:用于快速恢复界面状态
    ├── 服务端购买令牌验证:用于确认真实权益
    └── 服务端订阅通知:用于跟踪续订、取消、暂停、退款和撤销

推荐采用以下流程:

  1. 客户端购买成功后,将 purchaseToken 和商品信息发送到业务服务端。
  2. 服务端通过 Google Play Developer API 查询订阅状态。
  3. 服务端保存到期时间、自动续订状态和当前权益状态。
  4. 收到服务端订阅通知后,再次调用 Developer API 获取最终状态。
  5. 客户端从业务服务端读取权益,并在启动或恢复前台时刷新。

不要只根据客户端保存的 expirationDate 判断最终权益。设备时间可以被修改,本地缓存也可能已经过期。如果暂时没有服务端,可以把”当前时间超过已知到期时间后重新查询购买记录”作为过渡方案。刷新间隔应使用系统经过时间控制,并以 Google Play 返回的状态为准:

private boolean shouldRefresh(long knownExpiryTimeMillis) {
    return System.currentTimeMillis() >= knownExpiryTimeMillis;
}

这段判断只能决定”是否需要刷新”,不能直接证明订阅已经失效。用户关闭自动续订后,也不能在到期前立即撤销当前周期的权益。

使用早期 API 时,可以通过 restoreTransactions() 重新获取购买状态。当前 Billing Library 应使用购买查询接口,新代码不应继续依赖旧的恢复交易机制。

4. 为什么会出现 $0 FAILED transaction

实际扣款记录旁出现一条 $0 FAILED 记录,不一定表示用户被重复收费,也不应直接影响订阅权益。这条记录可能来自支付方式验证、预授权尝试、结算系统内部记录,或一次未完成的支付尝试。

仅凭现有描述无法确定它的具体用途。Google Play 的账务展示、测试购买行为和支付处理流程会随版本及支付渠道变化,因此不能把”每次订阅都会产生一笔 $0 FAILED 记录”当成固定规则。

排查时应重点核对:

  • 两条记录的 orderId 是否相同;
  • 是否只有一笔实际扣款;
  • 有效订单是否对应客户端取得的 purchaseToken;
  • Developer API 返回的订阅状态是否有效;
  • $0 FAILED 记录是否只出现在特定测试账号或支付方式下。

业务代码不应根据 Merchant Center 中的交易行数量授予权益。权益判断应基于经过验证的 purchaseToken、订阅有效期,以及撤销、退款等状态。

如果 $0 FAILED 记录持续出现在真实交易中,或同时发生重复扣款、订阅无法激活等问题,应保存订单号、发生时间、测试账号类型和 Billing Library 版本,再提交给 Google Play 支持团队核查。

实现时的注意事项

  • 不要把购买回调当成完整的订阅状态同步机制。
  • 不要把取消订阅等同于立即过期。
  • 不要依赖设备本地时间直接授予或撤销权益。
  • 不要根据 Merchant Center 的交易行数量判断购买是否成功。
  • 多设备场景必须提供主动刷新机制或”恢复购买”入口。
  • 仍在开发的应用应通过 Play Console 测试轨道和测试账号完成端到端测试。
  • 生产环境应保存并验证 purchaseToken,同时正确处理续订、宽限期、暂停、到期、退款和撤销等状态。

跨设备没有收到事件,以及取消订阅后客户端没有立即收到通知,通常与同步方式和订阅生命周期有关。取消页面出现 Item not found,更可能是测试发布或商品配置有误。对于 $0 FAILED 记录,则需要结合订单和支付数据进一步判断。

备注:内容仅供参考。