如何测试 Google Play Android 订阅和 Google API?
结论
如果用户已经购买订阅,但服务端没有保存 purchaseToken,无需再次购买。客户端应查询或恢复当前 Google 账号拥有的订阅,从购买记录中重新取得 purchaseToken,上传到服务端,再通过 Google Play Developer API 验证。
旧版 In-app Billing API v2 可以发起 RESTORE_TRANSACTIONS。当前的 Google Play Billing Library 则应使用 queryPurchasesAsync(ProductType.SUBS)。
confirmNotifications() 不是扣款确认接口。Google Play 在购买成功时已经完成交易,并将款项计入商家账户。这个方法只是告诉 Google Play,应用已经处理购买通知。没有调用它并不会阻止扣款。
恢复丢失的订阅 Token
订阅与购买时使用的 Google Play 账号绑定。恢复订阅需要满足以下条件:
- 设备上的 Google Play 登录了原购买账号。
- 安装包的
packageName与购买时一致。 - 应用签名与 Google Play 中的对应版本一致。
- 查询的商品类型是订阅,也就是
subs或ProductType.SUBS。 - 订阅仍处于 Google Play 可以返回的有效购买状态。
“You already own this subscription” 表示 Google Play 仍记录着该账号对该商品的所有权。此时应该查询已有购买,不要再次启动购买流程。
当前 Billing Library 的处理方式
使用 Google Play Billing Library 时,可以查询该账号当前拥有的订阅:
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build(),
(billingResult, purchases) -> {
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
return;
}
for (Purchase purchase : purchases) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
String purchaseToken = purchase.getPurchaseToken();
List<String> productIds = purchase.getProducts();
// 将 purchaseToken、productId 和 packageName 重新发送到服务端
sendSubscriptionToServer(productIds, purchaseToken);
}
}
}
);
恢复的数据仍须交给服务端验证,不能只根据客户端的查询结果授予订阅权益。服务端至少要保存:
packageNameproductIdpurchaseToken- 应用内部用户 ID
- Google API 返回的订阅状态和到期时间
- 最近一次验证时间
如果购买尚未完成 acknowledgement,服务端完成验证和持久化后,还要按照当前使用的 Billing Library 版本执行 acknowledgement。acknowledgement 并不是支付捕获操作。
旧版 In-app Billing API v2 的处理方式
问题中的 mBillingService.confirmNotifications()、BILLING_REQUEST_API_VERSION 和 RESTORE_TRANSACTIONS 都属于已经淘汰的旧接口。在这套接口中,可以向 Google Play 请求恢复购买:
Bundle request = new Bundle();
request.putString("BILLING_REQUEST", "RESTORE_TRANSACTIONS");
request.putInt("API_VERSION", 2);
request.putString("PACKAGE_NAME", context.getPackageName());
request.putLong("NONCE", nonce);
Bundle response = mBillingService.sendBillingRequest(request);
long requestId = response.getLong("REQUEST_ID");
int responseCode = response.getInt("RESPONSE_CODE");
这个请求通常不会直接同步返回完整的购买记录。应用还需要处理 Google Play 随后发送的购买状态广播,并验证其中的 signedData 和 signature。如果恢复数据中含有对应订阅的 purchaseToken,就可以把它重新提交给服务端。
伪代码如下:
void handlePurchaseStateChanged(String signedData, String signature) {
if (!verifySignature(signedData, signature)) {
return;
}
JSONObject root = new JSONObject(signedData);
JSONArray orders = root.getJSONArray("orders");
for (int i = 0; i < orders.length(); i++) {
JSONObject order = orders.getJSONObject(i);
String productId = order.getString("productId");
String purchaseToken = order.optString("purchaseToken", null);
int purchaseState = order.getInt("purchaseState");
if (purchaseState == 0 && purchaseToken != null) {
sendSubscriptionToServer(productId, purchaseToken);
}
}
}
具体字段要以应用实际收到的数据和当时使用的 Billing API 版本为准。示例中出现了某个字段,不等于所有历史购买记录都会返回该字段。
旧版 Billing API 已经不适合新项目。如果仍在维护这套代码,可以把恢复 Token 作为临时补救措施,但应尽快迁移到当前的 Google Play Billing Library。
服务端验证订阅
旧版 Android Publisher API 的调用方式大致如下:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}
Authorization: Bearer {accessToken}
其中:
{packageName}是应用包名。{subscriptionId}是订阅商品 ID。{token}是客户端购买记录中的purchaseToken。Authorization中的访问令牌是服务端调用 Google API 时使用的 OAuth 2.0 token,不是用户的订阅 Token。
这里需要区分两种 Token:
OAuth 2.0 access token
用于授权服务端调用 Google Play Developer API
purchaseToken
用于标识某一笔 Google Play 购买或订阅
Google Play Developer API 通常用于验证 Token 已知的购买,不能根据账号、订单号或商品 ID 反向列出并找回任意丢失的 purchaseToken。因此,恢复 Token 通常要从购买客户端入手,而不是通过 Publisher API。
服务端收到 Token 后,应先写入数据库,再向客户端返回成功结果。可以为 purchaseToken 添加唯一约束,让客户端重复上传时能够安全地执行幂等更新。
INSERT INTO subscriptions (
user_id,
package_name,
product_id,
purchase_token
) VALUES (?, ?, ?, ?)
ON CONFLICT (purchase_token)
DO UPDATE SET
user_id = EXCLUDED.user_id,
product_id = EXCLUDED.product_id;
为什么未调用 confirmNotifications() 仍然会扣款
confirmNotifications() 用于通知 Google Play,应用已经接收并处理指定的购买通知。它不负责决定交易是否授权、是否扣款,也不控制款项是否进入商家账户。
流程大致如下:
用户确认购买
→ Google Play 完成支付交易
→ Google Play 更新订阅所有权
→ 客户端收到购买结果或通知
→ 客户端上传 purchaseToken
→ 服务端保存并验证
→ 客户端确认已处理通知
所以,在调用 confirmNotifications() 之前看到 charge 或 deposit 属于正常现象。它处理的是通知投递和重复通知,并不是两阶段支付提交机制。延迟确认或漏掉确认可能导致购买通知再次发送,但不会自动撤销已经完成的收费。
“Item not found” 的处理
点击 Details 后看到 “Item not found”,不代表订阅一定不存在。这个提示可能与旧版 Google Play 客户端、商品上下架状态、测试账号、地区、缓存或商品详情页面有关。订阅能否恢复,应以购买查询和服务端验证结果为准,不能只看详情页的提示。
可以按以下顺序检查:
- 切换到当初购买订阅的 Google 账号。
- 确认安装的是包名相同、签名正确的应用。
- 查询当前拥有的
SUBS类型购买。 - 将恢复出的
purchaseToken重新上传。 - 由服务端调用 Google Play Developer API 验证。
- 确认数据已经持久化,再完成 acknowledgement 或旧版通知确认。
如果客户端查询不到这项订阅,仅凭 “You already own this subscription” 提示或商家后台的订单记录,无法可靠地重新构造 purchaseToken。此时需要检查实际登录的账号和订阅状态。在测试环境中,也可以等待测试订阅到期,或者在 Play Console 中取消、退款或撤销测试购买后重新测试。取消通常只会关闭自动续订,不一定立即移除当前所有权。只有订阅到期或被有效撤销后,系统才可能允许再次购买。
备注:内容仅供参考。
