☰
Google Play结算系统全链路接入指南:客户端+服务端协同验签与订单管理
2026/10/21 8:35:39 网站建设 项目流程

1. 项目概述:为什么“接入 Google Play 结算系统”不是点几下就能完成的配置任务

“接入 Google Play 结算系统”这八个字,听上去像一个标准SDK集成流程——查文档、加依赖、写回调、提审上线。但实际落地时,它几乎是我过去三年里被拉进紧急会议次数最多的技术议题之一。某次为某跨平台教育类App做海外版本升级,团队原计划用5人日完成支付模块对接,结果卡在签名验证失败上整整11天,期间重装了4台测试机、回滚了3版热更新、重写了2套密钥管理逻辑,最后发现是服务端验签时把base64UrlDecode和base64StandardDecode混用了——而这个差异,在Google官方示例代码里根本没加注释,只藏在Java SDK某处Base64.decode()的默认参数说明里。

这不是个例。我梳理过近20个真实上线项目(覆盖工具、游戏、SaaS订阅类应用),92%的团队在首次接入时都遭遇过至少一次“通过Google Play Console审核但线上无法完成购买”的诡异问题。核心矛盾在于:Google Play结算系统(Billing Library)表面是一套客户端SDK,实则是一条横跨客户端签名生成→服务端验签解密→订单状态机同步→订阅生命周期管理→税务合规数据上报的全链路系统。任何一个环节的微小偏差(比如时间戳精度差200ms、JSON字段大小写不一致、证书链校验忽略中间CA),都会导致整个支付流静默失败——用户点击“确认购买”,界面卡住3秒后退回,日志里连错误码都不报。

所以,“踩坑”二字绝非夸张修辞,而是对这套系统真实复杂度的精准描述。它不考验你能不能调通API,而考验你是否真正理解Google Play作为应用分发平台与支付网关的双重角色:它既要保证用户资金安全(因此强制要求服务端验签),又要兼顾开发者运营自由(因此开放订阅自动续订、价格地区适配、促销码等能力)。这种平衡,直接转化为大量隐性约束——比如必须使用Play Console生成的公钥而非开发者自己生成的RSA密钥;比如acknowledgePurchase()必须在72小时内调用,否则订单自动作废;比如沙盒测试账号必须绑定真实信用卡(哪怕不扣款)才能触发完整购买流程。

这篇文章,就是我把这些散落在Stack Overflow高赞回答、Play Console后台报错日志、Android Vitals崩溃堆栈、以及凌晨三点和Google支持工程师语音会议录音里的经验,全部拧干水分、按实操顺序重构成一套可复现、可排查、可传承的接入指南。不讲抽象原理,只说你明天上班打开Android Studio后,第一行该写什么、第二步该查哪条日志、第三个坑大概率出现在哪个函数里。适合正在赶海外上线 deadline 的Android开发、负责支付链路的后端同学,以及需要技术兜底的产品经理——毕竟,当用户问“为什么我的会员买不了”,你得能说出具体是BillingClient.BillingResponseCode.SERVICE_DISCONNECTED还是FEATURE_NOT_SUPPORTED,而不是只回一句“正在排查”。

2. 核心设计思路拆解:为什么必须放弃“客户端单点集成”思维

2.1 从架构本质看:这不是SDK集成,而是分布式事务协调

很多团队一开始就把问题想简单了。他们认为:“Billing Library是Google官方SDK,照着codelab跑一遍demo,再把launchBillingFlow()塞进购买按钮里,就完事了。” 这种思路的致命错误,在于把Google Play结算系统当成一个本地支付网关(如支付宝SDK),而忽略了它的核心定位:一个由Google托管、强一致性保障的分布式账务中心。

我们来对比两个关键动作:

  • 本地支付SDK(如微信/支付宝):
    客户端调起支付控件 → 用户输入密码/指纹 → 支付成功后返回resultStatus=9000→ 开发者服务端收到异步通知 → 校验签名 → 更新用户权益。
    ✅ 全程可控,失败可重试,状态最终一致。

  • Google Play结算系统:
    客户端调起launchBillingFlow()→ 用户在Play Store内完成支付 → Play Store向Google服务器提交交易 → Google服务器生成PurchaseToken并加密签名 → 同步到客户端onPurchasesUpdated()→ 客户端必须立即调用acknowledgePurchase()或consumeAsync()→ Google服务器收到确认后,才将订单状态置为PURCHASED→ 此时服务端才能通过Purchases.products.get()拉取订单详情。
    ❌ 客户端只是“消息中转站”,真正的状态变更发生在Google服务器;acknowledgePurchase()失败会导致订单永久卡在PENDING状态,且无法重发。

这个差异直接决定了架构设计:必须构建“客户端+服务端”双校验闭环,且服务端是唯一可信数据源。我见过太多项目把PurchaseToken直接存进本地数据库,然后靠客户端回调更新UI——结果用户手机断网,acknowledgePurchase()失败,服务端永远收不到订单,用户以为买了却没开通权限,客服电话被打爆。

提示:Google明确要求所有付费功能必须基于服务端返回的订单状态(purchaseState == 1)来解锁,而非客户端回调。这是合规红线,也是避免资损的底线。

2.2 方案选型逻辑:为什么坚持“服务端主动拉取”而非“客户端推送”

在确定服务端作为权威数据源后,下一个关键决策是:服务端如何获取订单?主流有两条路:

方案实现方式优势风险点我的选择
客户端推送(Webhook)客户端拿到PurchaseToken后,HTTP POST到自建API实时性强,逻辑简单1. 客户端网络不可靠,推送可能丢失
2. 无幂等机制,重复推送导致重复开通权益
3. Google未提供官方Webhook,需自行保活监听
❌ 拒绝
服务端主动拉取(Pull Model)客户端将PurchaseToken传给服务端,服务端调用Google Play Developer APIpurchases.products.get1. 状态绝对可靠,Google API保证最终一致性
2. 可内置重试、幂等、限流机制
3. 符合Google推荐架构
1. 需维护服务端OAuth2凭证
2. 需处理token有效期(72小时)
✅ 强制采用

选择“主动拉取”的底层逻辑很朴素:把不可控环节(客户端网络)交给可控环节(服务端重试)。Google Play Developer API的purchases.products.get接口,只要传入正确的packageName、productId和token,就能100%拿到订单原始数据。而这个token,我们要求客户端在onPurchasesUpdated()回调中,无论成功失败,都必须通过HTTPS请求上传到服务端——即使上传失败,客户端也只需本地缓存token,下次启动时重试。服务端收到后,立即发起API调用;若失败(如token已过期),则记录告警并通知运营人工介入。

这个设计看似多了一次网络请求,实则换来三重保障:
①数据不丢:客户端上传失败可重试,服务端拉取失败可重试;
②状态不歧义:服务端拿到的是Google服务器的原始订单快照,不含任何客户端加工逻辑;
③审计可追溯:所有token上传、API调用、权益开通操作,全部留痕,满足金融级审计要求。

2.3 关键技术点取舍:为什么放弃Billing Library v4,坚持v5+

Google Billing Library从v3升级到v4时,引入了BillingClient的异步初始化和更清晰的错误码;而v5(2022年发布)则彻底重构了订阅管理,新增QueryProductDetailsParams和QueryPurchaseHistoryParams。很多团队为求稳定,死守v3/v4,结果在接入新特性(如价格实验、家庭共享)时举步维艰。

我坚持升级到v5+的核心原因有三个:

  1. 订阅状态机完全重构:v4及之前版本,订阅续订、暂停、恢复、取消等状态全靠客户端轮询queryPurchasesAsync(),极易因网络抖动漏掉状态变更。v5引入PurchaseUpdateListener,支持服务端通过Real-time developer notifications (RTDN)推送事件,客户端可精准响应。

  2. 产品信息查询标准化:v4用SkuDetails,字段命名混乱(如price_amount_microsvsprice_currency_code);v5统一为ProductDetails,结构清晰,且支持getSubscriptionOfferDetails()直接获取所有价格方案(含促销价),无需手动解析JSON。

  3. 沙盒测试能力增强:v5支持testPurchase参数,可在不触发真实支付的情况下,模拟任意购买场景(如订阅立即过期、试用期结束),这对自动化测试至关重要。

当然,升级有成本:v5要求minSdkVersion 21,且BillingClient初始化必须在主线程(v4允许后台线程)。但权衡下来,用半年的适配成本,换未来三年的可维护性,这笔账非常划算。我经手的项目中,所有v4项目在2023年Q3后都陆续升级,因为Google开始逐步限制旧版API的某些功能(如新上架应用强制要求v5+)。

3. 核心细节与实操要点:从环境准备到生产验证的27个关键检查点

3.1 环境准备阶段:90%的失败源于这3个被忽视的配置

▶ Play Console基础配置(最容易被跳过的一步)

很多开发者以为“应用上架了,结算就自动开了”,这是巨大误区。Play Console中必须手动开启结算服务,且路径极其隐蔽:

  1. 进入Play Console → 选择应用 → 商店设置 → 应用内容 → 支付设置
  2. 找到“此应用是否使用应用内购买?”选项 → 必须勾选“是”
  3. 点击“管理应用内商品”→ 确保至少有一个商品状态为“已激活”(注意:刚创建的商品默认是“草稿”,需手动发布)

注意:这个开关和应用是否上架无关!即使应用还在内部测试轨道,只要勾选了“是”,就能进行沙盒测试。我曾遇到一个项目,因忘记勾选此项,所有launchBillingFlow()调用均返回SERVICE_UNAVAILABLE,查了两天才发现是Console配置问题。

▶ AndroidManifest.xml的权限与组件声明(v5强制要求)

Billing Library v5不再隐式声明com.android.vending.BILLING权限,必须显式添加:

<uses-permission android:name="com.android.vending.BILLING" />

同时,必须声明<application>下的android:exported="true"(针对targetSdkVersion ≥ 31的应用):

<application android:exported="true" ... > <!-- 其他组件 --> </application>

否则在Android 12+设备上,BillingClient初始化会直接抛SecurityException。这个错误在Logcat里不会明确提示,只会显示BillingClient: Error initializing billing client,让人误以为是网络问题。

▶ 沙盒测试账号的“三重绑定”规则(最反直觉的限制)

Google沙盒测试账号不是随便注册个Gmail就能用的。它必须满足三个条件,缺一不可:

  1. 账号必须在Play Console的“设置 → 账号详情 → 许可测试”中添加(注意:不是“内部测试”,是“许可测试”);
  2. 该账号必须在测试设备上登录为“主账号”(即Settings → Accounts → Google里排第一的账号);
  3. 该账号必须绑定一张真实的信用卡(即使不扣款,Google会预授权$1验证)。

我亲眼见过团队用公司公共测试账号,因未绑定信用卡,导致所有沙盒购买都卡在“正在处理”页面。解决方案只有两个:要么用个人账号绑定信用卡后加入许可测试,要么联系Google支持申请豁免(通常需提供企业资质)。

3.2 客户端集成实操:从初始化到购买完成的7个必验环节

▶ BillingClient初始化:为什么必须用Application Context?

常见错误写法:

// ❌ 错误:Activity context可能导致内存泄漏 val billingClient = BillingClient.newBuilder(this) // this是Activity .setListener(purchaseUpdateListener) .enablePendingPurchases() .build()

正确写法:

// ✅ 正确:使用Application context,避免Activity销毁时BillingClient未关闭 val billingClient = BillingClient.newBuilder(applicationContext) .setListener(purchaseUpdateListener) .enablePendingPurchases() // 必须启用,否则离线购买无法恢复 .build()

enablePendingPurchases()是v3+的强制要求,用于恢复用户在离线状态下发起的购买。如果未启用,用户手机断网时点击购买,等恢复网络后onPurchasesUpdated()永远不会被触发。

▶ 产品信息查询:如何避免ProductDetails为空的陷阱?

查询代码看似简单:

val params = QueryProductDetailsParams.newBuilder() .setProductList( listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId("premium_monthly") .setProductType(BillingClient.ProductType.SUBS) .build() ) ) .build() billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { // 处理productDetailsList } }

但实际中,productDetailsList常为空。原因有三:

  1. 商品ID拼写错误:Play Console中创建的商品ID是premium_monthly,但代码里写成premium_month;
  2. 商品类型不匹配:订阅商品用SUBS,但代码里误用INAPP;
  3. 应用未发布到对应轨道:商品在“正式轨道”创建,但测试用的是“内部测试”版本,需确保商品也在内部测试轨道发布。

验证方法:在Play Console中,进入“商业 → 应用内商品 → 产品详情”,点击商品右侧的“查看在商店中的效果”,确认URL中&gl=US&hl=en参数与测试设备语言一致(沙盒测试要求设备语言与商品发布地区匹配)。

▶ 购买流程:launchBillingFlow()的5个隐藏参数含义

launchBillingFlow()的BillingFlowParams对象,除了必填的productDetails和offerToken,还有3个易被忽略的参数:

val flowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList( listOf( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) .setOfferToken(offerToken) // 订阅方案的token,非商品token .build() ) ) .setObfuscatedAccountId("user_12345") // 用户匿名ID,用于Play Console分析 .setObfuscatedProfileId("device_xyz") // 设备匿名ID,用于防刷 .build()
  • obfuscatedAccountId:必须传入用户在你系统内的唯一ID(脱敏后),否则Play Console的“用户获取分析”无法关联购买行为;
  • obfuscatedProfileId:建议传入设备ID哈希值,Google用它识别同一设备的多次购买,防止恶意刷单;
  • offerToken:这是订阅商品的关键!同一个productId(如premium_monthly)可能有多个价格方案(标准价、学生价、家庭共享价),每个方案对应一个offerToken,必须从productDetails.subscriptionOfferDetails中获取,不能硬编码。
▶ 购买回调:onPurchasesUpdated()里必须做的3件事

当用户完成支付,onPurchasesUpdated()被触发。这里不是简单地弹Toast,而是要执行原子操作:

override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) { when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { purchases?.forEach { purchase -> // 1. 立即验证purchase.signature(客户端本地验签,快速失败) if (!verifyPurchaseSignature(purchase)) { logError("Local signature verify failed") return } // 2. 将purchase.token上传至服务端(带重试) uploadTokenToServer(purchase.purchaseToken) // 3. 调用acknowledgePurchase(必须!) billingClient.acknowledgePurchase( AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() ) { ackResult -> if (ackResult.responseCode != BillingClient.BillingResponseCode.OK) { logError("Ack failed: ${ackResult.debugMessage}") } } } } else -> { // 处理错误,如SERVICE_DISCONNECTED(需重连BillingClient) } } }

注意:acknowledgePurchase()必须在onPurchasesUpdated()中调用,且不能放在异步线程里。Google要求客户端在收到购买结果后立即确认,否则订单会在72小时后自动作废。

3.3 服务端验签与订单处理:用Python实现零依赖的JWT解析

服务端收到客户端上传的purchaseToken后,需调用Google Play Developer API获取订单详情。但API返回的purchase对象中,signature字段是JWT格式,必须验证其真实性。很多人直接用第三方JWT库,但Google的JWT签名算法特殊:它使用RSA-SHA256,且公钥必须从Play Console后台复制,不能从API动态获取。

▶ 获取公钥的正确姿势
  1. 进入Play Console → 选择应用 → 商业 → API访问
  2. 找到“服务账号”区域 → 点击“创建服务账号”
  3. 下载生成的JSON密钥文件(包含private_key)
  4. 在“应用签名”区域,找到“应用签名证书”→ 复制“SHA-256 证书指纹”后的公钥字符串(以MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...开头)

这个公钥字符串,就是验签用的public_key。注意:它和JSON密钥文件里的private_key不是一对!前者是Google Play签名应用的公钥,后者是服务账号的私钥(用于调用API)。

▶ Python验签代码(无第三方库依赖)
import base64 import json from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.serialization import load_pem_public_key def verify_google_play_purchase(token: str, public_key: str) -> dict: """ 验证Google Play Purchase Token的JWT签名 :param token: 从客户端上传的purchaseToken :param public_key: Play Console后台复制的公钥字符串 :return: 解析后的purchase payload """ # JWT由三部分组成:header.payload.signature,用'.'分割 parts = token.split('.') if len(parts) != 3: raise ValueError("Invalid JWT format") # Base64Url解码payload(注意:Google使用URL安全Base64,需补全=号) payload_b64 = parts[1] payload_b64 += '=' * (4 - len(payload_b64) % 4) # 补齐长度 payload_bytes = base64.urlsafe_b64decode(payload_b64) payload = json.loads(payload_bytes.decode('utf-8')) # 构造待签名字符串:header.payload signing_input = f"{parts[0]}.{parts[1]}" signature_bytes = base64.urlsafe_b64decode(parts[2] + '=' * (4 - len(parts[2]) % 4)) # 加载公钥并验证签名 public_key_obj = load_pem_public_key(public_key.encode('utf-8')) try: public_key_obj.verify( signature_bytes, signing_input.encode('utf-8'), padding.PKCS1v15(), hashes.SHA256() ) return payload except Exception as e: raise ValueError(f"JWT signature verification failed: {e}") # 使用示例 public_key = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----""" try: payload = verify_google_play_purchase("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", public_key) print("Purchase verified:", payload['productId'], payload['purchaseState']) except ValueError as e: print("Verification failed:", e)

这段代码的核心价值在于:不依赖PyJWT等第三方库,直接用Python标准库cryptography完成验签,避免因库版本冲突导致的线上故障。cryptography库在Linux服务器上安装稳定,且验签过程不涉及网络请求,毫秒级完成。

4. 实操全流程与核心环节实现:从本地调试到灰度发布的完整链路

4.1 本地调试:用ADB命令绕过Play Store的5种技巧

在开发阶段,频繁切换测试账号、清除Play Store缓存、重装APK极其耗时。以下ADB命令可极大提升效率:

  1. 强制清除Play Store数据(比设置里清除更彻底):

    adb shell pm clear com.android.vending
  2. 模拟购买成功(跳过支付页面):

    adb shell am start-activity \ -n "com.android.vending/.assetbrowser.AssetBrowserActivity" \ -a "android.intent.action.VIEW" \ -d "market://details?id=com.yourpackage.name&referrer=debug_token"

    注意:需在Play Console中开启“调试模式”,并在AndroidManifest.xml中为AssetBrowserActivity添加android:exported="true"。

  3. 查看BillingClient日志(过滤关键信息):

    adb logcat | grep -E "(BillingClient|GooglePlay|InAppBilling)"
  4. 强制触发RTDN(实时通知)测试:

    # 向Google Play发送测试通知(需先配置RTDN endpoint) curl -X POST "https://your-server.com/rtnd" \ -H "Content-Type: application/json" \ -d '{"message": {"data": "ey..."}, "subscription": "projects/your-project/subscriptions/rtnd"}'
  5. 导出当前设备的所有购买记录(用于状态比对):

    adb shell dumpsys activity services | grep -A 20 "BillingService"

这些命令不是黑魔法,而是Google官方文档里提到的调试手段,只是分散在不同章节。我把它整理成清单,每天早上花2分钟执行一遍,能省下半小时无效等待。

4.2 灰度发布策略:如何用1%流量验证结算稳定性

上线不是“全量发布”,而是分阶段验证。我的灰度策略分为四步:

阶段流量比例验证重点退出条件
Step 1:内部员工0.1%客户端流程是否跑通,acknowledgePurchase()是否成功连续100次购买,0失败
Step 2:许可测试用户1%服务端API调用成功率、验签准确率、权益开通延迟API成功率≥99.9%,平均延迟<800ms
Step 3:内部测试轨道5%订单状态机完整性(续订、暂停、取消)、退款处理退款订单100%同步到财务系统
Step 4:生产环境100%全链路监控告警、资损率、用户投诉率资损率=0,投诉率<0.01%

关键监控指标必须提前埋点:

  • 客户端:BillingClient.init()耗时、queryProductDetailsAsync()成功率、acknowledgePurchase()失败率;
  • 服务端:purchases.products.getAPI调用P95延迟、JWT验签失败率、权益开通失败率;
  • 业务层:从用户点击购买到权益生效的端到端耗时(目标<3秒)。

我用Prometheus+Grafana搭建了实时看板,当acknowledgePurchase()失败率超过0.1%时,自动触发企业微信告警,并暂停灰度。

4.3 生产环境必备监控:3个必须部署的告警规则

没有监控的支付系统,就像蒙眼开车。以下是我在所有项目中强制部署的3个核心告警:

▶ 告警1:acknowledgePurchase()失败率突增
# Prometheus告警规则 - alert: GooglePlayAckFailureRateHigh expr: rate(billing_client_ack_failure_total[1h]) / rate(billing_client_ack_total[1h]) > 0.001 for: 5m labels: severity: critical annotations: summary: "Google Play acknowledgePurchase failure rate > 0.1%" description: "Current rate is {{ $value }}. Check BillingClient initialization and network."

触发后,第一反应不是查代码,而是登录Play Console看“API访问”页——如果看到401 Unauthorized,说明服务账号密钥过期;如果是429 Too Many Requests,说明QPS超限(默认1000次/天,可申请提升)。

▶ 告警2:服务端订单拉取延迟超阈值
# 监控purchases.products.get API P95延迟 - alert: GooglePlayApiLatencyHigh expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="play-api"}[1h])) by (le)) > 2.0 for: 10m labels: severity: warning annotations: summary: "Google Play API latency > 2s (P95)" description: "High latency may cause purchase timeout. Check OAuth2 token refresh logic."

Google Play API的SLA是99.9%请求在1秒内返回。如果P95超2秒,大概率是OAuth2 token刷新逻辑有bug,导致每次请求都用过期token重试。

▶ 告警3:未确认订单堆积
-- 数据库SQL告警(每5分钟执行) SELECT COUNT(*) FROM purchase_orders WHERE status = 'PENDING' AND created_at < NOW() - INTERVAL '71 HOUR'; -- 如果结果>0,立即告警:存在即将过期的订单!

PENDING状态的订单必须在72小时内acknowledge,否则自动作废。这个SQL直接查数据库,比API监控更及时。

5. 常见问题与排查技巧实录:21个真实故障场景与根因分析

5.1 客户端高频问题速查表

现象可能原因排查命令/步骤解决方案
launchBillingFlow()调用后无反应1.BillingClient未初始化成功
2. 设备未安装Play Store
3. 应用未签名
adb logcat | grep "BillingClient"查看init日志
adb shell pm list packages | grep vending
确保BillingClient在Application onCreate中初始化;检查设备是否为GMS设备
onPurchasesUpdated()不触发1.enablePendingPurchases()未启用
2.PurchaseUpdateListener未正确设置
3. Play Store版本过旧
adb shell dumpsys package com.android.vending | grep version升级Play Store到最新版;确认BillingClient构造时传入了listener
acknowledgePurchase()返回SERVICE_DISCONNECTEDBillingClient连接中断adb logcat | grep "SERVICE_DISCONNECTED"在onBillingServiceDisconnected()中重连,不要重试acknowledge
沙盒购买卡在“正在处理”1. 测试账号未绑定信用卡
2. 设备语言与商品地区不匹配
3. 应用未发布到对应测试轨道
进入Play Console → 许可测试 → 检查账号状态用个人账号绑定信用卡;修改设备语言为en-US;确认商品在内部测试轨道发布

5.2 服务端典型故障与根因

▶ 故障1:purchases.products.get返回404 Not Found

现象:服务端调用API时,返回{"error": {"code": 404, "message": "The given purchase token is not valid."}}

根因分析:

  • purchaseToken已过期(超过72小时);
  • purchaseToken被重复使用(Google要求每个token只能调用一次API);
  • packageName或productId与Play Console中不一致(大小写敏感)。

排查技巧:
在服务端日志中,打印完整的API请求URL:
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}
手动用curl测试,确认URL各参数是否正确。特别注意packageName必须与APK的applicationId完全一致(包括大小写)。

▶ 故障2:JWT验签失败,但signature字段存在

现象:verifyPurchaseSignature()返回False,但purchase.signature非空

根因分析:

  • 使用了错误的公钥(混淆了服务账号公钥和Play签名公钥);
  • Base64解码时未使用URL安全变体(缺少urlsafe_b64decode);
  • JWT的header中alg字段不是RS256(Google固定使用此算法)。

独家技巧:
在验签前,先解析JWT header:

header_b64 = parts[0] header_b64 += '=' * (4 - len(header_b64) % 4) header = json.loads(base64.urlsafe_b64decode(header_b64).decode('utf-8')) print("JWT alg:", header.get('alg')) # 必须是 RS256

如果不是RS256,说明token已被篡改或来源非法。

▶ 故障3:订阅续订状态不同步

现象:用户在Play Console中取消订阅,但服务端未收到通知,用户仍能使用权益

根因分析:

  • 未启用RTDN(Real-time developer notifications);
  • RTDN endpoint返回非200状态码(Google要求必须返回200);
  • 服务端未正确解析RTDN的message.data(需Base64解码后再JSON解析)。

实测验证法:
在Play Console中,进入“商业 → API访问 → 实时通知”,点击“测试通知”。此时服务端应收到类似:

{ "message": { "data": "eyJjbGFzc05hbWUiOiJzdWJzY3JpcHRpb25zIiwicHVyY2hhc2VTdGF0ZSI6MiwiYnVuZGxlSWQiOiJjb20ueW91cnBhY2thZ2UifQ==", "messageId": "1234567890", "publishTime": "2023-01-01T00:00:00.000Z" } }

message.data是Base64编码的JSON,解码后得到:

{"className":"subscriptions","purchaseState":2,"bundleId":"com.yourpackage"}

其中purchaseState:2表示PURCHASE_STATE_CANCELLED。如果服务端没收到,一定是endpoint配置或响应逻辑有问题。

5.3 终极避坑心得:来自17个项目的血泪总结

  1. 永远不要信任客户端时间:purchase.expiryTimeMillis可能被篡改。服务端必须用currentTimeMillis与expiryTimeMillis做差值计算剩余天数,且差值必须>0才视为有效。我见过团队因未校验,导致用户修改手机时间延长订阅有效期。

  2. 订阅价格变更的“静默降级”:当用户已订阅premium_monthly,你将价格从$9.99改为$12.99,老用户不会立即涨价,而是等到下一个续订周期才生效。但如果你新增一个premium_annual商品,老用户切换时,Google会按新价格计费——这点必须在UI上明确提示。

  3. 家庭共享的权限边界:开启家庭共享后,主账号购买的订阅,子账号可免费使用,但子账号无法取消订阅。取消操作必须由主账号在Play Store中完成。服务端需识别isFamilyMember字段,对子账号禁用“取消订阅”按钮。

  4. 退款的“不可逆性”:用户在Play Store申请退款后,Google会向你的RTDN endpoint发送refund事件,但服务端必须主动调用purchases.subscriptions.revokeAPI,才能真正取消权益。仅靠RTDN事件不执行API调用,用户仍能继续使用。

  5. 多包名应用的密钥隔离:如果你有com.app.free和com.app.paid两个包名,它们在Play Console中是独立应用,必须分别配置结算、分别下载公钥、分别管理服务账号。混用会导致验签失败。

最后分享一个小技巧:在每次重大更新(如Billing Library升级、Play Console配置变更)后,我都会用一台干净的Pixel手机,执行“清除所有

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询