Android应用内订阅:支付成功后queryPurchasesAsync未返回订阅详情
Android内购订阅异常(Paytm/UPI支付场景)解决方案
针对Paytm或UPI支付成功后,billingClient.queryPurchasesAsync无法返回订阅结果,但清除Play Store缓存后恢复正常的问题,这是印度地区UPI类支付场景下常见的同步延迟问题,以下是经过验证的解决思路和代码优化方案:
核心原因分析
- UPI支付的异步特性:不同于测试卡的即时同步,UPI/Paytm支付需要银行端确认后才会向Play Store发送支付成功通知,这个过程可能存在延迟,导致Play Store本地缓存未及时更新,而
queryPurchasesAsync仅读取本地缓存数据,因此无结果返回。 - Play Store本地缓存一致性问题:部分设备上Play Store的订阅缓存未及时与服务器同步,清除缓存后会强制拉取最新服务器数据,所以能正常识别订阅状态。
具体解决/规避方案
1. 补充查询购买历史(queryPurchaseHistoryAsync)
queryPurchasesAsync仅返回当前有效的订阅,而queryPurchaseHistoryAsync会返回用户最近的购买记录(包括已过期或pending状态的记录),可以在现有查询逻辑中补充该查询,覆盖延迟同步的场景:
private void queryEbPurchases() { if (billingClient == null || !billingClient.isReady()) return; // 查询当前有效订阅 billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build(), (billingResult, purchases) -> { if (isDestroyed() || isFinishing()) return; runOnUiThread(() -> { if (isDestroyed() || isFinishing()) return; if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { if (purchases.size() > 0) { for (Purchase purchase : purchases) { handlePurchase(purchase); } } else { // 当前无有效订阅, fallback 到购买历史查询 queryPurchaseHistory(); } } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ERROR) { showToast(R.string.query_purchase_error); queryPurchaseHistory(); } else { showToast(getString(R.string.billing_error, billingResult.getResponseCode())); } }); }); } private void queryPurchaseHistory() { if (billingClient == null || !billingClient.isReady()) return; billingClient.queryPurchaseHistoryAsync(QueryPurchaseHistoryParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) .build(), (billingResult, purchaseHistoryRecords) -> { runOnUiThread(() -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchaseHistoryRecords.size() > 0) { // 取最近的购买记录触发后端验证 PurchaseHistoryRecord latestRecord = purchaseHistoryRecords.get(0); verifyPurchase(latestRecord.getPurchaseToken(), ebPremiumProductId); // 延迟5秒后再次查询当前订阅,等待缓存同步 new Handler(Looper.getMainLooper()).postDelayed(this::queryEbPurchases, 5000); } else { subsBtn.setText(R.string.subscribe); subsBtn.setEnabled(true); verifyPurchase("null", ebPremiumProductId);//移除过期订阅标记 } }); }); }
2. 实现Play实时开发者通知(RTDN)
这是最可靠的解决方案:当用户支付成功后,Play会主动向你的后端服务器发送状态变更通知,后端直接通过Google Play Developer API验证token有效性,无需依赖客户端查询。
- 操作:在Play控制台配置后端通知URL,当支付状态(从pending到purchased)变更时,Play会发送包含购买token的POST请求,后端验证后直接给用户授予权益。
- 优势:完全不受客户端缓存影响,即使客户端查询失败,后端也能主动感知支付成功。
3. 优化Pending状态的重试逻辑
UPI支付经常会出现较长时间的Pending状态,需要延长重试间隔并增加重试次数,避免频繁查询:
private int retryCount = 0; private static final int MAX_RETRY = 5; private void showPendingPurchase() { if (isFinishing() || isDestroyed()) return; subsBtn.setText(R.string.purchase_pending); subsBtn.setEnabled(true); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.purchase_pending); builder.setMessage(R.string.pending_purchase_msg); builder.setPositiveButton(R.string.ok, null); builder.setNegativeButton(R.string.retry, (dialog, which) -> { if (retryCount < MAX_RETRY) { retryCount++; // 渐进式延迟:10s、20s、30s... long delay = retryCount * 10000; new Handler(Looper.getMainLooper()).postDelayed(this::queryEbPurchases, delay); } else { showToast(R.string.retry_limit_reached); showSubscriptionCheckGuide(); } }); builder.show(); } private void showSubscriptionCheckGuide() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.subscription_check_guide); builder.setMessage("请打开Play Store应用,进入「订阅」页面确认状态,之后返回本应用即可同步权益"); builder.setPositiveButton(R.string.ok, null); builder.show(); }
4. 临时用户引导方案
对于已经出现问题的用户,可以在应用内提供清晰的引导:
- 告知用户前往Play Store的「我的订阅」页面确认状态
- 作为临时方案,引导用户清除Play Store缓存(仅用于应急,不推荐作为常规流程)
代码优化建议
- 移除
onResume中频繁调用queryEbPurchases的逻辑,改为仅在支付流程结束、应用从后台切回前台时触发一次查询,避免不必要的网络请求。 - 在
handlePurchase中,对于Pending状态的订阅,除了弹窗提示,还可以启动后台定时任务自动重试查询,无需用户手动触发。
内容的提问来源于stack exchange,提问作者Sujith S Manjavana
相关产品推荐
相关产品推荐

