PAYATHON 2026

Billing API v3 回调中 INAPP_PURCHASE_DATA 为 null

数据阿明

结论

Activity.RESULT_OK 只表示购买界面正常返回,并不代表购买成功。必须读取回调 Intent 中的 RESPONSE_CODE。只有它的值为 0(BILLING_RESPONSE_RESULT_OK)时,才能继续解析 INAPP_PURCHASE_DATA 和 INAPP_DATA_SIGNATURE。

如果用户已经拥有该 subscription,无论订阅仍在自动续订,还是已经取消自动续订但尚未到期,它通常都属于“已拥有”状态,不能再次走首次购买流程。此时可能返回 ITEM_ALREADY_OWNED(响应码 7),并且不会提供新的 INAPP_PURCHASE_DATA。

因此,不能只根据 resultCode == Activity.RESULT_OK 判断付款成功,也不能假定 INAPP_PURCHASE_DATA 一定存在。

处理购买回调

处理回调时,需要检查以下内容:

  • requestCode 是否对应本次购买;
  • data 是否为 null;
  • RESPONSE_CODE 是否为 0;
  • 购买数据和签名是否存在;
  • 返回的商品 ID、开发者载荷和签名是否符合预期。
private static final int REQUEST_CODE_BUY = 1001;

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode != REQUEST_CODE_BUY) {
        return;
    }

    if (data == null) {
        // 购买流程没有返回可解析的数据,不能视为购买成功
        handlePurchaseError("Purchase result intent is null");
        return;
    }

    int responseCode = data.getIntExtra("RESPONSE_CODE", -1);

    if (resultCode == Activity.RESULT_OK && responseCode == 0) {
        String purchaseData =
                data.getStringExtra("INAPP_PURCHASE_DATA");
        String dataSignature =
                data.getStringExtra("INAPP_DATA_SIGNATURE");

        if (purchaseData == null || dataSignature == null) {
            handlePurchaseError("Purchase data or signature is missing");
            return;
        }

        // 在授予订阅权益前验证签名并校验购买内容
        handleSuccessfulPurchase(purchaseData, dataSignature);
        return;
    }

    if (responseCode == 1) {
        handlePurchaseCancelled();
    } else if (responseCode == 7) {
        // ITEM_ALREADY_OWNED:通过 getPurchases() 恢复并确认现有订阅
        restoreExistingSubscription();
    } else {
        handlePurchaseError(
                "Billing failed: resultCode=" + resultCode
                        + ", responseCode=" + responseCode);
    }
}

不同 Play Store/Billing 实现返回的 RESPONSE_CODE 可能存在类型差异。为了兼容旧环境,可以同时处理 Integer 和 Long:

private int getResponseCodeFromIntent(Intent intent) {
    Object value = intent.getExtras() != null
            ? intent.getExtras().get("RESPONSE_CODE")
            : null;

    if (value == null) {
        return 0;
    }

    if (value instanceof Integer) {
        return (Integer) value;
    }

    if (value instanceof Long) {
        return (int) ((Long) value).longValue();
    }

    return -1;
}

发起购买前查询现有订阅

调用 getBuyIntent() 前,可以先用 getPurchases() 查询当前账户仍然拥有的 subscription。这样既可以避免重复拉起付款界面,也能恢复用户已经购买、但应用本地状态丢失的订阅。

Bundle purchases = mService.getPurchases(
        3,
        getPackageName(),
        "subs",
        null
);

int responseCode = purchases.getInt("RESPONSE_CODE");

if (responseCode == 0) {
    ArrayList<String> ownedSkus =
            purchases.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
    ArrayList<String> purchaseDataList =
            purchases.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
    ArrayList<String> signatureList =
            purchases.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");

    if (ownedSkus != null && ownedSkus.contains(mProduct)) {
        // 用户仍拥有该订阅:验证购买数据后恢复订阅权益
    } else {
        // 未发现仍拥有的订阅,可以继续调用 getBuyIntent()
    }
}

如果结果中包含 INAPP_CONTINUATION_TOKEN,还要携带该 token 继续查询,直到没有下一页,不能只检查第一页。

处理已取消但未到期的订阅

取消自动续订不会立即终止订阅权益。在当前计费周期结束前,用户通常仍然拥有该商品。因此,应用需要继续提供订阅权益,不能把“已取消自动续订”视为“可以重新购买”。

这笔订阅仍可能出现在 getPurchases() 的结果中。此时再次购买同一商品,可能得到 ITEM_ALREADY_OWNED,而不会生成新的购买记录。

只有在订阅真正到期后,它才会从当前已拥有的购买记录中消失。具体状态最好由可信的服务端校验确定,不要只依赖客户端缓存。

其他注意事项

不要把 startIntentSenderForResult() 返回的 Activity.RESULT_OK、付款窗口关闭或界面上的成功提示直接当作订阅已经生效。授予权益前,至少要确认:

  • Billing RESPONSE_CODE 为 0;
  • INAPP_PURCHASE_DATA 和 INAPP_DATA_SIGNATURE 都不为 null;
  • 签名验证成功;
  • productId 与发起购买的商品一致;
  • developerPayload 与发起购买时保存的值一致;
  • 如果对订阅状态的准确性要求较高,应通过服务端向 Google Play 验证购买令牌。

示例中的这一行缺少右括号和分号,实际代码应写成:

String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");

如果已经确认 RESPONSE_CODE == 0,但购买数据仍然为空,应将其视为异常或不完整的结果。记录 requestCode、resultCode、RESPONSE_CODE 和 Intent 中的键名,再通过 getPurchases() 恢复真实的购买状态。在确认状态前,不能直接授予订阅权益。

备注:内容仅供参考。