1. 功能概述
1.1 需求介绍
对于出海要上架到Google Play的应用,集成Play结算库是必要的,国内的应用通常是集成支付宝或者微信,本文是基于最新结算库版本6进行编写
Google对于上架Play的应用,结算库版本有要求,并且每个版本都有时间截止日期,不同版本的支持时间表,见下表
版本(包括 次要版本) 最后日期版本 可用于发布新应用 最后日期版本 可用于发布 现有应用的更新 4 2023 年 8 月 1 日 2023 年 11 月 1 日 5 2024 年 8 月 1 日 2024 年 11 月 1 日 6 2025 年 8 月 1 日 2025 年 11 月 1 日
1.2 业务流程
- 用户通过某个入口,比如商品列表某个商品发起支付
- 前端弹出收银台,通过信用卡等方式进行支付
- 支付完成后,相应的业务UI刷新,比如界面商品样式和余额等等
1.3 目标步骤
- Google Play后台和管理系统后台商品配置
- 查询商品并显示
- 通过调用后台下单接口进行预下单
- 调用结算库相关接口发起支付
- 调用后端接口验单入账
- 调用结算库相关接口进行消耗或者确认商品
- 对于支付成功的订单进行事件上报
- 对于异常的订单进行补单
- 结果处理
- 测试
2. 准备条件
- Google Play后台商品配置完成
- 管理系统后台商品配置完成
- play结算库等前置工作接入完成
3. 技术方案
3.1 技术原理
首先,Google Play管理后台创建了一套商品,然后在系统管理系统后台配置同一套商品,同时将该套商品与Google Play后台的商品通过内购ID进行一一关联,前端通过查询Google Play后台商品和系统管理后台配置商品进行对应,最后显示在商品列表里。
通过上图可以看出,App端一方面通过Play服务进行API调用来完成用户支付流程, 另一方面通过后端API完成必要开发者流程,这里面包括不限于下单、校验入账等等,整个流程大致过程如下:
- 首先App端向用户展示可以购买的商品
- 用户启动购买流程,以便其接受购买交易
- 在后端服务验证该笔购买交易
- App端向用户提供权益及相关内容
- 确认相关权益以及内容送达用户。对于消耗型商品,用户要先消耗掉已购商品,才能再次购买。
其中,订阅会自动续订,直到被取消。对于整个过程,也可以通过下图来看:

4. 功能实现
4.1 商品配置与库接入
Google Play后台以及后台管理系统商品配置可以交给产品来负责完成,对于Google Play结算库,我们尽可能采用最新版本,
dependencies {
val billing_version = "6.1.0"
implementation("com.android.billingclient:billing-ktx:$billing_version")
}4.2 查询商品并显示
在查询商品前,客户端需要做一系列的SDK初始化等准备工作:
-
初始化BillingClient
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> // To be implemented in a later section. } private var billingClient = BillingClient.newBuilder(context) .setListener(purchasesUpdatedListener) .enablePendingPurchases() .build()对于billingClient建议每个购买流程只初始化一个实例,这样也可以避免上面Listener被同时调用多次
-
连接到Google Play服务
billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingResponseCode.OK) { // The BillingClient is ready. You can query purchases here. } } override fun onBillingServiceDisconnected() { // Try to restart the connection on the next request to // Google Play by calling the startConnection() method. } })这里,如果用户在进行购买流程时发生错误,建议进行简单重试策略,将尝试次数上限作为退出条件,比如下面代码:
billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { Log.d(TAG, "Billing response OK") // The BillingClient is ready. You can now query Products Purchases. } else { Log.e(TAG, billingResult.debugMessage) retryBillingServiceConnection() } } override fun onBillingServiceDisconnected() { Log.e(TAG, "GBPL Service disconnected") retryBillingServiceConnection() } }) // Billing connection retry logic. This is a simple max retry pattern private fun retryBillingServiceConnection() { val maxTries = 3 var tries = 1 var isConnectionEstablished = false do { try { billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { isConnectionEstablished = true Log.d(TAG, "Billing connection retry succeeded.") } else { Log.e( TAG, "Billing connection retry failed: ${billingResult.debugMessage}" ) } } }) } catch (e: Exception) { e.message?.let { Log.e(TAG, it) } tries++ } } while (tries <= maxTries && !isConnectionEstablished) } -
结合后台API和Google Play服务SDK API查询可供购买的商品并展示,对于后台API部分可以参考各个指定商品列表章节,这里只展示Google Play服务SDK API相关代码
suspend fun processPurchases() { val productList = ArrayList<String>() productList.add( QueryProductDetailsParams.Product.newBuilder() .setProductId("product_id_example") .setProductType(BillingClient.ProductType.SUBS) .build() ) val params = QueryProductDetailsParams.newBuilder() params.setProductList(productList) // leverage queryProductDetails Kotlin extension function val productDetailsResult = withContext(Dispatchers.IO) { billingClient.queryProductDetails(params.build()) } // fetch product list from backend // Process the result and show UI }上面代码如果不使用协程,也可以通过
queryProductDetailsAsync方法来进行查询,其结果是一样的,样例代码中指定的productType是订阅,当然也可以选择针对一次性商品ProductType.INAPP同时,我们也可以通过调用
/api/v3/getProductList接口,将上面Play服务SDK查询的结果和该接口返回的结果做一一对应,最终将结果显示在UI上面。
4.3 预下单
在发起购买前,我们先通过后端接口/trade/api/v1/2005进行下单,而接口所需参数即为通过后台接口查询到的商品ID(注意不是内购ID)
4.3 发起支付
如需发起购买请求,请从主线程调用launchBillingFlow()方法,该方法接受BillingFlowParams对象的引用,其所包含的参数都来自上一步查询商品详情返回的:
// An activity reference from which the billing flow will be launched.
val activity : Activity = ...;
val productDetailsParamsList = listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
// retrieve a value for "productDetails" by calling queryProductDetailsAsync()
.setProductDetails(productDetails)
// For One-time product, "setOfferToken" method shouldn't be called.
// For subscriptions, to get an offer token, call ProductDetails.subscriptionOfferDetails()
// for a list of offers that are available to the user
.setOfferToken(selectedOfferToken)
.setObfuscatedAccountId(md5(accountId)) // Specifies an optional obfuscated string that is uniquely associated with the user's account in your app.
.setObfuscatedProfileId(md5(accountId)) // Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app.
.build()
)
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build()
// Launch the billing flow
val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)正如上面示例代码注释中所描述的,setOfferToken只针对订阅型商品提供其offer token,该值从productDetails.subscriptionOfferDetails()方法中获取,而一次性商品不使用setOfferToken方法。成功发起支付后,系统会显示Google Play的购买界面,即收银台,支付完成后,Google Play会调用onPurchasesUpdated()方法,以将购买操作的结果传送给实现PurchasesUpdatedListener接口的监听器,该监听器已在初始化BillingClient客户端时已设置。我们可以在这里处理结果:
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
handlePurchase(purchase)
}
} else if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}上面BilingResult结果,可分为可重试和不可重试两种,其中可重试的错误类型有:
- NETWORK_ERROR(12)
- SERVICE_TIMEOUT(-3)
- SERVICE_DISCONNECTED(-1)
- SERVICE_UNAVAILABLE(2)
- BILLING_UNAVAILABLE(3)
- ERROR(6)
- ITEM_ALREADY_OWNED
- ITEM_NOT_OWNED
不可重试的错误类型有:
- FEAture_NOT_SUPPORTED
- USER_CANCELED
- ITEM_UNAVAILABLE
- DEVELOPER_ERROR
对于可重试的错误类型,前面4个往往可以通过使用简单策略或指数退避算法策略重试请求进行解决,而BILLING_UNAVAILABLE则可能是包括但不限于以下原因:
- 用户设备上的 Play 商店应用已过期。
- 用户位于不受支持的国家/地区。
- 用户是企业用户,其企业管理员已禁止用户进行购买。
- Google Play 无法通过用户的付款方式扣款。例如,用户的信用卡可能已过期。
通常这些情况往往不太可能通过前面的自动重试去解决,这时候就有必要再发生错误时,给用户错误信息,指导其检查并手动处理问题后,提供一个“重试”按钮,以便其手动重试。
ERROR错误往往是由于Google Play内部问题导致,这时可以通过指数退避算法进行重试来缓解此问题,而最后两个很多情况是由于缓存问题导致,可重新调用查询商品详情SDK方法,来获取相应商品,从而检查用户是否已经购买过该商品,如果没有,请实现一个简单的重试逻辑来重新尝试购买。
对于不可重试的错误类型,第一个错误:功能不支持,建议检查Play商店版本与Google Play结算服务版本兼容性,同时可以通过SDK方法billingClient.isFeatureSupported()检查功能支持情况。第二个错误往往是用户取消了支付。第三个错误当前商品不可用,需要检查Google Play后台商品配置是否正常,最后一个错误往往是开发者没有正确的使用Play结算库导致的,可根据官方文档进一步排查用法问题。
4.5 验单合法性
在授予用户权益前,我们需要验证购买交易的合法性,这一步可以通过调用后端API/trade/api/v1/2006,接口中所需的payload和sign参数我们可以通过onPurchasesUpdated方法的回调参数Purchase中获得,后端从传输的参数中获取purchaseToken,验证当前交易是否与以前的任何purchaseToken值都不匹配,如果购买交易合法且过去没有使用过,那么就可以放心地授予用户当前商品权益。
4.6 消耗或者确认
授予用户当前商品权益后,我们还需要对其进行消耗或者确认,否则如果在三天内未确认购买交易,用户会自动收到退款,并且 Google Play 会撤消该购买交易。 对于消耗性商品,我们使用Google Play结算库中的consumeAsync()方法,而对于非消耗性商品或者订阅,我们使用使用 Google Play 结算库中的acknowledgePurchase()方法,在确认购买交易之前,需要检查是否已通过使用Google Play结算库中的isAcknowledged()方法进行确认。以下是部分代码示例说明:
suspend fun handlePurchase(purchase: Purchase) {
// Purchase retrieved from BillingClient#queryPurchasesAsync or your PurchasesUpdatedListener.
val purchase : Purchase = ...;
// Verify the purchase.
// Ensure entitlement was not already granted for this purchaseToken.
// Grant entitlement to the user.
val consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build()
val consumeResult = withContext(Dispatchers.IO) {
client.consumePurchase(consumeParams)
}
}
val client: BillingClient = ...
val acknowledgePurchaseResponseListener: AcknowledgePurchaseResponseListener = ...
suspend fun handlePurchase() {
if (purchase.purchaseState === PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
val ackPurchaseResult = withContext(Dispatchers.IO) {
client.acknowledgePurchase(acknowledgePurchaseParams.build())
}
}
}
}但是谷歌官方建议为了安全起见,消耗或者确认商品都建议放在后端进行。如果您想消耗和确认消耗型商品,后端可以通过Google Play Developer API Purchases.products:consume,如需确认非消耗型商品或订阅,后端可以通过Google Play Developer API Purchases.products:acknowledge 或 Purchases.subscriptions:acknowledge。由于目前后端暂未实现,可以后期等实现后一同修改,之所以使用后端来进行确认或者消耗购买交易,是因为服务器端 API 可以针对网络连接不佳和恶意活动等问题多提供一重保护。例如,假设用户已从您的应用购买商品,但在购买交易验证期间网络连接中断了。如果没有服务器确认,他们可能需要通过应用重新登录才能完成确认流程。否则,如果用户在三天内未重新登录,购买交易会因未经确认而自动退款。服务器确认可防止出现这种情况,因为它会在 Google Play 将购买交易有效的消息告知服务器后立即发送确认信息。
4.7 事件上报
在后端正确处理完所有充值逻辑后,会给客户端发送一条pay_result_notice充值成功的消息通知,客户端收到该条消息,解析出所需要的参数,然后进行处理:
{
"msgTypeCode":"pay_result_notice",
"msgData":{
"status":"0",
"orderNo":"", //订单号
"amount":"", //价格(分)
"goodsType":"", //商品类型
"payChannel":"", //支付通道
"goodsId":"" //商品ID
}
}对于Firebase事件,上报示例代码:
Firebase.analytics.logEvent("manual_event") {
param(FirebaseAnalytics.Param.VALUE, amount)
}
if ("2" == it.goodsType) {
Firebase.analytics.logEvent("manual_vip_purchase") {
param(FirebaseAnalytics.Param.VALUE, amount)
}
} else {
Firebase.analytics.logEvent("manual_recharge_purchase") {
param(FirebaseAnalytics.Param.VALUE, amount)
}
}对于Facebook事件,需要amount先除以100,然后再进行上报:
AppEventsLogger.newLogger(this).logPurchase(
amount,
Currency.getInstance("USD")
)
val params = Bundle().apply {
putString(EVENT_PARAM_CURRENCY, "USD")
putString(EVENT_PARAM_CONTENT_TYPE, "product")
putString(EVENT_PARAM_CONTENT_ID, orderNo)
putString(EVENT_PARAM_CONTENT, payChannel)
}
if ("0" == amount) {
AppEventsLogger.newLogger(this).logEvent(
AppEventsConstants.EVENT_NAME_ADDED_TO_CART,
amount,
params
)
} else {
AppEventsLogger.newLogger(this).logEvent(
AppEventsConstants.EVENT_NAME_SUBSCRIBE,
amount,
params
)
}4.8 补单
大多数情况下,我们的应用会通过PurchasesUpdatedListener收到购买交易的通知,但在某些情况下,我们的应用将通过调用BillingClient.queryPurchasesAsync()来获知购买交易,如下一些情况,我们无法从上面监听跟踪到购买交易:
- 在购买过程中出现网络问题:用户成功购买了商品并收到了 Google 的确认消息,但用户设备在通过
PurchasesUpdatedListener收到购买交易的通知之前失去了网络连接。 - 多部设备:用户在一部设备上购买了一件商品,然后在切换设备时期望看到该商品。
- 处理在您的应用外进行的购买交易:某些购买交易(如促销活动兑换)可能会在您的应用外进行。
为了处理这些情况,官方建议请确保应用在onResume方法中调用BillingClient.queryPurchasesAsync(),以确保所有购买交易都得到成功处理,当然有的开发者也可以在应用启动时重新查询,并执行后续流程逻辑。
4.9 结果处理
前面所有支付流程正常完成以后,我们仍然需要处理一些后续事宜,比如UI刷新,账户余额刷新等等
4.10 测试
Google Play后台支持加入内测人员,这样可以在不支付真实金额的情况下模拟支付流程,它有以下优势:
- 一般来说,未经过签名并上传到 Google Play 的应用不能使用 Google Play 结算库。内测人员可以绕过此检查,也就是说你可以旁加载应用进行测试,甚至可以无需上传新版应用,直接旁加载带有调试签名的调试 build 应用。请注意,软件包名称必须与针对 Google Play 配置的应用名称一致,并且 Google 账号必须是内测人员的 Google Play 管理中心账号。
- 内测人员可以使用测试用付款方式,该方式不会针对测试人员的购买交易收取真实费用。此外,该方式也可以用来模拟付款遭拒等情况。
- 一些订阅期比较长的,会由于时间问题不方便测试,内测人员可以快速测试订阅功能
需要注意的几点补充说明:
- 进行购买测试时,采用的应用购买流程与实际购买流程相同。
- 系统不会针对购买测试计算税费。
- Google Play 会在购买对话框中心显示一条通知,指明此为购买测试。
- 必须在测试人员的 Android 设备上设置测试账号。
- 如果设备上有多个账号,会使用下载应用时所用的账号进行购买。
- 如果没有账号下载过应用,会使用第一个账号进行购买。
- 应用发布到测试轨道后,可能需要过几个小时才能由测试人员使用
在测试一次性消耗型商品时,针对不同测试情况,其中包括
- 购买交易成功,用户收到商品。对于内测人员,可以使用测试用付款方式,一律批准付款方式。
- 在购买交易中,通过付款方式扣款失败,用户不应收到商品。对于内测人员,可以使用测试用付款方式,一律拒绝付款方式。
- 确保商品可以多次购买。
对于内测人员发起的购买交易,如果您的应用未确认购买交易,将在 3 分钟后退款,并且您将收到一封关于取消购买交易的电子邮件。您也可以前往 Google Play 管理中心的订单标签页,查看是否有个订单在 3 分钟后退款。
测试订阅型商品时,对于测试续订,可以使用内测人员可用的测试用付款方式,一律批准和测试用付款方式,一律拒绝这两种付款方式,
测试订阅不仅在续订速度上比实际订阅快,而且最多可续订六次。下表列出了时长不同的订阅的测试续订时间:
| 生产订阅期 | 测试订阅续订 |
|---|---|
| 1周 | 5分钟 |
| 1个月 | 5分钟 |
| 3个月 | 10分钟 |
| 6个月 | 15分钟 |
| 1年 | 30分钟 |
基于时间的订阅功能(如免费试订)也为了方便测试而缩短了时间。下表列出了与基于时间的订阅功能关联的测试时间段:
| 功能 | 测试期 |
|---|---|
| 购买交易确认 | 5分钟 |
| 免费试用 | 3分钟 |
| 初次体验价周期 | 与订阅测试周期相同 |
| 宽限期(3天和7天) | 5分钟 |
| 账号保留功能 | 10分钟 |
| 暂停(1个月) | 5分钟 |
| 暂停(2个月) | 10分钟 |
| 暂停(3个月) | 15分钟 |
对于不同地区,内测人员可以在任何地区测试购买流程,而无需针对该国家/地区提供真实的付款方式。请按照以下步骤进行测试:
- 创建一个新的 Gmail 账号。我们可以在任何国家/地区创建账号。
- 将用户设置为内测人员。
- 通过 VPN 进入所需的测试国家/地区。
- 启动购买流程。
我们可以清除 Play 商店数据和缓存,然后针对要测试的任何国家/地区重复第 3 步和第 4 步。切换到新的国家/地区后,还需要清除 Google Play 商店的数据,以移除与之前的国家/地区相关的数据。
4.11 业务场景
业务中常见的触发支付场景有以下情况,具体可以产品需求为准:
- 首页:金币促销
- 首页:切换国家
- 主播列表:拨打视频通话
- 主播资料页:拨打视频通话
- 主播资料页:相册解锁
- 个人中心:钱包金币订阅和购买
- 个人中心:会员订阅
- 设置:我的订阅
- IM聊天:礼物发送-金币弹窗
- IM聊天:消息发送失败-金币/VIP 提示语
- IM聊天:拨打视频通话
- IM聊天:购买主播视频或图片资源
- IM聊天:送礼物
- 假视频:拨打视频通话
- 系统弹窗:拨打视频通话
- 视频通话:切换/关闭摄像头
- 视频通话:送礼物
- 视频通话:挂起充值
- 系统通知:弹出VIP弹窗或者金币页面
- 卡片匹配/视频匹配/随机匹配:匹配卡购买以及订阅
- 卡片匹配和视频匹配冻结页面:会员订阅/金币订阅
- 卡片匹配和视频匹配营销商品
- 签到:金币订阅与匹配卡订阅
5. 常见问题
- 问题1:未能正确处理结果,比如UI刷新,余额刷新等
- 原因:中间过程由于某种错误未能按照上面步骤走完整
- 解决方案:检查完整支付流程
- 问题2:未能唤起Google Play收银台
- 原因:launchBillingFlow方法未能正确调用
- 解决方案:检查代码,发起支付方法所传参数是否有误,与Google Play服务是否断开连接,或者商品未能查询出来,必要时可断点调试
- 对于其他支付失败问题,也可根据错误码结合这篇文章来排查:https://juejin.cn/post/7020659041651130405 (opens in a new tab)
6. 参考引用
- 结算库集成:https://developer.android.com/google/play/billing/integrate?hl=zh-cn (opens in a new tab)
- 错误码处理:https://developer.android.com/google/play/billing/errors?hl=zh-cn (opens in a new tab)
- 安全验证:https://developer.android.com/google/play/billing/security?hl=zh-cn (opens in a new tab)
- 库版本废弃问题解答:https://developer.android.google.cn/google/play/billing/deprecation-faq?hl=zh-cn (opens in a new tab)