收到 Play RTDN 就当开通是错的:三天不 acknowledge 会自动退款,状态必须回查 subscriptionsv2
事实来源:Android Developers 的 RTDN reference / Subscription lifecycle / Fight fraud and abuse,以及 purchases.subscriptionsv2 REST 参考。
- https://developer.android.com/google/play/billing/rtdn-reference
- https://developer.android.com/google/play/billing/lifecycle/subscriptions
- https://developer.android.com/google/play/billing/security
- https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2
典型场景:安卓订阅接完 Play Billing,客户端拿到 purchaseToken 就调后端开会员。上线后遇到三类问题:
- 用户付款成功但会员没开——后端漏处理 RTDN,客户端又丢了结果。
- 用户会员开了三天后被收回、钱退了,用户以为被坑。
- 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 数值与真实语义
- SUBSCRIPTION_RECOVERED 从 account hold 恢复
- SUBSCRIPTION_RENEWED 续费
- SUBSCRIPTION_CANCELED 用户或系统取消(关键:未到期前仍有权限)
- SUBSCRIPTION_PURCHASED 新购
- SUBSCRIPTION_ON_HOLD 进入 account hold(此时 expiryTime 已被设为过去时间,但它不是 EXPIRED)
- SUBSCRIPTION_IN_GRACE_PERIOD 宽限期(仍有权限)
- SUBSCRIPTION_RESTARTED 用户从 Play 订阅页恢复
- SUBSCRIPTION_DEFERRED 续期时间被推后
- SUBSCRIPTION_PAUSED 暂停
- SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED
- SUBSCRIPTION_REVOKED 提前撤销(退款或违规),立即收回权限
- SUBSCRIPTION_EXPIRED 过期
- SUBSCRIPTION_ITEMS_CHANGED 套餐项变了
- SUBSCRIPTION_CANCELLATION_SCHEDULED 分期/合约计划取消已排期
- SUBSCRIPTION_PRICE_CHANGE_UPDATED
- SUBSCRIPTION_PENDING_PURCHASE_CANCELED 待付款交易被取消
- 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 窗口与预付费规则以官方文档当前版本为准。