hyapp-server/docs/flutter对接/Google Play充值Flutter对接.md
2026-05-26 01:02:12 +08:00

13 KiB
Raw Permalink Blame History

Flutter Google Play 充值对接

本文描述 Android App 接入 Google Play 一次性金币充值的 Flutter 调用流程。Flutter 只负责购买流程和提交 Google purchase token订单校验、到账金额、入账、幂等和消费确认都以后端 wallet-service 为准。

所有 gateway HTTP 接口都返回统一外壳:

{
  "code": "OK",
  "message": "ok",
  "request_id": "server-generated-request-id",
  "data": {}
}

Flutter 只有在 code == "OK" 时读取 data。其他错误必须记录 request_id,方便后端排查。

1. 当前实现状态

已实现:

  • GET /api/v1/wallet/recharge/productsApp 端充值商品列表。
  • POST /api/v1/wallet/payments/google/confirm:提交 Google purchase token后端调用 Google Play Developer API 校验、入账、写支付订单审计并 consume。
  • GET /api/v1/wallet/me/balances:刷新金币余额。
  • 钱包余额 IM 通知:WalletBalanceChanged
  • payment_orders:保存 Google provider 订单审计purchase token 只落 SHA256不落明文。

后端待补齐:

  • Google Play RTDN / Pub/Sub 回调:只做补偿通知,不替代客户端确认接口。

Flutter 可以按本文契约联调商品列表、本地 Google Billing 流程和确认支付接口。

2. Flutter 依赖

建议使用 Flutter 官方维护的 in_app_purchase 插件接 Google Play Billing。

pubspec.yaml

dependencies:
  in_app_purchase: <compatible_version>

Android 真机联调要求:

  • App 使用正式包名和签名,包名必须与 Play Console App 一致。
  • 测试账号加入 Play Console license testing 或内部测试轨道。
  • Play Console 中的一次性商品必须激活,并覆盖测试账号所在地区。

3. 获取充值商品列表

GET /api/v1/wallet/recharge/products

登录接口,需要 Authorization: Bearer <access_token>。gateway 会根据当前登录用户资料里的 region_id 过滤商品Flutter 不传用户 ID 和区域 ID。

请求头:

Header 必填 说明
Authorization App 登录 access token。
X-App-Code App 编码,默认 lalu
X-App-Platform 固定传 android。也可以用 query platform=android 覆盖。

示例:

GET /api/v1/wallet/recharge/products
Authorization: Bearer <access_token>
X-App-Code: lalu
X-App-Platform: android

成功响应:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {
    "channels": ["google"],
    "products": [
      {
        "product_id": 11,
        "product_code": "iap_android_11",
        "product_name": "1500 Coins",
        "description": "coin pack",
        "platform": "android",
        "channel": "google",
        "currency_code": "USDT",
        "coin_amount": 1500,
        "amount_minor": 1500000,
        "amount_micro": 1500000,
        "policy_version": "",
        "region_ids": [2002],
        "resource_asset_type": "COIN",
        "status": "active",
        "enabled": true,
        "sort_order": 0
      }
    ]
  }
}

字段规则:

字段 Flutter 用途
product_id 后端本地商品 ID提交确认支付时原样带回。
product_code 当前作为 Google Play productId 使用。必须与 Play Console 商品 ID 完全一致。
channel Android Google 支付固定为 google
coin_amount 到账金币数,展示和到账都以后端为准。
amount_micro / amount_minor 后台配置金额仅用于后台审计Google 支付价格展示必须用 Google ProductDetails
resource_asset_type 当前固定为 COIN
enabled / status 只展示 enabled=true && status=active 的商品。

注意:当前后端 product_code 是自动生成值,例如 iap_android_11。如果 Play Console 已经创建了其他商品 ID例如 coins_150000_usd499,后端需要先新增 google_product_idprovider_product_id 字段Flutter 不要自行硬编码映射。

4. 查询 Google ProductDetails

Flutter 拿到后端商品列表后,只对 channel=google 且启用的商品查询 Google Play

final Set<String> googleProductIds = backendProducts
    .where((item) => item.channel == 'google' && item.enabled)
    .map((item) => item.productCode)
    .where((value) => value.isNotEmpty)
    .toSet();

final ProductDetailsResponse response =
    await InAppPurchase.instance.queryProductDetails(googleProductIds);

展示合并规则:

  • 金币数量用后端 coin_amount
  • 商品是否展示以后端上架状态和 Google 查询结果共同决定。
  • 价格文案用 Google ProductDetails.price,不要用后端 amount_micro
  • 如果后端商品存在但 Google 查不到 ProductDetails,该商品不展示,并记录客户端日志。

合并后的本地模型建议保留:

class GoogleRechargeSku {
  GoogleRechargeSku({
    required this.productId,
    required this.googleProductId,
    required this.coinAmount,
    required this.productDetails,
  });

  final int productId;
  final String googleProductId;
  final int coinAmount;
  final ProductDetails productDetails;
}

5. 发起购买

金币充值是 consumable。为了避免客户端先消费导致后端未入账Android 不要自动 consume后端校验并入账后由后端调用 Google consume。

final PurchaseParam purchaseParam = PurchaseParam(
  productDetails: sku.productDetails,
);

await InAppPurchase.instance.buyConsumable(
  purchaseParam: purchaseParam,
  autoConsume: false,
);

Flutter 必须监听购买流:

late final StreamSubscription<List<PurchaseDetails>> _purchaseSub;

void startPurchaseListener() {
  _purchaseSub = InAppPurchase.instance.purchaseStream.listen(
    _handlePurchases,
    onError: (Object error, StackTrace stackTrace) {
      // 记录错误并恢复按钮状态。
    },
  );
}

状态处理:

PurchaseStatus Flutter 行为
pending 展示处理中,不提交后端入账。
purchased 提交后端确认接口。
restored Android 一次性消耗品通常不依赖 restored可以按补单逻辑提交后端去重。
error 展示失败,恢复按钮。
canceled 用户取消,不提示错误。

6. 确认 Google 支付

后端已实现接口:

POST /api/v1/wallet/payments/google/confirm

登录接口。Flutter 在 PurchaseStatus.purchased 后调用。接口必须可重复调用;同一个 purchase token 重复提交只能返回同一笔入账结果。

请求头:

Header 必填 说明
Authorization App 登录 access token。
X-App-Code App 编码,默认 lalu
X-App-Platform 固定传 android

请求体:

{
  "command_id": "google-pay-<stable-id>",
  "product_id": 11,
  "product_code": "iap_android_11",
  "package_name": "com.example.hyapp",
  "purchase_token": "google_purchase_token",
  "order_id": "GPA.1111-2222-3333-44444",
  "purchase_time_ms": 1710000000000
}

字段规则:

字段 必填 说明
command_id 客户端幂等 ID。同一个 purchase token 重试建议使用同一个值。后端最终还会按 purchase token 去重。
product_id 后端商品列表返回的本地商品 ID。
product_code 后端商品列表返回的 Google productId。
package_name 当前 Android 包名。
purchase_token PurchaseDetails.verificationData.serverVerificationData
order_id 插件能拿到时传;后端最终以 Google API 查询结果为准。
purchase_time_ms 客户端购买时间,仅用于日志。

command_id 建议由 token 派生,确保重试稳定:

final String commandId = 'google-pay-${sha256Of(purchaseToken).substring(0, 24)}';

成功响应:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {
    "payment_order_id": "gpay_xxx",
    "transaction_id": "wtx_xxx",
    "status": "credited",
    "product_id": 11,
    "product_code": "iap_android_11",
    "coin_amount": 1500,
    "balance": {
      "asset_type": "COIN",
      "available_amount": 12800,
      "frozen_amount": 0,
      "version": 9
    },
    "idempotent_replay": false,
    "consume_state": "consumed"
  }
}

Flutter 成功处理:

  • status=credited:展示充值成功,刷新 /wallet/me/balances
  • idempotent_replay=true:按成功处理,不重复提示异常。
  • consume_state=consumed:后端已完成 Google consume。
  • consume_state=consume_pending:仍按到账成功处理;说明入账已成功但后端 consume 调用短时失败,下一次提交同一 token 会继续尝试 consume。
  • 后端成功后调用 InAppPurchase.instance.completePurchase(purchaseDetails),结束客户端 pending 状态。

失败处理:

  • UNAUTHORIZED:登录失效,走登录态恢复。
  • INVALID_ARGUMENT:商品 ID、token、包名等参数错误或 Google 判定 token 无效,记录 request_id
  • CONFLICT商品未上架、商品区域不匹配、Google purchase state 不是 PURCHASED、Google productId/orderId 与后端商品不一致;不入账。
  • UPSTREAM_ERROR / INTERNAL_ERROR:保留订单为待确认,稍后重试。
  • provider 校验失败:不入账,不 complete purchase记录 request_id 并触发补单或客服入口。

后端校验和审计规则:

  • 后端使用 Google Play Developer API purchases.productsv2.getproductpurchasev2 查询 purchase token只接受 purchaseState=PURCHASED
  • Google 返回的 productLineItem[0].productId 必须等于后端商品 product_code
  • 后端以 payment_orders(app_code, provider, purchase_token_hash) 做 provider 幂等,同一个 token 不会重复发币。
  • 后端入账成功后写 wallet_transactionswallet_entrieswallet_recharge_recordspayment_orderswallet_outbox
  • 后端不会记录明文 purchase_token,只保存 SHA256 hash 和 Google 返回快照。

7. 补单规则

Flutter 必须做补单,因为用户可能在支付成功后断网、杀进程或后端短时失败。

触发时机:

  • App 启动后。
  • 登录成功后。
  • 进入钱包页后。
  • 购买确认接口返回 5xx 或网络失败后延迟重试。

处理规则:

  1. 查询 Google Play 当前未完成购买。
  2. 对每个 purchase token 查本地 pending 队列。
  3. 重新调用 /api/v1/wallet/payments/google/confirm
  4. 后端返回 credited 后 complete purchase。

本地 pending 记录建议保存:

{
  "product_id": 11,
  "product_code": "iap_android_11",
  "purchase_token_hash": "sha256-token",
  "purchase_token": "secure-local-token",
  "command_id": "google-pay-xxxx",
  "created_at_ms": 1710000000000,
  "last_error": "network timeout"
}

purchase_token 属于敏感数据。客户端日志不要打印明文 token本地持久化也应尽量缩短保留时间。

8. 余额刷新

充值确认成功后Flutter 需要主动刷新余额:

GET /api/v1/wallet/me/balances?asset_type=COIN

成功响应:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {
    "balances": [
      {
        "asset_type": "COIN",
        "available_amount": 12800,
        "frozen_amount": 0,
        "version": 9
      }
    ]
  }
}

后端也会通过 IM 私信发送 WalletBalanceChanged,但 Flutter 不应只依赖 IM 通知。充值确认成功后主动刷新一次IM 只作为实时补偿。

9. Flutter 页面流程

推荐顺序:

  1. 进入钱包页,调用 /wallet/recharge/products
  2. 用返回的 product_code 查询 Google ProductDetails
  3. 合并后展示充值档位。
  4. 用户点击档位,调用 buyConsumable(autoConsume: false)
  5. purchase stream 收到 purchased
  6. 调用 /wallet/payments/google/confirm
  7. 后端返回 credited
  8. 调用 completePurchase
  9. 刷新 /wallet/me/balances
  10. 清理本地 pending 记录。

10. 不允许的做法

  • 不允许 Flutter 根据本地配置决定到账金币数。
  • 不允许 Flutter 只看 Google 支付成功就本地加余额。
  • 不允许客户端自动 consume 后再提交后端。
  • 不允许把 purchase_token、Google 原始订单响应打印到普通日志。
  • 不允许用价格文案匹配商品;必须用 product_code / Google productId 匹配。

11. 与当前 Flutter 代码的差异

当前 hyapp-flutter 钱包页充值档位仍是本地假数据,后续需要替换为:

  • 新增 wallet recharge product API model。
  • WalletLogic.onInit 拉取 /wallet/recharge/products
  • 查询 Google ProductDetails 并合并展示。
  • topUp() 从占位提示改为 Google Billing 购买。
  • 新增 purchase stream listener 和 pending purchase 补单队列。