PAYATHON 2026

Android Developer Notification 如何更新自动续订状态?

数据阿明

结论

Real-time developer notifications(RTDN)会在 subscriptionNotification.purchaseToken 中返回订阅的 purchase token。

后端收到通知后,应按以下流程处理:

  1. 从 Pub/Sub 消息中解码 RTDN 数据。
  2. 读取 packageName 和 subscriptionNotification.purchaseToken。
  3. 使用该 token 调用 Google Play Developer API。
  4. 根据 API 返回的订阅状态、到期时间和自动续订设置,更新后端 entitlement。
  5. 使用后端保存的 token 查找对应用户,因为 RTDN 通常不包含业务系统的用户 ID。

不要只依赖在订阅周期结束时运行一次 cron job。RTDN 适合处理实时状态变化,同时还应保留定时对账任务,以补救消息延迟、配置错误和处理失败等情况。

RTDN 中是否包含 purchase token

订阅通知解码后,结构通常如下:

{
  "version": "1.0",
  "packageName": "com.example.app",
  "eventTimeMillis": "1720000000000",
  "subscriptionNotification": {
    "version": "1.0",
    "notificationType": 2,
    "purchaseToken": "example-purchase-token"
  }
}

purchase token 位于:

subscriptionNotification.purchaseToken

Pub/Sub Push 请求的外层还有一层消息封装,其中 message.data 是经过 Base64 编码的数据:

{
  "message": {
    "data": "eyJ2ZXJzaW9uIjoiMS4wIiwiLi4uIjoiLi4uIn0=",
    "messageId": "1234567890",
    "publishTime": "2026-09-14T10:00:00Z"
  },
  "subscription": "projects/example/subscriptions/google-play-rtdn"
}

所以,不能直接从 HTTP 请求顶层读取 purchaseToken,必须先解码 message.data。

推荐的状态更新流程

1. 首次购买时保存关联关系

App 完成购买后,应将 purchase token 发送到后端。后端不能直接相信客户端提交的商品、到期时间或购买状态,而要立即调用 Google Play Developer API 进行验证。

至少保存这些信息:

user_id
package_name
purchase_token
subscription_product_id
base_plan_id
subscription_state
expiry_time
auto_renew_enabled
entitlement_status
last_verified_at

purchase token 是关联 RTDN 和业务用户的主要字段。由于 RTDN 不包含业务系统的 user_id,后端需要通过 token 查找用户。

如果业务允许,还可以在创建购买流程时设置经过哈希或随机化处理的账户标识,例如 obfuscatedAccountId。不要在这个字段中直接填写邮箱、手机号等明文个人信息。

2. 收到 RTDN 后解码通知

下面是一个简化的 Node.js 处理示例:

import {google} from "googleapis";

const PACKAGE_NAME = "com.example.app";

export async function handleGooglePlayNotification(req, res) {
  try {
    const pubsubMessage = req.body?.message;
    if (!pubsubMessage?.data) {
      return res.status(400).send("Missing Pub/Sub message data");
    }

    const notification = JSON.parse(
      Buffer.from(pubsubMessage.data, "base64").toString("utf8")
    );

    if (notification.packageName !== PACKAGE_NAME) {
      return res.status(400).send("Unexpected package name");
    }

    const subscriptionNotification =
      notification.subscriptionNotification;

    if (!subscriptionNotification?.purchaseToken) {
      // RTDN 还可能是 oneTimeProductNotification、
      // voidedPurchaseNotification 或 testNotification。
      return res.status(204).send();
    }

    const purchaseToken = subscriptionNotification.purchaseToken;

    await refreshSubscriptionFromGoogle({
      packageName: notification.packageName,
      purchaseToken
    });

    return res.status(204).send();
  } catch (error) {
    console.error("Failed to process Google Play notification", error);

    // 返回非 2xx,使 Pub/Sub 按其重试策略重新投递。
    return res.status(500).send("Processing failed");
  }
}

生产环境还应使用 messageId 做幂等处理,因为 Pub/Sub 可能会重复投递同一条消息。

3. 使用 purchase token 查询完整状态

新实现应优先使用 purchases.subscriptionsv2.get:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}

Node.js 示例:

import {google} from "googleapis";

async function refreshSubscriptionFromGoogle({
  packageName,
  purchaseToken
}) {
  const auth = new google.auth.GoogleAuth({
    scopes: ["https://www.googleapis.com/auth/androidpublisher"]
  });

  const androidPublisher = google.androidpublisher({
    version: "v3",
    auth
  });

  const response =
    await androidPublisher.purchases.subscriptionsv2.get({
      packageName,
      token: purchaseToken
    });

  const subscription = response.data;
  const lineItem = subscription.lineItems?.[0];

  const subscriptionState = subscription.subscriptionState;
  const expiryTime = lineItem?.expiryTime ?? null;
  const autoRenewEnabled =
    lineItem?.autoRenewingPlan?.autoRenewEnabled ?? false;

  const entitledStates = new Set([
    "SUBSCRIPTION_STATE_ACTIVE",
    "SUBSCRIPTION_STATE_IN_GRACE_PERIOD"
  ]);

  const entitlementStatus = entitledStates.has(subscriptionState)
    ? "ACTIVE"
    : "INACTIVE";

  await updateSubscriptionInDatabase({
    purchaseToken,
    productId: lineItem?.productId ?? null,
    subscriptionState,
    expiryTime,
    autoRenewEnabled,
    entitlementStatus,
    rawGoogleResponse: subscription
  });
}

这里的 entitlement 判断只是一个基本示例。实际业务还需要明确宽限期内是否允许访问、是否支持账号保留和暂停订阅,以及订阅到期后的访问规则。

旧版 purchases.subscriptions.get 通常需要以下参数:

packageName
subscriptionId
token

如果仍在使用这个接口,正常自动续订通常会继续使用该订阅对应的 purchase token。不过,升级、降级、重新订阅或订阅替换可能产生新的 token,并通过 linkedPurchaseToken 建立关联。不要假定一个用户始终只有一个固定 token。新系统应优先使用 purchases.subscriptionsv2.get,并用 RTDN 实际返回的 token 发起查询。

自动续订时是否需要 cron job

不需要在每月或每年到期的准确时刻,为每笔订阅单独创建 cron job。正常流程应由事件驱动:

首次购买
    ↓
后端验证并保存 purchase token
    ↓
Google Play 发生续订、取消、暂停、宽限期或到期等变化
    ↓
RTDN 发送到 Pub/Sub
    ↓
后端使用通知中的 purchase token 查询 Developer API
    ↓
更新 subscription state、expiry time 和 entitlement

但 RTDN 不是唯一的数据保障。建议增加定时对账任务,定期检查近期即将到期、长时间未验证状态或曾经处理失败的订阅。这个任务用于修复遗漏,不能代替 RTDN。

执行定时对账时,应使用数据库中保存的有效 purchase token 查询 Google Play。如果订阅发生过替换,还要正确维护新旧 token 的关联,避免一直查询已经被替代的旧 token。

不要只根据 notificationType 更新权限

notificationType 表示触发通知的事件,例如续订、取消、进入宽限期或到期,但不能直接代表用户当前的最终权限状态。

例如,“用户取消订阅”通常表示关闭下一周期的自动续订,不一定会让当前周期立即失效。因此,收到取消通知后不能直接撤销访问权限。后端仍然需要调用 purchases.subscriptionsv2.get,读取:

subscriptionState
lineItems[].expiryTime
lineItems[].autoRenewingPlan.autoRenewEnabled

通常需要分别保存两个概念:

  • autoRenewEnabled:当前是否计划继续自动续订。
  • entitlementStatus:用户目前是否有权访问订阅内容。

用户关闭自动续订后,前者可能是 false,但在 expiryTime 之前,后者仍可能有效。

实现时的注意事项

  • Pub/Sub 可能重复投递消息,也可能乱序到达,因此处理逻辑必须幂等。
  • 不要只根据 RTDN 的内容授予或撤销权限,应以 Google Play Developer API 的查询结果为准。
  • 校验通知中的 packageName,并保护 Pub/Sub Push endpoint,防止任意请求触发状态更新。
  • 调用 Developer API 的服务账号需要正确配置 Google Play Console 权限和 androidpublisher OAuth scope。
  • RTDN 可能包含测试通知或其他购买类型,不能假定每条消息都有 subscriptionNotification。
  • 数据库应允许同一用户保存多个 token,并正确处理升级、降级、重新订阅和订阅替换。
  • API 查询失败时,不要立即将订阅设为失效。应记录失败并安排重试。
  • entitlement 规则需要覆盖 active、grace period、on hold、paused、expired、canceled 等业务场景。
  • 月订阅和年订阅可以使用相同的 RTDN 与后端处理流程。周期差异应由 Google Play 返回的商品、base plan 和到期时间决定。

备注:内容仅供参考。