6.7 KiB
6.7 KiB
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、code 和 request_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_id、region_id、coin_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_after和coin_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 成功回执更新当前页面,其他页面进入时重新读取钱包余额。