9.1 KiB
9.1 KiB
谷歌支付流程
本文档描述 Android 上 Google Play 内购的完整流程,与 recharge_screen.dart、GooglePlayPurchaseService、PaymentApi 实现对应。
1. 流程总览
- 第三方支付开启(
enable_third_party_payment === true且已登录):先创建订单 → 调起谷歌支付 → 支付成功后回调/v1/payment/googlepay。 - 第三方支付关闭或未登录:仅 Android 直接调起谷歌支付,不创建订单、不回调 googlepay。
用户点击 Buy(某商品)
│
├─ 第三方支付开 + 已登录
│ ├─ getPaymentMethods(activityId)
│ ├─ 弹窗选择支付方式
│ ├─ 若选「Google Pay」
│ │ ├─ POST /v1/payment/createPayment → 得到 federation(订单 id)
│ │ ├─ 调起 Google Play 内购(productId = 商品 code/helm)
│ │ └─ 支付成功后 POST /v1/payment/googlepay(见下文)
│ └─ 若选其他方式 → 打开 createPayment 返回的 payUrl
│
└─ 第三方支付关或未登录
└─ 仅 Android:直接调起 Google Play 内购(productId = 商品 code),无 createPayment、无 googlepay 回调
2. 创建订单(仅第三方 + 选 Google Pay 时)
| 项目 | 说明 |
|---|---|
| 接口 | POST /v1/payment/createPayment |
| 入参 | sentinel, asset(userId), warrior(activityId), resource, ceremony(选 Google Pay 时 resource/ceremony 含 "GooglePay") |
| 关键响应 | federation:订单 id,后续 googlepay 回调必传;convert:其他支付方式的 payUrl,选 Google Pay 时不使用 |
- 当返回的 federation(订单 id)不为空时,继续调起谷歌支付并可在成功后回调 googlepay。
- 当返回的 federation 为空时:可按业务要求重试创建订单(如最多 3 次),仍失败则提示失败。
3. 调起谷歌支付
| 项目 | 说明 |
|---|---|
| 代码 | GooglePlayPurchaseService.launchPurchaseAndReturnData(productId)(所有内购统一使用) |
| productId | 当前商品的 code(即接口里的 helm),须与 Google Play 后台「产品 ID」完全一致 |
| 成功结果 | 返回的凭据用于构造 googlepay 回调 body(见下节) |
仅 Android 执行;非 Android 提示 "Google Pay is only available on Android"。
4. 支付成功后回调:POST /v1/payment/googlepay
仅在第三方支付开 + 选 Google Pay + 已先创建订单时调用。请求体为 JSON,服务端用于校验并落单。
4.1 请求体字段(body)
请求体为四个顶层字段,语义与取值来源对应关系如下(id / purchaseData / signature / userId 分别对应 federation / merchant / sample / asset):
| 请求体字段 | 含义(填入的值) | 客户端取值来源 |
|---|---|---|
| sample | 签名(signature) | 谷歌支付成功后,GooglePlayPurchaseDetails.billingClientPurchase 的 signature |
| merchant | 购买凭据 JSON(purchaseData) | 同上 billingClientPurchase 的 originalJson |
| federation | 支付/订单 id(id) | 创建订单接口 createPayment 返回的 federation |
| asset | 用户 id(userId) | 当前登录用户 id(与 createPayment 的 asset 一致) |
4.2 示例 body 结构
{
"sample": "YbOntv0sVOsZ5d4F8hIYdPNSMy9a4+5oAsV/...",
"merchant": "{\"orderId\":\"GPA.3327-0087-2324-9960\",\"packageName\":\"com.xxx.xxxx\",\"productId\":\"com.xxx.xxxx599\",\"purchaseTime\":1773305500428,\"purchaseState\":0,\"purchaseToken\":\"...\",\"quantity\":1,\"acknowledged\":false}",
"federation": "1315538320560683421235",
"asset": "135303839048"
}
- federation:来自 createPayment 的 federation(服务端订单 id)。
- merchant:来自 Google Play 的 originalJson(整段购买凭据 JSON 字符串)。
- sample:来自 Google Play 的 signature,服务端用其校验 merchant。
- asset:当前用户 id。
4.3 与客户端实现对应
- 内购成功后,从
GooglePlayPurchaseDetails.billingClientPurchase(PurchaseWrapper)取 originalJson、signature,分别填入请求体的 merchant、sample。 - createPayment 返回的 federation 填入 federation,当前用户 id 填入 asset。
5. 获取未核销订单
「未核销」指 Google Play 侧尚未被确认(isAcknowledged == false)的购买,通常出现在:上次支付成功后未完成 completePurchase、或未成功回调服务端即退出应用。
5.1 客户端如何获取
- 方法:
GooglePlayPurchaseService.getUnacknowledgedPurchases() - 实现:仅 Android,通过
InAppPurchaseAndroidPlatformAddition.queryPastPurchases()查询本地/缓存的购买,再筛选billingClientPurchase.isAcknowledged == false的项。 - 返回:
List<UnacknowledgedGooglePayPurchase>,每项包含orderId(Google 订单号)、productId、payload(purchaseData + signature,用于回调 body 的 merchant/sample)。
5.2 典型用法
- 应用启动时:调用
getUnacknowledgedPurchases(),若有未核销订单,补单流程会使用本地保存的「创建订单时的 federation」逐笔回调;无保存 federation 的订单会跳过。 - 注意:
queryPastPurchases不包含已消耗(consumed)的商品;未确认的消耗型商品会一直在列表中直到被确认或消耗。
6. 补单流程(自动)
客户端已实现完整补单:拉取未核销订单 → 用本地保存的创建订单时的 federation 逐笔回调 googlepay → 服务端返回 line == 'SUCCESS' 则 completePurchase 并刷新用户信息。补单必须使用创建订单时的订单 id,不能使用 Google 的 orderId。
6.1 创建订单 id 的持久化
- 每次调起内购前都会先 createPayment 拿到 federation(服务端订单 id)。
- 在发起
POST /v1/payment/googlepay前,将 Google orderId → federation 写入本地(SharedPreferences),供补单时使用。 - 回调成功或补单成功后删除对应映射。
6.2 入口与触发时机
| 项目 | 说明 |
|---|---|
| 方法 | GooglePlayPurchaseService.runOrderRecovery() |
| 触发时机 | ① 应用启动:main() 中 AuthService.loginComplete.then((_) => runOrderRecovery())② 进入充值页: RechargeScreen.initState 中调用 |
| 前置条件 | 仅 Android;已登录(UserState.userId 非空) |
6.3 流程步骤
- 若非 Android 或未登录,直接返回。
- 调用
getUnacknowledgedPurchases()获取未核销列表。 - 对每笔订单:用 Google orderId 查本地保存的 federation(创建订单时的订单 id);若无则跳过该笔并打日志。
- 用查到的 federation 调用
POST /v1/payment/googlepay。 - 若响应成功且
line == 'SUCCESS':completePurchase、删除该笔映射、标记需要刷新。 - 若有任意一笔补单成功,最后调用
refreshAccount()。
6.4 服务端约定
- 补单请求的 federation 与正常回调一致,均为 createPayment 返回的订单 id;服务端按订单 id 落单/去重。
- 校验逻辑与正常回调一致(sample=签名、merchant=购买凭据),成功时返回
line: 'SUCCESS'。
7. 代码位置速查
| 步骤 | 位置 |
|---|---|
| Buy 分支(第三方 vs 直接谷歌) | recharge_screen.dart:_onBuy → _runThirdPartyPayment / _runGooglePay |
| 创建订单 | PaymentApi.createPayment;调用处在 _createOrderAndOpenUrl |
| 调起内购并拿凭据 | GooglePlayPurchaseService.launchPurchaseAndReturnData(productId) |
| 回调 googlepay | PaymentApi.googlepay(...),在 _createOrderAndOpenUrl 内、内购成功后调用 |
| 凭据数据结构 | Android:GooglePlayPurchaseDetails.billingClientPurchase(orderId, originalJson, signature) |
| 获取未核销订单 | GooglePlayPurchaseService.getUnacknowledgedPurchases(),见第 5 节 |
| 补单流程 | GooglePlayPurchaseService.runOrderRecovery(),见第 6 节 |
8. 小结
- 创建订单:仅在选择「Google Pay」且第三方支付开启时调用 createPayment;拿到 federation 后才会调起谷歌支付并回调 googlepay。
- 调起谷歌支付:productId 固定为当前商品的 code(helm),与 Play 后台产品 ID 一致。
- 回调 googlepay:body 为四字段 sample(signature)、merchant(purchaseData/originalJson)、federation(订单 id)、asset(userId);federation 为空则不回调,可按策略重试创建订单或提示失败。
- 未核销订单:通过
getUnacknowledgedPurchases()获取isAcknowledged == false的购买;每项含purchaseDetails用于补单成功后completePurchase。 - 补单:
runOrderRecovery()在应用启动(登录完成后)与进入充值页时执行;补单使用的 federation 为创建订单时的订单 id(内购前会持久化 Google orderId→federation,补单时按 Google orderId 取回);无保存 federation 的未核销订单会跳过。