170 lines
5.6 KiB
Markdown
170 lines
5.6 KiB
Markdown
# Flutter 礼物墙对接
|
||
|
||
本文描述 Flutter App 查询用户礼物墙的 HTTP 接口。礼物墙事实由 `wallet-service` 在送礼扣费成功后通过异步投影聚合,客户端只展示 gateway 返回的当前投影,不从房间消息或本地缓存推算收礼数量。
|
||
|
||
## 接口
|
||
|
||
`GET /api/v1/users/me/gift-wall`
|
||
|
||
`GET /api/v1/users/{user_id}/gift-wall`
|
||
|
||
登录接口,需要 `Authorization: Bearer <access_token>`。
|
||
|
||
- `GET /api/v1/users/me/gift-wall` 查询当前登录用户。
|
||
- `GET /api/v1/users/{user_id}/gift-wall` 查询资料卡目标用户,`user_id` 是路径参数,必须是正整数。
|
||
|
||
一致性边界:礼物墙是最终一致读模型,送礼成功后允许延迟数秒到几十秒才出现在接口结果中。客户端不要用该接口判断刚刚送礼是否成功;送礼成功状态仍以送礼接口响应和房间 IM 消息为准。
|
||
|
||
请求头:
|
||
|
||
| Header | 必填 | 说明 |
|
||
| --- | --- | --- |
|
||
| `Authorization` | 是 | App 登录 access token。 |
|
||
| `X-App-Code` | 否 | App 编码,默认 `lalu`。 |
|
||
|
||
示例:
|
||
|
||
```http
|
||
GET /api/v1/users/me/gift-wall
|
||
Authorization: Bearer <access_token>
|
||
X-App-Code: lalu
|
||
```
|
||
|
||
```http
|
||
GET /api/v1/users/10002/gift-wall
|
||
Authorization: Bearer <access_token>
|
||
X-App-Code: lalu
|
||
```
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"code": "OK",
|
||
"message": "ok",
|
||
"request_id": "req_abc",
|
||
"data": {
|
||
"items": [
|
||
{
|
||
"gift_id": "rose",
|
||
"gift_name": "Rose",
|
||
"resource_id": 11,
|
||
"resource": {
|
||
"resource_id": 11,
|
||
"resource_code": "gift_rose",
|
||
"resource_type": "gift",
|
||
"name": "Rose",
|
||
"status": "active",
|
||
"grantable": true,
|
||
"grant_strategy": "increase_quantity",
|
||
"usage_scopes": ["gift"],
|
||
"asset_url": "https://cdn.example/rose.png",
|
||
"preview_url": "",
|
||
"animation_url": "",
|
||
"metadata_json": "{}",
|
||
"sort_order": 10,
|
||
"created_at_ms": 1710000000000,
|
||
"updated_at_ms": 1710000000000
|
||
},
|
||
"resource_snapshot_json": "{\"ResourceID\":11}",
|
||
"gift_type_code": "normal",
|
||
"presentation_json": "{}",
|
||
"gift_count": 5,
|
||
"total_value": 50,
|
||
"total_coin_value": 50,
|
||
"total_diamond_value": 0,
|
||
"total_gift_point": 50,
|
||
"total_heat_value": 50,
|
||
"charge_asset_type": "COIN",
|
||
"last_price_version": "v1",
|
||
"first_received_at_ms": 1710000000000,
|
||
"last_received_at_ms": 1710000005000,
|
||
"sort_order": 10
|
||
}
|
||
],
|
||
"gift_kind_count": 1,
|
||
"gift_total_count": 5,
|
||
"total_value": 50,
|
||
"total_coin_value": 50,
|
||
"total_diamond_value": 0,
|
||
"total_gift_point": 50,
|
||
"total_heat_value": 50
|
||
}
|
||
}
|
||
```
|
||
|
||
字段规则:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `items` | 按 `gift_id` 聚合后的礼物列表,固定按后台礼物排序、价值和最近收礼时间排序。 |
|
||
| `gift_count` | 当前用户收到该礼物的累计个数。 |
|
||
| `gift_kind_count` | 当前用户收到过的礼物种类数,等于 `items.length`。 |
|
||
| `gift_total_count` | 当前用户收到的所有礼物数量总和。 |
|
||
| `total_value` | 礼物总价值,按送礼时服务端结算的 `charge_amount` 累加。 |
|
||
| `total_coin_value` / `total_diamond_value` | 按收费资产拆分的总价值;当前普通礼物通常只读 `total_coin_value`。 |
|
||
| `total_gift_point` | 主播礼物积分累计值,不等同于可提现余额。 |
|
||
| `total_heat_value` | 房间热度累计值,可用于展示贡献热度,不要反推价格。 |
|
||
| `charge_asset_type` | 该礼物历史上只用一种资产收费时为 `COIN` 或 `DIAMOND`;发生过资产切换时为 `MIXED`。 |
|
||
| `resource` | 礼物展示资源快照,客户端优先用 `asset_url` / `preview_url` / `animation_url` 渲染。 |
|
||
| `resource_snapshot_json` | 原始资源快照 JSON,主要用于客户端兜底和日志排查。 |
|
||
| `*_ms` | Unix epoch milliseconds;展示层按用户本地时区格式化。 |
|
||
|
||
## Flutter 解析示例
|
||
|
||
```dart
|
||
class GiftWall {
|
||
GiftWall({
|
||
required this.items,
|
||
required this.giftKindCount,
|
||
required this.giftTotalCount,
|
||
required this.totalValue,
|
||
});
|
||
|
||
final List<GiftWallItem> items;
|
||
final int giftKindCount;
|
||
final int giftTotalCount;
|
||
final int totalValue;
|
||
|
||
factory GiftWall.fromJson(Map<String, dynamic> json) {
|
||
return GiftWall(
|
||
items: (json['items'] as List<dynamic>? ?? const [])
|
||
.map((item) => GiftWallItem.fromJson(item as Map<String, dynamic>))
|
||
.toList(),
|
||
giftKindCount: (json['gift_kind_count'] as num?)?.toInt() ?? 0,
|
||
giftTotalCount: (json['gift_total_count'] as num?)?.toInt() ?? 0,
|
||
totalValue: (json['total_value'] as num?)?.toInt() ?? 0,
|
||
);
|
||
}
|
||
}
|
||
|
||
class GiftWallItem {
|
||
GiftWallItem({
|
||
required this.giftId,
|
||
required this.giftName,
|
||
required this.giftCount,
|
||
required this.totalValue,
|
||
required this.assetUrl,
|
||
});
|
||
|
||
final String giftId;
|
||
final String giftName;
|
||
final int giftCount;
|
||
final int totalValue;
|
||
final String assetUrl;
|
||
|
||
factory GiftWallItem.fromJson(Map<String, dynamic> json) {
|
||
final resource = json['resource'] as Map<String, dynamic>? ?? const {};
|
||
return GiftWallItem(
|
||
giftId: json['gift_id'] as String? ?? '',
|
||
giftName: json['gift_name'] as String? ?? '',
|
||
giftCount: (json['gift_count'] as num?)?.toInt() ?? 0,
|
||
totalValue: (json['total_value'] as num?)?.toInt() ?? 0,
|
||
assetUrl: resource['asset_url'] as String? ?? '',
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
调用时先解析外层 envelope:只有 `code == "OK"` 时读取 `data`;其他错误按统一网关错误处理,并把 `request_id` 写入客户端日志。
|