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

12 KiB
Raw Permalink Blame History

创建房间与 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_sigexpire_at_ms 缓存;过期前可以复用。
  • UNAUTHORIZEDPROFILE_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_idgroup_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_idroom_idevent_typeroom_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_idroom_idgroup_id 上报。
IM 登录票据过期 重新调用 /api/v1/im/usersiglogin