hyapp-server/docs/flutter对接/Flutter App 通知与钱包余额对接.md

386 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 | 否 | 业务附加信息,不作为余额展示依赖 |
### 返回值
无。该事件由服务端单向投递给 AppApp 不需要回调后端确认。
### 错误代码
无 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 私有事件或余额刷新接口为准。