386 lines
14 KiB
Markdown
386 lines
14 KiB
Markdown
# 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 私有事件或余额刷新接口为准。
|