PAYATHON 2026

Android InApp Billing V3 如何验证 Subscription?

支付小周

结论

在 In-app Billing V3 中,针对 subs 类型调用 getPurchases() 时,只会返回 Google Play 当前仍视为有效的订阅。已经过期或因付款失败而失效的订阅不会出现在结果中。

因此:

  • getPurchases() 可以用来判断当前设备上的 Google 账号是否持有某项有效订阅。
  • 它不能作为安全、权威的订阅验证机制。
  • 服务端可以通过 Google Play Developer API 验证订阅。应用需要把购买时获得的 purchaseToken 发送到自己的服务器,再由服务器调用 Android Publisher API。
  • 用户取消自动续订后,订阅通常不会立即失效。只要当前计费周期尚未结束,getPurchases() 仍可能返回该订阅。

“不再返回失败或已过期的 Subscription”是什么意思

调用:

Bundle purchases = billingService.getPurchases(
        3,
        packageName,
        "subs",
        continuationToken
);

返回的是该账号当前持有的订阅,不是完整的订阅历史记录。

假设用户订阅了 premium_monthly:

  • 订阅正常且尚未到期:会返回。
  • 用户取消续订,但当前周期尚未结束:通常仍会返回。
  • 当前周期已经结束:不再返回。
  • 续费付款失败,并且订阅最终失效:不再返回。
  • 用户从未购买过:不会返回。

“取消”和“过期”是两种不同的状态。取消通常只是关闭下一周期的自动续订。在已经付款的有效期内,用户仍然拥有订阅权益。

getPurchases() 反映的只是调用时的持有状态,不适合查询历史订单,也无法说明某个旧 purchaseToken 失效的原因。

客户端获取订阅信息

V3 返回的 INAPP_PURCHASE_DATA_LIST 中,每条购买记录都包含类似以下的数据:

{
  "orderId": "GPA.1234-5678-9012-34567",
  "packageName": "com.example.app",
  "productId": "premium_monthly",
  "purchaseTime": 1450000000000,
  "purchaseState": 0,
  "purchaseToken": "example-purchase-token",
  "autoRenewing": true
}

客户端可以读取当前订阅,并把必要字段发送给自己的服务器:

Bundle result = billingService.getPurchases(
        3,
        context.getPackageName(),
        "subs",
        null
);

int responseCode = result.getInt("RESPONSE_CODE");
if (responseCode == 0) {
    ArrayList<String> purchaseDataList =
            result.getStringArrayList("INAPP_PURCHASE_DATA_LIST");

    if (purchaseDataList != null) {
        for (String purchaseData : purchaseDataList) {
            JSONObject purchase = new JSONObject(purchaseData);

            String productId = purchase.getString("productId");
            String purchaseToken = purchase.getString("purchaseToken");

            // 通过 HTTPS 发送给自己的服务器进行验证。
            sendToBackend(productId, purchaseToken);
        }
    }
}

实际代码还需要处理 INAPP_CONTINUATION_TOKEN,直至读取完所有分页结果。

使用 Developer API 验证

In-app Billing V3 是应用与 Google Play 商店交互时使用的客户端协议,Google Play Developer API 则是服务器接口。使用 V3 不影响服务端调用 Developer API。

服务端验证至少需要以下信息:

  • 应用的 packageName
  • 订阅商品 ID
  • 购买时获得的 purchaseToken
  • 有权访问对应 Google Play Console 应用的服务端凭据

传统订阅查询接口的请求形式为:

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

例如:

packageName     = com.example.app
subscriptionId  = premium_monthly
token           = purchaseToken returned by Google Play

接口响应中通常包含到期时间、是否自动续订、取消原因和付款状态等信息,可用于判断订阅状态。不过,Android Publisher API 的具体资源和字段会随版本演进。服务端应以当前使用的官方 API 资源定义为准,不能只依赖某一个字段。

以传统响应结构为例,可以通过 expiryTimeMillis 判断订阅是否仍在有效期内:

long expiryTimeMillis =
        Long.parseLong(subscriptionPurchase.getExpiryTimeMillis());

boolean notExpired = expiryTimeMillis > System.currentTimeMillis();
boolean paymentAccepted =
        subscriptionPurchase.getPaymentState() != null
        && subscriptionPurchase.getPaymentState() == 1;

boolean entitlementActive = notExpired && paymentAccepted;

这只是一个简化示例。订阅还可能处于免费试用、宽限期、暂停或账号保留等特殊状态。是否继续提供权益,需要结合业务规则和 Developer API 当前返回的信息处理。

推荐的验证流程

  1. 应用完成购买后,取得 productId 和 purchaseToken。
  2. 应用通过 HTTPS 将这两个字段发送给自己的服务器。
  3. 服务器校验用户身份,并确认 packageName 和订阅商品属于自己的应用。
  4. 服务器使用受保护的凭据调用 Google Play Developer API。
  5. 确认接口调用成功,并核对商品、购买令牌、到期时间及相关状态。
  6. 服务器保存订阅状态,并将最终的权益结果返回给客户端。
  7. 订阅续订、取消或付款状态发生变化后重新查询,或者结合 Google Play 提供的实时开发者通知更新状态。

注意事项

不要仅凭“getPurchases() 是否返回该商品”来保护服务器端的付费内容。客户端结果可能被篡改,离线、账号切换、缓存和网络异常也可能影响查询结果。

也不要只检查 autoRenewing:

  • autoRenewing == false 不代表订阅已经失效。
  • 用户取消续订后,当前已付费周期可能仍然有效。
  • 是否提供权益,通常取决于订阅是否到期以及当前的付款状态,而不是以后是否继续续费。

购买签名校验和订阅状态查询也是两回事。签名可以帮助确认购买数据由 Google Play 签发,但不能证明订阅此刻仍然有效,因为订阅可能在签名生成后被取消、退款或过期。如果需要可靠地控制权益,应以服务端调用 Developer API 获得的最新状态为准。

备注:内容仅供参考。