hyapp-server/docs/flutter对接/创建房间与IM入群Flutter对接.md

404 lines
12 KiB
Markdown
Raw 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.

# 创建房间与 IM 入群 Flutter 对接
本文描述 Flutter App 从创建语音房到加入腾讯云 IM 房间群的调用顺序。当前服务端已改为:`CreateRoom` 返回前同步创建腾讯 IM 群;创建成功后,`data.room.im_group_id` 对应的腾讯 IM 群已经存在。
## 接口地址
涉及接口:
| 方法 | 接口 | 说明 |
| --- | --- | --- |
| `GET` | `/api/v1/im/usersig` | 获取腾讯 IM SDK 登录票据。 |
| `POST` | `/api/v1/rooms/create` | 创建房间;成功前同步创建腾讯 IM 群。 |
| `POST` | `/api/v1/rooms/join` | 建立 room-service 业务 presence成功后才调用腾讯 IM `joinGroup`。 |
## 请求头
| Header | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token服务端从 token 读取当前用户 ID。 |
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
| `Content-Type` | POST 必填 | `application/json` | JSON 请求体。 |
## 正确调用顺序
```text
1. App 登录后获取 access_token
2. GET /api/v1/im/usersig
3. 腾讯 IM SDK login(user_id, user_sig)
4. POST /api/v1/rooms/create
5. POST /api/v1/rooms/join
6. 腾讯 IM SDK joinGroup(join_response.data.im.group_id)
7. 成功后进入房间页并启动 heartbeat / RTC
```
关键规则:
| 规则 | 说明 |
| --- | --- |
| `CreateRoom` 成功代表 IM 群已创建 | 服务端同步调用腾讯 IM `create_group`,失败时创建接口直接失败,不落房间。 |
| `CreateRoom` 后不要直接 `joinGroup` | 腾讯 IM 回调守卫依赖 room-service presence必须先 `JoinRoom`。 |
| `im_group_id` 当前等于 `room_id` | 客户端仍然必须使用接口返回字段,不能硬编码映射规则。 |
| `joinGroup` 成功或“已在群内”都按成功处理 | 页面重进、SDK 重连或重复 join 时可能出现已在群内。 |
| `command_id` 同一次用户动作重试必须复用 | 创建房间和进房分别使用不同 `command_id`。 |
## 获取 IM UserSig
请求:
```http
GET /api/v1/im/usersig
Authorization: Bearer <access_token>
X-App-Code: lalu
```
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"sdk_app_id": 20036101,
"user_id": "10001",
"user_sig": "xxx",
"expire_at_ms": 1778007200000
}
}
```
客户端行为:
- `user_id` 是腾讯 IM identifier直接传给腾讯 IM SDK。
- `user_sig``expire_at_ms` 缓存;过期前可以复用。
- `UNAUTHORIZED``PROFILE_REQUIRED` 时不进入房间流程。
## 创建房间
请求:
```http
POST /api/v1/rooms/create
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"command_id": "cmd_create_room_10001_1778000000000",
"seat_count": 10,
"mode": "voice",
"room_name": "Live Room",
"room_avatar": "https://cdn.example/room.png",
"room_description": "welcome"
}
```
请求字段:
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `command_id` | 是 | string | 创建房间动作幂等键;同一次点击超时重试必须复用。 |
| `seat_count` | 否 | int | 不传或传 `0` 使用后台默认麦位数;正数必须命中后台启用座位数。 |
| `mode` | 是 | string | 当前传 `voice`。 |
| `room_name` | 是 | string | 房间标题,最长 128 个字符。 |
| `room_avatar` | 是 | string | 房间封面 URL不传或空字符串会返回 `INVALID_ARGUMENT`。 |
| `room_description` | 否 | string | 房间简介,最长 512 个字符。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"result": {
"applied": true,
"room_version": 1,
"server_time_ms": 1778000000123
},
"room": {
"room_id": "lalu_abc123",
"im_group_id": "lalu_abc123",
"room_short_id": "10001",
"title": "Live Room",
"cover_url": "https://cdn.example/room.png",
"description": "welcome",
"owner_user_id": "10001",
"mode": "voice",
"status": "active",
"chat_enabled": true,
"locked": false,
"heat": 0,
"online_count": 1,
"seat_count": 10,
"occupied_seat_count": 0,
"visible_region_id": 1001,
"version": 1
},
"seats": [
{
"seat_no": 1,
"locked": false
}
],
"im": {
"group_id": "lalu_abc123",
"need_join_group": true
},
"server_time_ms": 1778000000123
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `data.room.room_id` | 房间 ID后续 JoinRoom、heartbeat、RTC 都使用它。 |
| `data.room.im_group_id` | 腾讯 IM 房间群 ID创建成功时群已存在。 |
| `data.im.group_id` | 同一个 IM 群 ID客户端最终以 JoinRoom 响应里的 `data.im.group_id` 入群。 |
| `data.result.applied` | `true` 表示本次创建写入;同一 `command_id` 重试可能为 `false`。 |
创建失败处理:
| code | HTTP | 客户端处理 |
| --- | --- | --- |
| `INVALID_ARGUMENT` | 400 | 检查必填字段、座位数、标题长度。 |
| `UNAUTHORIZED` | 401 | 回登录。 |
| `PROFILE_REQUIRED` | 403 | 引导补全资料。 |
| `CONFLICT` | 409 | 当前用户已经有房间;调用 `/api/v1/rooms/me` 恢复已有房间。 |
| `UPSTREAM_ERROR` | 502 | 可能是腾讯 IM 建群失败或 room-service 不可用;不要进入房间页,可提示稍后重试。 |
## JoinRoom 后加入 IM 群
创建房间成功后,房主仍然要调用 `JoinRoom`。原因是 IM 入群回调会检查 room-service presence只创建房间不能替代进房动作。
请求:
```http
POST /api/v1/rooms/join
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"room_id": "lalu_abc123",
"command_id": "cmd_join_room_lalu_abc123_10001_1778000001000",
"role": "audience"
}
```
成功响应只展示 IM 相关字段:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_def",
"data": {
"room": {
"room_id": "lalu_abc123",
"im_group_id": "lalu_abc123",
"version": 2
},
"viewer": {
"user_id": "10001",
"role": "owner",
"joined_at_ms": 1778000001000,
"last_seen_at_ms": 1778000001000
},
"im": {
"group_id": "lalu_abc123",
"need_join_group": true
},
"rtc": {
"need_token": true,
"available": true
},
"server_time_ms": 1778000001000
}
}
```
客户端行为:
| 条件 | 行为 |
| --- | --- |
| `data.im.need_join_group=true` | 调腾讯 IM SDK `joinGroup(data.im.group_id)`。 |
| `joinGroup` 成功 | 开始监听房间群 `TIMCustomElem`,继续 RTC 进房。 |
| `joinGroup` 返回已在群内 | 按成功处理。 |
| `joinGroup` 返回群不存在 | 视为异常;当前服务端 CreateRoom 已同步建群,记录 `request_id``group_id` 上报。 |
| `joinGroup` 返回被拒绝/无权限 | 重新调用 `JoinRoom`;仍失败则退出房间页。 |
## Flutter 示例
下面示例只展示调用边界,项目内可替换为现有 Dio 封装、错误码枚举和腾讯 IM SDK 适配层。
```dart
class VoiceRoomApi {
VoiceRoomApi(this._dio);
final Dio _dio;
Future<TencentIMUserSig> fetchIMUserSig() async {
final resp = await _dio.get('/api/v1/im/usersig');
final data = resp.data['data'] as Map<String, dynamic>;
return TencentIMUserSig.fromJson(data);
}
Future<CreateRoomResult> createRoom({
required String commandId,
required String roomName,
String mode = 'voice',
int seatCount = 0,
String roomAvatar = '',
String roomDescription = '',
}) async {
final resp = await _dio.post(
'/api/v1/rooms/create',
data: {
'command_id': commandId,
'seat_count': seatCount,
'mode': mode,
'room_name': roomName,
'room_avatar': roomAvatar,
'room_description': roomDescription,
},
);
return CreateRoomResult.fromJson(resp.data['data'] as Map<String, dynamic>);
}
Future<JoinRoomResult> joinRoom({
required String roomId,
required String commandId,
String role = 'audience',
String password = '',
}) async {
final resp = await _dio.post(
'/api/v1/rooms/join',
data: {
'room_id': roomId,
'command_id': commandId,
'role': role,
if (password.isNotEmpty) 'password': password,
},
);
return JoinRoomResult.fromJson(resp.data['data'] as Map<String, dynamic>);
}
}
```
IM 登录和入群:
```dart
class VoiceRoomIMBridge {
Future<void> ensureLoggedIn(TencentIMUserSig sig) async {
// 根据项目使用的腾讯云 IM Flutter SDK 版本适配。
// 常见入参是 sdkAppID、userID、userSig。
await TencentIMSDK.login(
sdkAppId: sig.sdkAppId,
userId: sig.userId,
userSig: sig.userSig,
);
}
Future<void> joinRoomGroup(String groupId) async {
try {
await TencentIMSDK.joinGroup(groupId: groupId);
} on TencentIMException catch (error) {
if (_isAlreadyInGroup(error)) {
return;
}
throw error;
}
}
bool _isAlreadyInGroup(TencentIMException error) {
// 按项目实际 SDK 错误码收敛,例如已在群内、重复入群等。
return error.code == TencentIMErrors.alreadyInGroup;
}
}
```
创建并进入自己房间:
```dart
Future<void> createAndEnterRoom({
required VoiceRoomApi api,
required VoiceRoomIMBridge im,
required String roomName,
}) async {
final sig = await api.fetchIMUserSig();
await im.ensureLoggedIn(sig);
final create = await api.createRoom(
commandId: newCommandId('create_room'),
roomName: roomName,
);
final roomId = create.room.roomId;
final join = await api.joinRoom(
roomId: roomId,
commandId: newCommandId('join_room_$roomId'),
);
if (join.im.needJoinGroup) {
await im.joinRoomGroup(join.im.groupId);
}
// join.rtc.available=true 时继续 enterRoom无论是否进 RTC业务 presence 已经建立。
openVoiceRoomPage(join);
}
```
`command_id` 生成建议:
```dart
String newCommandId(String action) {
final now = DateTime.now().millisecondsSinceEpoch;
final nonce = randomBase32(8);
return 'cmd_${action}_${now}_$nonce';
}
```
同一次点击因网络超时重试时,必须复用原 `command_id`;用户重新点击创建或进房时再生成新的 `command_id`
## 状态机建议
| 状态 | 入口 | 成功下一步 | 失败处理 |
| --- | --- | --- | --- |
| `imLogin` | `/im/usersig` + IM `login` | `createRoom` | 回登录或提示 IM 初始化失败。 |
| `createRoom` | `/rooms/create` | `joinRoom` | 不进入房间页;`CONFLICT` 时查询我的房间。 |
| `joinRoom` | `/rooms/join` | `joinIMGroup` | 不调用 `joinGroup`;展示进房失败。 |
| `joinIMGroup` | IM SDK `joinGroup` | `enterRTC` / 打开房间页 | 已在群内按成功;其他错误退出房间页或重试 JoinRoom。 |
| `inRoom` | 房间页 active | heartbeat loop | heartbeat 失败按重连策略处理。 |
## IM 消息监听
房间系统消息通过腾讯 IM 群 `TIMCustomElem` 下发:
| 字段 | 说明 |
| --- | --- |
| `MsgType` | `TIMCustomElem` |
| `Ext` | `room_system_message` |
| `Data` | JSON 字符串,包含 `event_id``room_id``event_type``room_version` 等。 |
| `CloudCustomData` | 与 `Data` 相同,便于漫游和排障。 |
客户端处理规则:
-`event_id` 去重。
-`room_version` 丢弃旧事件。
- 收到无法解析或版本断层时,调用 `GET /api/v1/rooms/snapshot?room_id={room_id}` 拉最新快照。
## 常见错误处理
| 场景 | 客户端动作 |
| --- | --- |
| `/rooms/create` 返回 `UPSTREAM_ERROR` | 不调用 JoinRoom不进入房间页提示稍后重试。 |
| `/rooms/create` 返回 `CONFLICT` | 用户已有房间,调用 `/api/v1/rooms/me` 恢复。 |
| `/rooms/join` 返回 `PERMISSION_DENIED` | 可能被 ban 或锁房密码错误;不调用 `joinGroup`。 |
| IM `joinGroup` 群不存在 | 当前不应出现;记录 `request_id``room_id``group_id` 上报。 |
| IM 登录票据过期 | 重新调用 `/api/v1/im/usersig``login`。 |