Google Play in-app billing subscription callbacks 如何实现?
结论
可以跟踪订阅续费、取消、宽限期、账号保留、暂停、撤销、到期等状态变化,但不能依靠旧版 InAppBillingService 的客户端回调来完成。
推荐采用以下服务端方案:
- 在 Google Play Console 中启用 Real-time Developer Notifications(RTDN)。
- 通过 Google Cloud Pub/Sub 接收订阅事件。
- 收到通知后,使用其中的
purchaseToken调用 Google Play Developer API 的purchases.subscriptionsv2.get。 - 根据 Developer API 返回的订阅状态,更新本地数据库和用户权益。
RTDN 只用于提示订阅状态可能发生了变化。它既不是完整订单,也不能说明款项已经结算到 merchant account。是否续费成功,可以通过订阅状态判断;实际结算金额、手续费、退款和打款情况,则需要查看 Google Play 财务报表或付款报告。
为什么客户端回调不够
客户端可以在购买和订阅流程中获取购买结果及 purchaseToken,但订阅是一个持续变化的状态。即使应用没有运行,用户的订阅也可能发生以下变化:
- 取消自动续订
- 完成下一周期续费
- 续费扣款失败
- 进入宽限期
- 进入账号保留状态
- 恢复订阅
- 被退款或撤销
- 到期失效
因此,客户端购买响应只能反映当次购买流程的结果,无法持续反映后续周期的订阅状态。
旧版 InAppBillingService 已不适合用于新的实现。客户端应改用 Google Play Billing Library,服务端则通过 RTDN 和 Google Play Developer API 管理订阅状态。
推荐架构
Google Play
│
│ RTDN
▼
Google Cloud Pub/Sub
│
│ Push 或 Pull
▼
订阅通知处理服务
│
│ 使用 purchaseToken 查询
▼
Google Play Developer API
│
▼
本地订阅数据库 / 用户权益系统
这套架构中,通知负责触发查询,Developer API 提供当前的权威状态。
配置实时订阅通知
在 Google Play Console 中为应用配置 Real-time Developer Notifications,并关联一个 Google Cloud Pub/Sub topic。
接着创建 Pub/Sub subscription。接收方式有两种:
- Push:Pub/Sub 将消息发送到你的 HTTPS endpoint。
- Pull:服务器主动拉取消息。
生产环境通常还需要完成以下配置:
- 授予 Google Play 服务账号向 topic 发布消息的权限。
- 为处理服务配置调用 Android Publisher API 所需的凭据。
- 验证 Push endpoint 的身份。
- 对消息做幂等处理。
- 配置重试和死信队列。
Google Cloud 和 Google Play Console 更新后,具体入口和 IAM 角色名称可能发生变化,请以当前控制台及官方文档为准。
接收 RTDN 消息
Pub/Sub Push 请求中的消息数据位于 message.data,内容是经过 Base64 编码的 JSON。
下面是一个简化的 Node.js/Express 示例:
import express from "express";
const app = express();
app.use(express.json());
app.post("/google-play/rtdn", async (req, res) => {
const pubsubMessage = req.body?.message;
if (!pubsubMessage?.data) {
return res.status(400).send("Missing Pub/Sub message data");
}
let notification;
try {
const json = Buffer.from(pubsubMessage.data, "base64").toString("utf8");
notification = JSON.parse(json);
} catch (error) {
return res.status(400).send("Invalid notification payload");
}
try {
const subscriptionNotification = notification.subscriptionNotification;
if (!subscriptionNotification) {
// 也可能是 oneTimeProductNotification、testNotification 等。
return res.status(204).send();
}
const { purchaseToken, notificationType } = subscriptionNotification;
// 先持久化消息,并使用 messageId 或业务键进行幂等去重。
await saveIncomingNotification({
messageId: pubsubMessage.messageId,
packageName: notification.packageName,
purchaseToken,
notificationType,
eventTimeMillis: notification.eventTimeMillis
});
// 不直接根据 notificationType 修改最终权益,
// 而是查询 Developer API 获得订阅的当前状态。
await refreshSubscriptionFromGooglePlay({
packageName: notification.packageName,
purchaseToken
});
return res.status(204).send();
} catch (error) {
console.error(error);
// 返回非 2xx,使 Pub/Sub 按策略重试。
return res.status(500).send("Processing failed");
}
});
app.listen(3000);
saveIncomingNotification 和 refreshSubscriptionFromGooglePlay 是业务函数,需要根据数据库、鉴权方式和部署环境自行实现。
查询订阅的权威状态
收到 RTDN 后,调用以下接口:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}
Authorization: Bearer {access_token}
对应的 API 是:
purchases.subscriptionsv2.get
处理返回结果时,应重点检查:
subscriptionStatelineItems[].expiryTimelineItems[].autoRenewingPlan.autoRenewEnabledlineItems[].productIdacknowledgementState- 取消、暂停及其他上下文信息
示例处理逻辑:
async function refreshSubscriptionFromGooglePlay({
packageName,
purchaseToken
}) {
const accessToken = await getGoogleAccessToken();
const url =
`https://androidpublisher.googleapis.com/androidpublisher/v3/` +
`applications/${encodeURIComponent(packageName)}/` +
`purchases/subscriptionsv2/tokens/${encodeURIComponent(purchaseToken)}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(
`Google Play API returned ${response.status}: ${await response.text()}`
);
}
const subscription = await response.json();
await updateLocalSubscription({
purchaseToken,
subscriptionState: subscription.subscriptionState,
acknowledgementState: subscription.acknowledgementState,
lineItems: subscription.lineItems ?? [],
rawResponse: subscription
});
}
不要仅凭 RTDN 的 notificationType 判断用户是否仍有权益。通知可能延迟、重复或乱序,而 Developer API 的查询结果反映的是查询时的当前订阅状态。
如何判断“用户取消了订阅”
取消自动续订并不等于立即失去权益。
用户关闭自动续订后,通常仍可在已经支付的周期内继续使用服务,直到 expiryTime。因此,本地数据最好分别保存:
autoRenewEnabled = false
entitlementActive = true
expiryTime = 当前付费周期结束时间
只有当订阅到期、被撤销,或者查询结果明确表明权益已经失效时,才应停止服务。
可以把权益判断写成独立逻辑,例如:
function hasEntitlement(subscription, now = new Date()) {
const activeStates = new Set([
"SUBSCRIPTION_STATE_ACTIVE",
"SUBSCRIPTION_STATE_IN_GRACE_PERIOD"
]);
if (!activeStates.has(subscription.subscriptionState)) {
return false;
}
return (subscription.lineItems ?? []).some((item) => {
if (!item.expiryTime) {
return false;
}
return new Date(item.expiryTime) > now;
});
}
宽限期内是否继续提供服务,取决于产品策略,也会受到 Play Console 配置的影响。账号保留状态通常意味着需要暂停权益,但具体处理方式仍应结合业务要求和当前的 Google Play Billing 规则确认。
是否能由服务器取消订阅
服务器可以调用 Google Play Developer API 取消订阅,但需要谨慎选择取消方式。
当前 API 体系提供了类似以下接口:
POST https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}:cancel
Authorization: Bearer {access_token}
Content-Type: application/json
请求体示例:
{
"cancellationContext": {
"cancellationType": "USER_REQUESTED_STOP_RENEWALS"
}
}
不同的取消类型可能只停止后续续费,也可能由开发者停止付款并影响现有权益。可用枚举及其具体行为,应以调用时的 Android Publisher API 文档为准。不能把所有“取消”都理解成立即失效。
如果只是让用户自行管理订阅,通常应打开 Google Play 的订阅管理页面,而不是由应用模拟取消流程。
如何判断每个周期是否续费成功
续费成功后,RTDN 会发送对应的订阅通知。处理服务收到消息后,应再次查询 purchases.subscriptionsv2.get,确认以下信息:
- 订阅状态是否有效
expiryTime是否已经推进到新的周期- 当前 line item 和产品是否符合预期
- 订阅是否进入宽限期或账号保留状态
本地应记录每次查询结果和更新后的到期时间,但不能假设每个续费周期只会收到一条通知。消息可能重复,短时间内也可能连续出现多次状态变化。
可以结合以下信息设计幂等规则:
- Pub/Sub
messageId purchaseTokennotificationTypeeventTimeMillis- 查询得到的
expiryTime
续费成功不等于商户已收到结算款
RTDN 和订阅查询用于判断购买结果及订阅生命周期,不能用于确认银行结算情况。
即使订阅状态显示续费成功,实际款项仍可能受到以下因素影响:
- 税费
- Google Play 服务费
- 汇率换算
- 后续退款或拒付
- 付款周期差异
- 结算账户调整
因此,“用户续费成功”可以作为更新用户权益的依据。至于款项是否按期转入 merchant account,需要通过 Google Play 财务报告、收益报告或付款记录进行对账。这两类状态不应保存在同一个字段中。
实现时容易忽略的问题
确认购买
新的订阅购买需要按照 Google Play Billing 的要求及时执行 acknowledge。确认操作既可以在客户端完成,也可以通过对应的 Developer API 在服务端完成。购买如果没有在规定时间内确认,可能会被自动退款。
保存 purchaseToken
purchaseToken 是服务端查询订阅时使用的主要标识。购买完成后,应立即将它安全地上传到服务器,并与应用包名、用户账号和产品建立关联。
订阅升级、降级或重新订阅时,可能产生新的 token 或 token 关联关系,因此不能长期假设一个用户只有一个 token。
幂等与乱序
RTDN 通过 Pub/Sub 投递,业务处理必须允许通知重复。不能把“收到一次通知”直接理解为“执行了一次扣款”或“增加了一个订阅周期”。
定期校验
RTDN 应作为主要触发机制,同时建议增加低频后台校验任务。对于即将到期、处于宽限期或长时间没有更新的订阅,可以重新调用 Developer API,减少通知配置错误或处理异常造成的影响。
不要信任客户端状态
客户端返回值可以改善界面体验,但服务器必须结合 purchaseToken 和 Google Play Developer API 的查询结果决定最终权益。客户端提交的产品 ID、到期时间或“购买成功”标记,都不能直接作为授权依据。
备注:内容仅供参考。
