Android Developer Notification 如何更新自动续订状态?
结论
Real-time developer notifications(RTDN)会在 subscriptionNotification.purchaseToken 中返回订阅的 purchase token。
后端收到通知后,应按以下流程处理:
- 从 Pub/Sub 消息中解码 RTDN 数据。
- 读取
packageName和subscriptionNotification.purchaseToken。 - 使用该 token 调用 Google Play Developer API。
- 根据 API 返回的订阅状态、到期时间和自动续订设置,更新后端 entitlement。
- 使用后端保存的 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 权限和
androidpublisherOAuth scope。 - RTDN 可能包含测试通知或其他购买类型,不能假定每条消息都有
subscriptionNotification。 - 数据库应允许同一用户保存多个 token,并正确处理升级、降级、重新订阅和订阅替换。
- API 查询失败时,不要立即将订阅设为失效。应记录失败并安排重试。
- entitlement 规则需要覆盖 active、grace period、on hold、paused、expired、canceled 等业务场景。
- 月订阅和年订阅可以使用相同的 RTDN 与后端处理流程。周期差异应由 Google Play 返回的商品、base plan 和到期时间决定。
备注:内容仅供参考。
