# Flutter App 通知与钱包余额对接 本文只定义 App 需要对接的 HTTP 接口和 IM 私有事件契约,不约束 Flutter 端内部状态管理实现。 ## 通用 HTTP 约定 ### Base URL ```text https://{api_host} ``` 本地开发: ```text http://127.0.0.1:13000 ``` ### Headers | Header | 必填 | 说明 | | -------------------------------------- | --------- | -------------------------------------------- | | `Authorization: Bearer {access_token}` | 是 | 登录后后端返回的 App 访问令牌 | | `Content-Type: application/json` | POST 必填 | 请求体为 JSON | | `X-App-Code` | 否 | 多 App 包场景可传;通常由 gateway 按包名解析 | | `X-App-Platform` | 否 | `android` / `ios`,部分钱包商品接口使用 | ### 响应 Envelope 所有 `/api/v1` 业务接口使用统一响应: ```json { "code": "OK", "message": "ok", "request_id": "req_xxx", "data": {} } ``` 失败时没有 `data`: ```json { "code": "UNAUTHORIZED", "message": "unauthorized", "request_id": "req_xxx" } ``` ## 错误代码 | HTTP | code | message | 说明 | | ---- | ---------------------- | ------------------------ | ------------------------------- | | 200 | `OK` | `ok` | 成功 | | 400 | `INVALID_JSON` | `invalid json` | JSON 格式错误 | | 400 | `INVALID_ARGUMENT` | `invalid argument` | 参数缺失或非法 | | 401 | `UNAUTHORIZED` | `unauthorized` | 未登录、token 过期或 token 无效 | | 403 | `PROFILE_REQUIRED` | `profile required` | 用户资料未完成 | | 403 | `PERMISSION_DENIED` | `permission denied` | 无权限 | | 404 | `NOT_FOUND` | `not found` | 资源不存在 | | 409 | `INSUFFICIENT_BALANCE` | `insufficient balance` | 金币/余额不足 | | 409 | `LEDGER_CONFLICT` | `conflict` | 钱包幂等冲突 | | 409 | `IDEMPOTENCY_CONFLICT` | `conflict` | `command_id` 重复但请求内容不同 | | 409 | `ROOM_CLOSED` | `room closed` | 房间已关闭 | | 409 | `CONFLICT` | `conflict` | 当前状态不允许操作 | | 429 | `RATE_LIMITED` | `rate limited` | 触发限流 | | 500 | `INTERNAL_ERROR` | `internal error` | 服务端内部错误 | | 502 | `UPSTREAM_ERROR` | `upstream service error` | 内部依赖不可用 | App 处理建议:`UNAUTHORIZED` 重新登录;`UPSTREAM_ERROR` 可提示重试;`INSUFFICIENT_BALANCE` 引导充值;其它错误按页面语义提示。 ## 1. 获取腾讯 IM 登录票据 ```text GET /api/v1/im/usersig ``` ### Headers | Header | 必填 | | --------------- | ---- | | `Authorization` | 是 | ### Query 参数 无。 ### 返回值 `data` ```json { "sdk_app_id": 1400000000, "user_id": "10001", "user_sig": "xxx", "expire_at_ms": 1710000000000, "join_groups": [ { "group_id": "hy_lalu_bc_g", "type": "global_broadcast", "region_id": 0 } ] } ``` | 字段 | 类型 | 说明 | | -------------- | ------ | -------------------------------------- | | `sdk_app_id` | int64 | 腾讯云 IM AppID | | `user_id` | string | 腾讯云 IM identifier,直接用于 IM 登录 | | `user_sig` | string | 腾讯云 IM 登录票据 | | `expire_at_ms` | int64 | 票据过期时间,UTC epoch ms | | `join_groups` | array | App 登录 IM 后需要加入的服务端群 | ### 可能错误 `UNAUTHORIZED`、`PROFILE_REQUIRED`、`PERMISSION_DENIED`、`INTERNAL_ERROR`、`UPSTREAM_ERROR`。 ## 2. 获取我的钱包余额 ```text GET /api/v1/wallet/me/balances ``` ### Headers | Header | 必填 | | --------------- | ---- | | `Authorization` | 是 | ### Query 参数 | 参数 | 类型 | 必填 | 说明 | | ------------- | --------------- | ---- | --------------------------------------------- | | `asset_type` | string/repeated | 否 | 可重复传,也可逗号分隔;不传默认只返回 `COIN` | | `asset_types` | string/repeated | 否 | 同 `asset_type` | 示例: ```text GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=HOST_SALARY_USD&asset_type=AGENCY_SALARY_USD&asset_type=BD_SALARY_USD&asset_type=ADMIN_SALARY_USD ``` ### 返回值 `data` ```json { "balances": [ { "asset_type": "COIN", "available_amount": 1123123, "frozen_amount": 0, "version": 18 } ] } ``` | 字段 | 类型 | 说明 | | ------------------ | ------ | ------------------------------------------------- | | `asset_type` | string | `COIN` / `DIAMOND` / `HOST_SALARY_USD` / `AGENCY_SALARY_USD` / `BD_SALARY_USD` / `ADMIN_SALARY_USD` / `GIFT_POINT` | | `available_amount` | int64 | 可用余额 | | `frozen_amount` | int64 | 冻结余额 | | `version` | int64 | 该资产余额版本,用于和 IM 通知去重/防乱序 | ### 可能错误 `UNAUTHORIZED`、`PROFILE_REQUIRED`、`INVALID_ARGUMENT`、`UPSTREAM_ERROR`。 ## 3. 获取钱包首页摘要 ```text GET /api/v1/wallet/me/overview ``` ### Headers | Header | 必填 | | --------------- | ---- | | `Authorization` | 是 | ### Query 参数 无。 ### 返回值 `data` ```json { "balances": [ { "asset_type": "COIN", "available_amount": 1123123, "frozen_amount": 0, "version": 18 }, { "asset_type": "DIAMOND", "available_amount": 200, "frozen_amount": 0, "version": 4 }, { "asset_type": "HOST_SALARY_USD", "available_amount": 1500, "frozen_amount": 0, "version": 2 } ], "feature_flags": { "recharge_enabled": true, "diamond_exchange_enabled": true } } ``` | 字段 | 类型 | 说明 | | ---------------------------------------- | ----- | -------------------------- | | `balances` | array | 钱包首页需要展示的资产余额 | | `feature_flags.recharge_enabled` | bool | 是否允许充值 | | `feature_flags.diamond_exchange_enabled` | bool | 是否允许钻石兑换 | ### 可能错误 `UNAUTHORIZED`、`PROFILE_REQUIRED`、`UPSTREAM_ERROR`。 ## 4. 送礼 ```text POST /api/v1/rooms/gift/send ``` ### Headers | Header | 必填 | | -------------------------------- | ---- | | `Authorization` | 是 | | `Content-Type: application/json` | 是 | ### Body 参数 ```json { "room_id": "lalu_xxx", "command_id": "gift_1700000000000_10001", "target_type": "user", "target_user_ids": [10002], "gift_id": "car_001", "gift_count": 1 } ``` | 参数 | 类型 | 必填 | 说明 | | ----------------- | ------- | ---- | --------------------------------------------------------- | | `room_id` | string | 是 | 房间 ID | | `command_id` | string | 是 | 本次用户动作幂等键;HTTP 重试必须复用同一个值 | | `target_type` | string | 否 | 当前支持 `user`;后续扩展 `all_mic`、`all_room`、`couple` | | `target_user_ids` | int64[] | 是 | 收礼用户 ID;`target_type=user` 时必须且只能 1 个 | | `target_user_id` | int64 | 否 | 单目标兼容字段;新代码优先用 `target_user_ids` | | `gift_id` | string | 是 | 礼物 ID | | `gift_count` | int32 | 是 | 礼物数量 | ### 返回值 `data` ```json { "result": { "applied": true, "room_version": 14, "server_time_ms": 1710000000000 }, "billing_receipt_id": "br_xxx", "room_heat": 123456, "gift_rank": [], "room": {} } ``` | 字段 | 类型 | 说明 | | ----------------------- | ------ | ------------------------------------------ | | `result.applied` | bool | 本次命令是否实际落地;幂等重放可能为 false | | `result.room_version` | int64 | 房间版本 | | `result.server_time_ms` | int64 | 服务端时间 | | `billing_receipt_id` | string | 钱包扣费回执 | | `room_heat` | int64 | 房间热度 | | `gift_rank` | array | 房间礼物榜快照 | | `room` | object | 房间快照 | 送礼成功不代表 App 必须从该响应里更新钱包余额。金币余额以 `WalletBalanceChanged` IM 私有事件或 `/wallet/me/balances` 刷新结果为准。 ### 可能错误 `UNAUTHORIZED`、`PROFILE_REQUIRED`、`INVALID_ARGUMENT`、`INSUFFICIENT_BALANCE`、`ROOM_CLOSED`、`LEDGER_CONFLICT`、`IDEMPOTENCY_CONFLICT`、`UPSTREAM_ERROR`、`INTERNAL_ERROR`。 ## IM 私有事件:钱包余额变更 ### 事件名称 ```text WalletBalanceChanged ``` ### IM 通道 腾讯云 IM C2C 自定义消息。 | 字段 | 值 | | ------------------- | ------------------------------------------ | | `MsgType` | `TIMCustomElem` | | `Desc` | `WalletBalanceChanged` | | `Ext` / `extension` | `wallet_notice` | | `To_Account` | 当前登录用户的 IM identifier | | `CloudCustomData` | 与 `TIMCustomElem.Data` 相同的 JSON 字符串 | ### Headers 无。IM 私有事件不是 HTTP 请求,没有 HTTP header。 ### 参数 / Payload `TIMCustomElem.Data` 是 JSON 字符串: ```json { "event_type": "WalletBalanceChanged", "event_id": "wev_xxx", "app_code": "lalu", "transaction_id": "tx_xxx", "command_id": "gift_1700000000000_10001", "user_id": "10001", "asset_type": "COIN", "available_delta": -9900, "frozen_delta": 0, "available_after": 1123123, "frozen_after": 0, "balance_version": 18, "created_at_ms": 1710000000000, "source_created_at_ms": 1710000000000, "metadata": {} } ``` | 字段 | 类型 | 必填 | 说明 | | ---------------------- | ------ | ---- | -------------------------------- | | `event_type` | string | 是 | 固定 `WalletBalanceChanged` | | `event_id` | string | 是 | 事件幂等键 | | `app_code` | string | 是 | App 租户 | | `transaction_id` | string | 是 | 钱包交易 ID | | `command_id` | string | 是 | 触发该余额变化的业务命令 ID | | `user_id` | string | 是 | 当前用户 ID 字符串 | | `asset_type` | string | 是 | 资产类型 | | `available_delta` | int64 | 是 | 可用余额变化量 | | `frozen_delta` | int64 | 是 | 冻结余额变化量 | | `available_after` | int64 | 是 | 变化后的可用余额 | | `frozen_after` | int64 | 是 | 变化后的冻结余额 | | `balance_version` | int64 | 是 | 该用户该资产的新版本 | | `created_at_ms` | int64 | 是 | 钱包事件创建时间 | | `source_created_at_ms` | int64 | 是 | 源 outbox 创建时间 | | `metadata` | object | 否 | 业务附加信息,不作为余额展示依赖 | ### 返回值 无。该事件由服务端单向投递给 App,App 不需要回调后端确认。 ### 错误代码 无 HTTP 错误代码。App 本地处理规则: | 场景 | 处理 | | ------------------------------------------------- | -------------------------------------------- | | `Desc` 不是 `WalletBalanceChanged` | 忽略 | | `Ext/extension` 不是 `wallet_notice` | 忽略 | | JSON 解析失败 | 忽略并记录客户端日志 | | `event_id` 已处理 | 忽略 | | 本地 `asset_type` 的 `version >= balance_version` | 忽略 | | 长时间未收到通知 | 调 `GET /api/v1/wallet/me/balances` 兜底刷新 | ## App 最小对接顺序 1. 登录后调用 `GET /api/v1/im/usersig`。 2. 用返回的 `user_id/user_sig/sdk_app_id` 登录腾讯云 IM。 3. 调 `GET /api/v1/wallet/me/balances` 拉取初始余额。 4. 监听 C2C 自定义消息 `WalletBalanceChanged`。 5. 送礼调用 `POST /api/v1/rooms/gift/send`。 6. 送礼后的余额以 IM 私有事件或余额刷新接口为准。