12 KiB
12 KiB
创建房间与 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 请求体。 |
正确调用顺序
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
请求:
GET /api/v1/im/usersig
Authorization: Bearer <access_token>
X-App-Code: lalu
成功响应:
{
"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时不进入房间流程。
创建房间
请求:
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 个字符。 |
成功响应:
{
"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;只创建房间不能替代进房动作。
请求:
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 相关字段:
{
"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 适配层。
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 登录和入群:
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;
}
}
创建并进入自己房间:
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 生成建议:
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。 |