ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android Google Play支付SDK接入实战指南

Android Google Play支付SDK接入实战指南 1. 项目概述这不是“调个API”那么简单的事“Android Google Play 支付SDK接入指南”——看到这个标题很多刚接手商业化模块的Android开发者第一反应是“不就是集成个SDK、调几个接口、走一遍官方文档吗”我当年也是这么想的直到在上线前48小时被线上订单漏单、订阅状态错乱、退款回调丢失这三连击打懵连续熬了三个通宵翻日志、抓包、比对Google Play Console后台数据才真正明白这不是一次技术集成而是一场涉及客户端稳定性、服务端幂等设计、Google Play政策合规性、用户生命周期管理的系统性工程。核心关键词“Android”“Google Play”“支付SDK”“接入指南”背后藏着远超代码层面的硬约束你必须同时满足Android平台的运行时权限模型、Google Play强制的Billing Library版本要求v5已成事实标准、订阅类商品的自动续订与取消逻辑、以及Google Play审核团队对UI一致性、错误提示、隐私声明的显性审查。比如哪怕你代码里所有接口都调通了只要在结算页没按规范显示“由Google Play处理付款”或者没在manifest中正确声明uses-permission android:namecom.android.vending.BILLING /审核就直接拒掉——这种细节官方文档不会加粗标红但会真实卡住你整个App的上架节奏。这个指南适合三类人一是刚从纯功能开发转向商业化模块的Android工程师需要避开那些“文档没写但线上必踩”的坑二是负责App整体交付的技术负责人得清楚支付链路里哪些环节必须由客户端强控如购买流程中断恢复、哪些必须和服务端协同如订阅状态同步三是独立开发者或小团队没有专职后端支撑得用最小成本把支付闭环跑稳。它不讲“Hello World”式Demo而是聚焦真实项目里90%团队都会遇到的场景如何让一次购买在弱网下不丢单怎么处理用户突然切到后台再回来时的购买状态为什么测试环境能过正式环境却收不到purchaseUpdated回调这些不是Bug而是Google Play支付生态的固有特性必须用设计去适配而不是靠运气去绕开。2. 整体架构设计与方案选型逻辑2.1 为什么必须用Billing Library而非直连AIDL十年前Android支付还能通过绑定IInAppBillingServiceAIDL接口手动调用但现在Google已彻底废弃该路径。Billing Library v5当前最新稳定版不是“可选升级”而是强制依赖——它封装了底层AIDL通信、签名验证、本地缓存、线程调度等复杂逻辑更重要的是它内置了Google Play对现代支付流程的强制约束。比如v5强制要求所有购买流程必须通过launchBillingFlow()触发该方法会校验Activity是否处于前台、是否已初始化BillingClient、是否满足Play服务版本任何一项不满足都会抛出明确异常如SERVICE_DISCONNECTED而不是让你在回调里收到一个模糊的BILLING_RESPONSE_CODE_SERVICE_UNAVAILABLE。这种“失败前置化”设计本质是Google在帮你规避大量隐性兼容性问题。我对比过v3/v4/v5三个版本的实际表现v3在Android 12上因后台启动Activity限制导致购买页白屏率高达17%v4虽修复了部分问题但在多进程App中常出现BillingClient实例被意外销毁导致onPurchasesUpdated回调丢失而v5通过BillingClient.newBuilder().setListener(...)的单例注册机制配合enablePendingPurchases()的异步初始化将这类问题收敛到可预测范围内。更关键的是v5的queryPurchasesAsync()返回PurchaseHistoryRecord对象天然支持订阅商品的autoRenewing状态解析而旧版需手动解析JSON字符串——这对需要实时展示用户订阅到期时间的业务如视频会员、工具类App是刚需。提示Billing Library v5的Gradle依赖必须使用implementation com.android.billingclient:billing-ktx:6.0.1ktx版本而非Java版。Kotlin协程封装不仅简化了launchBillingFlow的调用链避免嵌套Callback其queryPurchasesAsync返回的DeferredPurchaseResult能无缝接入现有协程作用域避免主线程阻塞。实测在低端机上Java版因频繁Handler切换导致购买页加载延迟增加200ms以上而ktx版通过Dispatchers.IO调度性能更稳。2.2 客户端-服务端协同模型为什么不能只靠客户端很多团队试图“客户端全量处理”即用queryPurchasesAsync()拉取本地购买记录直接更新UI并完成业务逻辑。这在测试环境看似可行但线上必然崩盘。原因有三第一Google Play的购买确认存在网络延迟。用户点击“购买”后Google服务器需完成支付风控、账户扣款、订单生成这个过程可能长达数秒。此时客户端queryPurchasesAsync()返回的仍是旧状态若直接据此更新UI会出现“已支付但显示未购买”的诡异现象。第二用户主动取消订阅或退款后Google Play不会实时通知客户端。queryPurchasesAsync()默认只返回“有效购买”但已取消的订阅仍会保留在本地缓存中直到下次主动查询或App重启。若业务逻辑仅依赖本地状态会导致用户已退订却继续享受服务。第三跨设备状态同步失效。用户在手机端购买后又在平板登录同一账号客户端无法感知该购买行为——因为queryPurchasesAsync()只查本机缓存不触发跨设备同步。因此我们采用“客户端轻量交互 服务端权威校验”模型客户端负责触发购买流程、捕获用户操作意图、处理UI反馈服务端则作为唯一可信源通过Google Play Developer API定期轮询或接收Real-time Developer Notifications获取真实订单状态并下发给客户端。具体分工如下客户端调用launchBillingFlow()启动支付页监听onPurchasesUpdated()捕获购买结果用queryPurchasesAsync()做本地状态快照仅用于UI降级展示向服务端上报purchaseToken和productId。服务端收到客户端上报后立即调用purchases.products.get一次性商品或purchases.subscriptions.get订阅商品验证purchaseToken有效性校验成功后持久化订单并触发业务逻辑如开通会员通过WebSocket或消息队列将状态变更推送给客户端。这种设计将Google Play的不可控延迟网络、风控和服务端的可控性解耦确保用户操作后3秒内获得确定性反馈同时避免客户端因状态不同步导致的资损。2.3 订阅与一次性商品的架构分层Google Play将商品分为两类INAPP一次性购买和SUBS订阅。表面看只是类型参数差异但实际影响整个状态机设计。一次性商品的核心是“原子性”——支付成功即永久生效无需后续续期管理而订阅商品的核心是“生命周期管理”涉及首次购买、自动续订、用户主动取消、到期终止、宽限期、试用期等多个状态节点。我们为此设计了双通道状态机一次性商品通道客户端监听onPurchasesUpdated()若responseCode OK且purchases.isNotEmpty()立即提取purchaseToken并上报服务端服务端验证通过后直接标记订单为PAID并开通权益。客户端本地缓存该购买记录后续启动时通过queryPurchasesAsync()校验若本地无记录则主动向服务端请求状态同步。订阅商品通道客户端除基础购买外还需监听onPurchasesUpdated()中的isAutoRenewing字段并在queryPurchasesAsync()结果中解析expiryTimeMillis。但关键决策点在服务端当服务端收到purchaseToken后必须调用purchases.subscriptions.get获取完整订阅对象其中autoRenewing、cancelReason、expiryTimeMillis字段共同决定用户当前权益状态。例如autoRenewingfalse且cancelReasonUSER_CANCELLED说明用户已主动取消但权益仍持续到expiryTimeMillis而autoRenewingtrue则表示正常续订中。客户端只根据服务端下发的状态更新UI绝不自行判断续订状态。注意Google Play对订阅商品有强制UI要求——必须在设置页提供“取消订阅”入口且该入口必须跳转至Google Play的官方取消页https://play.google.com/store/account/subscriptions。很多团队试图自建取消按钮结果被审核驳回。正确做法是在App内设置页添加Intent跳转val intent Intent(Intent.ACTION_VIEW, Uri.parse(https://play.google.com/store/account/subscriptions))并确保该链接在WebView中可正常打开。3. 核心细节解析与实操要点3.1 BillingClient初始化时机、作用域与生命周期绑定BillingClient不是“创建一次全局复用”那么简单。它的初始化必须严格绑定Activity生命周期否则极易引发内存泄漏或回调丢失。常见错误是将其声明为Application单例认为“全局唯一”最省事。但BillingClient内部持有Activity Context引用若在Activity销毁后未调用endConnection()该Context会被长期持有导致Activity无法GC——在列表页频繁进出的场景下内存占用飙升是必然结果。正确做法是在需要支付功能的Activity或Fragment中于onCreate()或onViewCreated()中初始化BillingClient并在onDestroy()或onDestroyView()中显式断开连接。具体代码如下class PurchaseActivity : AppCompatActivity() { private lateinit var billingClient: BillingClient private var isBillingReady false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_purchase) // 初始化BillingClient绑定当前Activity Context billingClient BillingClient.newBuilder(this) .setListener(purchasesUpdatedListener) // 监听购买结果 .enablePendingPurchases() // 启用待处理购买v5必需 .build() // 连接Google Play服务 billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode BillingClient.BillingResponseCode.OK) { isBillingReady true // 连接成功可开始查询商品 querySkuDetails() } else { // 连接失败需重试或提示用户 handleBillingError(billingResult) } } override fun onBillingServiceDisconnected() { // 服务断开需重新连接 isBillingReady false } }) } override fun onDestroy() { super.onDestroy() // 必须断开连接释放资源 if (::billingClient.isInitialized billingClient.isReady) { billingClient.endConnection() } } }这里的关键细节enablePendingPurchases()是v5的强制调用它允许Google Play在App未运行时处理用户购买如用户在Play商店点击购买后启动App此时onPurchasesUpdated()会收到待处理订单。若不启用这部分订单将永远丢失。onBillingSetupFinished()的responseCode判断必须精确到BillingClient.BillingResponseCode.OK而非简单判空。因为responseCode为SERVICE_UNAVAILABLE时billingResult.debugMessage会包含“Google Play services not available”等提示这是用户未安装Play服务的明确信号需引导用户安装。onBillingServiceDisconnected()回调并非异常而是Google Play服务升级、手机重启后的正常现象。此时应记录日志并在用户再次触发购买时自动重连而非直接报错。3.2 商品查询与SKU配置避免审核被拒的硬性规则Google Play要求所有在App内展示的商品必须在Play Console后台预先配置SKUStock Keeping Unit且SKU ID必须与客户端查询时传入的skuList完全一致。常见错误是客户端硬编码SKU ID如premium_monthly而后台配置为monthly_premium导致querySkuDetailsAsync()返回空列表用户看到“暂无商品”。更隐蔽的坑在于商品类型匹配querySkuDetailsAsync()需指定BillingClient.SkuType且必须与后台配置类型严格对应。若后台将vip_yearly配置为SUBS订阅但客户端用INAPP类型查询结果必然为空。正确做法是建立SKU映射表商品用途SKU ID后台配置类型SkuType客户端用途月度会员monthly_vipSUBS订阅类商品查询年度会员yearly_vipSUBS订阅类商品查询去广告remove_adsINAPP一次性商品查询查询代码需按类型分组private fun querySkuDetails() { // 订阅商品列表 val subsList listOf(monthly_vip, yearly_vip) val subsParams SkuDetailsParams.newBuilder() .setSkusList(subsList) .setSkusType(BillingClient.SkuType.SUBS) .build() billingClient.querySkuDetailsAsync(subsParams) { billingResult, skuDetailsList - if (billingResult.responseCode BillingClient.BillingResponseCode.OK skuDetailsList ! null) { // 处理订阅商品详情 updateSubscriptionUi(skuDetailsList) } } // 一次性商品列表 val inappList listOf(remove_ads) val inappParams SkuDetailsParams.newBuilder() .setSkusList(inappList) .setSkusType(BillingClient.SkuType.INAPP) .build() billingClient.querySkuDetailsAsync(inappParams) { billingResult, skuDetailsList - if (billingResult.responseCode BillingClient.BillingResponseCode.OK skuDetailsList ! null) { // 处理一次性商品详情 updateOneTimeUi(skuDetailsList) } } }实操心得Google Play对商品描述有严格审核。例如“永久去广告”必须改为“一次性去广告”因为Google不允许承诺“永久”用户可能卸载重装状态不保留“无限流量”需注明“仅限本App内使用”。我们在第一次提交时因描述含“永久”被审核员要求修改耗时2天。建议所有商品文案先用Play Console的“预发布测试”功能验证避免正式提交后返工。3.3 购买流程实现从触发到结果的全链路控制launchBillingFlow()是购买流程的唯一入口但它的调用时机和参数构造决定成败。常见错误是直接在RecyclerView Item点击事件中调用导致用户快速连点多次触发多个购买流程。正确做法是添加防抖和状态锁private var isProcessingPurchase false fun onPurchaseClick(sku: String, skuType: String) { if (isProcessingPurchase) return // 防抖 isProcessingPurchase true val params BillingFlowParams.newBuilder() .setSkuDetails(skuDetailsMap[sku]!!) // 必须传入querySkuDetailsAsync获取的SkuDetails对象 .build() billingClient.launchBillingFlow(this, params) .apply { // launchBillingFlow返回BillingResult但实际购买结果在onPurchasesUpdated回调中 // 此处仅用于检查启动是否成功 if (responseCode ! BillingClient.BillingResponseCode.OK) { showToast(启动支付页失败${debugMessage}) isProcessingPurchase false } } }购买结果通过PurchasesUpdatedListener回调但这里有两个关键陷阱第一onPurchasesUpdated()可能被多次调用。例如用户购买后立即取消会先后收到PURCHASED和PENDING状态。必须根据purchase.purchaseState字段判断最终状态Purchase.PurchaseState.PURCHASED支付成功可安全上报服务端Purchase.PurchaseState.PENDING支付待确认如银行卡扣款中此时不能开通权益需等待后续状态更新Purchase.PurchaseState.UNSPECIFIED未知状态需忽略。第二purchases列表可能为空。当用户在支付页点击“取消”或“返回”onPurchasesUpdated()仍会被调用但purchases为空。此时需重置UI状态而非报错。完整回调处理逻辑private val purchasesUpdatedListener PurchasesUpdatedListener { billingResult, purchases - isProcessingPurchase false // 重置状态锁 when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK - { if (purchases ! null purchases.isNotEmpty()) { val purchase purchases.first() when (purchase.purchaseState) { Purchase.PurchaseState.PURCHASED - { // 支付成功上报服务端 reportPurchaseToServer(purchase) // 更新UI显示“已购买” updatePurchaseStatus(true) } Purchase.PurchaseState.PENDING - { // 支付待确认显示“处理中” updatePurchaseStatus(false, 支付处理中...) } else - { // 其他状态忽略 updatePurchaseStatus(false) } } } else { // purchases为空说明用户取消了购买 updatePurchaseStatus(false) } } BillingClient.BillingResponseCode.USER_CANCELED - { // 明确的用户取消可记录埋点 logEvent(purchase_cancelled, mapOf(sku to sku)) updatePurchaseStatus(false) } else - { // 其他错误如网络超时、服务不可用 showToast(购买失败${billingResult.debugMessage}) updatePurchaseStatus(false) } } }注意reportPurchaseToServer()必须包含重试机制。因网络波动上报可能失败。我们采用指数退避策略首次失败后3秒重试第二次失败后10秒重试第三次失败后30秒重试超过3次则存入本地数据库App下次启动时补报。实测该策略将上报失败率从12%降至0.3%。4. 实操过程与核心环节实现4.1 服务端验证用Developer API穿透Google Play的黑盒客户端拿到purchaseToken后服务端必须调用Google Play Developer API进行验证这是防止伪造订单的唯一防线。很多人误以为purchaseToken是JWT可自行解析但实际上它是Google加密的opaque token必须通过官方API校验。验证流程分三步获取OAuth2访问令牌服务端需用Service Account密钥.json文件换取access_token。注意该密钥需在Google Cloud Console中开启androidpublisherAPI权限。调用purchases.products.get或purchases.subscriptions.get根据商品类型选择Endpoint传入packageName、productId、token。解析响应并校验关键字段重点检查purchaseState必须为0、acknowledgementState必须为1表示已确认、orderId非空且唯一。Java服务端示例Spring BootService public class GooglePlayValidator { private final String PACKAGE_NAME com.yourcompany.yourapp; private final String SERVICE_ACCOUNT_KEY_PATH /path/to/service-account-key.json; public boolean validateInAppPurchase(String productId, String purchaseToken) { try { // 1. 获取AccessToken GoogleCredential credential GoogleCredential.fromStream( new FileInputStream(SERVICE_ACCOUNT_KEY_PATH) ).createScoped(Collections.singletonList(https://www.googleapis.com/auth/androidpublisher)); credential.refreshToken(); String accessToken credential.getAccessToken(); // 2. 调用API String url String.format( https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s, PACKAGE_NAME, productId, purchaseToken ); HttpClient client HttpClientBuilder.create().build(); HttpGet request new HttpGet(url); request.setHeader(Authorization, Bearer accessToken); HttpResponse response client.execute(request); String jsonResponse EntityUtils.toString(response.getEntity()); // 3. 解析JSON并校验 JSONObject json new JSONObject(jsonResponse); int purchaseState json.getInt(purchaseState); int acknowledgementState json.getInt(acknowledgementState); String orderId json.optString(orderId, ); return purchaseState 0 acknowledgementState 1 !orderId.isEmpty(); } catch (Exception e) { log.error(Google Play验证失败, e); return false; } } }关键细节acknowledgementState必须为1。Google Play要求服务端在验证成功后必须调用purchases.products.acknowledge一次性商品或purchases.subscriptions.acknowledge订阅商品进行确认否则订单会在72小时后自动退款。很多团队只验证不确认导致大量自动退款。purchaseState 0表示“已购买”但1表示“已取消”2表示“待定”。若收到purchaseState 2需等待Google Play后续状态更新而非立即开通权益。orderId是唯一订单号可用于对账。Google Play保证同一笔交易的orderId全局唯一即使用户重复购买同一商品。4.2 订阅状态同步应对自动续订与用户取消的复杂性订阅商品的状态不是静态的而是随时间动态变化。服务端必须建立定时任务轮询Google Play API获取最新状态。我们采用“增量轮询事件驱动”双模式增量轮询每小时调用purchases.subscriptions.list传入startTime参数只拉取该时间点后的变更订单。避免全量查询的性能压力。事件驱动配置Real-time Developer NotificationsRTDN当用户购买、取消、续订时Google Play会向你的Webhook推送事件。RTDN是准实时的延迟通常在1秒内但需自行实现幂等消费同一事件可能重复推送。RTDN Webhook处理示例app.route(/google-play-webhook, methods[POST]) def handle_rtdn(): # 1. 验证签名Google提供公钥 signature request.headers.get(X-Goog-Signature) message request.data if not verify_signature(message, signature): return Invalid signature, 401 # 2. 解析事件 event request.json if event[message][attributes][notificationType] SUBSCRIPTION_PURCHASED: # 用户新购订阅 processSubscriptionPurchase(event[message][data]) elif event[message][attributes][notificationType] SUBSCRIPTION_RENEWED: # 自动续订 processSubscriptionRenewed(event[message][data]) elif event[message][attributes][notificationType] SUBSCRIPTION_CANCELED: # 用户取消 processSubscriptionCanceled(event[message][data]) return OK, 200状态同步的核心是维护一个“订阅状态机”其状态包括ACTIVE正常付费中IN_GRACE_PERIOD宽限期用户取消后权益延续至到期日CANCELED已取消且宽限期结束ON_HOLD因支付失败暂停如银行卡过期IN_TRIAL试用期中。客户端只需根据服务端下发的当前状态更新UI例如当状态为IN_GRACE_PERIOD时显示“您的会员将于[日期]到期到期后将停止服务”。4.3 本地缓存与离线体验让支付链路不依赖网络用户在地铁、电梯等弱网环境下触发购买若客户端完全依赖网络体验极差。我们通过三级缓存策略保障离线可用性内存缓存SkuDetails对象在Activity存活期内常驻内存避免重复查询磁盘缓存使用Room数据库持久化Purchase对象字段包括skuId、purchaseToken、purchaseTime、state。当onPurchasesUpdated()收到PURCHASED时立即存入当服务端验证成功后更新state为CONFIRMED。启动时兜底查询App启动时若网络不可用优先从Room读取state CONFIRMED的购买记录直接更新UI若网络恢复则用queryPurchasesAsync()同步最新状态。Room Entity定义Entity(tableName purchase_cache) data class PurchaseCache( PrimaryKey val skuId: String, val purchaseToken: String, val purchaseTime: Long, val state: Int, // 0: PENDING, 1: CONFIRMED, 2: FAILED val lastSyncTime: Long )启动时同步逻辑private fun syncPurchasesOnStart() { lifecycleScope.launch { // 1. 优先读取本地缓存 val confirmedPurchases purchaseDao.getConfirmedPurchases() for (purchase in confirmedPurchases) { updateUiForPurchase(purchase.skuId, true) } // 2. 若网络可用执行远程同步 if (isNetworkAvailable()) { val remotePurchases withContext(Dispatchers.IO) { billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS) .await() .purchasesList } // 合并本地与远程状态解决冲突 mergeAndSync(remotePurchases) } } }实操心得Google Play的queryPurchasesAsync()在弱网下超时时间为30秒但用户等待阈值是3秒。因此我们设置withTimeout(3_000)包裹该调用超时后直接返回本地缓存结果并显示“正在同步最新状态…”的提示。实测该策略将弱网下的用户放弃率从35%降至8%。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查步骤解决方案launchBillingFlow()无响应不弹出支付页BillingClient未连接成功检查onBillingSetupFinished()是否被调用responseCode是否为OK确保startConnection()后等待回调勿在未连接时调用onPurchasesUpdated()从未被触发Activity Context被回收或BillingClient未正确绑定在onDestroy()中检查billingClient.endConnection()是否执行使用WeakReference持有Activity或改用Fragment生命周期绑定查询商品返回空列表SKU ID与后台配置不一致或SkuType类型错误对比Play Console后台的SKU ID和类型检查客户端querySkuDetailsAsync()参数严格按后台配置的SKU ID和类型编写查询代码支付成功但服务端未收到purchaseToken客户端上报网络失败或服务端未正确处理HTTP 5xx查看客户端上报日志检查服务端Nginx access log客户端增加上报重试服务端确保API高可用用户取消订阅后仍能使用服务服务端未监听RTDN或轮询未覆盖取消事件检查RTDN Webhook是否收到SUBSCRIPTION_CANCELED事件或轮询purchases.subscriptions.list结果确保RTDN配置正确轮询任务每小时执行一次5.2 深度排查案例为什么测试环境能过正式环境却收不到回调这是最常被问到的问题。根本原因在于Google Play的沙箱环境与生产环境的差异沙箱环境使用测试账号需在Play Console中配置所有支付均模拟成功purchaseToken为固定字符串如inapp:IAB_TEST_PURCHASE且onPurchasesUpdated()回调几乎瞬时触发。生产环境真实支付流程涉及银行风控、Google风控purchaseToken为加密长字符串且回调存在网络延迟。我们曾遇到一个案例测试时一切正常上线后onPurchasesUpdated()回调丢失率高达40%。排查发现问题出在BillingClient初始化时机——测试时Activity启动快连接总在3秒内完成而生产环境因App体积大、冷启动慢startConnection()后onBillingSetupFinished()平均耗时8秒此时用户已点击购买按钮launchBillingFlow()因isBillingReady false被跳过但无任何提示。解决方案添加连接等待机制在购买按钮点击时若isBillingReady false显示“正在准备支付服务…”并启动倒计时最长10秒超时后提示用户重试优化BillingClient初始化将startConnection()移至Application的onCreate()中利用App启动时间提前连接但需确保BillingClient实例与Activity解耦通过EventBus或LiveData传递状态增加连接健康检查在onResume()中调用billingClient.isReady若为false则主动重连。5.3 避坑经验那些文档不会写的细节调试模式开关Google Play强制要求上线前关闭enablePendingPurchases()的调试日志。在BillingClient.newBuilder()中添加.enablePendingPurchases()即可但切勿在BuildConfig.DEBUG中条件编译因为Play Console审核会扫描所有代码路径。多语言商品描述Play Console中配置商品时必须为每种语言填写描述。若只填英文中文用户看到的将是空白价格栏。我们曾因漏填日文描述导致日本区用户无法购买损失30%订单。退款处理逻辑Google Play的退款操作不会触发onPurchasesUpdated()服务端必须通过purchases.products.get定期查询purchaseState若变为1已取消则主动收回权益。客户端无需特殊处理因queryPurchasesAsync()下次调用会自然过滤掉已取消订单。测试账号限制每个测试账号每月只能进行5次真实支付模拟超出后需更换账号。建议建立测试账号池自动化轮换使用。我在实际项目中踩过的最大坑是忽略了Google Play的“地区限制”策略。某次上线后东南亚用户集体反馈“支付页空白”日志显示BillingClient连接失败。排查发现该地区Google Play服务版本老旧不支持Billing Library v5。最终方案是降级到v4并在onBillingServiceDisconnected()中检测Play服务版本自动切换SDK版本。这个教训让我明白支付SDK的接入本质是与Google Play生态的深度协同而非孤立的技术集成。每一行代码都必须考虑它在真实用户设备上的生存环境。
返回列表