# 语音房房间用户列表前端接入文档

## 1. 接入范围


- 房间在线用户列表接口。
- 设置或取消房间管理员接口。
- 相关腾讯云 IM 房间系统消息。
- 前端错误处理建议。

## 2. 通用约定

### 2.1 鉴权

所有接口都需要登录态：

```http
Authorization: Bearer <access_token>
```

### 2.2 ID 类型

HTTP 返回里的用户 ID 统一按字符串处理，避免 JavaScript 大整数精度问题。

示例：

```json
{
  "user_id": "10001"
}
```

### 2.3 时间类型

所有时间字段都是毫秒时间戳：

```json
{
  "server_time_ms": 1760000006000
}
```

### 2.4 房间身份

房间用户身份拆成两个字段：

| 字段 | 说明 |
| --- | --- |
| `is_owner` | 是否是当前房间房主 |
| `room_role` | 是否是当前房间管理员，只表示管理员集合 |

`room_role` 只会返回：

| 值 | 说明 |
| --- | --- |
| `normal` | 当前房间普通用户 |
| `admin` | 当前房间管理员 |

注意：

- 房主不是管理员，房主用户返回 `is_owner = true`。
- 房主如果不在管理员集合里，`room_role` 返回 `normal`。
- 前端展示“房主”标识时看 `is_owner`，展示“管理员”标识时看 `room_role = admin`。
- 当前登录用户是否展示管理员操作入口，优先看房间详情里的 `permissions.is_owner`；如果只在用户列表内判断，则看当前登录用户对应 item 的 `is_owner`。
- `role` 是进房 presence 角色，例如 `audience`，不要用于判断房主或管理员。

## 3. 查询房间在线用户列表

### 3.1 接口定义

```http
GET /api/v1/rooms/{room_id}/online-users
```

### 3.2 Path 参数

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID |

### 3.3 Query 参数

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `page` | int | 否 | `1` | 页码，从 `1` 开始 |
| `page_size` | int | 否 | `50` | 每页数量，最大 `100` |
| `sort` | string | 否 | `online` | `online` 按在线活跃排序；`gift_value` 按礼物价值排序 |

### 3.4 请求示例

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.global-interaction.com/api/v1/rooms/room_10001/online-users?page=1&page_size=50"
```

### 3.5 返回示例

```json
{
  "items": [
    {
      "user_id": "10001",
      "role": "audience",
      "is_owner": true,
      "room_role": "normal",
      "gift_value": 188800,
      "joined_at_ms": 1760000000000,
      "last_seen_at_ms": 1760000005000,
      "profile": {
        "user_id": "10001",
        "username": "Alice",
        "avatar": "https://example.com/a.png",
        "display_user_id": "163001",
        "gender": "female",
        "age": 26,
        "country": "US",
        "country_name": "United States",
        "country_display_name": "United States",
        "country_flag": "🇺🇸"
      }
    },
    {
      "user_id": "10002",
      "role": "audience",
      "is_owner": false,
      "room_role": "admin",
      "gift_value": 0,
      "joined_at_ms": 1760000001000,
      "last_seen_at_ms": 1760000004000,
      "profile": {
        "user_id": "10002",
        "username": "Bob",
        "avatar": "https://example.com/b.png",
        "display_user_id": "163002",
        "gender": "male",
        "age": 24,
        "country": "SG",
        "country_name": "Singapore",
        "country_display_name": "Singapore",
        "country_flag": "🇸🇬"
      }
    }
  ],
  "total": 2,
  "page": 1,
  "page_size": 50,
  "server_time_ms": 1760000006000
}
```

### 3.6 返回字段

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `items` | array | 当前页在线用户 |
| `items[].user_id` | string | 用户 ID |
| `items[].role` | string | 进房 presence 角色 |
| `items[].is_owner` | bool | 是否是当前房间房主 |
| `items[].room_role` | string | 当前房间管理员身份：`normal` 或 `admin` |
| `items[].gift_value` | int64 | 当前房间内累计送礼价值，未送礼返回 `0` |
| `items[].joined_at_ms` | int64 | 进房时间 |
| `items[].last_seen_at_ms` | int64 | 最近房间业务心跳时间 |
| `items[].profile.user_id` | string | 用户 ID |
| `items[].profile.username` | string | 用户昵称 |
| `items[].profile.avatar` | string | 用户头像 |
| `items[].profile.display_user_id` | string | 展示 ID |
| `items[].profile.gender` | string | 用户性别，来自 user-service |
| `items[].profile.age` | int | 用户年龄，由后端按生日计算；生日为空或非法时返回 `0` |
| `items[].profile.country` | string | 用户国家码，ISO Alpha-2，例如 `US` |
| `items[].profile.country_name` | string | 国家名称，来自国家主数据 |
| `items[].profile.country_display_name` | string | 国家展示名称，来自国家主数据 |
| `items[].profile.country_flag` | string | 国家旗帜 emoji，由国家码生成；国家码为空或非法时返回空字符串 |
| `total` | int64 | 在线用户总数 |
| `page` | int | 当前页 |
| `page_size` | int | 当前每页数量 |
| `server_time_ms` | int64 | 服务端时间 |

### 3.7 前端展示建议

- `gift_value` 展示的是该用户在当前房间内“送出”的礼物价值，不是收到的礼物价值。
- `gift_value = 0` 时仍然展示该用户。
- 房主标识看 `is_owner = true`。
- 管理员标识看 `room_role = admin`。
- 房主不是管理员，不要因为 `is_owner = true` 就把管理员标识点亮。
- 在线列表只返回 `gender` 和 `age`，不会返回用户生日 `birth`。
- `age = 0` 表示用户未填写生日或生日格式异常，前端可隐藏年龄。
- 国家展示优先使用 `country_display_name`，没有时可降级使用 `country_name` 或 `country`。
- `country_flag` 是 emoji 字符串，前端无需自行按国家码换算。
- 用户列表弹窗打开时，可以先拉第一页；滚动到底再拉下一页。
- 收到相关 IM 后，如果列表弹窗正在展示，可以局部更新；状态不确定时重新拉接口。

## 4. 设置或取消管理员，只有房主才显示

### 4.1 接口定义

```http
POST /api/v1/rooms/admin/set
Content-Type: application/json
```

### 4.2 请求参数

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID |
| `command_id` | string | 是 | 客户端生成的幂等 ID，重试同一次操作时保持不变 |
| `target_user_id` | string/int64 | 是 | 被设置或取消管理员的用户 ID |
| `enabled` | bool | 是 | `true` 设置管理员；`false` 取消管理员 |

### 4.3 设置管理员请求示例

```json
{
  "room_id": "room_10001",
  "command_id": "cmd-10001",
  "target_user_id": "10002",
  "enabled": true
}
```

### 4.4 取消管理员请求示例

```json
{
  "room_id": "room_10001",
  "command_id": "cmd-10002",
  "target_user_id": "10002",
  "enabled": false
}
```

### 4.5 返回示例

建议后端返回：

```json
{
  "result": {
    "applied": true,
    "room_version": 12
  },
  "target_user_id": "10002",
  "room_role": "admin",
  "server_time_ms": 1760000006000
}
```

字段说明：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次请求是否实际改变了房间状态 |
| `result.room_version` | int64 | 房间状态版本 |
| `target_user_id` | string | 被操作用户 |
| `room_role` | string | 操作后的身份，`admin` 或 `normal` |
| `server_time_ms` | int64 | 服务端时间 |

如果后端第一版仍直接返回完整 room-service gRPC 结构，前端只需要关注命令是否成功；最终用户列表状态以接口重拉或 IM 刷新为准。

### 4.6 前端操作边界

- 只有房主可以设置或取消管理员。
- 房主可以操作所有非房主用户的管理员身份。
- 管理员之间不可互相设置或取消管理员。
- 房主不是管理员，不允许对房主执行设置或取消管理员。
- 目标用户必须当前在房间内。
- 前端重试同一次操作时，必须复用同一个 `command_id`。
- 操作成功后，可以等待 `room_admin_changed` IM，也可以直接刷新用户列表。

## 5. 错误处理

### 5.1 通用错误

| HTTP 状态 | 场景 | 前端处理 |
| --- | --- | --- |
| `401` | 未登录或 token 失效 | 跳登录或刷新登录态 |
| `403` | 无权限，例如用户不在房间或不是房主 | 提示无权限，并刷新房间状态 |
| `404` | 房间不存在或目标用户不在房间 | 提示目标不存在或用户已离开 |
| `409` | 状态冲突，例如目标用户是房主 | 提示当前状态不允许操作，并刷新用户列表 |
| `5xx` | 服务端异常 | 保留当前 UI，提示稍后重试 |

### 5.2 查询用户列表错误

| 场景 | 前端处理 |
| --- | --- |
| 当前用户不在房间 | 关闭用户列表弹窗，刷新当前房间状态 |
| 分页为空 | 正常展示空列表或结束加载更多 |
| `page_size` 超过上限 | 使用服务端返回的 `page_size` 为准 |

### 5.3 设置管理员错误

| 场景 | 前端处理 |
| --- | --- |
| 非房主操作 | 隐藏或禁用管理入口，提示无权限 |
| 操作目标是房主 | 不展示管理员操作入口；如果服务端返回错误，刷新列表 |
| 目标用户已离房 | 提示用户已离开，刷新列表 |
| 重复点击 | 前端按钮进入 loading；同一次请求复用同一个 `command_id` |
| 请求超时 | 可以用同一个 `command_id` 重试 |

## 6. 相关 IM

### 6.1 IM 消息形态

房间系统消息通过腾讯云 IM `TIMCustomElem` 下发。

前端解析入口：

```text
TIMCustomElem.MsgContent.Data
```

`Data` 是 JSON 字符串。

示例：

```json
{
  "event_id": "evt-admin-10001",
  "room_id": "room_10001",
  "event_type": "room_admin_changed",
  "actor_user_id": 10001,
  "target_user_id": 10002,
  "room_version": 12,
  "attributes": {
    "enabled": "true"
  }
}
```

通用字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | 事件唯一 ID，前端用于去重 |
| `room_id` | string | 房间 ID |
| `event_type` | string | 事件类型 |
| `actor_user_id` | int64 | 操作人 |
| `target_user_id` | int64 | 被操作人 |
| `room_version` | int64 | 房间版本 |
| `attributes` | object | 字符串键值扩展字段 |

### 6.2 管理员变更 IM

事件类型：

```text
room_admin_changed
```

消息示例：

```json
{
  "event_id": "evt-admin-10001",
  "room_id": "room_10001",
  "event_type": "room_admin_changed",
  "actor_user_id": 10001,
  "target_user_id": 10002,
  "room_version": 12,
  "attributes": {
    "enabled": "true"
  }
}
```

字段说明：

| 字段 | 说明 |
| --- | --- |
| `actor_user_id` | 发起操作的房主 |
| `target_user_id` | 被设置或取消管理员的用户 |
| `attributes.enabled` | `"true"` 设置管理员；`"false"` 取消管理员 |

前端处理：

- 使用 `event_id` 去重。
- 如果当前用户是 `target_user_id`，更新当前用户管理权限 UI。
- 如果用户列表已打开，更新对应用户的 `room_role`。
- 如果列表数据不完整，重新请求在线用户列表接口。

### 6.3 送礼成功 IM

事件类型：

```text
room_gift_sent
```

消息示例：

```json
{
  "event_id": "evt-gift-10001",
  "room_id": "room_10001",
  "event_type": "room_gift_sent",
  "actor_user_id": 10002,
  "target_user_id": 10003,
  "gift_value": 5200,
  "room_heat": 999999,
  "room_version": 13,
  "attributes": {
    "gift_id": "rose",
    "pool_id": "",
    "gift_count": "10",
    "billing_receipt_id": "receipt-10001",
    "coin_spent": "5200",
    "gift_point_added": "5200",
    "price_version": "v1"
  }
}
```

字段说明：

| 字段 | 说明 |
| --- | --- |
| `actor_user_id` | 送礼用户，用户列表里需要累加 `gift_value` 的用户 |
| `target_user_id` | 收礼用户 |
| `gift_value` | 本次送礼价值增量 |
| `room_heat` | 送礼后的房间热度 |
| `attributes.gift_id` | 礼物 ID |
| `attributes.gift_count` | 礼物数量，字符串 |
| `attributes.billing_receipt_id` | 钱包结算流水 |

前端处理：

- 如果用户列表已打开，并且当前页包含 `actor_user_id`，可以执行 `gift_value += event.gift_value`。
- 如果当前列表按 `gift_value` 排序，建议重新拉接口，避免分页顺序错乱。
- 如果当前页不包含 `actor_user_id`，不要手动插入用户，分页完整性以接口为准。

### 6.4 进出房 IM

用户列表也会受进出房影响：

| 事件 | 建议处理 |
| --- | --- |
| `room_user_joined` | 用户列表打开时重新拉列表 |
| `room_user_left` | 用户列表打开时移除用户或重新拉列表 |

## 7. 前端刷新策略

推荐策略：

1. 打开用户列表弹窗时，请求 `GET /api/v1/rooms/{room_id}/online-users`。
2. 收到 `room_admin_changed` 时，局部更新 `room_role`；不确定时重拉。
3. 收到 `room_gift_sent` 时，局部累加 `gift_value`；如果当前排序依赖礼物价值，重拉。
4. 收到 `room_user_joined` 或 `room_user_left` 时，列表打开状态下重拉。
5. 所有 IM 使用 `event_id` 去重。
