# 进房接口 Flutter 对接

## 接口地址

```http
POST /api/v1/rooms/join
Authorization: Bearer <access_token>
Content-Type: application/json
```

新版本进房默认使用 `response_mode=lite`。`full` 仍保留给旧客户端兼容，不建议新 Flutter 首屏使用。

## 参数

Body：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。 |
| `command_id` | string | 是 | 本次进房命令 ID。同一次点击超时重试复用同一个值，下一次重新进房换新值。 |
| `role` | string | 否 | 不传默认 `audience`。房主身份由服务端按房间 owner 判断，客户端不要靠传 `owner` 获取权限。 |
| `password` | string | 否 | 密码房传房间密码，非密码房不传或传空。 |
| `response_mode` | string | 是 | 新版固定传 `lite`。可选值：`lite` / `full`。 |

请求示例：

```json
{
  "room_id": "lalu_abc123",
  "command_id": "cmd_join_room_lalu_abc123_10001_1778000001000",
  "role": "audience",
  "password": "",
  "response_mode": "lite"
}
```

也支持 query 参数：

```http
POST /api/v1/rooms/join?response_mode=lite
```

Flutter 建议放在 body 里，避免调用层漏拼 query。

## 返回值

统一外壳：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {}
}
```

成功返回示例：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "result": {
      "applied": true,
      "room_version": 8,
      "server_time_ms": 1778000001123
    },
    "room": {
      "room_id": "lalu_abc123",
      "im_group_id": "lalu_abc123",
      "room_short_id": "10001",
      "title": "Live Room",
      "cover_url": "https://cdn.example/room.png",
      "background_url": "https://cdn.example/bg.png",
      "description": "welcome",
      "owner_user_id": "10001",
      "mode": "voice",
      "status": "active",
      "chat_enabled": true,
      "locked": false,
      "heat": 88,
      "online_count": 200,
      "seat_count": 10,
      "occupied_seat_count": 2,
      "visible_region_id": 1001,
      "version": 8
    },
    "viewer": {
      "user_id": "10001",
      "role": "audience",
      "joined_at_ms": 1778000001000,
      "last_seen_at_ms": 1778000001100
    },
    "seats": [
      {
        "seat_no": 1,
        "user_id": "10002",
        "locked": false,
        "publish_state": "publishing",
        "mic_session_id": "mic_10002",
        "publish_deadline_ms": 1778000030000,
        "mic_muted": false,
        "seat_status": "publishing",
        "mic_heartbeat_at_ms": 1778000000000,
        "gift_value": 0
      },
      {
        "seat_no": 2,
        "locked": false,
        "mic_muted": false,
        "gift_value": 0
      }
    ],
    "profiles": [
      {
        "user_id": "10001",
        "username": "host",
        "avatar": "https://cdn.example/avatar.png",
        "display_user_id": "10001"
      },
      {
        "user_id": "10002",
        "username": "speaker",
        "avatar": "https://cdn.example/avatar2.png",
        "display_user_id": "10002"
      }
    ],
    "im": {
      "group_id": "lalu_abc123",
      "need_join_group": true
    },
    "rtc": {
      "need_token": true,
      "available": true,
      "token": {
        "sdk_app_id": 1400000000,
        "user_id": "10001",
        "rtc_user_id": "10001",
        "user_sig": "xxx",
        "expire_at_ms": 1778007200000,
        "room_id": "lalu_abc123",
        "rtc_room_id": "lalu_abc123",
        "rtc_room_id_type": "string",
        "str_room_id": "lalu_abc123",
        "app_scene": "voice_chat_room",
        "role": "user"
      }
    },
    "server_time_ms": 1778000001123
  }
}
```

字段说明：

| 字段 | 说明 |
| --- | --- |
| `data.result.applied` | 本次进房是否真正写入。重复 `command_id` 重试可能为 `false`。 |
| `data.result.room_version` | 房间版本，用于客户端状态更新判断。 |
| `data.room.room_id` | 房间 ID。 |
| `data.room.im_group_id` | 腾讯 IM 房间群 ID。当前通常等于 `room_id`，客户端仍以返回值为准。 |
| `data.room.online_count` | 房间在线人数。`lite` 不返回在线用户明细，只返回人数。 |
| `data.room.seat_count` | 麦位总数。 |
| `data.room.occupied_seat_count` | 已占用麦位数。 |
| `data.viewer` | 当前登录用户在房间内的 presence。 |
| `data.viewer.role` | 当前用户房间角色，房主会由服务端返回 `owner`。 |
| `data.seats` | 首屏麦位状态。 |
| `data.seats[].user_id` | 麦上用户 ID；空麦不返回该字段。 |
| `data.seats[].publish_state` | 麦上发流状态，常见值：`pending_publish` / `publishing`。 |
| `data.seats[].mic_session_id` | 麦位发流会话 ID。麦上心跳、发流确认需要使用它。 |
| `data.seats[].seat_status` | 麦位展示状态，常见值：`empty` / `locked` / `occupied` / `publishing` / `muted`。 |
| `data.profiles` | 首屏基础资料，只包含房主、当前用户、麦上用户。 |
| `data.profiles[].username` | 昵称。 |
| `data.profiles[].avatar` | 头像。 |
| `data.profiles[].display_user_id` | 展示 ID。 |
| `data.im.group_id` | Flutter 调腾讯 IM `joinGroup` 使用的群 ID。 |
| `data.im.need_join_group` | 为 `true` 时需要加入腾讯 IM 群。 |
| `data.rtc.available` | 是否拿到 RTC 票据。 |
| `data.rtc.token` | RTC 进房票据。`available=false` 时可能为空。 |
| `data.rtc.reason` | RTC 不可用原因。业务进房成功时，RTC 不可用不代表进房失败。 |
| `data.server_time_ms` | 服务端时间，UTC epoch milliseconds。 |

`lite` 不返回这些内容：

| 字段 | 说明 |
| --- | --- |
| `contribution_rank` | 贡献榜不在进房首屏主包返回。 |
| `profiles[].avatar_frame` | 头像框后置拉。 |
| `profiles[].profile_card` | 资料卡后置拉。 |
| `profiles[].vehicle` | 座驾后置拉。 |
| `profiles[].mic_seat_animation` | 麦位动画后置拉。 |
| `profiles[].badges` | 等级/成就徽章后置拉。 |
| `profiles[].vip` | VIP 展示后置拉。 |

## 相关 IM

进房成功后 Flutter 执行：

```text
1. POST /api/v1/rooms/join，body 传 response_mode=lite
2. 如果 data.im.need_join_group=true，调用腾讯 IM joinGroup(data.im.group_id)
3. joinGroup 成功或已在群内，都按进房 IM 成功处理
4. 页面开始房间 heartbeat
5. 如果 data.rtc.available=true，用 data.rtc.token 进入 RTC
```

说明：

| 项 | 说明 |
| --- | --- |
| IM 登录 | 进房接口不返回 IM UserSig。Flutter 仍然通过 `GET /api/v1/im/usersig` 获取并登录腾讯 IM。 |
| IM 入群 | 必须先 `JoinRoom` 成功，再调腾讯 IM `joinGroup`。不要先入 IM 群。 |
| 入房广播 | 服务端会发 `RoomUserJoined` 房间事件。Flutter 按现有房间 IM 事件逻辑展示入房飘窗。 |
| 重复进房 | 用户重新打开房间页或重连时，服务端仍会给展示用入房事件，Flutter 正常展示即可。 |
| 入场座驾 | `lite` 进房不携带座驾快照。座驾、头像框、资料卡、麦位动画等展示资料在进房 UI 出来后异步补齐。 |

## 后置资料接口

进房首屏出来后，再按需要异步拉展示资料：

```http
GET /api/v1/users/room-display-profiles:batch?user_ids=10001,10002
Authorization: Bearer <access_token>
```

参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `user_ids` | string | 是 | 逗号分隔的用户 ID。也支持多个 `user_id=10001&user_id=10002`。 |

返回值：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "profiles": [
      {
        "user_id": "10001",
        "username": "host",
        "avatar": "https://cdn.example/avatar.png",
        "display_user_id": "10001",
        "avatar_frame": {},
        "profile_card": {},
        "vehicle": {},
        "mic_seat_animation": {},
        "badges": [],
        "vip": {},
        "level": {},
        "level_badges": {},
        "charm": 0
      }
    ]
  }
}
```

当前用户自己的装扮也可以后置拉：

```http
GET /api/v1/appearance
GET /api/v1/users/me/appearance
```

## 常见错误

| code | HTTP | 说明 |
| --- | --- | --- |
| `INVALID_JSON` | 400 | 请求体不是合法 JSON。 |
| `INVALID_ARGUMENT` | 400 | 参数错误，例如 `response_mode` 不是 `lite` 或 `full`。 |
| `UNAUTHORIZED` | 401 | 未登录或 token 无效。 |
| `PROFILE_REQUIRED` | 403 | 用户资料未补全。 |
| `PERMISSION_DENIED` | 403 | 密码错误、被踢/被禁等权限问题。 |
| `ROOM_CLOSED` | 409 | 房间已关闭；非房主不能进入。 |
| `NOT_FOUND` | 404 | 房间不存在。 |
| `UPSTREAM_ERROR` | 502 | 上游服务异常。 |
