PAYATHON 2026

Billing-in-app 购买数字商品时出现 server error

支付阿杰

结论

这类提示通常来自 Google Play 购买页面,而非应用自身的业务服务器。建议先检查以下几项:

  1. 应用必须通过 Google Play 测试轨道安装。直接使用 Android Studio 安装的 APK 无法完成完整的购买测试。
  2. applicationId、签名证书必须与 Play Console 中上传的应用一致。
  3. 商品 ID 必须完全匹配,商品也要处于启用状态,并允许在当前地区购买。
  4. 测试账号必须加入测试人员名单,并已登录设备上的 Google Play 商店。
  5. 先查询商品详情,再使用 Google Play 返回的 ProductDetails 发起购买,不要自行构造商品信息。

如果刚刚修改过配置,还要等待 Google Play 后台完成同步。同步时间没有固定保证,控制台显示已保存,不代表配置已经在设备端生效。

常见原因

应用不是从 Google Play 安装的

即使本地调试包的包名相同,也可能因安装来源或签名不符,导致测试购买无法正常启动。

可以将构建产物上传到内部测试或封闭测试轨道,然后:

  • 将购买测试账号加入对应的测试轨道。
  • 通过测试邀请链接加入测试。
  • 从 Google Play 商店页面安装应用。
  • 安装后不要再用 Android Studio 覆盖,否则可能再次导致签名或安装来源不一致。

商品配置不正确

逐项检查 Play Console 中的商品配置:

  • 商品 ID 与代码中的字符串完全一致,包括大小写。
  • 一次性商品或订阅已经启用。
  • 商品已经设置价格。
  • 当前测试账号所在的国家或地区允许购买该商品。
  • 订阅所需的基础方案、优惠等配置处于可用状态。

查询一次性商品和订阅时,必须使用对应的商品类型。类型不正确,通常无法获得有效的商品详情。

包名或签名不一致

确认当前安装包的 applicationId 与 Play Console 中登记的应用包名一致。例如:

android {
    defaultConfig {
        applicationId = "com.example.app"
    }
}

还要确认上传版本与设备安装版本使用兼容的签名配置。如果启用了 Play App Signing,需要分清上传密钥和应用签名密钥,不能只根据本地 keystore 文件名判断签名是否一致。

测试账号配置有误

用于购买的 Google 账号通常需要满足以下条件:

  • 已加入应用测试轨道。
  • 已通过测试邀请链接加入测试。
  • 已配置为许可测试账号(如果当前测试流程需要)。
  • 是 Google Play 商店当前实际使用的账号。

如果设备上存在多个 Google 账号,购买页面使用的账号可能不是预期账号。可以先切换 Play 商店账号,必要时暂时移除其他账号后再测试。

测试人员不要使用与发布应用关联的开发者收款账号进行购买。相关限制可能随 Google Play 政策调整,请以当前 Play Console 的提示为准。

建议的排查顺序

1. 确认商品能被查询到

先不要直接调用购买流程。应通过 Billing Library 查询商品,并记录响应码、调试信息和返回结果。

下面以一次性商品为例:

val productList = listOf(
    QueryProductDetailsParams.Product.newBuilder()
        .setProductId("premium_upgrade")
        .setProductType(BillingClient.ProductType.INAPP)
        .build()
)

val params = QueryProductDetailsParams.newBuilder()
    .setProductList(productList)
    .build()

billingClient.queryProductDetailsAsync(params) { billingResult, queryResult ->
    Log.d(
        "Billing",
        "query responseCode=${billingResult.responseCode}, " +
            "debugMessage=${billingResult.debugMessage}"
    )

    val productDetailsList = queryResult.productDetailsList

    if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
        Log.e("Billing", "Product query failed")
        return@queryProductDetailsAsync
    }

    val productDetails = productDetailsList.firstOrNull()
    if (productDetails == null) {
        Log.e("Billing", "Product not found: premium_upgrade")
        return@queryProductDetailsAsync
    }

    Log.d("Billing", "Product found: ${productDetails.productId}")
}

如果在这里查不到商品,问题通常出在 Play Console 配置、账号、包名、签名、安装渠道或后台同步。此时不必反复打开购买页面。

不同 Google Play Billing Library 版本的查询结果类型可能略有不同。请以项目当前依赖版本的 API 签名为准,排查思路不变。

2. 使用查询结果发起购买

发起购买时,应使用查询得到的 ProductDetails:

fun launchPurchase(
    activity: Activity,
    billingClient: BillingClient,
    productDetails: ProductDetails
) {
    val productDetailsParams =
        BillingFlowParams.ProductDetailsParams.newBuilder()
            .setProductDetails(productDetails)
            .build()

    val billingFlowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(listOf(productDetailsParams))
        .build()

    val result = billingClient.launchBillingFlow(activity, billingFlowParams)

    Log.d(
        "Billing",
        "launch responseCode=${result.responseCode}, " +
            "debugMessage=${result.debugMessage}"
    )
}

订阅商品可能还需要从查询结果中选择有效的 offerToken:

val offerToken = productDetails.subscriptionOfferDetails
    ?.firstOrNull()
    ?.offerToken
    ?: error("No available subscription offer")

val productDetailsParams =
    BillingFlowParams.ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setOfferToken(offerToken)
        .build()

如果没有可用的订阅优惠,不要伪造或写死 offerToken。应返回 Play Console,检查基础方案、地区和资格条件。

3. 记录购买回调

val purchasesUpdatedListener =
    PurchasesUpdatedListener { billingResult, purchases ->
        Log.d(
            "Billing",
            "purchase responseCode=${billingResult.responseCode}, " +
                "debugMessage=${billingResult.debugMessage}"
        )

        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases.orEmpty().forEach { purchase ->
                    // 将 purchaseToken 发送到可信服务器校验。
                    // 校验成功后,再发放权益并确认购买。
                }
            }

            BillingClient.BillingResponseCode.USER_CANCELED -> {
                Log.d("Billing", "User canceled purchase")
            }

            BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
                Log.w("Billing", "Item is already owned; query existing purchases")
            }

            else -> {
                Log.e(
                    "Billing",
                    "Purchase failed: ${billingResult.debugMessage}"
                )
            }
        }
    }

不要只记录界面显示的 “server error”。responseCode 和 debugMessage 能更准确地区分服务异常、商品不可用、账号问题和商品已购买等情况。

4. 检查设备环境

可以按顺序尝试以下操作:

  • 更新 Google Play 商店和 Google Play 服务。
  • 确认设备网络正常,并关闭可能干扰 Google Play 请求的代理或 VPN。
  • 确认设备的日期、时间和时区准确。
  • 清除 Google Play 商店缓存,然后重新打开。
  • 退出测试轨道,再重新加入。
  • 卸载应用,然后从 Google Play 测试页面重新安装。
  • 换一个明确加入测试的账号,或使用另一台具备 Google Play 服务的设备验证。

模拟器必须使用包含 Google Play 商店的系统镜像。只有基础 Google APIs、没有 Play 商店的镜像,不适合验证完整的购买流程。

服务端处理注意事项

购买界面成功打开,并不表示交易已经可靠完成。在生产环境中,应将 purchaseToken 发送到自己的服务端,通过 Google Play Developer API 校验购买状态,确认无误后再发放数字权益。

一次性商品的确认购买逻辑类似:

val acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
    .setPurchaseToken(purchase.purchaseToken)
    .build()

billingClient.acknowledgePurchase(acknowledgeParams) { result ->
    Log.d(
        "Billing",
        "acknowledge responseCode=${result.responseCode}, " +
            "debugMessage=${result.debugMessage}"
    )
}

只有当购买尚未确认,并且交易已通过可信方式验证时,才能执行确认。可消耗型商品需要使用 consume 流程,不能直接套用非消耗型商品的处理方式。

仍然报错时如何定位

如果商品可以正常查询,但打开购买页面后仍然显示服务器错误,请保留以下信息:

  • 错误发生的时间和时区。
  • 应用包名、版本号和版本代码。
  • 安装应用所用的测试轨道。
  • 商品 ID 和商品类型。
  • BillingResult.responseCode。
  • BillingResult.debugMessage。
  • Google Play 商店版本。
  • 测试账号是否已加入测试轨道并配置许可测试。
  • 所有测试账号和设备是否都能复现。

如果多个合规测试账号在不同设备和网络下都能稳定复现,并且商品查询成功、配置也已同步,可能是 Google Play 服务端出现了临时异常。可以稍后重试,并携带上述信息通过 Play Console 支持渠道反馈,不要用循环重试掩盖问题。

备注:内容仅供参考。