如何通过 Pub/Sub 接收 Google Play 应用内购买通知
明确结论
Google Play Console 没有专门用来“开启 in-app purchase 通知”的额外开关。只要 RTDN 已正确绑定 Google Cloud Pub/Sub,受支持的一次性商品购买就会通过 oneTimeProductNotification(旧文档中可能写作 OneTimePurchaseNotification)发送到同一个 Pub/Sub topic。
不过,RTDN 并不是一套完整的支付状态 Webhook。对于一次性商品,它主要发送以下两类事件:
ONE_TIME_PRODUCT_PURCHASED:一次性商品购买成功。ONE_TIME_PRODUCT_CANCELED:处于PENDING状态的购买最终被取消。
确认、消耗等状态变化不会逐一触发通知。客户端购买成功后,仍应立即把 purchaseToken、productId 和 packageName 发送给后端,再由后端调用 Google Play Developer API 验证购买并发放权益。RTDN 主要用于处理客户端未能上报、延迟付款、退款和撤销等异步情况,不能作为唯一的支付入口。
退款或撤销购买通过 voidedPurchaseNotification 接收,也可以定期使用 Voided Purchases API 对账。
一次性商品支持哪些通知
一次性商品通知的数据结构通常如下:
{
"version": "1.0",
"packageName": "com.example.app",
"eventTimeMillis": "1720000000000",
"oneTimeProductNotification": {
"version": "1.0",
"notificationType": 1,
"purchaseToken": "purchase-token",
"sku": "product_id"
}
}
notificationType 的含义如下:
| 值 | 常量 | 含义 |
|---|---|---|
1 | ONE_TIME_PRODUCT_PURCHASED | 一次性商品购买完成 |
2 | ONE_TIME_PRODUCT_CANCELED | 延迟付款或其他待处理购买最终被取消 |
所谓“一些类型”,准确地说,是 RTDN 只覆盖一次性商品生命周期中的部分事件,并不会提供完整的状态变更流。例如:
- 普通一次性商品购买成功时,可以产生
ONE_TIME_PRODUCT_PURCHASED。 - 原本处于
PENDING状态的购买后来付款成功,也可以产生购买完成通知。 - 原本处于
PENDING状态的购买最终取消,可以产生ONE_TIME_PRODUCT_CANCELED。 acknowledge、consume和客户端查询购买记录等操作不会分别产生通知。- 促销、测试购买、旧版结算流程或某些特殊购买渠道是否产生通知,可能受购买方式和 Billing Library 版本影响,不能只根据 Pub/Sub 测试消息判断。
旧资料使用 OneTimePurchaseNotification,较新的文档和消息字段通常使用 OneTimeProductNotification 或 oneTimeProductNotification。实现时应以实际收到的 JSON 字段和当前官方 RTDN reference 为准。
正确的整体处理流程
正常购买和异步通知应分成两条处理路径:
正常路径:
客户端完成购买
→ 客户端把 purchaseToken 发送到后端
→ 后端调用 Google Play Developer API 验证
→ 后端保存订单并发放权益
→ 后端或客户端 acknowledge/consume
补偿路径:
Google Play 发布 RTDN
→ Pub/Sub 推送或拉取消息
→ 后端提取 purchaseToken
→ 再次调用 Google Play Developer API 查询真实状态
→ 幂等更新订单和权益
RTDN 消息本身不是购买凭证,也没有提供完整的订单状态。收到消息后,必须使用 purchaseToken 查询 Google Play Developer API,不能仅根据 notificationType 发放权益。
Google Play Console 和 Pub/Sub 配置
1. 创建 Pub/Sub topic
在 Google Cloud 中创建 topic,例如:
projects/example-project/topics/google-play-rtdn
然后创建 subscription,可选择以下两种方式:
- Push subscription:Pub/Sub 将消息发送到 HTTPS 地址。
- Pull subscription:后端通过 Pub/Sub client library 拉取消息。
如果只有 topic,没有 subscription,应用后端就无法消费消息。
2. 授予 Google Play 发布权限
需要允许 Google Play 对该 topic 执行 pubsub.topics.publish。通常应为 Google Play 使用的服务账号或服务代理授予:
roles/pubsub.publisher
将权限授予具体 topic 即可,不需要给出整个项目的管理权限。
3. 在 Play Console 中填写 topic
在应用的 monetization setup / real-time developer notifications 配置中填写完整的 topic 名称:
projects/example-project/topics/google-play-rtdn
保存配置后发送测试通知。
测试通知成功只能证明以下链路已经打通:
Play Console → Pub/Sub topic → subscription → 后端
它不能证明实际购买满足通知触发条件,也不能证明后端能够正确处理 oneTimeProductNotification。
4. 检查应用和购买条件
如果测试通知正常,但真实购买没有通知,可以重点检查:
- 购买是否发生在配置了 RTDN 的同一个 Play Console 应用中。
packageName是否完全一致。- 商品是否已激活,购买是否通过从 Google Play 安装的应用完成。
- 客户端是否使用受支持版本的 Google Play Billing Library。
- 后端是否只处理了
subscriptionNotification,却遗漏了oneTimeProductNotification。 - Pub/Sub subscription 是否存在积压、重试或 dead-letter 消息。
- JSON 解析代码是否错误地假设每条消息都包含同一种通知对象。
- 测试是否在购买后过早结束。RTDN 不保证与客户端购买回调同时到达。
Pub/Sub Push 接收示例
Pub/Sub push 请求中的业务数据位于 message.data,并经过 Base64 编码。下面是一个简化的 Node.js/Express 示例:
import express from "express";
const app = express();
app.use(express.json());
app.post("/google-play/rtdn", async (req, res) => {
try {
const encodedData = req.body?.message?.data;
if (!encodedData) {
return res.status(400).send("Missing Pub/Sub message data");
}
const notification = JSON.parse(
Buffer.from(encodedData, "base64").toString("utf8")
);
if (notification.testNotification) {
console.log("Received RTDN test notification");
}
if (notification.oneTimeProductNotification) {
const event = notification.oneTimeProductNotification;
await handleOneTimeProduct({
packageName: notification.packageName,
productId: event.sku,
purchaseToken: event.purchaseToken,
notificationType: event.notificationType,
eventTimeMillis: notification.eventTimeMillis
});
}
if (notification.voidedPurchaseNotification) {
await handleVoidedPurchase(
notification.voidedPurchaseNotification
);
}
// 返回 2xx 后 Pub/Sub 才认为消息已处理。
res.sendStatus(204);
} catch (error) {
console.error(error);
// 非 2xx 会触发 Pub/Sub 重试。
res.sendStatus(500);
}
});
处理一次性商品时,还需要调用 Google Play Developer API 查询购买:
import { google } from "googleapis";
const androidPublisher = google.androidpublisher("v3");
async function handleOneTimeProduct({
packageName,
productId,
purchaseToken,
notificationType
}) {
const auth = new google.auth.GoogleAuth({
scopes: ["https://www.googleapis.com/auth/androidpublisher"]
});
const result = await androidPublisher.purchases.products.get({
auth,
packageName,
productId,
token: purchaseToken
});
const purchase = result.data;
if (purchase.purchaseState === 0) {
// 使用 purchaseToken 或订单号执行幂等写入。
await grantEntitlementOnce({
packageName,
productId,
purchaseToken,
orderId: purchase.orderId
});
}
if (purchase.purchaseState === 2) {
// 仍为 pending,不应发放权益。
}
if (purchase.purchaseState === 1) {
// 已取消。根据本地订单状态撤销或保持未发放状态。
}
}
部分新购买模型可以使用 purchases.productsv2.getproductpurchasev2。具体使用哪个接口,取决于当前的商品模型和 API 支持情况。处理原则不变:使用 RTDN 中的 token 查询服务端状态,不直接信任通知本身。
如何接收退款和撤销通知
退款、拒付及其他购买作废事件使用 voidedPurchaseNotification:
{
"version": "1.0",
"packageName": "com.example.app",
"eventTimeMillis": "1720000000000",
"voidedPurchaseNotification": {
"purchaseToken": "purchase-token",
"orderId": "GPA.1234-5678-9012-34567",
"productType": 2,
"refundType": 1
}
}
常见字段包括:
purchaseToken:原购买 token。orderId:Google Play 订单号。productType:区分订阅与一次性商品。refundType:区分全额退款与受支持的部分退款形式。
收到通知后,应通过 purchaseToken 或 orderId 查找本地订单,再根据业务规则撤销尚未消费的权益:
async function handleVoidedPurchase(event) {
const order = await findOrder({
purchaseToken: event.purchaseToken,
orderId: event.orderId
});
if (!order) {
// 记录后交给对账任务处理,不能静默丢弃。
return;
}
await revokeEntitlementIdempotently({
orderId: order.orderId,
refundType: event.refundType
});
}
退款后不一定能直接“回收商品”。例如,消耗型商品可能已经兑换成虚拟货币并被使用,因此具体撤销方式需要由业务系统决定。遇到部分数量退款时,也不能无条件撤销整笔订单。
为避免漏单,还应定期调用 Voided Purchases API 对账。RTDN 用于提供低延迟通知,Voided Purchases API 则负责补偿消息丢失、处理失败、部署中断以及历史数据回查。
必须注意的可靠性问题
Pub/Sub 和 RTDN 都应按至少一次投递设计:
- 同一通知可能重复到达。
- 通知可能延迟到达。
- 不同事件可能乱序。
- 后端处理失败时,Pub/Sub 会重试。
- 测试通知与真实购买通知的数据结构不同。
数据库应围绕 purchaseToken、orderId 和事件类型建立幂等处理机制。即使收到两次 ONE_TIME_PRODUCT_PURCHASED,也不能发放两次商品。
使用 push subscription 时,还应验证 Pub/Sub 推送请求携带的 OIDC token,并限制 endpoint 只接受预期的 audience、issuer 和 service account。能够解析 Base64 数据,并不能证明请求确实来自 Google。
购买成功的主链路仍然是“客户端上报 token,后端立即验证”。RTDN 只负责异步补偿和状态同步。这样,即使一次性商品通知发生延迟,或者某个购买流程没有产生 RTDN,支付处理也不会被卡住。
备注:内容仅供参考。
