hyapp-server/docs/flutter对接/Fami积分兑换金币Flutter对接.md
2026-07-20 16:48:30 +08:00

6.7 KiB
Raw Blame History

Fami 积分兑换金币 Flutter 对接

本文描述 Fami/Huwaa App 查看 POINT 兑换 COIN 比例和执行兑换的 HTTP 接口。页面文案可将 POINT 展示为“钻石积分”,但 Flutter 模型和日志建议保留真实资产类型。

1. 通用约定

Base URL 由 App 环境配置提供:

线上https://api.global-interaction.com
本地http://127.0.0.1:13000

请求头:

Header 必填 说明
Authorization: Bearer {access_token} 当前登录用户 token。
X-App-Code: fami Fami App 固定传 fami;不允许用户在页面切换 App。
Content-Type: application/json POST 是 兑换请求体为 JSON。

成功 envelope

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {}
}

Flutter 只在 code == "OK" 时解析 data;失败日志记录 path、coderequest_id,不记录 token。

2. 查看积分兑换比例

GET /api/v1/point-wallet/exchange-config
Authorization: Bearer {access_token}
X-App-Code: fami

无 query 和 body。地区由 gateway 根据当前登录用户资料确定Flutter 不提交 region_id

成功响应:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_config",
  "data": {
    "source_asset_type": "POINT",
    "target_asset_type": "COIN",
    "points_per_usd": 1000000,
    "coins_per_usd": 80000,
    "ratio_point_amount": 1000000,
    "ratio_coin_amount": 80000,
    "rounding_mode": "floor",
    "policy_instance_code": "fami_salary_policy_202607"
  }
}

字段说明:

字段 说明
points_per_usd 当前用户地区已发布工资政策中的 POINT/USD 比例。
coins_per_usd 统一工资兑金币比例,当前为 80000
ratio_point_amount / ratio_coin_amount Flutter 直接展示的 POINT → COIN 比例。
rounding_mode 目标金币使用整数向下取整。
policy_instance_code 已发布政策快照编码,可用于客户端调试日志。

预览公式:

coinAmount = floor(pointAmount * ratio_coin_amount / ratio_point_amount)

预览只用于界面展示,不能作为入账事实。如果 Admin 在用户确认前发布了新政策,兑换接口会在账变事务内重新读取当前政策。

3. 积分兑换金币

POST /api/v1/point-wallet/exchange-to-coins
Authorization: Bearer {access_token}
X-App-Code: fami
Content-Type: application/json

请求参数:

字段 类型 必填 说明
command_id string 最大 128 字节的幂等键,建议 point_exchange_ + UUID v4。
point_amount int64 要扣减的 POINT 整数,必须大于 0。

客户端不提交 user_idregion_idcoin_amount、兑换比例或政策编码。

请求示例:

{
  "command_id": "point_exchange_550e8400-e29b-41d4-a716-446655440000",
  "point_amount": 1000000
}

成功响应:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_exchange",
  "data": {
    "transaction_id": "fami_xxx",
    "point_amount": 1000000,
    "coin_amount": 80000,
    "point_balance_after": 2500000,
    "coin_balance_after": 180000,
    "ratio_point_amount": 1000000,
    "ratio_coin_amount": 80000
  }
}

账务语义:

  • wallet-service 在同一 MySQL 事务中扣减本人 POINT 并增加本人 COIN
  • Flutter 成功后必须用 point_balance_aftercoin_balance_after 覆盖本地余额,不要用预览值自行加减。
  • 超时或 5xx 重试必须复用原 command_id 和完全相同的 point_amount。用户发起新一次兑换时生成新 command_id
  • 最终兑换比例以成功回执的 ratio_point_amount / ratio_coin_amount 为准。

4. Flutter DTO 示例

class PointCoinExchangeConfig {
  const PointCoinExchangeConfig({
    required this.pointsPerUsd,
    required this.coinsPerUsd,
    required this.ratioPointAmount,
    required this.ratioCoinAmount,
    required this.roundingMode,
    required this.policyInstanceCode,
  });

  final int pointsPerUsd;
  final int coinsPerUsd;
  final int ratioPointAmount;
  final int ratioCoinAmount;
  final String roundingMode;
  final String policyInstanceCode;

  int previewCoins(int pointAmount) =>
      pointAmount * ratioCoinAmount ~/ ratioPointAmount;

  factory PointCoinExchangeConfig.fromJson(Map<String, dynamic> json) {
    return PointCoinExchangeConfig(
      pointsPerUsd: (json['points_per_usd'] as num).toInt(),
      coinsPerUsd: (json['coins_per_usd'] as num).toInt(),
      ratioPointAmount: (json['ratio_point_amount'] as num).toInt(),
      ratioCoinAmount: (json['ratio_coin_amount'] as num).toInt(),
      roundingMode: json['rounding_mode'] as String? ?? 'floor',
      policyInstanceCode: json['policy_instance_code'] as String? ?? '',
    );
  }
}

class PointCoinExchangeReceipt {
  const PointCoinExchangeReceipt({
    required this.transactionId,
    required this.pointAmount,
    required this.coinAmount,
    required this.pointBalanceAfter,
    required this.coinBalanceAfter,
    required this.ratioPointAmount,
    required this.ratioCoinAmount,
  });

  final String transactionId;
  final int pointAmount;
  final int coinAmount;
  final int pointBalanceAfter;
  final int coinBalanceAfter;
  final int ratioPointAmount;
  final int ratioCoinAmount;

  factory PointCoinExchangeReceipt.fromJson(Map<String, dynamic> json) {
    return PointCoinExchangeReceipt(
      transactionId: json['transaction_id'] as String? ?? '',
      pointAmount: (json['point_amount'] as num).toInt(),
      coinAmount: (json['coin_amount'] as num).toInt(),
      pointBalanceAfter: (json['point_balance_after'] as num).toInt(),
      coinBalanceAfter: (json['coin_balance_after'] as num).toInt(),
      ratioPointAmount: (json['ratio_point_amount'] as num).toInt(),
      ratioCoinAmount: (json['ratio_coin_amount'] as num).toInt(),
    );
  }
}

5. 常见错误

HTTP code Flutter 处理
400 INVALID_ARGUMENT point_amount/command_id 不合法,阻止提交并记录客户端参数问题。
401 UNAUTHORIZED 进入统一刷新 token/重新登录流程。
403 PERMISSION_DENIED 当前 App 不支持,或用户地区没有已发布兑换政策;隐藏兑换入口。
409 INSUFFICIENT_BALANCE 刷新 POINT 余额并提示余额不足。
409 IDEMPOTENCY_CONFLICT 同一 command_id 被用于不同参数;不要自动换键重试。
502/503 UPSTREAM_ERROR / UNAVAILABLE 保留原 command_id,允许用户重试原操作。

6. 相关 IM

无。积分兑换是私有钱包账变,不发送房间或 C2C IM。App 以 HTTP 成功回执更新当前页面,其他页面进入时重新读取钱包余额。