11 KiB
11 KiB
房间锁房接口 Flutter 对接
本文描述 Flutter App 对接房间锁房能力的 HTTP 接口。房间密码由 room-service 只保存 bcrypt 哈希;客户端只提交明文用于本次设置或进房校验,任何列表、搜索、详情、房间消息和响应体都不会返回明文或哈希。
接口地址
POST /api/v1/rooms/password
POST /api/v1/rooms/join
GET /api/v1/rooms
GET /api/v1/rooms/feeds
本地开发地址:
http://127.0.0.1:13000
请求方法
| 方法 | 接口 | 说明 |
|---|---|---|
POST |
/api/v1/rooms/password |
房主设置或清空房间入房密码。 |
POST |
/api/v1/rooms/join |
进入房间;带锁房间需要传 password。 |
GET |
/api/v1/rooms |
公共房间列表和搜索;房间卡片返回 locked。 |
GET |
/api/v1/rooms/feeds |
Mine 页房间流;房间卡片返回 locked。 |
请求头
| Header | 必填 | 模拟值 | 说明 |
|---|---|---|---|
Authorization |
是 | Bearer <access_token> |
登录 access token;服务端从 token 读取当前用户 ID。 |
X-App-Code |
否 | lalu |
App 编码;登录态接口最终以 token 内的 app_code 为准。 |
设置或清空房间密码
只有房主可以调用。房管、主持人和普通观众调用会返回 PERMISSION_DENIED。
请求体:
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|---|---|---|---|---|
room_id |
是 | string | room_10001 |
房间 ID。 |
command_id |
是 | string | cmd_room_password_room_10001_1710000000 |
房间命令幂等键;同一次点击重试必须复用。 |
locked |
是 | bool | true |
true 表示设置密码,false 表示清空密码。 |
password |
locked=true 时必填 |
string | 1234 |
入房密码;服务端 trim 后最长 64 个字符。locked=false 时忽略。 |
设置密码请求示例:
POST /api/v1/rooms/password
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"room_id": "room_10001",
"command_id": "cmd_room_password_room_10001_1710000000",
"locked": true,
"password": "1234"
}
清空密码请求示例:
POST /api/v1/rooms/password
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"room_id": "room_10001",
"command_id": "cmd_room_password_room_10001_1710000100",
"locked": false
}
成功响应:
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"result": {
"applied": true,
"room_version": 12,
"server_time_ms": 1710000000456
},
"room": {
"room_id": "room_10001",
"im_group_id": "room_10001",
"room_short_id": "100001",
"title": "Live Room",
"cover_url": "https://cdn.example/room.png",
"owner_user_id": "10001",
"mode": "voice",
"status": "active",
"chat_enabled": true,
"locked": true,
"heat": 1000,
"online_count": 12,
"seat_count": 15,
"occupied_seat_count": 2,
"version": 12
},
"seats": [],
"server_time_ms": 1710000000456
}
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
data.result.applied |
bool | 本次命令是否产生状态变更;重复提交相同命令可能为 false。 |
data.result.room_version |
int64 | 命令完成后房间版本。 |
data.room.locked |
bool | 最新锁房状态;true 表示后续新用户进房需要密码。 |
data.room.version |
int64 | 最新房间快照版本;客户端本地房间状态以更大版本覆盖旧状态。 |
data.seats |
array | 当前座位列表;锁房接口成功后可直接复用该房间快照刷新页面。 |
带密码进入房间
客户端进入房间前先看房间卡片或详情里的 locked:
locked |
客户端行为 |
|---|---|
false |
直接调用 JoinRoom,password 可不传或传空字符串。 |
true |
先弹出密码输入,再调用 JoinRoom 并传 password。 |
请求体:
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|---|---|---|---|---|
room_id |
是 | string | room_10001 |
房间 ID。 |
command_id |
是 | string | cmd_join_room_10001_20001_1710000000 |
进房命令幂等键;同一次进房重试必须复用。 |
role |
否 | string | audience |
当前固定传 audience。 |
password |
锁房时必填 | string | 1234 |
只用于本次进房校验,服务端不保存明文。 |
请求示例:
POST /api/v1/rooms/join
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"room_id": "room_10001",
"command_id": "cmd_join_room_10001_20001_1710000000",
"role": "audience",
"password": "1234"
}
成功响应只展示锁房相关字段:
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"result": {
"applied": true,
"room_version": 13,
"server_time_ms": 1710000001456
},
"room": {
"room_id": "room_10001",
"im_group_id": "room_10001",
"locked": true,
"version": 13
},
"viewer": {
"user_id": "20001",
"role": "audience",
"joined_at_ms": 1710000001456,
"last_seen_at_ms": 1710000001456
},
"im": {
"group_id": "room_10001",
"need_join_group": true
},
"rtc": {
"need_token": true,
"available": true
},
"server_time_ms": 1710000001456
}
}
进房规则:
| 场景 | 结果 |
|---|---|
| 房间未锁 | 不校验 password。 |
| 房间已锁且密码正确 | JoinRoom 成功,客户端再调用腾讯 IM joinGroup 和 RTC enterRoom。 |
| 房间已锁且密码错误或缺失 | 返回 403 PERMISSION_DENIED,不会创建 presence,不会返回 RTC token。 |
| 房主进入自己的房间 | 不需要密码。 |
| 用户已经在房间内 | 再次 JoinRoom 按幂等恢复当前房间态,不会因为后续锁房被踢出。 |
房间列表和搜索 locked 字段
公共列表和搜索使用同一个接口:
GET /api/v1/rooms?tab=hot&query=100001
Authorization: Bearer <access_token>
X-App-Code: lalu
Mine 页房间流:
GET /api/v1/rooms/feeds?tab=followed
Authorization: Bearer <access_token>
X-App-Code: lalu
列表响应示例:
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"rooms": [
{
"room_id": "room_10001",
"im_group_id": "room_10001",
"owner_user_id": "10001",
"title": "Live Room",
"cover_url": "https://cdn.example/room.png",
"mode": "voice",
"status": "active",
"locked": true,
"heat": 1000,
"online_count": 12,
"seat_count": 15,
"occupied_seat_count": 2,
"visible_region_id": 1,
"app_code": "lalu",
"room_short_id": "100001"
}
],
"next_cursor": ""
}
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
rooms[].locked |
bool | 房间是否需要入房密码;搜索结果和普通列表一致。 |
rooms[].im_group_id |
string | 腾讯 IM 群 ID;只用于 JoinRoom 成功后的 joinGroup,不能绕过 JoinRoom。 |
next_cursor |
string | 下一页游标;为空表示没有下一页。 |
错误处理
失败返回仍使用统一 envelope:
{
"code": "PERMISSION_DENIED",
"message": "permission denied",
"request_id": "req_abc"
}
| HTTP 状态码 | code |
典型场景 | 处理方式 |
|---|---|---|---|
400 |
INVALID_ARGUMENT |
room_id、command_id 缺失;设置锁房时 password 为空或超过 64 字符。 |
客户端修正参数,不自动重试。 |
401 |
UNAUTHORIZED |
token 缺失、无效、过期或会话失效。 | 重新登录。 |
403 |
PROFILE_REQUIRED |
用户资料未完成。 | 跳转资料补全流程。 |
403 |
PERMISSION_DENIED |
非房主设置密码;进锁房密码错误;用户被 ban。 | 设置密码时隐藏入口或提示无权限;进房时提示密码错误。 |
404 |
NOT_FOUND |
房间不存在。 | 刷新列表或退出房间页。 |
409 |
ROOM_CLOSED |
房间已关闭。 | 刷新列表或退出房间页。 |
409 |
CONFLICT |
同一个 command_id 被不同请求体复用。 |
为新的用户动作生成新的 command_id。 |
502 |
UPSTREAM_ERROR |
内部服务暂不可用。 | 可提示稍后重试。 |
幂等规则:
| 场景 | 返回 |
|---|---|
同一个 command_id 重试同一次设置密码 |
返回同一命令结果,不重复变更房间。 |
同一个 command_id 换另一个密码 |
409 CONFLICT。 |
| 重复清空已解锁房间 | 200 OK,data.room.locked=false。 |
| 同一个用户重复 JoinRoom | 返回当前房间首屏数据,不重复创建 presence。 |
Flutter 解析示例
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 RoomCard {
RoomCard({
required this.roomId,
required this.imGroupId,
required this.title,
required this.locked,
required this.onlineCount,
});
final String roomId;
final String imGroupId;
final String title;
final bool locked;
final int onlineCount;
factory RoomCard.fromJson(Map<String, dynamic> json) {
return RoomCard(
roomId: json['room_id'] as String? ?? '',
imGroupId: json['im_group_id'] as String? ?? '',
title: json['title'] as String? ?? '',
locked: json['locked'] as bool? ?? false,
onlineCount: (json['online_count'] as num?)?.toInt() ?? 0,
);
}
}
class RoomLockResult {
RoomLockResult({
required this.result,
required this.room,
required this.serverTimeMs,
});
final RoomCommandResult result;
final RoomCard room;
final int serverTimeMs;
factory RoomLockResult.fromJson(Map<String, dynamic> json) {
return RoomLockResult(
result: RoomCommandResult.fromJson(
json['result'] as Map<String, dynamic>? ?? const {},
),
room: RoomCard.fromJson(
json['room'] as Map<String, dynamic>? ?? const {},
),
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
);
}
}
调用时先解析外层 envelope:只有 code == "OK" 时读取 data;其他 code 按登录态、参数错误、权限错误或服务错误处理,并把 request_id 写入客户端日志。
客户端生成 command_id 时按一次用户动作生成一次,例如:
String newRoomCommandId(String action, String roomId, String userId) {
return 'cmd_${action}_${roomId}_${userId}_${DateTime.now().millisecondsSinceEpoch}';
}
同一次 HTTP 重试复用原 command_id;用户重新输入密码或再次点击设置按钮时生成新的 command_id。