编程 Google Play 订阅 RTDN 只是通知:三天不 acknowledge 自动退款,权限要按 subscriptionState 给

2026-11-10 09:01:21

收到 Play RTDN 就当开通是错的:三天不 acknowledge 会自动退款,状态必须回查 subscriptionsv2

事实来源:Android Developers 的 RTDN reference / Subscription lifecycle / Fight fraud and abuse,以及 purchases.subscriptionsv2 REST 参考。

典型场景:安卓订阅接完 Play Billing,客户端拿到 purchaseToken 就调后端开会员。上线后遇到三类问题:

  1. 用户付款成功但会员没开——后端漏处理 RTDN,客户端又丢了结果。
  2. 用户会员开了三天后被收回、钱退了,用户以为被坑。
  3. iOS 一切正常,安卓续费月份对不上。

一、RTDN 只说明「某个东西变了」,不携带状态

RTDN 走 Google Cloud Pub/Sub,push 到你的 HTTPS 端点。message.data 是 base64 的 JSON,解开只有 version、packageName、eventTimeMillis,以及 subscriptionNotification / oneTimeProductNotification / voidedPurchaseNotification / pendingRefundReviewNotification / testNotification 之一。

订阅通知本身只有三个字段:

{"version":"1.0","notificationType":4,"purchaseToken":"xxx"}

没有商品 ID,没有到期时间,没有金额。官方原话是必须先调 Google Play Developer API 才能拿到完整状态,通知只说明购买状态变了。

所以处理顺序是:收到通知后,先保存原始消息,再用 purchaseToken 调 purchases.subscriptionsv2.get,根据返回的 subscriptionState / lineItems 写入数据库,然后才决定是否授予权限。不要把 notificationType 直接当成业务状态。

二、notificationType 数值与真实语义

  1. SUBSCRIPTION_RECOVERED 从 account hold 恢复
  2. SUBSCRIPTION_RENEWED 续费
  3. SUBSCRIPTION_CANCELED 用户或系统取消(关键:未到期前仍有权限)
  4. SUBSCRIPTION_PURCHASED 新购
  5. SUBSCRIPTION_ON_HOLD 进入 account hold(此时 expiryTime 已被设为过去时间,但它不是 EXPIRED)
  6. SUBSCRIPTION_IN_GRACE_PERIOD 宽限期(仍有权限)
  7. SUBSCRIPTION_RESTARTED 用户从 Play 订阅页恢复
  8. SUBSCRIPTION_DEFERRED 续期时间被推后
  9. SUBSCRIPTION_PAUSED 暂停
  10. SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED
  11. SUBSCRIPTION_REVOKED 提前撤销(退款或违规),立即收回权限
  12. SUBSCRIPTION_EXPIRED 过期
  13. SUBSCRIPTION_ITEMS_CHANGED 套餐项变了
  14. SUBSCRIPTION_CANCELLATION_SCHEDULED 分期/合约计划取消已排期
  15. SUBSCRIPTION_PRICE_CHANGE_UPDATED
  16. SUBSCRIPTION_PENDING_PURCHASE_CANCELED 待付款交易被取消
  17. SUBSCRIPTION_PRICE_STEP_UP_CONSENT_UPDATED(仅部分地区要求价格上调的二次同意)

8(SUBSCRIPTION_PRICE_CHANGE_CONFIRMED)已废弃。

两个常见错误:收到 3(CANCELED)就关权限——取消只是自动续费关了,expiryTime 之前用户还有权;收到 5(ON_HOLD)看到 expiryTime 在过去就判过期——这会把「催用户更新支付方式」的窗口直接跳过。

权限判定只看 subscriptionsv2.get 返回的 subscriptionState:

  • ACTIVE:给
  • IN_GRACE_PERIOD:给(账在催,别断)
  • ON_HOLD:不给,但保留数据并提示更新支付方式
  • PAUSED:不给
  • CANCELED:给到 expiryTime
  • EXPIRED:不给
  • PENDING:不给(签约时等待付款)
  • PENDING_PURCHASE_CANCELED:不给,且要用 linkedPurchaseToken 回查原订阅当前状态

三、三天不 acknowledge:自动退款 + 撤销

新订阅必须在 3 天内 acknowledge,否则 Google 自动退款并撤销该购买。这一步必须在服务端做(Purchases.subscriptions.acknowledge),不能只依赖客户端的 acknowledgePurchase():用户付完款断网,或者买完不再打开 App,客户端那一次永远补不上。

预付费计划窗口更短:时长 ≥1 周是 3 天;,校验 iss(accounts.google.com)、aud(等于你配置的 audience,通常就是端点 URL)、exp,并用 Google 公钥验签。跳过这步,任何知道 URL 的人都能伪造一条「续费成功」。 2. 返回 2xx 才算消费成功;非 2xx(含 5xx、超时)会被重投,可能重复,也可能乱序。用 messageId 去重,写入数据库前先插入唯一键。 3. 不要按 eventTimeMillis 覆盖状态:重投和并发会让旧事件后到。以 subscriptionsv2.get` 的实时快照为权威,通知只当触发器;如果一定要按事件写,比较 eventTimeMillis,只接受更新的。
4. 端点响应要快:先保存原始消息再异步处理,避免 Pub/Sub 超时重投放大流量。

五、purchaseToken 与错误码

purchaseToken 全局唯一,可以直接当主键,工单系统里也拿它对账。有效期:从签约起,到订阅过期后 60 天;之后查接口返回 410 subscriptionNoLongerAvailable。用户账号被删除或购买记录不存在是 410 purchaseTokenNoLongerValid。

常见返回:

  • 400 invalidValue:token 格式错、被截断(复制粘贴丢字符)
  • 400 purchaseTokenMismatch:token 与 packageName / productId 对不上(多包名、多渠道包最容易)
  • 400 invalidPurchaseState:对已 consume 的购买再 consume,或对非活跃订阅做取消
  • 400 productNotOwnedByUser:token 有效但用户已不拥有(已退款/撤销/过期)
  • 403 userInsufficientPermission:服务账号没在 Play Console 授权,或缺财务/订单权限
  • 409 concurrentUpdate:同一对象并发修改,指数退避重试
  • 5xx:重试

服务端鉴权用 service account JSON + scope https://www.googleapis.com/auth/androidpublisher;JSON 放环境变量或 KMS,不要进 git。

六、用户关联与测试的两个尾巴

关联用户靠 BillingFlowParams.setObfuscatedAccountId / setObfuscatedProfileId,返回值在 externalAccountIdentifiers.obfuscatedAccountId。不要往里塞明文邮箱或手机号:官方明确说存 PII 会导致购买被拦,用哈希或加密后的 ID(限 64 字符)。

升级、降级、重新签约时,linkedPurchaseToken 指向旧 token,记得把旧 token 的权限收掉,否则一个用户可以靠旧订阅多拿一份权益。

测试购买(license tester)会在返回里带 testPurchase 字段;把它算进收入或活跃订阅会让数据长期偏差,报表要排除。

通知能丢、能重、能乱序,这是设计前提,不是异常。把「以 subscriptionsv2.get 的状态为准」和「三天内 ack」两件事做对,剩下的都是细节。

本文根据 Google 官方文档整理,没有在生产环境跑过完整生命周期;acknowledge 窗口与预付费规则以官方文档当前版本为准。

推荐文章

程序员茄子在线接单