Billing-in-app 购买数字商品时出现 server error
结论
这类提示通常来自 Google Play 购买页面,而非应用自身的业务服务器。建议先检查以下几项:
- 应用必须通过 Google Play 测试轨道安装。直接使用 Android Studio 安装的 APK 无法完成完整的购买测试。
applicationId、签名证书必须与 Play Console 中上传的应用一致。- 商品 ID 必须完全匹配,商品也要处于启用状态,并允许在当前地区购买。
- 测试账号必须加入测试人员名单,并已登录设备上的 Google Play 商店。
- 先查询商品详情,再使用 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 支持渠道反馈,不要用循环重试掩盖问题。
备注:内容仅供参考。
