# 个人信息页用户上麦状态 Flutter 对接 ## 1. 接口职责 个人信息页通过本接口判断被查看用户当前是否占用语音房麦位,并取得可直接交给统一进房入口的房间信息。 - 上麦事实只来自 `room-service` 当前 Room Cell 快照;不要用用户是否为房主、房间搜索结果、IM 本地状态或历史缓存推断。 - 接口返回的是带时间戳的瞬时快照;点击 Live 后仍以 `POST /api/v1/rooms/join` 的实时校验为准。 - 当前接口仅用于客态个人信息页。请求中包含当前登录用户本人时,该项固定返回 `not_on_mic`。 ## 2. 请求 ```http GET /api/v1/users/voice-room-presence:batch?user_ids=163020,163021 Authorization: Bearer {access_token} ``` | 参数 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `user_ids` | string | 是 | 逗号分隔的系统 `user_id`,不接受展示短号 | 约束: - 去重后最多 100 个 ID,响应顺序按请求中第一次出现的顺序返回。 - 空值、非数字、`0`、负数、空分段或超过上限都会整批返回 `INVALID_USER_IDS`。 - `viewer_user_id`、`app_code` 和 `request_id` 由 gateway 从登录态和请求上下文注入,客户端不能提交。 - 接口要求有效登录态且用户已完成资料。 ## 3. 成功响应 ```json { "code": "OK", "message": "ok", "request_id": "req_xxx", "data": { "items": [ { "user_id": "163020", "state": "on_mic", "seat_no": 3, "joinable": true, "observed_at_ms": 1784265600000, "room": { "room_id": "100086", "im_group_id": "100086", "rtc_room_id": "100086", "room_short_id": "886688", "title": "Room name", "owner_user_id": "12001", "host_user_id": "12001", "mode": "voice", "cover_url": "https://cdn.example.com/room-cover.png", "background_url": "https://cdn.example.com/room-bg.png", "online_count": 56, "seat_count": 10, "locked": true } }, { "user_id": "163021", "state": "not_on_mic", "seat_no": 0, "joinable": false, "observed_at_ms": 1784265600000, "room": null } ], "server_time_ms": 1784265600123 } } ``` 字段说明: | 字段 | 类型 | 说明 | | --- | --- | --- | | `items[].user_id` | string | 系统用户 ID;JSON 中按字符串返回 | | `items[].state` | string | `on_mic` 或 `not_on_mic` | | `items[].seat_no` | int | 当前麦位编号;未上麦为 `0` | | `items[].joinable` | bool | 当前 viewer 是否允许通过该入口尝试进房 | | `items[].observed_at_ms` | int64 | 本批状态完成判定时的 UTC epoch ms | | `items[].room` | object/null | 仅 `on_mic && joinable` 时非空 | | `server_time_ms` | int64 | 服务端本批响应时间,UTC epoch ms | 房间字段可以直接构造现有 `VoiceRoomEntryArgs`: - `room_id` 是业务房间主键。 - `im_group_id` 当前等于 `room_id`。 - `rtc_room_id` 当前使用 TRTC string room ID,等于 `room_id`。 - 当前房间模型没有独立主持人字段,`host_user_id` 与 `owner_user_id` 一致。 - `locked=true` 表示密码房;点击后继续复用现有密码输入和进房流程,不能在个人页自行索取或缓存密码。 ## 4. Flutter 展示和刷新规则 只有下面四个条件全部成立时显示 Live: ```dart final showLive = item.state == 'on_mic' && item.joinable && item.room != null && item.room!.roomId.isNotEmpty; ``` 点击时: 1. 调用现有 `VoiceRoomNavigation.open()`。 2. 用 `room` 构造 `VoiceRoomEntryArgs`。 3. `entrySource` 使用 `profile_card_live_badge`。 4. 正常调用 `POST /api/v1/rooms/join`;如果用户已经下麦但房间仍允许进入,可以正常进入。 5. 进房返回房间关闭、封禁、密码错误等业务错误时,移除当前 Live 状态并按现有错误规则提示。 刷新时机: - 首次进入客态个人信息页。 - App 从后台回到前台且个人信息页仍可见。 - 从语音房返回个人信息页。 - 用户手动刷新个人信息页。 不要轮询、持久化或使用上一次成功结果兜底。接口失败时只隐藏 Live,不影响个人信息页其他资料。 ## 5. 后端状态边界 - `on_mic` 表示房间仍为 `active`、目标用户仍在房间在线集合中,并且占用一个当前有效麦位。`MicUp` 后的 `pending_publish` 和 RTC 已确认的 `publishing` 都属于已占麦。 - 房主或管理员只有实际占用麦位时才算上麦;身份本身不算。 - 同一 App 的 `room_user_presence` 使用 `(app_code, user_id)` 主键定位候选房间,最终状态再由 Room Cell 快照确认。 - 跨 `app_code` 房间不会返回。 - viewer 被目标房间封禁、房间已关闭、presence 已滞后或快照事实不完整时,统一返回 `not_on_mic + room:null`,不泄露旧房间 ID。 - 密码房允许展示 Live,返回 `locked=true`,正式进房仍校验密码。 - 当前房间产品模型没有“隐藏房/隐身上麦”字段,用户关系也没有资料位置黑名单能力;本版不做不存在的状态推断。以后增加此类产品能力时,必须在后端权限判断中收敛后再返回,Flutter 不得自行判断。 ## 6. 错误响应 | HTTP | code | 处理方式 | | --- | --- | --- | | 400 | `INVALID_USER_IDS` | 修正参数;不要重试同一请求 | | 401 | `UNAUTHORIZED` / `ACCESS_TOKEN_EXPIRED` | 走现有登录或刷新 token 流程 | | 403 | `PROFILE_REQUIRED` | 先完成用户资料 | | 503 | `VOICE_ROOM_PRESENCE_UNAVAILABLE` | 隐藏 Live;页面其他内容继续展示 | 示例: ```json { "code": "INVALID_USER_IDS", "message": "invalid user ids", "request_id": "req_xxx" } ``` 排障日志只记录接口路径、目标 ID 数量、响应业务码和 `request_id`;不要记录 access token 或密码房密码。 ## 7. 联调清单 - 未进房、只在房内未上麦:不显示 Live。 - 在自己的房间或其他人的房间实际占麦:显示 Live,并返回实际所在房间。 - `pending_publish`、`publishing`:都显示 Live。 - 下麦、离房、被踢或关房后重新请求:返回 `not_on_mic`。 - 密码房:显示 Live,点击后进入既有密码流程。 - viewer 被目标房间封禁、跨 App、查询本人:不返回房间定位。 - 一批中混合上麦、未上麦和重复用户:每个去重用户一条结果,顺序稳定。 - 查询成功后立即下麦:点击仍以正式进房接口结果为准。