PAYATHON 2026

Google in-app billing 如何取消测试购买

支付老李

结论

ITEM_ALREADY_OWNED 表示 Google Play 仍将该商品视为当前账号已拥有的商品。对于可重复购买的消耗型商品,不需要在 Play Console 中“取消购买”。应用应查询尚未处理的购买,取得 purchaseToken,再调用 Google Play Billing Library 的消费接口:

billingClient.consumeAsync(...)

消费成功后,该商品便可再次购买。

如果应用已经无法取得这笔购买,也可以在权限配置正确并完成服务端校验的前提下,通过 Google Play Developer API 消费对应的购买令牌。退款、清除 Play Store 数据或重新安装应用,通常不能代替消费操作。

为什么会出现这个错误

消耗型商品的正常处理流程如下:

  1. 用户完成购买。
  2. 应用通过 queryPurchasesAsync() 获取购买记录。
  3. 应用验证购买状态和签名。
  4. 发放商品。
  5. 调用 consumeAsync() 消费购买。
  6. Google Play 解除该账号对商品的所有权,允许用户再次购买。

如果旧版本在第 5 步之前发生错误,购买记录仍会保存在 Google Play 服务端。即使更新或重新安装应用,或者清除应用数据,这条所有权记录通常也不会消失。再次购买相同商品时,Google Play 就会返回:

ITEM_ALREADY_OWNED

推荐解决方法:恢复并消费旧购买

修复后的应用应在启动时主动查询当前账号尚未处理的应用内购买,不能只在新购买的回调中处理。

下面的示例适用于较新的 Google Play Billing Library。不同版本的接口签名可能略有差异,请以项目实际使用的版本为准。

QueryPurchasesParams params =
        QueryPurchasesParams.newBuilder()
                .setProductType(BillingClient.ProductType.INAPP)
                .build();

billingClient.queryPurchasesAsync(
        params,
        (billingResult, purchases) -> {
            if (billingResult.getResponseCode()
                    != BillingClient.BillingResponseCode.OK) {
                return;
            }

            for (Purchase purchase : purchases) {
                if (purchase.getPurchaseState()
                        == Purchase.PurchaseState.PURCHASED) {
                    consumePurchase(purchase);
                }
            }
        }
);

消费购买时需要传入 purchaseToken:

private void consumePurchase(Purchase purchase) {
    ConsumeParams consumeParams =
            ConsumeParams.newBuilder()
                    .setPurchaseToken(purchase.getPurchaseToken())
                    .build();

    billingClient.consumeAsync(
            consumeParams,
            (billingResult, purchaseToken) -> {
                if (billingResult.getResponseCode()
                        == BillingClient.BillingResponseCode.OK) {
                    // 消费成功,可以再次购买该商品。
                } else {
                    // 记录响应码,并根据需要重试。
                }
            }
    );
}

如果只处理某个特定商品,还应检查购买记录中的商品 ID:

if (purchase.getProducts().contains("your_product_id")) {
    consumePurchase(purchase);
}

消费操作不可逆。正式环境中,不能仅为消除错误就无条件消费所有购买。应用必须先完成服务端验证,并确认商品已经正确发放。

使用旧版 Billing API 时

旧版 Billing Library 或 AIDL 接口使用的名称不同,但处理原则不变:先查询账号已拥有的商品,再使用购买令牌调用消费接口。

旧版 Billing Library 的常见写法如下:

billingClient.consumeAsync(
        purchase.getPurchaseToken(),
        (responseCode, purchaseToken) -> {
            if (responseCode == BillingClient.BillingResponseCode.OK) {
                // 已成功消费
            }
        }
);

如果项目使用更早的 In-app Billing AIDL API,需要从 getPurchases() 返回的数据中取得 purchaseToken,然后调用:

int response = service.consumePurchase(
        3,
        getPackageName(),
        purchaseToken
);

不要把旧代码直接复制到新版 Billing Library 中,应根据项目当前使用的依赖版本调整接口。

应用无法取得购买令牌怎么办

先检查以下条件:

  • Play Store 当前登录的是当时完成购买的测试账号。
  • 安装应用的包名与创建商品时使用的包名一致。
  • 应用签名和测试轨道配置正确。
  • Billing Client 已成功连接。
  • 查询的商品类型是 BillingClient.ProductType.INAPP。
  • 购买状态是 PurchaseState.PURCHASED,不是 PENDING。

如果客户端仍然无法恢复购买,可以检查自己的服务端、日志或数据库,看其中是否保存过 purchaseToken。取得令牌后,可以通过 Google Play Developer API 的 purchases.products.consume 接口消费一次性商品。

请求形式如下:

POST https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:consume

这种方式要求正确配置 Google Play Developer API、服务账号和应用权限。不要把服务账号密钥放在 Android 客户端中。

如果购买记录无法查询,也没有保存购买令牌,只能尝试在 Play Console 的订单管理功能中查找测试订单,并执行退款或撤销。相关入口是否显示,以及测试订单能否操作,取决于账号权限、购买类型和当前 Play Console 界面,因此不能保证所有测试订单都能通过控制台处理。

订阅不能使用消费接口

订阅和消耗型商品的处理方式不同。

对于订阅:

  • 不能调用 consumeAsync()。
  • 可以让测试账号在 Google Play 的订阅管理页面取消续订。
  • 有权限的服务端也可以调用 Google Play Developer API。
  • purchases.subscriptions.cancel 通常只会取消自动续订,不一定立即终止当前订阅权益。
  • 如果需要立即撤销权益,应根据实际 API 能力选择 revoke 等操作,并核对当前官方接口说明。

因此,文档中提到的 subscription cancel() 不适用于普通的一次性应用内商品,也无法解决一次性商品未消费的问题。

不建议采用的处理方式

以下操作通常无法可靠清除服务端保存的所有权状态:

  • 卸载并重新安装应用。
  • 清除应用数据。
  • 清除 Google Play Store 或 Google Play 服务的数据。
  • 更换设备。
  • 删除本地购买记录。
  • 只执行退款,不处理仍能查询到的购买状态。

这些操作最多只能刷新本地缓存,无法代替 consumeAsync()。换用新的测试账号虽然可以暂时绕过问题,但旧账号仍处于异常状态,也无法借此验证应用是否具备购买恢复和补偿处理能力。

测试时还要注意确认期限

非消耗型商品应调用 acknowledgePurchase(),不能将其消费:

AcknowledgePurchaseParams params =
        AcknowledgePurchaseParams.newBuilder()
                .setPurchaseToken(purchase.getPurchaseToken())
                .build();

billingClient.acknowledgePurchase(
        params,
        billingResult -> {
            // 检查响应结果
        }
);

两者的区别如下:

  • consumeAsync():用于可重复购买的消耗型商品。消费后,用户可以再次购买。
  • acknowledgePurchase():用于非消耗型商品或订阅。确认购买后,用户仍然拥有该商品。

如果商品本来就应当永久拥有,例如“去广告”或“永久解锁”,那么出现 ITEM_ALREADY_OWNED 是正常的。此时不应消费商品,而应查询已有购买并恢复用户权益。

对于当前情况,只要确认该商品属于消耗型商品,就可以在更新应用后查询未处理的购买,并使用其 purchaseToken 调用 consumeAsync()。消费成功后,ITEM_ALREADY_OWNED 状态会解除,可以继续测试。

备注:内容仅供参考。