代码 Play Billing 服务端验单与 RTDN:acknowledge 窗口最短只有 1.5 天

2026-09-26 09:01:00

Play Billing 服务端验单与 RTDN:acknowledge 窗口最短只有 1.5 天

本文整理自 Google Play Billing 官方文档,未做真机与沙盒实测,字段名、时间窗口与状态流转以官方文档为准。

官方参考:

  • 订阅生命周期:https://developer.android.com/google/play/billing/lifecycle/subscriptions
  • RTDN 参考:https://developer.android.com/google/play/billing/rtdn-reference
  • 购买生命周期和 RTDN:https://developer.android.com/google/play/billing/lifecycle
  • purchases.subscriptionsv2.get:https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2/get
  • purchases.subscriptions.acknowledge:https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions/acknowledge
  • Codelab 最佳实践:https://codelabs.developers.google.com/maximize-your-play-billing-integration

RTDN 只是通知,不含完整交易信息

实时开发者通知(RTDN)里能拿到的字段只有这些:

version
packageName
eventTimeMillis   // 毫秒时间戳
purchaseToken
notificationType
sku

收到通知后必须再调 Google Play Developer API 拉完整状态,落到自己后端。把 RTDN 当成"状态变更信号"而不是"交易数据",是这一整套集成的前提。eventTimeMillis 是毫秒,别当成秒直接塞进 fromtimestamp。

推送和拉取都走 Cloud Pub/Sub:message.data 是 base64 编码的 DeveloperNotification,message.messageId 是唯一标识,应当用它查重。重复处理不只是逻辑问题,还会白烧 Developer API 配额。投递方式两种:push 到 HTTPS 端点,或者 pull 用客户端库。

DeveloperNotification 里的业务字段是互斥的,同一时刻只会出现其中一个:

  • subscriptionNotification
  • oneTimeProductNotification
  • voidedPurchaseNotification
  • pendingRefundReviewNotification
  • testNotification

acknowledge 是硬性要求,窗口按方案时长折半

新订阅购买后 3 天内未确认,用户会被自动退款,Google Play 撤销该笔购买。这条不做,钱直接退回去。

预付费方案的窗口更紧:

  • 一周或更长的方案:3 天内确认
  • 短于一周的方案:必须在方案时长的一半内确认。3 天的方案只有 1.5 天窗口

Play 结算库提供了 acknowledgePurchase() 和 isAcknowledged(),但建议放在后端处理,客户端断开、重装、进程被杀都会让窗口白白流走。

一次性商品的确认走另外两个端点:消耗型用 purchases.products.consume,非消耗型用 purchases.products.acknowledge,同样提交到后端。

验单接口:subscriptionsv2.get,别再碰 subscriptions.get

订阅的验单事实来源是 purchases.subscriptionsv2.get。purchases.subscriptions.get 已经废弃,只为向后兼容保留,新集成不要用。purchases.subscriptions 端点的其他方法仍然可用。

订阅资源里几个关键字段:

  • subscriptionState
  • acknowledgementState:PENDING / ACKNOWLEDGED
  • lineItems[].productId、lineItems[].expiryTime
  • autoRenewingPlan.autoRenewEnabled
  • offerPhase
  • latestOrderId
  • ExternalAccountIdentifiers:obfuscatedAccountId、obfuscatedProfileId

购买令牌有效期与续订

购买令牌从订阅注册起,到订阅过期后 60 天内有效,超过这个范围就不能再调 Developer API 了。

续订行为:非分期的自动续订会发 SUBSCRIPTION_RENEWED,续订不需要再次确认。

notificationType 常用取值

订阅类:

1   SUBSCRIPTION_RECOVERED
2   SUBSCRIPTION_RENEWED
3   SUBSCRIPTION_CANCELED
4   SUBSCRIPTION_PURCHASED
5   SUBSCRIPTION_ON_HOLD
6   SUBSCRIPTION_IN_GRACE_PERIOD
7   SUBSCRIPTION_RESTARTED
9   SUBSCRIPTION_DEFERRED
10  SUBSCRIPTION_PAUSED
12  SUBSCRIPTION_REVOKED
13  SUBSCRIPTION_EXPIRED
17  SUBSCRIPTION_ITEMS_CHANGED
20  SUBSCRIPTION_PENDING_PURCHASE_CANCELED
22  SUBSCRIPTION_PRICE_STEP_UP_CONSENT_UPDATED

一次性商品只有两个:

1  ONE_TIME_PRODUCT_PURCHASED
2  ONE_TIME_PRODUCT_CANCELED

状态机:宽限期、静默宽限期、账号保留

订阅状态包括 SUBSCRIPTION_STATE_ACTIVE、IN_GRACE_PERIOD、ON_HOLD、CANCELED、EXPIRED。

宽限期(grace period)默认对自动续订基础方案启用,宽限期内用户仍应有权限。

静默宽限期可以设为 0 天,但 Play 至少会等 1 天重试付款。这 24 小时内订阅保持 ACTIVE,期间不发宽限期 RTDN;之后可能收到 ON_HOLD、CANCELED、EXPIRED 或 RENEWED。

账号保留(on hold)默认对基础方案和分期方案启用,时长 = 60 天减宽限期。on hold 期间 queryPurchasesAsync() 不返回订阅,应阻止访问;恢复后原购买令牌不变,会收到 SUBSCRIPTION_RECOVERED;从账号保留恢复后,结算日期变成恢复当天。

状态流转:宽限期结束后未修正付款进入 on hold;on hold 结束仍未修正,发 SUBSCRIPTION_CANCELED,随后立即发 SUBSCRIPTION_EXPIRED。这两条通知挨着到,后端要么幂等处理,要么把 CANCELED 和 EXPIRED 合并成一次终态更新。

29/30/31 日的订阅会顺延到 28 号

订阅日在 29、30、31 日的,在下一个非闰年二月会先变成 2 月 28 日,之后每月 28 号续订。做权益到期计算时不要把"每月同一天"当成不变量。

重新订阅:linkedPurchaseToken 与 outOfAppPurchaseContext

应用外升级等操作会签发新的购买令牌,可能带 linkedPurchaseToken。

如果原订阅已完全过期,则没有 linkedPurchaseToken,此时响应里会有 outOfAppPurchaseContext(仅出现在未确认的重新订阅里),提供 expiredExternalAccountIdentifiers 和 expiredPurchaseToken,用来把新购买关联到正确用户。之后仍需调 purchases.subscriptions.acknowledge 确认,否则同样会被自动退款。

退款与作废

VoidedPurchaseNotification 包含:

  • purchaseToken
  • orderId
  • productType:1 订阅 / 2 一次性
  • refundType:1 全额 / 2 基于数量的部分退款

也可以用 Voided Purchases API 主动拉取。

待审核退款走 PendingRefundReviewNotification,字段是 pendingRefundToken、orderId、refundReason(仅 CHARGEBACK=7)、obfuscatedAccountId / obfuscatedProfileId。这条必须在 24 小时内调用 ReviewRefund 提供建议与使用证据,超时等于放弃申诉机会。

一条完整的处理链

购买令牌全局唯一,可以直接拿来做数据库主键。

正式流程:

  1. 解 base64 拿到 DeveloperNotification
  2. 校验 purchaseToken 未被处理过(配合 message.messageId 查重)
  3. 调 purchases.products.get / purchases.subscriptionsv2.get 向 Google 核实
  4. 授予权益
  5. consume 或 acknowledge

第 2 步和第 3 步之间是幂等和配额的平衡点:查重放在前面能省 API 调用,但状态更新最好以 API 返回为准,别只信通知里那个 notificationType。

复制全文 生成海报 Google Play Billing 内购 RTDN 服务端验单 支付

推荐文章

程序员茄子在线接单