hyapp-server/docs/flutter对接/房间锁房接口Flutter对接.md
2026-05-14 00:25:02 +08:00

384 lines
11 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 对接
本文描述 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`
本地开发地址:
```text
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` 时忽略。 |
设置密码请求示例:
```http
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"
}
```
清空密码请求示例:
```http
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
}
```
成功响应:
```json
{
"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` | 只用于本次进房校验,服务端不保存明文。 |
请求示例:
```http
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"
}
```
成功响应只展示锁房相关字段:
```json
{
"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 字段
公共列表和搜索使用同一个接口:
```http
GET /api/v1/rooms?tab=hot&query=100001
Authorization: Bearer <access_token>
X-App-Code: lalu
```
Mine 页房间流:
```http
GET /api/v1/rooms/feeds?tab=followed
Authorization: Bearer <access_token>
X-App-Code: lalu
```
列表响应示例:
```json
{
"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
```json
{
"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 解析示例
```dart
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` 时按一次用户动作生成一次,例如:
```dart
String newRoomCommandId(String action, String roomId, String userId) {
return 'cmd_${action}_${roomId}_${userId}_${DateTime.now().millisecondsSinceEpoch}';
}
```
同一次 HTTP 重试复用原 `command_id`;用户重新输入密码或再次点击设置按钮时生成新的 `command_id`