← 返回文章列表
开发笔记

在 Flutter 中接入 Google Play 应用内订阅:客户端、实时通知与 Supabase Edge Function

本文用分步方式说明如何在 Flutter(Android) 中实现 Google Play 应用内购买 / 订阅,并配合 Google Play 实时开发者通知(Real-time developer notifications, RTDN)Google Cloud Pub/Sub,由 Supabase Edge Function 接收推送、同步服务端权益。文中会涉及:套餐变更宽限期(grace period)取消免费试用 等概念及推荐处理方式。

Play 的计费与政策更新较快,实施时请以 Google Play Billing订阅生命周期 与 Play控制台当前界面为准。

你将完成什么

  • 在 Play控制台创建订阅商品(基础方案、优惠/试用)并在 Flutter 中发起购买与监听更新
  • 理解 宽限期账号保留(hold)用户取消免费试用 在状态上的区别
  • 在 Google Cloud 配置 Pub/Sub,并在 Play 控制台启用 RTDN
  • 使用 Pub/Sub 推送订阅(Push) 将消息投递到你的 Edge Function HTTP 端点
  • 在 Edge Function 中解析通知,并结合 Google Play Developer API(Android Publisher)校验/拉取最新订阅状态,写入数据库

核心概念(先对齐名词)

概念说明
一次性商品 vs 订阅一次性(消耗型/非消耗型)按次购买;订阅按周期续费,状态会随续费、失败、取消而变化。本文侧重订阅。
基础方案(Base plan)订阅商品的计费周期与价格模板(如月费、年费);同一订阅可有多套基础方案。
优惠(Offer)挂在基础方案上的定价策略,可包含 免费试用介绍期价格 等。
免费试用用户在试用期内不扣费;试用结束若未取消则进入正式扣费周期。状态仍以 Play 返回与 RTDN 为准。
宽限期(Grace period)续费失败时,若你在控制台启用宽限,用户可能在一段时间内仍保留订阅权益以便更新付款方式;对应通知类型含 IN_GRACE_PERIOD
账号保留(Account hold)付款问题持续未解决时可能进入 ON_HOLD,权益通常应视为暂停,直至恢复或过期。
用户取消(Cancel)用户关闭自动续费后,当前周期往往仍有效,直到 过期(EXPIRED);不要一看到 CANCELED 就立刻关掉所有权益,需结合到期时间与 API 查询结果。
套餐 / 计划变更用户升级/降级/换基础方案时,Play 会推送相应通知(如续费、价格变更确认等),以服务端查询 purchases.subscriptionsv2(或当前推荐 API)结果为准 更新档位。
RTDNPlay 向你的后端路径发送订阅生命周期事件;官方路径是关联 Cloud Pub/Sub 主题,再由 推送订阅 转发到你的 HTTPS端点(如 Edge Function)。
Developer API服务端用服务账号调用 Android Publisher API 查询购买令牌、订阅状态,不可只在客户端信本地缓存。

前置条件

  • Flutter 工程已配置 Android,applicationId 与 Play 控制台应用一致
  • Google Play 开发者账号,应用已创建(至少内部测试轨道便于联调)
  • 一个 Google Cloud 项目(可与 Play 关联),能使用 Pub/Sub
  • Supabase 项目,已会用 CLI 部署 Edge Function、配置 Secrets
  • 熟悉 服务账号 JSON 与密钥管理(提交到仓库)

第一步:在 Play 控制台创建订阅与优惠

  1. 打开 Play Console → 你的应用 → 获利订阅
  2. 新建订阅,填写订阅 ID(即客户端使用的 productId)。
  3. 添加 基础方案:选择结算周期、价格、是否允许试用区域等。
  4. 在基础方案下配置 优惠
    • 免费试用:设置试用天数;注意各地区政策与展示文案。
    • 如需「首月特价」等,使用介绍期定价(以控制台选项为准)。
  5. 激活商品并发布到测试轨道;测试人员使用许可测试账号可进行购买测试。

免费试用在客户端表现为用户接受优惠后进入试用;服务端应通过 API + RTDN 同步 linkedPurchaseToken、到期时间与是否仍在有效权益期。


第二步:Flutter 客户端集成购买(in_app_purchase

  1. pubspec.yaml 添加依赖:in_app_purchase(必要时配合平台实现包查阅官方说明)。
  2. 初始化 InAppPurchase.instance,监听 purchaseStream,处理:
    • pending →展示处理中
    • purchased / restoredpurchaseDetails.verificationData.serverVerificationData(购买令牌)交给服务端校验后再 completePurchase
    • error → 提示用户
  3. 查询商品:queryProductDetails 使用你在控制台创建的 订阅 productId
  4. 发起购买:buyNonConsumable 或订阅对应的购买 API(以当前插件对订阅的推荐用法为准)。
  5. 关联用户:使用 Play Billing 的 obfuscatedAccountId / obfuscatedProfileId(若插件暴露)传入你的 Supabase user.id 的混淆形式,便于跨设备与服务端对账(需按文档做哈希/混淆,传明文隐私)。

客户端流程要点:先服务端验签/查询成功,再 completePurchase,避免丢单与重复扣费纠纷。


第三步:服务端校验购买(Google Play Developer API)

仅依赖客户端不可靠,Edge Function(或任意后端)应:

  1. 在 Google Cloud 创建服务账号,授予访问 Google Play Android Developer API 所需角色(常见为与 Play 控制台关联后的权限;以当前 IAM 文档为准)。
  2. Play ConsoleAPI 访问 中关联该 Cloud 项目,并对服务账号授予财务数据/订单相关权限。
  3. 将服务账号 JSON 密钥 存为 Supabase Secret(例如 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON),在函数内用 JWT或服务账号库 换取访问令牌。
  4. 使用订阅查询接口(推荐跟随官方当前版本,例如 Subscriptions v2 相关 REST 资源)用 packageName + purchaseToken 拉取状态:是否有效、到期时间、自动续费、基础方案 ID、是否宽限/保留等。
  5. 把结果写入你的业务库(如 subscriptions 表),并以 user_id(或 obfuscated 映射)关联。

第四步:配置实时开发者通知(RTDN)与 Pub/Sub

整体数据流:Play → Pub/Sub 主题 →(推送订阅)→ Edge Function URL

1. 创建主题并授权 Play

  1. Google Cloud ConsolePub/Sub →创建主题(Topic)。
  2. Play 文档Google Play Android Developer 的发布者身份加入该主题的 发布权限(Play 控制台内复制「主题名称」并保存 RTDN 配置时会引导你完成授权)。
  3. Play Console获利获利设置(或「实时开发者通知」相关入口)中,填入该 Pub/Sub 主题完整名称(形如 projects/PROJECT_ID/topics/TOPIC_NAME)并保存。

2. 创建推送订阅(Push)指向 Supabase

  1. 在同一 GCP 项目中新建 订阅(Subscription),类型选 推送(Push)
  2. 端点 URL 填你的函数公网地址,例如:
    https://<project-ref>.supabase.co/functions/v1/google-play-rtdn
  3. 按安全要求配置身份验证:Pub/Sub 推送可携带 OIDC JWT;若开启,Edge Function 需验证签发者/受众,使用简单「匿名」暴露在未校验的公网(至少应校验令牌或 HMAC 类网关)。若你暂在测试环境关闭推送认证,务必在上线前收紧。

3. Edge Function 处理 Pub/Sub 信封

推送请求体通常为 JSON,核心字段包括:

  • message.dataBase64 编码的字符串,解码后是 Play 的 Developer Notification JSON(含 packageNameeventTimeMillissubscriptionNotification 等)。
  • message.messageId:可用于幂等去重。

函数逻辑建议:

  1. 解析 HTTP JSON,对 message.data 做 Base64 解码 → UTF-8 字符串 → JSON.parse。
  2. 读取 subscriptionNotificationnotificationTypepurchaseTokensubscriptionId
  3. 不要仅依赖 notificationType 定终态:用 purchaseToken 调用 Android Publisher API 再查一次订阅详情,避免竞态与乱序。
  4. 更新数据库后返回 200;若返回非 2xx,Pub/Sub 会按策略重试。

常见 notificationType(整数枚举,以官方最新表为准)与运维含义简述:

  • SUBSCRIPTION_RECOVERED / RENEWED / PURCHASED:一般表示付费关系恢复或续费成功,应刷新到期时间与档位。
  • SUBSCRIPTION_IN_GRACE_PERIOD:进入宽限期,可提示用户更新付款方式;权益策略由你产品决定(多数仍给短期完整权益)。
  • SUBSCRIPTION_ON_HOLD账号保留,通常应暂停高级权益。
  • SUBSCRIPTION_CANCELED:用户取消自动续费;通常当前周期仍有效,请用 API 看 expiryTime 与是否 autoRenewing
  • SUBSCRIPTION_EXPIRED / REVOKED:订阅结束或撤销,应移除权益。
  • SUBSCRIPTION_PRICE_CHANGE_CONFIRMED 等:与调价/计划变更相关,需重新查询 API 确认用户当前基础方案与价格。

第五步:计划变更、取消、试用与宽限期(产品层怎么接)

场景客户端服务端(Edge Function + API)
免费试用开始/结束展示 Play 系统结算页与文案以 API 返回的周期与优惠状态为准;试用结束日后续费失败则进入宽限/保留流程
升级/降级(换基础方案)走 Play 提供的变更流程RTDN + 查询 API 更新 basePlanId / offerId 等字段;注意按比例与生效时机以 Play 为准
宽限期可本地提示「付款失败,请更新支付方式」收到 IN_GRACE_PERIOD 后查 API;可发邮件/应用内提醒
用户取消引导到 Play 订阅中心管理CANCELED 后仍可能 autoRenewing=false 且未过期;过期再以 EXPIRED 或 API 为准关权益
恢复购买调用 restorePurchases以最新 purchaseToken 查询结果合并到当前登录用户(注意账号绑定与滥用)

第六步:Supabase Secrets 与部署提示

示例(密钥名自定,勿泄露真实值):

supabase secrets set \
  GOOGLE_PLAY_SERVICE_ACCOUNT_JSON='{"type":"service_account",...}' \
  GOOGLE_PLAY_PACKAGE_NAME=com.example.app
  • JSON 建议以单行或 Base64 存;函数内注意解析与缓存令牌。
  • 记录 Pub/Sub 推送日志函数日志,便于排查乱序与重复投递。
  • 对同一 messageId 或业务唯一键做幂等写入,避免重复通知导致重复扣权益或错误状态。

上线前检查清单

  • 测试轨道完成:试用、首购、续费失败、宽限、保留、取消后续费到期、换档
  • 所有权益变更以 Developer API 查询结果 为准,RTDN 仅作触发
  • 服务账号权限最小化,密钥仅存在于 Supabase Secrets
  • Push 端点认证与限流已配置,避免任意第三方伪造请求
  • 隐私与区域合规:价格、试用、取消说明符合 Play 政策

常见问题

现象建议
收不到 RTDN检查 Play 控制台主题名、主题 IAM、Pub/Sub 推送订阅是否活跃、URL 是否公网可达
函数401/403Pub/Sub OIDC 与 Supabase 函数鉴权冲突时,需在网关或函数内正确校验 Google 签发的 JWT
解码 message.data 失败确认 Base64 与 UTF-8;打印原始长度与枚举类型是否匹配当前 Play 版本
客户端已购买,库中无记录查是否完成服务端校验;completePurchase 是否过早;用户是否未登录导致无法关联 user_id

参考链接