增加礼物tab接口
This commit is contained in:
parent
ccd1967242
commit
9040b4b161
529
docs/flutter对接/礼物TabFlutter对接.md
Normal file
529
docs/flutter对接/礼物TabFlutter对接.md
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
# 礼物 Tab Flutter 对接
|
||||||
|
|
||||||
|
本文描述 Flutter App 在语音房内接入礼物 Tab 的接口、字段、交互和 IM 同步规则。礼物配置、价格、扣费、房间表现和统计事实都以后端为准;App 只展示 gateway 返回的数据,不在本地推算价格、余额、热度或礼物榜。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
- 礼物 Tab 基础配置入口是 `GET /api/v1/gift-tabs`,不需要 `room_id`,只返回后台启用的 Tab 配置。
|
||||||
|
- `GET /api/v1/gift-tabs` 不按礼物数量过滤;只要后台启用了 Tab,即使该 Tab 下当前没有礼物也必须返回给 App 展示。
|
||||||
|
- 房间送礼面板入口是 `GET /api/v1/rooms/{room_id}/gift-panel`,返回当前房间可送礼物、可收礼人、金币余额和房间内可展示 Tab。
|
||||||
|
- `GET /api/v1/gift-tabs` 不能替代 `GET /api/v1/rooms/{room_id}/gift-panel`;App 真正送礼前必须以房间面板返回的数据为准。
|
||||||
|
- 送礼只调用 `POST /api/v1/rooms/gift/send`。
|
||||||
|
- 金币余额以礼物面板、送礼响应后的钱包通知或 `GET /api/v1/wallet/me/balances` 为准。
|
||||||
|
- 房间内其他用户看到的礼物表现以腾讯云 IM 自定义消息 `room_gift_sent` 为准。
|
||||||
|
- `command_id` 是送礼幂等键;同一次用户点击重试必须复用同一个值。
|
||||||
|
- 当前 App 送礼主流程只按单个用户收礼处理:`target_type=user`,`target_user_ids` 只传 1 个用户。
|
||||||
|
|
||||||
|
## 交互流程
|
||||||
|
|
||||||
|
1. 用户进入房间并完成 `JoinRoom`。
|
||||||
|
2. App 可在启动或进入房间前调用 `GET /api/v1/gift-tabs` 预加载 Tab 配置。
|
||||||
|
3. 打开礼物 Tab 时调用 `GET /api/v1/rooms/{room_id}/gift-panel`。
|
||||||
|
4. 用 `tabs` 渲染当前房间分类,用 `gifts` 渲染礼物网格,用 `recipients` 渲染收礼人。
|
||||||
|
5. 用户选择收礼人、礼物和数量。
|
||||||
|
6. 点击发送时生成稳定 `command_id`,调用 `POST /api/v1/rooms/gift/send`。
|
||||||
|
7. 发送成功后用响应里的 `room_heat`、`gift_rank`、`treasure` 更新房间 UI。
|
||||||
|
8. 监听房间 IM `room_gift_sent`,同步播放礼物动效。
|
||||||
|
9. 监听私有钱包通知或主动刷新余额,不本地扣减余额作为最终值。
|
||||||
|
|
||||||
|
## 获取礼物 Tab 配置
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/gift-tabs
|
||||||
|
X-App-Code: lalu
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"key": "normal",
|
||||||
|
"gift_type_code": "normal",
|
||||||
|
"name": "普通礼物",
|
||||||
|
"label": "Gift",
|
||||||
|
"tab_key": "Gift",
|
||||||
|
"status": "active",
|
||||||
|
"order": 10,
|
||||||
|
"sort_order": 10,
|
||||||
|
"created_at_ms": 1710000000000,
|
||||||
|
"updated_at_ms": 1710000000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `key` | string | Tab 稳定 key,等同于礼物类型编码。 |
|
||||||
|
| `gift_type_code` | string | 礼物类型编码,用于和 `gifts[].gift_type_code` 对齐。 |
|
||||||
|
| `name` | string | 后台配置的中文显示名。 |
|
||||||
|
| `label` | string | App Tab 展示名,来源于后台 `tab_key`。 |
|
||||||
|
| `tab_key` | string | 后台配置的 Tab Name。 |
|
||||||
|
| `status` | string | 当前只返回启用中的配置,正常为 `active`。 |
|
||||||
|
| `order` | int | App 排序值,等同于 `sort_order`。 |
|
||||||
|
| `sort_order` | int | 后台配置排序值。 |
|
||||||
|
| `created_at_ms` | int64 | 创建时间,Unix epoch milliseconds。 |
|
||||||
|
| `updated_at_ms` | int64 | 更新时间,Unix epoch milliseconds。 |
|
||||||
|
|
||||||
|
## 获取礼物面板
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/rooms/{room_id}/gift-panel
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
```
|
||||||
|
|
||||||
|
路径参数:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `room_id` | string | 是 | 当前语音房 ID。 |
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"coin_balance": 1000000,
|
||||||
|
"recipients": [
|
||||||
|
{
|
||||||
|
"target_type": "user",
|
||||||
|
"user_id": "10002",
|
||||||
|
"seat_no": 1,
|
||||||
|
"label": "Alice",
|
||||||
|
"profile": {
|
||||||
|
"user_id": "10002",
|
||||||
|
"username": "Alice",
|
||||||
|
"avatar": "https://cdn.example/avatar.png",
|
||||||
|
"display_user_id": "10002",
|
||||||
|
"vip": {},
|
||||||
|
"level": {},
|
||||||
|
"badges": [],
|
||||||
|
"avatar_frame": {},
|
||||||
|
"vehicle": {},
|
||||||
|
"charm": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "all",
|
||||||
|
"order": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "normal",
|
||||||
|
"gift_type_code": "normal",
|
||||||
|
"label": "normal",
|
||||||
|
"order": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gifts": [
|
||||||
|
{
|
||||||
|
"gift_id": "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": "https://cdn.example/rose.svga",
|
||||||
|
"metadata_json": "{}",
|
||||||
|
"sort_order": 10,
|
||||||
|
"created_at_ms": 1710000000000,
|
||||||
|
"updated_at_ms": 1710000000000
|
||||||
|
},
|
||||||
|
"status": "active",
|
||||||
|
"name": "Rose",
|
||||||
|
"sort_order": 10,
|
||||||
|
"presentation_json": "{}",
|
||||||
|
"price_version": "v1",
|
||||||
|
"gift_type_code": "normal",
|
||||||
|
"charge_asset_type": "COIN",
|
||||||
|
"coin_price": 100,
|
||||||
|
"gift_point_amount": 100,
|
||||||
|
"heat_value": 100,
|
||||||
|
"effect_types": ["static", "svga"],
|
||||||
|
"region_ids": [688],
|
||||||
|
"created_at_ms": 1710000000000,
|
||||||
|
"updated_at_ms": 1710000000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quantity_presets": [1, 9, 99, 999]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `coin_balance` | 当前登录用户 COIN 可用余额快照。只用于打开面板时展示,最终余额仍以后续钱包事实为准。 |
|
||||||
|
| `recipients` | 当前房间可收礼人。当前主流程只启用 `target_type=user` 的单个用户。 |
|
||||||
|
| `tabs` | 礼物分类。`key=all` 是全量分类;其他 tab 通常对应 `gift_type_code`。 |
|
||||||
|
| `gifts` | 当前房间国家维度可用的 active 礼物列表。 |
|
||||||
|
| `quantity_presets` | 数量快捷档位,当前固定 `[1,9,99,999]`。 |
|
||||||
|
|
||||||
|
礼物字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `gift_id` | 送礼请求使用的礼物 ID。 |
|
||||||
|
| `name` | 展示名称。 |
|
||||||
|
| `gift_type_code` | 分类编码,例如 `normal`、`lucky`、`super_lucky`。 |
|
||||||
|
| `charge_asset_type` | 当前收费资产;普通金币礼物为 `COIN`。 |
|
||||||
|
| `coin_price` | 单个礼物金币价格。仅用于展示和按钮预估,扣费以后端为准。 |
|
||||||
|
| `gift_point_amount` | 主播收到的礼物积分增量口径。 |
|
||||||
|
| `heat_value` | 房间热度增量口径。 |
|
||||||
|
| `price_version` | 后端价格版本。App 不需要上传。 |
|
||||||
|
| `presentation_json` | 展示扩展 JSON,例如幸运礼物奖池 ID、角标、动效配置;解析失败时按空对象处理。 |
|
||||||
|
| `effect_types` | 可用动效类型。App 根据本地能力选择图片、SVGA、Lottie 或兜底静态图。 |
|
||||||
|
| `resource.asset_url` | 礼物图标。 |
|
||||||
|
| `resource.preview_url` | 预览图,可为空。 |
|
||||||
|
| `resource.animation_url` | 礼物动效资源,可为空。 |
|
||||||
|
| `region_ids` | 后端配置的可用区域;面板接口已经按房间区域过滤,App 不再二次过滤。 |
|
||||||
|
|
||||||
|
## Tab 渲染规则
|
||||||
|
|
||||||
|
- `tabs` 按 `order` 升序展示。
|
||||||
|
- `key=all` 展示全部 `gifts`。
|
||||||
|
- 非 all tab 用 `tab.gift_type_code` 或 `tab.key` 过滤 `gift.gift_type_code`。
|
||||||
|
- 礼物网格按 `gift.sort_order` 升序展示;相同排序保持接口返回顺序。
|
||||||
|
- `coin_price * selected_count > coin_balance` 时发送按钮置灰或引导充值,但最终仍以送礼接口错误为准。
|
||||||
|
- `resource.animation_url` 只在发送成功或收到 IM 后播放,不要在列表滚动中自动播放重动效。
|
||||||
|
|
||||||
|
## 送礼
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/gift/send
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"room_id": "lalu_room_001",
|
||||||
|
"command_id": "gift_1779259000000_10001_rose_01",
|
||||||
|
"target_type": "user",
|
||||||
|
"target_user_ids": [10002],
|
||||||
|
"gift_id": "rose",
|
||||||
|
"gift_count": 9
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `room_id` | string | 是 | 当前房间 ID。 |
|
||||||
|
| `command_id` | string | 是 | 本次送礼动作幂等键。同一次失败重试必须复用。 |
|
||||||
|
| `target_type` | string | 否 | 当前传 `user`;为空后端按 `user`。 |
|
||||||
|
| `target_user_ids` | int64[] | 是 | 当前只能传 1 个收礼用户。 |
|
||||||
|
| `target_user_id` | int64 | 否 | 旧单目标兼容字段;新代码优先用 `target_user_ids`。 |
|
||||||
|
| `gift_id` | string | 是 | 礼物 ID。 |
|
||||||
|
| `gift_count` | int32 | 是 | 礼物数量,必须大于 0。 |
|
||||||
|
| `pool_id` | string | 否 | 幸运礼物奖池 ID;普通礼物不传。 |
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"result": {
|
||||||
|
"applied": true,
|
||||||
|
"room_version": 14,
|
||||||
|
"server_time_ms": 1779259000000
|
||||||
|
},
|
||||||
|
"billing_receipt_id": "br_abc",
|
||||||
|
"room_heat": 123456,
|
||||||
|
"gift_rank": [
|
||||||
|
{
|
||||||
|
"user_id": "10001",
|
||||||
|
"score": 900,
|
||||||
|
"gift_value": 900,
|
||||||
|
"updated_at_ms": 1779259000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"room": {},
|
||||||
|
"treasure": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `result.applied` | 本次命令是否实际落地。重复提交同一个 `command_id` 时可能返回 `false`,App 不要重复播放发送者本地扣费动画。 |
|
||||||
|
| `result.room_version` | 房间版本,可用于丢弃旧房间快照。 |
|
||||||
|
| `result.server_time_ms` | 服务端时间。 |
|
||||||
|
| `billing_receipt_id` | 钱包扣费回执;非空表示扣费和房间表现已提交。 |
|
||||||
|
| `room_heat` | 最新房间热度。 |
|
||||||
|
| `gift_rank` | 最新房间礼物榜。 |
|
||||||
|
| `room` | 房间快照,字段较大;App 可按已有房间快照解析逻辑局部更新。 |
|
||||||
|
| `treasure` | 房间宝箱状态;宝箱 UI 以该字段或宝箱 IM 事件更新。 |
|
||||||
|
|
||||||
|
发送后处理:
|
||||||
|
|
||||||
|
- 成功后立即关闭 loading,保留礼物面板或按产品交互关闭。
|
||||||
|
- 用 `room_heat` 和 `gift_rank` 更新房间顶部和贡献榜。
|
||||||
|
- 播放当前用户送礼动效时按 `command_id` 去重,避免 HTTP 响应和 IM 各播放一次。
|
||||||
|
- 不要本地直接写最终余额;等待 `WalletBalanceChanged` 私有 IM 或主动刷新余额。
|
||||||
|
|
||||||
|
## 房间 IM 同步
|
||||||
|
|
||||||
|
送礼成功后,房间群会收到腾讯云 IM 自定义消息:
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
| --- | --- |
|
||||||
|
| `MsgContent.Desc` | `room_gift_sent` |
|
||||||
|
| `Data.event_type` | `room_gift_sent` |
|
||||||
|
|
||||||
|
当前消息核心字段来自 room outbox:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "room_gift_sent",
|
||||||
|
"event_id": "room_event_xxx",
|
||||||
|
"room_id": "lalu_room_001",
|
||||||
|
"room_version": 14,
|
||||||
|
"occurred_at_ms": 1779259000000,
|
||||||
|
"actor_user_id": "10001",
|
||||||
|
"target_user_id": "10002",
|
||||||
|
"gift_value": 900,
|
||||||
|
"attributes": {
|
||||||
|
"gift_id": "rose",
|
||||||
|
"gift_count": "9",
|
||||||
|
"billing_receipt_id": "br_abc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
处理规则:
|
||||||
|
|
||||||
|
- 按 `event_id` 去重。
|
||||||
|
- `event_type != room_gift_sent` 时交给其他房间消息处理器。
|
||||||
|
- `actor_user_id` 是送礼人,`target_user_id` 是收礼人。
|
||||||
|
- `attributes.gift_id` 对应面板里的礼物 ID;本地找不到配置时展示兜底礼物图标。
|
||||||
|
- `attributes.gift_count` 当前是字符串,Flutter 解析时需要 `int.tryParse`。
|
||||||
|
- `gift_value` 是本次房间价值/热度口径,不等同于客户端自己算的 `coin_price * count`。
|
||||||
|
- 自己发送的礼物如果已经由 HTTP 成功响应播放过,收到同一 `command_id` 或短时间同一礼物 IM 时要做播放去重。
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
外层 envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "INVALID_ARGUMENT",
|
||||||
|
"message": "invalid argument",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
处理规则:
|
||||||
|
|
||||||
|
| 场景 | 处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `UNAUTHENTICATED` | 重新登录或刷新 token。 |
|
||||||
|
| `INVALID_ARGUMENT` | 参数错误;记录 `request_id`,检查 `room_id`、`gift_id`、数量和收礼人。 |
|
||||||
|
| `NOT_FOUND` | 房间或礼物不存在;刷新房间和礼物面板。 |
|
||||||
|
| `FAILED_PRECONDITION` | 用户不在房间、房间不可送礼或状态不允许;刷新房间详情。 |
|
||||||
|
| `INSUFFICIENT_BALANCE` | 金币不足;跳转充值或刷新余额。 |
|
||||||
|
| `CONFLICT` | 幂等冲突或命令状态冲突;复用原 `command_id` 查询/重试,不生成新命令导致重复送礼。 |
|
||||||
|
| `UPSTREAM_ERROR` / 5xx | 保留原 `command_id` 允许用户重试。 |
|
||||||
|
|
||||||
|
## Flutter 数据模型示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class GiftPanel {
|
||||||
|
GiftPanel({
|
||||||
|
required this.coinBalance,
|
||||||
|
required this.recipients,
|
||||||
|
required this.tabs,
|
||||||
|
required this.gifts,
|
||||||
|
required this.quantityPresets,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int coinBalance;
|
||||||
|
final List<GiftRecipient> recipients;
|
||||||
|
final List<GiftTab> tabs;
|
||||||
|
final List<GiftItem> gifts;
|
||||||
|
final List<int> quantityPresets;
|
||||||
|
|
||||||
|
factory GiftPanel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GiftPanel(
|
||||||
|
coinBalance: (json['coin_balance'] as num?)?.toInt() ?? 0,
|
||||||
|
recipients: (json['recipients'] as List<dynamic>? ?? const [])
|
||||||
|
.map((item) => GiftRecipient.fromJson(item as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
tabs: (json['tabs'] as List<dynamic>? ?? const [])
|
||||||
|
.map((item) => GiftTab.fromJson(item as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
gifts: (json['gifts'] as List<dynamic>? ?? const [])
|
||||||
|
.map((item) => GiftItem.fromJson(item as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
quantityPresets: (json['quantity_presets'] as List<dynamic>? ?? const [])
|
||||||
|
.map((item) => (item as num).toInt())
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GiftRecipient {
|
||||||
|
GiftRecipient({
|
||||||
|
required this.targetType,
|
||||||
|
required this.userId,
|
||||||
|
required this.seatNo,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String targetType;
|
||||||
|
final String userId;
|
||||||
|
final int seatNo;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
factory GiftRecipient.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GiftRecipient(
|
||||||
|
targetType: json['target_type'] as String? ?? '',
|
||||||
|
userId: json['user_id'] as String? ?? '',
|
||||||
|
seatNo: (json['seat_no'] as num?)?.toInt() ?? 0,
|
||||||
|
label: json['label'] as String? ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GiftTab {
|
||||||
|
GiftTab({
|
||||||
|
required this.key,
|
||||||
|
required this.giftTypeCode,
|
||||||
|
required this.label,
|
||||||
|
required this.order,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
final String giftTypeCode;
|
||||||
|
final String label;
|
||||||
|
final int order;
|
||||||
|
|
||||||
|
factory GiftTab.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GiftTab(
|
||||||
|
key: json['key'] as String? ?? '',
|
||||||
|
giftTypeCode: json['gift_type_code'] as String? ?? '',
|
||||||
|
label: json['label'] as String? ?? '',
|
||||||
|
order: (json['order'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GiftItem {
|
||||||
|
GiftItem({
|
||||||
|
required this.giftId,
|
||||||
|
required this.name,
|
||||||
|
required this.giftTypeCode,
|
||||||
|
required this.coinPrice,
|
||||||
|
required this.assetUrl,
|
||||||
|
required this.animationUrl,
|
||||||
|
required this.presentationJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String giftId;
|
||||||
|
final String name;
|
||||||
|
final String giftTypeCode;
|
||||||
|
final int coinPrice;
|
||||||
|
final String assetUrl;
|
||||||
|
final String animationUrl;
|
||||||
|
final String presentationJson;
|
||||||
|
|
||||||
|
factory GiftItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
final resource = json['resource'] as Map<String, dynamic>? ?? const {};
|
||||||
|
return GiftItem(
|
||||||
|
giftId: json['gift_id'] as String? ?? '',
|
||||||
|
name: json['name'] as String? ?? '',
|
||||||
|
giftTypeCode: json['gift_type_code'] as String? ?? '',
|
||||||
|
coinPrice: (json['coin_price'] as num?)?.toInt() ?? 0,
|
||||||
|
assetUrl: resource['asset_url'] as String? ?? '',
|
||||||
|
animationUrl: resource['animation_url'] as String? ?? '',
|
||||||
|
presentationJson: json['presentation_json'] as String? ?? '{}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flutter 调用示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<GiftPanel> fetchGiftPanel({
|
||||||
|
required Dio dio,
|
||||||
|
required String roomId,
|
||||||
|
}) async {
|
||||||
|
final response = await dio.get('/api/v1/rooms/$roomId/gift-panel');
|
||||||
|
final body = response.data as Map<String, dynamic>;
|
||||||
|
if (body['code'] != 'OK') {
|
||||||
|
throw StateError('${body['code']}: ${body['message']}');
|
||||||
|
}
|
||||||
|
return GiftPanel.fromJson(body['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> sendGift({
|
||||||
|
required Dio dio,
|
||||||
|
required String roomId,
|
||||||
|
required String commandId,
|
||||||
|
required int targetUserId,
|
||||||
|
required String giftId,
|
||||||
|
required int giftCount,
|
||||||
|
String? poolId,
|
||||||
|
}) async {
|
||||||
|
final response = await dio.post(
|
||||||
|
'/api/v1/rooms/gift/send',
|
||||||
|
data: {
|
||||||
|
'room_id': roomId,
|
||||||
|
'command_id': commandId,
|
||||||
|
'target_type': 'user',
|
||||||
|
'target_user_ids': [targetUserId],
|
||||||
|
'gift_id': giftId,
|
||||||
|
'gift_count': giftCount,
|
||||||
|
if (poolId != null && poolId.isNotEmpty) 'pool_id': poolId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final body = response.data as Map<String, dynamic>;
|
||||||
|
if (body['code'] != 'OK') {
|
||||||
|
throw StateError('${body['code']}: ${body['message']}');
|
||||||
|
}
|
||||||
|
return body['data'] as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 客户端状态建议
|
||||||
|
|
||||||
|
- 礼物面板缓存以 `room_id` 为粒度;切房间必须重新拉取。
|
||||||
|
- 房间 `visible_region_id` 变化或用户重新 JoinRoom 后重新拉取礼物面板。
|
||||||
|
- 礼物图片可以缓存;`animation_url` 建议按需预加载当前 tab 前几项。
|
||||||
|
- 发送按钮只防重复点击当前 `command_id`,不要全局锁死礼物面板。
|
||||||
|
- HTTP 超时后不要自动生成新 `command_id`;让用户点击重试时复用原值。
|
||||||
|
- 礼物墙和礼物 Tab 是两个不同接口:礼物 Tab 是可发送配置,礼物墙是当前用户收到过的礼物聚合。
|
||||||
@ -1898,6 +1898,19 @@ paths:
|
|||||||
$ref: "#/responses/BadRequest"
|
$ref: "#/responses/BadRequest"
|
||||||
"502":
|
"502":
|
||||||
$ref: "#/responses/UpstreamError"
|
$ref: "#/responses/UpstreamError"
|
||||||
|
/api/v1/gift-tabs:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- resources
|
||||||
|
summary: App 礼物 Tab 配置列表
|
||||||
|
operationId: listGiftTabs
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 返回后台启用的礼物 Tab 配置;不按礼物数量过滤,即使 Tab 下当前没有礼物也返回。
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GiftTabListEnvelope"
|
||||||
|
"502":
|
||||||
|
$ref: "#/responses/UpstreamError"
|
||||||
/api/v1/users/me/resources:
|
/api/v1/users/me/resources:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@ -3622,6 +3635,49 @@ definitions:
|
|||||||
page_size:
|
page_size:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
|
GiftTabData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
description: Tab 稳定 key,等同于礼物类型编码。
|
||||||
|
gift_type_code:
|
||||||
|
type: string
|
||||||
|
description: 礼物类型编码,用于和 gifts[].gift_type_code 对齐。
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: 后台配置的中文显示名。
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
description: App Tab 展示名,来源于后台 tab_key。
|
||||||
|
tab_key:
|
||||||
|
type: string
|
||||||
|
description: 后台配置的 Tab Name。
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
order:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
description: App 排序值,等同于 sort_order。
|
||||||
|
sort_order:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
created_at_ms:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
updated_at_ms:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
GiftTabListData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/GiftTabData"
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
UserResourceData:
|
UserResourceData:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -4838,6 +4894,13 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: "#/definitions/GiftListData"
|
$ref: "#/definitions/GiftListData"
|
||||||
|
GiftTabListEnvelope:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: "#/definitions/GiftTabListData"
|
||||||
UserResourceListEnvelope:
|
UserResourceListEnvelope:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||||
|
|||||||
@ -84,6 +84,7 @@
|
|||||||
| GET | `/api/v1/resources` | resources | `listResources` | App 资源列表 |
|
| GET | `/api/v1/resources` | resources | `listResources` | App 资源列表 |
|
||||||
| GET | `/api/v1/resource-groups/{group_id}` | resources | `getResourceGroup` | App 资源组详情 |
|
| GET | `/api/v1/resource-groups/{group_id}` | resources | `getResourceGroup` | App 资源组详情 |
|
||||||
| GET | `/api/v1/gifts` | resources | `listGifts` | App 礼物列表 |
|
| GET | `/api/v1/gifts` | resources | `listGifts` | App 礼物列表 |
|
||||||
|
| GET | `/api/v1/gift-tabs` | resources | `listGiftTabs` | App 礼物 Tab 配置列表,不依赖房间 |
|
||||||
| GET | `/api/v1/users/me/resources` | resources | `listMyResources` | 我的资源权益 |
|
| GET | `/api/v1/users/me/resources` | resources | `listMyResources` | 我的资源权益 |
|
||||||
| POST | `/api/v1/users/me/resources/{resource_id}/equip` | resources | `equipMyResource` | 佩戴我的资源 |
|
| POST | `/api/v1/users/me/resources/{resource_id}/equip` | resources | `equipMyResource` | 佩戴我的资源 |
|
||||||
| GET | `/api/v1/messages/tabs` | messages | `listMessageTabs` | 消息 tab 分区摘要 |
|
| GET | `/api/v1/messages/tabs` | messages | `listMessageTabs` | 消息 tab 分区摘要 |
|
||||||
|
|||||||
@ -47,6 +47,7 @@ type AppHandlers struct {
|
|||||||
GetAppVersion http.HandlerFunc
|
GetAppVersion http.HandlerFunc
|
||||||
GetResourceGroup http.HandlerFunc
|
GetResourceGroup http.HandlerFunc
|
||||||
ListGifts http.HandlerFunc
|
ListGifts http.HandlerFunc
|
||||||
|
ListGiftTabs http.HandlerFunc
|
||||||
ListEmojiPacks http.HandlerFunc
|
ListEmojiPacks http.HandlerFunc
|
||||||
IssueTencentIMUserSig http.HandlerFunc
|
IssueTencentIMUserSig http.HandlerFunc
|
||||||
IssueTencentRTCToken http.HandlerFunc
|
IssueTencentRTCToken http.HandlerFunc
|
||||||
@ -245,6 +246,7 @@ func (r routes) registerAppRoutes() {
|
|||||||
r.public("/app/version", http.MethodGet, h.GetAppVersion)
|
r.public("/app/version", http.MethodGet, h.GetAppVersion)
|
||||||
r.public("/resource-groups/{group_id}", "", h.GetResourceGroup)
|
r.public("/resource-groups/{group_id}", "", h.GetResourceGroup)
|
||||||
r.public("/gifts", "", h.ListGifts)
|
r.public("/gifts", "", h.ListGifts)
|
||||||
|
r.public("/gift-tabs", http.MethodGet, h.ListGiftTabs)
|
||||||
r.public("/emoji-packs", http.MethodGet, h.ListEmojiPacks)
|
r.public("/emoji-packs", http.MethodGet, h.ListEmojiPacks)
|
||||||
r.profile("/im/usersig", http.MethodGet, h.IssueTencentIMUserSig)
|
r.profile("/im/usersig", http.MethodGet, h.IssueTencentIMUserSig)
|
||||||
r.profile("/rtc/token", http.MethodPost, h.IssueTencentRTCToken)
|
r.profile("/rtc/token", http.MethodPost, h.IssueTencentRTCToken)
|
||||||
|
|||||||
@ -32,6 +32,10 @@ func (h *Handler) ListGifts(writer http.ResponseWriter, request *http.Request) {
|
|||||||
h.listGifts(writer, request)
|
h.listGifts(writer, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListGiftTabs(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listGiftTabs(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) ListEmojiPacks(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) ListEmojiPacks(writer http.ResponseWriter, request *http.Request) {
|
||||||
h.listEmojiPacks(writer, request)
|
h.listEmojiPacks(writer, request)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,6 +91,19 @@ type giftConfigData struct {
|
|||||||
RegionIDs []int64 `json:"region_ids"`
|
RegionIDs []int64 `json:"region_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type giftTabData struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
GiftTypeCode string `json:"gift_type_code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
TabKey string `json:"tab_key"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Order int32 `json:"order"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
type emojiPackData struct {
|
type emojiPackData struct {
|
||||||
ResourceID int64 `json:"resource_id"`
|
ResourceID int64 `json:"resource_id"`
|
||||||
ResourceCode string `json:"resource_code"`
|
ResourceCode string `json:"resource_code"`
|
||||||
@ -218,6 +231,27 @@ func (h *Handler) listGifts(writer http.ResponseWriter, request *http.Request) {
|
|||||||
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listGiftTabs(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.walletClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.walletClient.ListGiftTypeConfigs(request.Context(), &walletv1.ListGiftTypeConfigsRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
ActiveOnly: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]giftTabData, 0, len(resp.GetGiftTypes()))
|
||||||
|
for _, item := range resp.GetGiftTypes() {
|
||||||
|
items = append(items, giftTabFromProto(item))
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) listEmojiPacks(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listEmojiPacks(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
@ -411,6 +445,24 @@ func giftFromProto(gift *walletv1.GiftConfig) giftConfigData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func giftTabFromProto(item *walletv1.GiftTypeConfig) giftTabData {
|
||||||
|
if item == nil {
|
||||||
|
return giftTabData{}
|
||||||
|
}
|
||||||
|
return giftTabData{
|
||||||
|
Key: item.GetTypeCode(),
|
||||||
|
GiftTypeCode: item.GetTypeCode(),
|
||||||
|
Name: item.GetName(),
|
||||||
|
Label: item.GetTabKey(),
|
||||||
|
TabKey: item.GetTabKey(),
|
||||||
|
Status: item.GetStatus(),
|
||||||
|
Order: item.GetSortOrder(),
|
||||||
|
SortOrder: item.GetSortOrder(),
|
||||||
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func userResourceFromProto(item *walletv1.UserResourceEntitlement) userResourceData {
|
func userResourceFromProto(item *walletv1.UserResourceEntitlement) userResourceData {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
return userResourceData{}
|
return userResourceData{}
|
||||||
|
|||||||
@ -368,6 +368,9 @@ type fakeWalletClient struct {
|
|||||||
resourcesByID map[int64]*walletv1.Resource
|
resourcesByID map[int64]*walletv1.Resource
|
||||||
lastGetResourceGroup *walletv1.GetResourceGroupRequest
|
lastGetResourceGroup *walletv1.GetResourceGroupRequest
|
||||||
resourceGroupsByID map[int64]*walletv1.ResourceGroup
|
resourceGroupsByID map[int64]*walletv1.ResourceGroup
|
||||||
|
lastListGiftConfigs *walletv1.ListGiftConfigsRequest
|
||||||
|
lastListGiftTypes *walletv1.ListGiftTypeConfigsRequest
|
||||||
|
listGiftTypesResp *walletv1.ListGiftTypeConfigsResponse
|
||||||
lastGrantResource *walletv1.GrantResourceRequest
|
lastGrantResource *walletv1.GrantResourceRequest
|
||||||
grantResourceResp *walletv1.ResourceGrantResponse
|
grantResourceResp *walletv1.ResourceGrantResponse
|
||||||
grantResourceErr error
|
grantResourceErr error
|
||||||
@ -1148,11 +1151,19 @@ func (f *fakeWalletClient) GetResourceGroup(_ context.Context, req *walletv1.Get
|
|||||||
return &walletv1.GetResourceGroupResponse{}, nil
|
return &walletv1.GetResourceGroupResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeWalletClient) ListGiftConfigs(context.Context, *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) {
|
func (f *fakeWalletClient) ListGiftConfigs(_ context.Context, req *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) {
|
||||||
|
f.lastListGiftConfigs = req
|
||||||
return &walletv1.ListGiftConfigsResponse{}, nil
|
return &walletv1.ListGiftConfigsResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeWalletClient) ListGiftTypeConfigs(context.Context, *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
|
func (f *fakeWalletClient) ListGiftTypeConfigs(_ context.Context, req *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
|
||||||
|
f.lastListGiftTypes = req
|
||||||
|
if f.err != nil {
|
||||||
|
return nil, f.err
|
||||||
|
}
|
||||||
|
if f.listGiftTypesResp != nil {
|
||||||
|
return f.listGiftTypesResp, nil
|
||||||
|
}
|
||||||
return &walletv1.ListGiftTypeConfigsResponse{}, nil
|
return &walletv1.ListGiftTypeConfigsResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3239,6 +3250,73 @@ func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListGiftTabsReturnsActiveTypeConfigs(t *testing.T) {
|
||||||
|
walletClient := &fakeWalletClient{
|
||||||
|
listGiftTypesResp: &walletv1.ListGiftTypeConfigsResponse{
|
||||||
|
GiftTypes: []*walletv1.GiftTypeConfig{
|
||||||
|
{
|
||||||
|
TypeCode: "normal",
|
||||||
|
Name: "普通礼物",
|
||||||
|
TabKey: "Gift",
|
||||||
|
Status: "active",
|
||||||
|
SortOrder: 10,
|
||||||
|
CreatedAtMs: 1_700_000_000_000,
|
||||||
|
UpdatedAtMs: 1_700_000_000_100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TypeCode: "lucky",
|
||||||
|
Name: "幸运礼物",
|
||||||
|
TabKey: "Lucky",
|
||||||
|
Status: "active",
|
||||||
|
SortOrder: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
handler.SetWalletClient(walletClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/gift-tabs", nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("gift tabs status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
var envelope struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Data struct {
|
||||||
|
Items []struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
GiftTypeCode string `json:"gift_type_code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
TabKey string `json:"tab_key"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Order int32 `json:"order"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
} `json:"items"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||||
|
t.Fatalf("decode gift tabs failed: %v", err)
|
||||||
|
}
|
||||||
|
if walletClient.lastListGiftTypes == nil || walletClient.lastListGiftTypes.GetAppCode() != "lalu" || !walletClient.lastListGiftTypes.GetActiveOnly() {
|
||||||
|
t.Fatalf("gift tabs request mismatch: %+v", walletClient.lastListGiftTypes)
|
||||||
|
}
|
||||||
|
if walletClient.lastListGiftConfigs != nil {
|
||||||
|
t.Fatalf("gift tabs must not filter tabs by gift configs: %+v", walletClient.lastListGiftConfigs)
|
||||||
|
}
|
||||||
|
if envelope.Code != httpkit.CodeOK || envelope.Data.Total != 2 || len(envelope.Data.Items) != 2 {
|
||||||
|
t.Fatalf("gift tabs envelope mismatch: %+v", envelope)
|
||||||
|
}
|
||||||
|
if envelope.Data.Items[0].Key != "normal" || envelope.Data.Items[0].GiftTypeCode != "normal" || envelope.Data.Items[0].Name != "普通礼物" || envelope.Data.Items[0].Label != "Gift" || envelope.Data.Items[0].TabKey != "Gift" || envelope.Data.Items[0].Order != 10 || envelope.Data.Items[0].SortOrder != 10 || envelope.Data.Items[0].UpdatedAtMS != 1_700_000_000_100 {
|
||||||
|
t.Fatalf("first gift tab mismatch: %+v", envelope.Data.Items[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMessageReadAllReadAndDeleteRoutes(t *testing.T) {
|
func TestMessageReadAllReadAndDeleteRoutes(t *testing.T) {
|
||||||
messageClient := &fakeMessageInboxClient{}
|
messageClient := &fakeMessageInboxClient{}
|
||||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
|||||||
@ -133,6 +133,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
|||||||
GetAppVersion: appAPI.GetAppVersion,
|
GetAppVersion: appAPI.GetAppVersion,
|
||||||
GetResourceGroup: resourceAPI.GetResourceGroup,
|
GetResourceGroup: resourceAPI.GetResourceGroup,
|
||||||
ListGifts: resourceAPI.ListGifts,
|
ListGifts: resourceAPI.ListGifts,
|
||||||
|
ListGiftTabs: resourceAPI.ListGiftTabs,
|
||||||
ListEmojiPacks: resourceAPI.ListEmojiPacks,
|
ListEmojiPacks: resourceAPI.ListEmojiPacks,
|
||||||
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
||||||
IssueTencentRTCToken: roomAPI.IssueTencentRTCToken,
|
IssueTencentRTCToken: roomAPI.IssueTencentRTCToken,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user