578 lines
20 KiB
Markdown
578 lines
20 KiB
Markdown
# 房间踢人接口 Flutter 对接
|
||
|
||
本文描述 Flutter App 对接房间踢人能力的 HTTP 接口,以及踢人后腾讯 IM 和腾讯 RTC 的客户端处理方式。踢人事实由 `room-service` Room Cell 提交;客户端不能只靠本地 UI 状态判断用户是否仍在房间。
|
||
|
||
## 接口地址
|
||
|
||
`POST /api/v1/rooms/user/kick`
|
||
|
||
`GET /api/v1/rooms/{room_id}/banned-users`
|
||
|
||
`POST /api/v1/rooms/user/unban`
|
||
|
||
本地开发地址:
|
||
|
||
```text
|
||
http://127.0.0.1:13000
|
||
```
|
||
|
||
## 请求方法
|
||
|
||
| 方法 | 接口 | 说明 |
|
||
| --- | --- | --- |
|
||
| `POST` | `/api/v1/rooms/user/kick` | 踢出房间用户,并写入房间 ban 集合。 |
|
||
| `GET` | `/api/v1/rooms/{room_id}/banned-users` | 查询房间维度当前仍有效的踢人列表,也就是房间黑名单列表。 |
|
||
| `POST` | `/api/v1/rooms/user/unban` | 解除房间 ban,允许用户后续重新 JoinRoom。 |
|
||
|
||
## 请求头
|
||
|
||
| Header | 必填 | 模拟值 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token;服务端从 token 读取当前用户 ID。 |
|
||
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
|
||
|
||
## 踢出房间用户
|
||
|
||
房主可以踢出房间内用户。当前后端权限矩阵也允许房管管理普通用户;端上如果只开放房主入口,后端仍兼容。
|
||
|
||
请求体:
|
||
|
||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||
| `command_id` | 是 | string | `cmd_kick_room_10001_20001_1710000000` | 房间命令幂等键;同一次踢人重试必须复用。 |
|
||
| `target_user_id` | 是 | int64 | `20001` | 被踢用户长 ID。 |
|
||
| `duration_ms` | 否 | int64 | `600000` | 禁止重新进房的毫秒时长;`0` 或不传表示永久,直到房间 owner 调用解封。 |
|
||
|
||
请求示例:
|
||
|
||
```http
|
||
POST /api/v1/rooms/user/kick
|
||
Authorization: Bearer <access_token>
|
||
X-App-Code: lalu
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"room_id": "room_10001",
|
||
"command_id": "cmd_kick_room_10001_20001_1710000000",
|
||
"target_user_id": 20001,
|
||
"duration_ms": 600000
|
||
}
|
||
```
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"code": "OK",
|
||
"message": "ok",
|
||
"request_id": "req_abc",
|
||
"data": {
|
||
"result": {
|
||
"applied": true,
|
||
"room_version": 18,
|
||
"server_time_ms": 1710000000456
|
||
},
|
||
"room": {
|
||
"room_id": "room_10001",
|
||
"status": "active",
|
||
"ban_user_ids": [20001],
|
||
"ban_states": [
|
||
{
|
||
"user_id": 20001,
|
||
"actor_user_id": 10001,
|
||
"created_at_ms": 1710000000456,
|
||
"expires_at_ms": 1710000600456
|
||
}
|
||
],
|
||
"online_users": [],
|
||
"mic_seats": [],
|
||
"version": 18
|
||
},
|
||
"rtc_kicked": true,
|
||
"rtc_kick_error": ""
|
||
}
|
||
}
|
||
```
|
||
|
||
字段说明:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| `data.result.applied` | bool | 本次命令是否产生状态变更;同一个 `command_id` 重试返回同一命令结果,新的 `command_id` 再踢同一用户会刷新 ban 状态。 |
|
||
| `data.room.ban_user_ids` | int64[] | 当前房间 ban 用户集合;被踢用户会进入该集合。 |
|
||
| `data.room.ban_states` | array | 当前房间 ban 的时间边界;`expires_at_ms=0` 表示永久,非 0 表示该 UTC 毫秒后可重新进房。 |
|
||
| `data.room.online_users` | array | 当前业务 presence;被踢用户会被移除。 |
|
||
| `data.room.mic_seats` | array | 如果被踢用户在麦上,对应麦位会被释放。 |
|
||
| `data.rtc_kicked` | bool | 服务端是否已调用 TRTC `RemoveUserByStrRoomId` 移除目标用户。 |
|
||
| `data.rtc_kick_error` | string | TRTC 服务端踢人失败原因;踢人事实已生效,客户端仍按被踢处理。 |
|
||
|
||
## 房间踢人列表
|
||
|
||
该接口用于房主或房管查看当前房间仍有效的踢人列表。列表只返回当前仍会阻止用户进房的记录;限时踢人已过期后不再返回。被解除的用户也不再返回。
|
||
|
||
请求示例:
|
||
|
||
```http
|
||
GET /api/v1/rooms/room_10001/banned-users?page=1&page_size=20
|
||
Authorization: Bearer <access_token>
|
||
X-App-Code: lalu
|
||
```
|
||
|
||
路径参数:
|
||
|
||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||
|
||
Query 参数:
|
||
|
||
| 字段 | 必填 | 类型 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `page` | 否 | int32 | `1` | 页码,从 1 开始。 |
|
||
| `page_size` | 否 | int32 | `20` | 每页数量;建议 App 使用 20,后端最大值按实现收敛。 |
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"code": "OK",
|
||
"message": "ok",
|
||
"request_id": "req_abc",
|
||
"data": {
|
||
"room_id": "room_10001",
|
||
"items": [
|
||
{
|
||
"user_id": "20001",
|
||
"display_user_id": "90020001",
|
||
"username": "Alice",
|
||
"avatar": "https://example.com/avatar.png",
|
||
"country": "US",
|
||
"country_name": "United States",
|
||
"country_display_name": "United States",
|
||
"country_flag": "🇺🇸",
|
||
"actor_user_id": "10001",
|
||
"created_at_ms": 1710000000456,
|
||
"unban_at_ms": 1710000600456,
|
||
"remaining_ms": 599000,
|
||
"permanent": false
|
||
}
|
||
],
|
||
"total": 1,
|
||
"page": 1,
|
||
"page_size": 20,
|
||
"server_time_ms": 1710000001456
|
||
}
|
||
}
|
||
```
|
||
|
||
字段说明:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| `data.room_id` | string | 当前查询的房间 ID。 |
|
||
| `data.items[].user_id` | string | 被踢用户长 ID;Flutter 侧按字符串保存,避免大整数精度问题。 |
|
||
| `data.items[].display_user_id` | string | 用户展示 ID;可能为空,端上需要兜底展示 `user_id`。 |
|
||
| `data.items[].username` | string | 用户昵称;可能为空,端上可兜底展示 `display_user_id` 或 `user_id`。 |
|
||
| `data.items[].avatar` | string | 用户头像 URL;可能为空,端上展示默认头像。 |
|
||
| `data.items[].country` | string | 用户国家二位码,例如 `US`。 |
|
||
| `data.items[].country_name` | string | 用户国家标准名。 |
|
||
| `data.items[].country_display_name` | string | App 展示优先使用的国家名。 |
|
||
| `data.items[].country_flag` | string | 国家国旗 emoji;为空时端上可按 `country` 本地兜底。 |
|
||
| `data.items[].actor_user_id` | string | 执行踢人的用户 ID;系统驱逐时可能为 `"0"`。 |
|
||
| `data.items[].created_at_ms` | int64 | 踢人状态写入时间,UTC epoch milliseconds。 |
|
||
| `data.items[].unban_at_ms` | int64 | 自动解封时间;`0` 表示永久,需要手动解除。 |
|
||
| `data.items[].remaining_ms` | int64 | 距离自动解除的剩余毫秒;永久踢人为 `0`。 |
|
||
| `data.items[].permanent` | bool | 是否永久禁止进房。 |
|
||
| `data.total` | int64 | 当前房间有效黑名单总数。 |
|
||
| `data.server_time_ms` | int64 | 服务端当前 UTC 毫秒;倒计时 UI 以它校正本地时间。 |
|
||
|
||
## 解除房间 ban
|
||
|
||
解除 ban 只恢复用户后续重新进房资格,不恢复历史 presence、麦位、管理员身份或 RTC 连接。
|
||
|
||
请求体:
|
||
|
||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||
| `command_id` | 是 | string | `cmd_unban_room_10001_20001_1710000100` | 房间命令幂等键;同一次解除操作重试必须复用。 |
|
||
| `target_user_id` | 是 | int64 | `20001` | 要解除踢人状态的用户长 ID。 |
|
||
|
||
请求示例:
|
||
|
||
```http
|
||
POST /api/v1/rooms/user/unban
|
||
Authorization: Bearer <access_token>
|
||
X-App-Code: lalu
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"room_id": "room_10001",
|
||
"command_id": "cmd_unban_room_10001_20001_1710000100",
|
||
"target_user_id": 20001
|
||
}
|
||
```
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"code": "OK",
|
||
"message": "ok",
|
||
"request_id": "req_abc",
|
||
"data": {
|
||
"result": {
|
||
"applied": true,
|
||
"room_version": 19,
|
||
"server_time_ms": 1710000100456
|
||
},
|
||
"room": {
|
||
"room_id": "room_10001",
|
||
"status": "active",
|
||
"ban_user_ids": [],
|
||
"ban_states": [],
|
||
"version": 19
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
字段说明:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| `data.result.applied` | bool | 是否实际删除了 ban 状态;目标用户本来不在黑名单时为 `false`。 |
|
||
| `data.room.ban_user_ids` | int64[] | 当前房间仍有效的 ban 用户集合;成功解除后不再包含目标用户。 |
|
||
| `data.room.ban_states` | array | 当前房间仍有效的 ban 状态;成功解除后不再包含目标用户对应状态。 |
|
||
|
||
## IM 和 RTC 交互
|
||
|
||
踢人成功后有三层效果,客户端都要按幂等方式处理:
|
||
|
||
| 层 | 服务端行为 | Flutter 处理 |
|
||
| --- | --- | --- |
|
||
| room-service | 移除目标 presence、释放麦位、写入 ban,后续 JoinRoom、RTC token、IM 进群和发言守卫都会拒绝目标用户。 | 被踢用户所有房间接口如果返回 `PERMISSION_DENIED`,立即退出房间页并清理房间态。 |
|
||
| 腾讯 IM 群 | room-service outbox worker 先调用 IM REST 把目标用户移出房间群,再发布 `room_user_kicked` 系统消息给房间内其他用户。 | 普通成员收到 `room_user_kicked` 后刷新在线列表和麦位;被踢用户要监听 IM 群被移除或退群回调作为主要退出信号。 |
|
||
| 腾讯 IM 单聊 | notice-service 监听 `room_outbox.RoomUserKicked`,给被踢用户发送 C2C `room_notice` 私有通知。 | 被踢用户收到 `room_notice` 时执行同一套本地退出逻辑;按 `event_id` 去重,避免和群消息或接口 403 重复弹窗。 |
|
||
| 腾讯 RTC | room-service 在踢人事实提交后尽力调用 TRTC `RemoveUserByStrRoomId`。 | 被踢用户收到 RTC 退房或连接断开回调时调用同一套本地退出逻辑;如果先收到 IM 回调,也要主动 `exitRoom`。 |
|
||
|
||
客户端退出房间的本地动作必须可重复调用:
|
||
|
||
1. 停止本地音频上行。
|
||
2. 调用 TRTC `exitRoom`。
|
||
3. 退出或清理当前房间 IM 群会话。
|
||
4. 停止房间 heartbeat。
|
||
5. 清空当前房间页状态,回到房间列表或 Mine 页面。
|
||
6. 弹出被踢提示。
|
||
|
||
被踢用户不一定能收到房间群里的 `room_user_kicked` 自定义消息,因为服务端会优先把他移出 IM 群。端上必须同时处理 notice-service 的 C2C `room_notice`、IM SDK 的群移除回调、RTC 退房回调和后续接口 403。
|
||
|
||
## IM 自定义消息
|
||
|
||
房间内其他用户会收到群 `TIMCustomElem`,核心字段如下:
|
||
|
||
```json
|
||
{
|
||
"event_id": "evt_abc",
|
||
"room_id": "room_10001",
|
||
"event_type": "room_user_kicked",
|
||
"actor_user_id": 10001,
|
||
"target_user_id": 20001,
|
||
"room_version": 18,
|
||
"attributes": {}
|
||
}
|
||
```
|
||
|
||
处理规则:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `event_type` | 固定 `room_user_kicked`。 |
|
||
| `actor_user_id` | 操作者用户 ID;系统驱逐时可能为 `0`。 |
|
||
| `target_user_id` | 被踢用户 ID。 |
|
||
| `room_version` | 房间版本;本地已有更高版本时丢弃旧消息。 |
|
||
|
||
如果 `target_user_id` 是当前登录用户,直接执行本地退出房间动作;如果不是当前用户,只刷新在线列表、麦位和管理面 UI。
|
||
|
||
被踢用户会收到 notice-service 发送的 C2C `TIMCustomElem`,`Ext=room_notice`,核心字段如下:
|
||
|
||
```json
|
||
{
|
||
"event_id": "evt_abc",
|
||
"event_type": "room_user_kicked",
|
||
"source_event_type": "RoomUserKicked",
|
||
"app_code": "lalu",
|
||
"room_id": "room_10001",
|
||
"room_version": 18,
|
||
"actor_user_id": 10001,
|
||
"target_user_id": 20001,
|
||
"occurred_at_ms": 1710000000456,
|
||
"source_created_at_ms": 1710000000456
|
||
}
|
||
```
|
||
|
||
C2C 通知和房间群消息使用同一个 `event_id`。Flutter 侧把 `room_user_kicked`、IM 群移除、RTC 退房和接口 403 都归一到同一个 `handleKickedFromRoom(roomId, eventId)` 流程;同一个 `event_id` 或同一个房间的短时间重复退出信号只处理一次。
|
||
|
||
## 错误处理
|
||
|
||
失败返回仍使用统一 envelope:
|
||
|
||
```json
|
||
{
|
||
"code": "PERMISSION_DENIED",
|
||
"message": "permission denied",
|
||
"request_id": "req_abc"
|
||
}
|
||
```
|
||
|
||
| HTTP 状态码 | `code` | 典型场景 | 处理方式 |
|
||
| --- | --- | --- | --- |
|
||
| `400` | `INVALID_ARGUMENT` | `room_id`、`command_id` 或 `target_user_id` 缺失;操作者踢自己。 | 客户端修正参数,不自动重试。 |
|
||
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效。 | 重新登录。 |
|
||
| `403` | `PROFILE_REQUIRED` | 用户资料未完成。 | 跳转资料补全流程。 |
|
||
| `403` | `PERMISSION_DENIED` | 非管理角色踢人;房管踢房主;操作者不在房间。 | 隐藏入口或提示无权限。 |
|
||
| `404` | `NOT_FOUND` | 房间不存在。 | 刷新列表或退出房间页。 |
|
||
| `409` | `ROOM_CLOSED` | 房间已关闭。 | 刷新列表或退出房间页。 |
|
||
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用。 | 为新的用户动作生成新的 `command_id`。 |
|
||
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用。 | 可提示稍后重试。 |
|
||
|
||
幂等规则:
|
||
|
||
| 场景 | 返回 |
|
||
| --- | --- |
|
||
| 同一个 `command_id` 重试同一次踢人 | 返回同一命令结果,不重复变更房间。 |
|
||
| 同一个 `command_id` 换另一个 `target_user_id` | `409 CONFLICT`。 |
|
||
| 新的 `command_id` 再次踢同一用户 | `200 OK`,刷新 ban 状态和 `duration_ms`。 |
|
||
| 解封未 ban 用户 | `200 OK`,`applied=false`。 |
|
||
|
||
## Flutter 解析示例
|
||
|
||
```dart
|
||
int parseIntLike(dynamic value) {
|
||
if (value is int) {
|
||
return value;
|
||
}
|
||
if (value is num) {
|
||
return value.toInt();
|
||
}
|
||
return int.tryParse(value?.toString() ?? '') ?? 0;
|
||
}
|
||
|
||
class RoomCommandResult {
|
||
RoomCommandResult({
|
||
required this.applied,
|
||
required this.roomVersion,
|
||
required this.serverTimeMs,
|
||
});
|
||
|
||
final bool applied;
|
||
final int roomVersion;
|
||
final int serverTimeMs;
|
||
|
||
factory RoomCommandResult.fromJson(Map<String, dynamic> json) {
|
||
return RoomCommandResult(
|
||
applied: json['applied'] as bool? ?? false,
|
||
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
||
);
|
||
}
|
||
}
|
||
|
||
class KickUserResult {
|
||
KickUserResult({
|
||
required this.result,
|
||
required this.rtcKicked,
|
||
required this.rtcKickError,
|
||
required this.banUserIds,
|
||
required this.banStates,
|
||
});
|
||
|
||
final RoomCommandResult result;
|
||
final bool rtcKicked;
|
||
final String rtcKickError;
|
||
final List<int> banUserIds;
|
||
final List<RoomBanState> banStates;
|
||
|
||
factory KickUserResult.fromJson(Map<String, dynamic> json) {
|
||
final room = json['room'] as Map<String, dynamic>? ?? const {};
|
||
return KickUserResult(
|
||
result: RoomCommandResult.fromJson(
|
||
json['result'] as Map<String, dynamic>? ?? const {},
|
||
),
|
||
rtcKicked: json['rtc_kicked'] as bool? ?? false,
|
||
rtcKickError: json['rtc_kick_error'] as String? ?? '',
|
||
banUserIds: (room['ban_user_ids'] as List<dynamic>? ?? const [])
|
||
.map(parseIntLike)
|
||
.toList(),
|
||
banStates: (room['ban_states'] as List<dynamic>? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(RoomBanState.fromJson)
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
class RoomBanState {
|
||
RoomBanState({
|
||
required this.userId,
|
||
required this.actorUserId,
|
||
required this.createdAtMs,
|
||
required this.expiresAtMs,
|
||
});
|
||
|
||
final int userId;
|
||
final int actorUserId;
|
||
final int createdAtMs;
|
||
final int expiresAtMs;
|
||
|
||
bool get permanent => expiresAtMs == 0;
|
||
|
||
factory RoomBanState.fromJson(Map<String, dynamic> json) {
|
||
return RoomBanState(
|
||
userId: parseIntLike(json['user_id']),
|
||
actorUserId: parseIntLike(json['actor_user_id']),
|
||
createdAtMs: parseIntLike(json['created_at_ms']),
|
||
expiresAtMs: parseIntLike(json['expires_at_ms']),
|
||
);
|
||
}
|
||
}
|
||
|
||
class RoomBannedUsersResult {
|
||
RoomBannedUsersResult({
|
||
required this.roomId,
|
||
required this.items,
|
||
required this.total,
|
||
required this.page,
|
||
required this.pageSize,
|
||
required this.serverTimeMs,
|
||
});
|
||
|
||
final String roomId;
|
||
final List<RoomBannedUserItem> items;
|
||
final int total;
|
||
final int page;
|
||
final int pageSize;
|
||
final int serverTimeMs;
|
||
|
||
factory RoomBannedUsersResult.fromJson(Map<String, dynamic> json) {
|
||
return RoomBannedUsersResult(
|
||
roomId: json['room_id'] as String? ?? '',
|
||
items: (json['items'] as List<dynamic>? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(RoomBannedUserItem.fromJson)
|
||
.toList(),
|
||
total: parseIntLike(json['total']),
|
||
page: parseIntLike(json['page']),
|
||
pageSize: parseIntLike(json['page_size']),
|
||
serverTimeMs: parseIntLike(json['server_time_ms']),
|
||
);
|
||
}
|
||
}
|
||
|
||
class RoomBannedUserItem {
|
||
RoomBannedUserItem({
|
||
required this.userId,
|
||
required this.displayUserId,
|
||
required this.username,
|
||
required this.avatar,
|
||
required this.country,
|
||
required this.countryName,
|
||
required this.countryDisplayName,
|
||
required this.countryFlag,
|
||
required this.actorUserId,
|
||
required this.createdAtMs,
|
||
required this.unbanAtMs,
|
||
required this.remainingMs,
|
||
required this.permanent,
|
||
});
|
||
|
||
final int userId;
|
||
final String displayUserId;
|
||
final String username;
|
||
final String avatar;
|
||
final String country;
|
||
final String countryName;
|
||
final String countryDisplayName;
|
||
final String countryFlag;
|
||
final int actorUserId;
|
||
final int createdAtMs;
|
||
final int unbanAtMs;
|
||
final int remainingMs;
|
||
final bool permanent;
|
||
|
||
factory RoomBannedUserItem.fromJson(Map<String, dynamic> json) {
|
||
return RoomBannedUserItem(
|
||
userId: parseIntLike(json['user_id']),
|
||
displayUserId: json['display_user_id'] as String? ?? '',
|
||
username: json['username'] as String? ?? '',
|
||
avatar: json['avatar'] as String? ?? '',
|
||
country: json['country'] as String? ?? '',
|
||
countryName: json['country_name'] as String? ?? '',
|
||
countryDisplayName: json['country_display_name'] as String? ?? '',
|
||
countryFlag: json['country_flag'] as String? ?? '',
|
||
actorUserId: parseIntLike(json['actor_user_id']),
|
||
createdAtMs: parseIntLike(json['created_at_ms']),
|
||
unbanAtMs: parseIntLike(json['unban_at_ms']),
|
||
remainingMs: parseIntLike(json['remaining_ms']),
|
||
permanent: json['permanent'] as bool? ?? false,
|
||
);
|
||
}
|
||
}
|
||
|
||
class RoomKickedEvent {
|
||
RoomKickedEvent({
|
||
required this.roomId,
|
||
required this.actorUserId,
|
||
required this.targetUserId,
|
||
required this.roomVersion,
|
||
});
|
||
|
||
final String roomId;
|
||
final int actorUserId;
|
||
final int targetUserId;
|
||
final int roomVersion;
|
||
|
||
bool isSelf(int currentUserId) => targetUserId == currentUserId;
|
||
|
||
factory RoomKickedEvent.fromJson(Map<String, dynamic> json) {
|
||
return RoomKickedEvent(
|
||
roomId: json['room_id'] as String? ?? '',
|
||
actorUserId: (json['actor_user_id'] as num?)?.toInt() ?? 0,
|
||
targetUserId: (json['target_user_id'] as num?)?.toInt() ?? 0,
|
||
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||
);
|
||
}
|
||
}
|
||
|
||
class RoomKickNotice {
|
||
RoomKickNotice({
|
||
required this.eventId,
|
||
required this.roomId,
|
||
required this.actorUserId,
|
||
required this.targetUserId,
|
||
required this.roomVersion,
|
||
});
|
||
|
||
final String eventId;
|
||
final String roomId;
|
||
final int actorUserId;
|
||
final int targetUserId;
|
||
final int roomVersion;
|
||
|
||
factory RoomKickNotice.fromJson(Map<String, dynamic> json) {
|
||
return RoomKickNotice(
|
||
eventId: json['event_id'] as String? ?? '',
|
||
roomId: json['room_id'] as String? ?? '',
|
||
actorUserId: (json['actor_user_id'] as num?)?.toInt() ?? 0,
|
||
targetUserId: (json['target_user_id'] as num?)?.toInt() ?? 0,
|
||
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
调用 HTTP 接口时先解析外层 envelope:只有 `code == "OK"` 时读取 `data`;其他 `code` 按登录态、参数错误、权限错误或服务错误处理,并把 `request_id` 写入客户端日志。
|
||
|
||
客户端生成 `command_id` 时按一次用户动作生成一次,同一次 HTTP 重试复用原 `command_id`;用户再次点击踢人时生成新的 `command_id`。
|