← 全部文章

Google Play IAP 支付完整对接流程

从 Play Console 商品、服务账号和 Android 客户端,到服务端 verify、Cloud Pub/Sub RTDN webhook 与内部测试的完整接入指南。

文章目录

本文记录 Google Play IAP 的完整接入流程。前提是 Google Play Console 已创建应用,目标是打通下面这条链路:

Play Console 配置商品
        ↓
Android App 查询商品并发起支付
        ↓
Google Play 返回 purchaseToken
        ↓
App 将 purchaseToken、productId、packageName 发给服务端
        ↓
服务端调用 Google Play Developer API 验单
        ↓
服务端幂等发放权益,并 acknowledge 或 consume
        ↓
Google Play → Cloud Pub/Sub → webhook 同步续费、退款等后续状态

客户端返回的购买结果只能作为验单参数,不能直接作为发放权益的依据。商品 ID、订单状态、归属用户、是否重复发放和最终权益都必须由服务端确认。

一、先确定商品类型

Google Play 的数字商品主要分为两类:

类型适用场景支付完成后的处理
一次性商品点数、次数包、永久功能可重复购买的商品执行 consume;永久权益执行 acknowledge
订阅月卡、季卡、年卡服务端验证订阅状态并执行 acknowledge,后续通过 RTDN 同步续费、暂停、取消和退款

consume 会同时确认购买并允许同一账号再次购买该商品。不要对永久解锁类商品执行 consume,否则用户可以重复购买;也不要把可重复购买的消耗品仅做 acknowledge,否则该账号不能再次购买同一商品。

以下示例以:

  • 包名:com.example.app
  • 一次性商品 ID:coins_100
  • 订阅 ID:vip_monthly
  • 服务端接口:POST /api/pay/google/verify
  • webhook:POST https://api.example.com/api/pay/google/webhook

为例。接入时全部替换为当前项目的真实值。

二、在 Play Console 配置商品

1. 完成收款前置配置

先在 Play Console 检查以下项目:

  1. 应用包名必须与 Android 工程的 applicationId 完全一致。
  2. 完成开发者账号、付款资料和商家资料配置。
  3. 至少上传一个带 Play Billing Library 的 AAB 到内部测试轨道。
  4. 完成 Play Console 要求的应用内容、目标受众、数据安全和国家/地区等必要配置。

如果“产品”或“订阅”创建入口不可用,通常是还没有上传 AAB、付款资料未完成,或当前账号没有管理商品的权限。

2. 创建一次性商品

进入当前应用,打开“获利/Monetize → 产品/Products → 一次性商品/One-time products”。不同语言和新版 Console 的菜单名称可能略有差异。

创建商品时填写:

字段示例注意事项
Product IDcoins_100发布后不要随意修改;必须与客户端、服务端数据库一致
名称100 Coins用户会在购买页看到
描述Get 100 coins补齐所有目标语言
Purchase optionBuy设置可购买的地区和价格
状态Active未激活的商品客户端查不到

Google Play Console 正在逐步使用新版一次性商品模型,商品下可能还需要创建并激活 purchase option/offer。最终要确认商品本身及购买选项都处于有效状态。官方将可重复购买和永久权益都归在一次性商品中,是否可重复购买由应用在支付后选择 consume 还是 acknowledge 决定。参见 管理商品目录

3. 创建订阅

进入“获利/Monetize → 产品/Products → 订阅/Subscriptions”,创建 vip_monthly,然后继续创建基础方案:

  1. 选择自动续订或预付费;常规会员一般使用自动续订。
  2. 设置结算周期,例如 P1M
  3. 选择可售国家/地区并设置价格。
  4. 如有试用或首购优惠,再在基础方案下创建 offer。
  5. 激活基础方案和订阅。

客户端购买订阅时不仅要匹配 product ID,还可能需要使用查询商品后返回的 offer token。服务端应保存内部套餐与 Google productId 的映射,不要相信客户端传入的价格或会员天数。

三、配置 Google Play Developer API

服务端需要通过 Android Publisher API 查询真实购买状态。官方流程包含 Google Cloud 和 Play Console 两部分,详见 Google Play Developer API 入门

1. Google Cloud Console

  1. 创建或选择一个 Google Cloud 项目。
  2. 打开“API 和服务 → 库”。
  3. 搜索并启用 Google Play Android Developer API
  4. 打开“IAM 和管理 → 服务账号”,创建一个仅供支付服务使用的服务账号,例如 google-play-iap-server
  5. 如果服务部署在 Google Cloud,优先使用绑定的服务账号或 Workload Identity,避免下载长期 JSON 密钥。
  6. 如果现有部署只能使用密钥,在服务账号的“密钥”中创建 JSON 密钥,将 client_emailprivate_key 保存到服务端密钥系统,绝不能放进 App、前端代码或 Git。

2. Google Play Console

  1. 打开“用户和权限/Users and permissions”。
  2. 邀请刚创建的服务账号邮箱,例如 google-play-iap-server@PROJECT_ID.iam.gserviceaccount.com
  3. 将权限限制到目标应用。
  4. 至少授予 查看财务数据、订单和取消调查回复管理订单和订阅。实际名称会随 Console 语言略有变化。
  5. 保存邀请,等待权限生效。

旧教程经常要求在“API access”中关联 Cloud 项目;官方当前说明已不再要求为了 Developer API 访问而关联开发者账号和 Cloud 项目。服务账号仍需在 Play Console 的“用户和权限”中被邀请并获得应用权限。

服务端配置示例:

google: {
  androidPublisher: {
    client_email: process.env.GOOGLE_PLAY_CLIENT_EMAIL,
    private_key: process.env.GOOGLE_PLAY_PRIVATE_KEY
  }
}

环境变量中的私钥通常把换行保存成 \\n,使用前需要恢复:

const privateKey = process.env.GOOGLE_PLAY_PRIVATE_KEY.replace(/\\n/g, '\n')

四、Android 客户端支付流程

客户端使用 Google Play Billing Library,核心步骤如下:

  1. 创建并连接 BillingClient
  2. queryProductDetailsAsync 查询商品,页面展示 Google 返回的本地化名称和价格。
  3. 调用 launchBillingFlow 发起购买。
  4. PurchasesUpdatedListener 中处理购买结果。
  5. 当购买状态为 PURCHASED,把 purchaseTokenproductIdpackageName 和内部预订单 ID 发给服务端。
  6. 只有服务端 verify 成功后,客户端才刷新权益状态。
  7. App 每次启动或 BillingClient 重连后,再用 queryPurchasesAsync 恢复未处理的购买,并重试 verify。

提交给服务端的请求可以是:

{
  "logVipId": "internal-order-id",
  "packageName": "com.example.app",
  "productId": "vip_monthly",
  "purchaseToken": "google-play-purchase-token"
}

不要上传或依赖客户端显示的 orderId、金额、币种、会员天数。purchaseToken 才是调用 Developer API 验单的关键凭证;业务权益应由服务端按可信的 productId → 套餐 映射决定。

如果购买状态为 PENDING,页面应显示“处理中”,不要发权益,也不要 consume/acknowledge。待状态变为 PURCHASED 后再走 verify。

五、服务端 verify 实现

下面的结构参考 det-server/src/api/pay/google,保留它的核心设计:服务账号换取 access token,按商品类型查询 Google,校验后幂等发权益,最后确认或消费购买。

1. 获取 Android Publisher access token

import jwt from 'jsonwebtoken'

const scope = 'https://www.googleapis.com/auth/androidpublisher'

async function accessTokenGet() {
  const now = Math.floor(Date.now() / 1000)
  const assertion = jwt.sign(
    {
      iss: process.env.GOOGLE_PLAY_CLIENT_EMAIL,
      scope,
      aud: 'https://oauth2.googleapis.com/token',
      iat: now,
      exp: now + 3600
    },
    process.env.GOOGLE_PLAY_PRIVATE_KEY.replace(/\\n/g, '\n'),
    { algorithm: 'RS256' }
  )

  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion
    })
  })
  const result = await response.json()
  if (!response.ok || !result.access_token) {
    throw new Error(`Google access token 获取失败: ${result.error_description || result.error}`)
  }
  return result.access_token
}

生产环境应缓存 access token 到过期前几分钟,避免每次验单都请求 OAuth token。也可以直接使用官方 Google Auth 库管理签名、换取和缓存 token。

2. 查询一次性商品

参考实现使用 purchases.products.get

async function productGet({ packageName, productId, purchaseToken }) {
  const accessToken = await accessTokenGet()
  const url =
    `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/` +
    `${encodeURIComponent(packageName)}/purchases/products/` +
    `${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}`

  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` }
  })
  const data = await response.json()
  if (!response.ok) throw new Error(data.error?.message || 'Google Play 验单失败')
  return data
}

发权益前至少检查:

  • 请求中的 packageName 必须等于服务端为当前 App 固定配置的包名,不能任由客户端指定任意应用。
  • Google 返回的商品必须与内部订单的 productId 一致。
  • purchaseState === 0,即已经购买;pending 或 canceled 都不能发权益。
  • purchaseToken + orderId 没有被其他订单或用户使用。
  • 一次性消耗品的 consumptionState !== 1

3. 查询订阅

订阅使用 purchases.subscriptionsv2.get

async function subscriptionGet({ packageName, purchaseToken }) {
  const accessToken = await accessTokenGet()
  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}` }
  })
  const data = await response.json()
  if (!response.ok) throw new Error(data.error?.message || 'Google Play 订阅查询失败')
  return data
}

lineItems 中找到对应 productId,并检查 subscriptionStateexpiryTime 和套餐关系。ACTIVE、仍在有效期内的 CANCELED,以及业务允许的宽限期状态可能仍有权益;EXPIREDPENDINGPAUSED 等状态不能简单当成已支付。

4. 幂等发放权益

推荐先建立内部订单,再发起 Play 支付。订单至少保存:

字段用途
id内部订单 ID
uid业务用户
source固定为 google
productId对应 Play 商品与内部套餐
purchaseTokenGoogle 购买凭证,建议加唯一索引
orderIdGoogle 订单号
statuspending、paid、refunded 等
payTimeexpiryTime支付和权益有效期

参考实现用 ${purchaseToken}|${orderId} 作为 sessionId,并在发权益前查询是否已存在成功订单。这一检查最好由数据库唯一索引和事务兜底,避免客户端重试、并发 verify 或 Pub/Sub 重投造成重复发放。

建议事务顺序是:

  1. 锁定内部订单。
  2. 检查 token 是否已绑定其他用户。
  3. 写入成功订单和权益变更。
  4. 提交事务。
  5. 调用 Google consume/acknowledge;失败时进入可靠重试队列。

不要为了等待 Google API 而长时间持有数据库事务。发权益与确认 Google 购买之间需要可恢复状态,确保进程在任一步骤崩溃后都能安全重试。

5. consume 与 acknowledge

一次性消耗品调用:

POST https://androidpublisher.googleapis.com/androidpublisher/v3/
applications/{packageName}/purchases/products/{productId}/tokens/{purchaseToken}:consume

一次性永久权益调用 products acknowledge,订阅调用 subscriptions acknowledge。请求 body 可包含服务端生成的 developerPayload,也可传空 JSON。

POST https://androidpublisher.googleapis.com/androidpublisher/v3/
applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{purchaseToken}:acknowledge

Google 要求新购买在规定期限内得到确认,否则会自动退款并撤销购买。参考目录目前能看到一次性商品 consume,但没有看到订阅 acknowledge;正式接入时必须补齐。只有首次购买需要 acknowledge,续费不需要重复 acknowledge。支付处理原则可参考 一次性商品生命周期

6. verify 接口的完整判断顺序

校验登录用户和请求字段
  → 校验 packageName 是服务端允许的固定包名
  → 根据内部订单读取可信 productId、商品类型和权益
  → 请求 Google Developer API
  → 校验购买状态、productId、有效期
  → 校验 purchaseToken/orderId 未绑定其他用户
  → 数据库事务内幂等发权益并更新订单
  → consume 消耗品,或 acknowledge 永久商品/新订阅
  → 返回最新权益

接口应该允许同一用户使用同一 token 重试并返回相同结果,但禁止同一 token 绑定到其他用户。

六、配置 RTDN webhook

RTDN(Real-time developer notifications)不是 Google Play 直接请求业务 webhook,而是:

Google Play → Pub/Sub Topic → Push Subscription → HTTPS webhook

RTDN 只表示购买状态发生变化。收到消息后仍必须用其中的 purchaseToken 调用 Google Play Developer API 查询完整、可信的最新状态,不能只按通知类型直接修改权益。参见 RTDN 参考

1. 在 Google Cloud 创建 Pub/Sub topic

  1. 选择用于 Play 支付的 Google Cloud 项目。
  2. 如果尚未启用,启用 Cloud Pub/Sub API
  3. 打开“Pub/Sub → Topics”,点击“Create topic”。
  4. Topic ID 填写 google-play-rtdn
  5. 创建后得到完整名称:
projects/PROJECT_ID/topics/google-play-rtdn
  1. 打开该 topic 的“Permissions”。
  2. 添加 principal:
google-play-developer-notifications@system.gserviceaccount.com
  1. 授予 Pub/Sub Publisherroles/pubsub.publisher)并保存。

这一步是允许 Google Play 往 topic 发布消息,不能把它误配成自己创建的 Developer API 服务账号。官方配置说明见 准备接入 Play Billing

2. 创建 Push subscription

在 topic 页面点击“Create subscription”:

  1. Subscription ID:google-play-rtdn-push
  2. Delivery type:Push。
  3. Endpoint URL:https://api.example.com/api/pay/google/webhook
  4. Endpoint 必须是公网可访问的 HTTPS 地址,并快速返回 2xx。
  5. 配置合理的重试策略和 dead-letter topic,避免持续失败的消息永久丢失。
  6. 建议启用 authenticated push,而不是暴露一个完全匿名的公网入口。

关于 push subscription 的创建字段可参考 创建 Push subscription

3. 为 Push 开启 OIDC 身份认证

创建专用服务账号,例如:

pubsub-push@PROJECT_ID.iam.gserviceaccount.com

配置 subscription 时勾选“Enable authentication”,选择该服务账号,并把 audience 设置为固定值,例如 webhook URL:

https://api.example.com/api/pay/google/webhook

同时确保:

  1. 创建 subscription 的操作者能 impersonate 这个 push 服务账号,通常需要 iam.serviceAccounts.actAs
  2. Pub/Sub service agent service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com 对 push 服务账号拥有 Service Account Token Creator
  3. webhook 验证 Authorization: Bearer <OIDC JWT> 的签名、issaudexpemail_verified,并确认 email 正是上面的 push 服务账号。

官方的完整权限和 JWT 校验要求见 Pub/Sub authenticated push。还可以在 URL 中附加一段独立随机验证 token 作为第二层校验,但它不能替代 OIDC JWT。

参考 det-server 的 webhook 代码目前只解析 body,没有看到 OIDC JWT 校验。生产环境应在路由中间件或网关层补齐认证,否则任何人都可以伪造 Pub/Sub envelope 请求该公网接口。

4. 在 Play Console 绑定 topic

进入当前应用的“获利设置/Monetization setup”或实时开发者通知配置:

  1. 找到 Real-time developer notifications
  2. Topic name 填完整资源名:
projects/PROJECT_ID/topics/google-play-rtdn
  1. 根据业务勾选订阅、一次性商品和 voided purchases 通知。
  2. 保存配置。
  3. 点击 Send test notification

测试消息应依次出现在 Pub/Sub 指标、push subscription 投递记录和服务端 webhook 日志中。测试消息只验证通知管道,不代表真实支付成功。

5. webhook 请求格式

Pub/Sub 默认以 wrapped JSON 发送:

{
  "message": {
    "messageId": "1234567890",
    "publishTime": "2026-08-27T10:00:00Z",
    "data": "base64-encoded-rtdn-json"
  },
  "subscription": "projects/PROJECT_ID/subscriptions/google-play-rtdn-push"
}

解码 message.data 后才是 RTDN:

function notificationParse(body) {
  const { message = {} } = body || {}
  if (!message.messageId || !message.data) {
    throw new Error('Google RTDN messageId 和 data 不能为空')
  }
  const data = JSON.parse(
    Buffer.from(message.data, 'base64').toString('utf8')
  )
  return { messageId: String(message.messageId), ...data }
}

解码后的对象只会出现某一种主要通知体,例如 subscriptionNotificationoneTimeProductNotificationvoidedPurchaseNotificationtestNotification

6. webhook 处理规则

推荐处理步骤:

  1. 验证 Pub/Sub OIDC JWT。
  2. 校验 envelope 并 Base64 解码。
  3. messageId 做接收幂等,插入 webhook inbox 表。
  4. 尽快返回 2xx,再由队列或后台任务处理;或确保同步处理能在 push 超时前完成。
  5. 根据 purchaseToken 调 Developer API 查询最新状态。
  6. 在事务中幂等更新订单和权益。
  7. 标记 inbox 已处理;失败则保留错误并重试。

只在成功接收并可靠保存消息后返回 2xx。返回非 2xx 或超时会触发 Pub/Sub 重投,所以重复消息属于正常情况。

参考实现已经用 google_webhook.messageId 去重,并针对部分订阅事件和 voidedPurchaseNotification 调 Google API 后续处理;正式上线还应:

  • messageId 加数据库唯一索引,避免并发重复插入。
  • 补齐 OIDC 验证。
  • 显式处理 testNotification
  • 补齐 oneTimeProductNotification,尤其是 pending 转 purchased/canceled。
  • 不要只硬编码少数通知数字;维护完整枚举并记录未知类型。
  • 将耗时操作移到队列,保证 push endpoint 快速响应。
  • 退款/撤销权益必须幂等,并保留人工审计记录。

七、如何测试支付

1. 添加 License testers

进入 Play Console 的“设置/Setup → License testing”,添加用于测试的 Google 账号,并保存许可响应配置。

License tester 的作用是显示 Google 提供的测试支付方式,不产生真实扣款,还能模拟成功、拒绝等结果。仅仅把账号加到内部测试名单并不等于 License tester;这两个名单建议都配置。

2. 创建内部测试版本

  1. 使用与 Play Console 应用完全一致的包名构建 release AAB。
  2. versionCode 必须高于已上传版本。
  3. 使用 Play App Signing 对应流程上传到“测试 → 内部测试”。
  4. 创建 release,完成检查并发布到内部测试轨道。
  5. 在“Testers”中创建邮件列表或 Google Group,加入测试账号。
  6. 复制内部测试的 opt-in link 发给测试人员。
  7. 测试账号打开链接并选择加入测试,然后从该页面跳转到 Google Play 安装 App。

内部测试版本发布后可能需要一段时间才对账号可见。测试账号必须接受 opt-in,且设备 Google Play 当前使用的账号必须正确。

3. 确认实际付款账号

一台设备登录多个 Google 账号时,购买账号通常与“从 Play 商店下载该 App 的账号”相关。测试前建议:

  1. 在 Play 商店切换到 License tester 账号。
  2. 用该账号打开 opt-in link 并安装 App。
  3. 在支付弹窗中展开账号信息,确认显示的是测试账号。
  4. 确认弹窗明确标识为测试购买,并出现测试卡。

如果弹窗出现真实银行卡或真实金额但没有测试提示,立即取消,重新检查 License tester 和下载账号。官方说明与完整测试场景见 测试 Google Play Billing 集成

4. 建议测试用例

场景预期结果
一次性商品成功verify 成功、权益发放一次、商品被 consume 后可再次购买
订阅首次购买verify 成功、权益到期时间正确、购买被 acknowledge
客户端重复 verify返回同一结果,不重复发权益
同一 token 换用户提交服务端拒绝
pending purchase不发权益,后续转为 purchased 后可恢复
测试卡拒绝不创建成功订单、不发权益
App 杀进程后恢复queryPurchasesAsync 找到购买并重新 verify
订阅自动续费收到 RTDN,查询 Google 后延长权益一次
取消订阅关闭自动续费,但在已付款有效期内仍保留权益
退款或撤销收到 voided/相关通知,幂等撤销对应权益
webhook 重复投递messageId/交易唯一键阻止重复处理
webhook 签名无效返回 401/403,不写业务数据

License tester 的订阅周期会被加速,例如常见月订阅可能数分钟续费一次,因此可以在较短时间内验证续费、宽限期、账号保留和到期。具体测试周期以官方测试页面的最新表格为准。

5. 测试 RTDN

按三层分别测试,排错会更快:

  1. 管道测试:Play Console 点击 Send test notification,确认 webhook 收到 testNotification
  2. 真实状态测试:测试账号购买、取消和等待续费,确认收到对应 RTDN,并二次查询 Developer API。
  3. 重投测试:让 webhook 临时返回 500,确认 Pub/Sub 会重试;恢复后消息只处理一次。

同时检查 Cloud Console 中 subscription 的 push 成功率、未确认消息数和 dead-letter topic,并为连续失败设置告警。

八、上线前检查清单

  • Play Console 商品、purchase option、订阅基础方案和 offer 都已激活。
  • 客户端包名、签名、Billing Library 和商品 ID 正确。
  • Developer API 已启用,服务账号只拥有目标应用所需权限。
  • 服务端固定校验允许的 package name,不信任客户端价格和权益参数。
  • verify 根据商品类型查询正确 API,并校验状态、productId、token 和有效期。
  • 数据库对 purchase token/交易键和 webhook message ID 有唯一约束。
  • 权益发放、续费和退款都支持幂等重试。
  • 消耗品使用 consume,永久商品和新订阅使用 acknowledge。
  • App 会恢复未处理购买,pending 状态不会提前发权益。
  • Pub/Sub topic 已授权 Google Play 系统账号发布消息。
  • Push subscription 使用 HTTPS、OIDC JWT 校验、重试策略和 dead-letter topic。
  • Play Console 已绑定完整 topic 名并成功发送测试通知。
  • webhook 收到 RTDN 后始终再查 Developer API,不直接相信通知内容。
  • 已用 License tester 和内部测试安装链接跑完成功、失败、续费、退款及重复投递测试。
  • 密钥只保存在服务端密钥系统,日志不会输出 purchase token、私钥或完整用户隐私数据。

完成以上步骤后,客户端同步 verify 负责让用户尽快拿到权益,RTDN 负责修正续费、取消、退款等客户端不一定在线时发生的状态变化,两条链路共同构成可靠的 Google Play IAP 支付系统。

搜索文章

输入关键词,搜索所有文章。