Google in-app billing 如何取消测试购买
结论
ITEM_ALREADY_OWNED 表示 Google Play 仍将该商品视为当前账号已拥有的商品。对于可重复购买的消耗型商品,不需要在 Play Console 中“取消购买”。应用应查询尚未处理的购买,取得 purchaseToken,再调用 Google Play Billing Library 的消费接口:
billingClient.consumeAsync(...)
消费成功后,该商品便可再次购买。
如果应用已经无法取得这笔购买,也可以在权限配置正确并完成服务端校验的前提下,通过 Google Play Developer API 消费对应的购买令牌。退款、清除 Play Store 数据或重新安装应用,通常不能代替消费操作。
为什么会出现这个错误
消耗型商品的正常处理流程如下:
- 用户完成购买。
- 应用通过
queryPurchasesAsync()获取购买记录。 - 应用验证购买状态和签名。
- 发放商品。
- 调用
consumeAsync()消费购买。 - 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 状态会解除,可以继续测试。
备注:内容仅供参考。