# Room Entry And Reconnect Development Requirements 本文档定义下一阶段必须完成的语音房基础闭环:创建房间、进房、离房、断线重连。目标是让一个房间可以被稳定打开、进入、退出,并在客户端断线或服务短暂抖动后恢复到可继续使用的状态。 ## Goal 本阶段只解决房间生命周期的最小业务闭环。 | Capability | Required Result | | --- | --- | | 创建房间 | 用户可以创建一个 active 语音房,并获得房间快照 | | 进房 | 用户可以进入房间业务 presence,并获得当前房间状态 | | 订阅房间消息 | 用户进房成功后可以登录腾讯云 IM 并加入房间群 | | 离房 | 用户主动退出后,房间 presence 被清理,后续进群和发言必须被业务守卫拒绝 | | 断线重连 | 用户短暂断线后,可以重新鉴权、恢复房间状态、重新登录腾讯云 IM 并进群 | | 防越权 | 未进房、被踢、被封禁用户不能继续进群或发公屏 | ## Non Goals 本阶段不做这些: | Not Included | Reason | | --- | --- | | 麦位完整玩法 | 放到下一阶段,避免和进房链路混在一起 | | 腾讯云 RTC token | 下一阶段接 RTC 时补齐 | | 金币和礼物 | 需要 wallet-service 先落地 | | 推荐流和房间列表 | 先保证单房间可用 | | 多端复杂同步 | 首版只保证同一用户重连恢复 | | 热门房主备热同步 | 当前仍是单房间单活 + lease 接管 | ## Ownership | Domain | Owner | Rule | | --- | --- | --- | | HTTP 房间命令 | `gateway-service` | 只做鉴权、入参校验、协议转换 | | 房间业务 presence | `room-service` | 创建房间、进房、离房、封禁、恢复 | | 实时连接和房间群 | 腾讯云 IM | 持有客户端长连接、群成员关系、房间消息历史 | | 身份和 token | `user-service` | 登录、token、用户状态 | | 真实语音链路 | 腾讯云 RTC | 后续阶段接入,不由本阶段实现 | 关键边界: - `room-service` 不保存 socket。 - 腾讯云 IM 不拥有房间业务 presence;业务守卫仍以 `room-service` 为准。 - `gateway-service` 不信任客户端自报 `owner_user_id`、`host_user_id` 或 `actor_user_id`。 - 所有业务身份以 access token 解析出的 `user_id` 为准。 - 腾讯云 IM 登录使用 `gateway-service` 签发的 UserSig,UserSig 里的 identifier 使用不可变长 `user_id`。 ## Presence Model 需要明确区分三种状态: | State | Owner | Meaning | | --- | --- | --- | | 房间业务 presence | `room-service` | 用户业务上属于这个房间 | | 房间群连接态 | 腾讯云 IM | 用户当前客户端是否登录 IM、是否在房间群内 | | RTC 在线态 | 腾讯云 RTC | 用户是否在真实音频频道中 | 本阶段只实现 room-service presence,并通过腾讯云 IM 群承载消息。RTC 在线态只预留字段和流程位置。 ## External API ### Create Room ```text POST /api/v1/rooms/create Authorization: Bearer ``` 建议请求: ```json { "room_id": "room_1001", "command_id": "cmd_create_room_1001_10001_1", "seat_count": 8, "mode": "voice", "room_name": "周末语音房", "room_avatar": "", "room_description": "本房间用于轻量聊天和上麦互动" } ``` 正式客户端不应该传 `owner_user_id` 和 `host_user_id`。gateway 必须从 access token 取当前 `user_id` 写入 `RequestMeta.actor_user_id`;room-service 使用该 actor 确定 owner 和默认 host。`room_name` 必填,`room_avatar` 为空时写入默认系统头像,房间展示资料统一落在 `RoomExt`。 响应必须包含: ```json { "result": { "applied": true, "room_version": 1, "server_time_ms": 1777000000000 }, "room": {} } ``` ### Join Room ```text POST /api/v1/rooms/join Authorization: Bearer ``` 请求: ```json { "room_id": "room_1001", "command_id": "cmd_join_room_1001_10002_1", "role": "audience" } ``` 响应必须包含用户业务 presence 和房间快照: ```json { "result": {}, "user": { "user_id": 10001, "role": "audience", "joined_at_ms": 1777000000000, "last_seen_at_ms": 1777000000000 }, "room": {} } ``` `JoinRoom` 必须幂等。用户已经在房间时,不能重复增加在线人数或重复广播进房消息,但需要刷新 `last_seen_at_ms`。 ### Leave Room ```text POST /api/v1/rooms/leave Authorization: Bearer ``` 请求: ```json { "room_id": "room_1001", "command_id": "cmd_leave_room_1001_10002_1" } ``` `LeaveRoom` 必须幂等。用户已经不在房间时,返回当前快照,不报业务错误。 ### Tencent IM Login And Join Group 客户端在 `JoinRoom` 成功后向 gateway 获取腾讯云 IM UserSig: ```text POST /api/v1/im/usersig Authorization: Bearer ``` 响应中的 `user_id` 是腾讯云 IM identifier,必须等于内部不可变长 `user_id` 的十进制字符串。 客户端随后使用腾讯云 IM SDK 登录并加入房间群: ```json { "sdk_app_id": 1400000000, "user_id": "10001", "user_sig": "usersig_xxx", "room_id": "room_1001" } ``` 进群前业务守卫仍然需要存在。首版可以由客户端先 `JoinRoom` 再进群;线上必须补腾讯云 IM 回调或 gateway 代理校验: ```text room-service.VerifyRoomPresence(room_id, user_id, session_id) ``` 只有 `present=true` 才允许进群或继续接收房间业务消息。被踢或封禁后,`room-service` 负责写 outbox 并通过腾讯云 IM 推系统事件,客户端必须按事件退房或触发重新同步。 ## Required Internal Changes 当前基础接口已经存在,但本阶段需要补齐这些行为。 ### gateway-service | Requirement | Detail | | --- | --- | | 创建房间身份收敛 | `CreateRoom` 的 owner/host 必须由 room-service 从 `RequestMeta.actor_user_id` 收敛,不接受客户端 body 自报 | | command_id | 所有房间命令请求体都支持稳定 `command_id`,用于 room-service command log 幂等。客户端重试同一业务动作必须复用该值;未传时 gateway 只为本次请求生成新值,不保证 HTTP 重试幂等 | | 统一错误映射 | 房间不存在、被踢、无权限、重复命令要映射成稳定 HTTP envelope | | 请求日志 | 记录 `request_id`、`user_id`、`room_id`、command type | ### room-service | Requirement | Detail | | --- | --- | | CreateRoom | 创建 meta、command log、snapshot、outbox,并安装 Room Cell | | 单 owner 单房间 | 同一个 `RequestMeta.actor_user_id` 只能作为 owner 创建一个房间;已有 owner 房间时返回 Conflict | | JoinRoom 幂等 | 已在房间时刷新 `last_seen_at_ms`,不重复广播 | | LeaveRoom 幂等 | 不在房间时返回当前快照 | | VerifyRoomPresence | 腾讯云 IM 回调或 gateway 代理进群前检查房间存在、用户在房间、未被 ban | | Presence 过期 | 支持断线宽限期,超过 TTL 后自动清理 | | Recovery | 节点重启后能通过 snapshot + command log 恢复房间 | 建议新增配置: ```yaml room_presence_reconnect_grace: "60s" room_presence_stale_timeout: "300s" room_presence_sweep_interval: "30s" ``` ### Tencent Cloud IM Integration | Requirement | Detail | | --- | --- | | UserSig | `gateway-service` 只给已鉴权用户签发腾讯云 IM UserSig | | EnsureRoomGroup | `room-service.CreateRoom` 必须确保腾讯云 IM 群组存在 | | PublishRoomEvent | 房间状态变更后同步推腾讯云 IM 群系统消息,失败不回滚房间状态 | | Guard Callback | 线上需要腾讯云 IM 进群/发言回调或等价 gateway 代理调用 `VerifyRoomPresence`、`CheckSpeakPermission` | | History | 房间最近消息优先使用腾讯云 IM 群历史;本仓库不再保存自研 IM 时间线 | ## Reconnect Design 断线分三种。 | Case | Expected Behavior | | --- | --- | | IM 短断 | room-service presence 保留,客户端重新登录腾讯云 IM 后重新进群 | | App 退后台后恢复 | 客户端先调用 `JoinRoom` 幂等刷新,再重新登录腾讯云 IM 并进群 | | 超过 stale timeout | room-service 自动清理 presence,客户端需要重新进房 | 客户端重连标准流程: ```text 1. 如果 access token 过期,先 RefreshToken 2. 调 POST /api/v1/rooms/join,刷新房间业务 presence 3. 调 POST /api/v1/im/usersig,拿腾讯云 IM UserSig 4. 使用腾讯云 IM SDK 登录 5. 加入 room_id 对应的腾讯云 IM 群 6. 拉取腾讯云 IM 群历史或等待房间系统消息补齐 7. 用 JoinRoom 返回的 RoomSnapshot 覆盖本地房间状态 ``` 服务端原则: - 断线不能立刻等同于离房。 - 主动 `LeaveRoom` 才是明确离房。 - 超时清理是兜底,不能依赖客户端一定发离房。 - 被踢或封禁后,即使客户端仍在腾讯云 IM 群内,也必须通过回调守卫、系统事件和客户端同步阻断继续发言或展示在房间内。 ## Data Requirements `RoomUserState` 至少需要支持: | Field | Meaning | | --- | --- | | `user_id` | 房间内业务用户 | | `role` | owner, host, admin, audience | | `joined_at_ms` | 首次进入当前房间时间 | | `last_seen_at_ms` | 最近一次 JoinRoom、订阅或心跳时间 | 建议后续追加: | Field | Meaning | | --- | --- | | `connection_state` | online, reconnecting, stale | | `reconnect_deadline_ms` | 断线宽限截止时间 | 首版可以先不把 `connection_state` 暴露给客户端,但内部清理逻辑要能基于 `last_seen_at_ms` 判断 stale。 ## Event Requirements 房间状态成功变更后,`room-service` 必须写 outbox。 | Action | Outbox Event | IM System Event | | --- | --- | --- | | 创建房间 | `RoomCreated` | 可不广播 | | 进房成功 | `RoomUserJoined` | `room_user_joined` | | 离房成功 | `RoomUserLeft` | `room_user_left` | | stale 自动离房 | `RoomUserLeft` | `room_user_left`,reason=`stale_timeout` | | 被踢 | `RoomUserKicked` | `room_user_kicked` | 同步 `PublishRoomEvent` 失败不能回滚房间状态,仍由 room outbox 补投到腾讯云 IM 和后续活动系统。 ## Acceptance Tests 必须覆盖这些测试。 | Test | Expected Result | | --- | --- | | 创建房间成功 | 返回 version=1,owner 在 online_users 内 | | 重复 CreateRoom command_id | 不重复创建,返回同一房间快照 | | 普通用户 JoinRoom | presence 增加,广播进房事件 | | 重复 JoinRoom | 不重复广播,不重复增加人数,刷新 last_seen | | LeaveRoom | presence 移除,进群/发言守卫拒绝后续实时消息权限 | | 重复 LeaveRoom | 幂等返回,不报错 | | JoinRoom 后进群 | `VerifyRoomPresence` 通过,腾讯云 IM 群收到房间消息 | | 未 JoinRoom 直接进群或发言 | `VerifyRoomPresence` 或 `CheckSpeakPermission` 返回 `not_in_room` | | Kick 后进群或发言 | 返回 `user_banned` | | IM 断开后 60 秒内重连 | JoinRoom 幂等刷新,腾讯云 IM 重新登录和进群成功,历史可拉 | | 超过 stale timeout | presence 自动清理,需要重新 JoinRoom | | room-service 重启恢复 | snapshot + command log 恢复后 JoinRoom/subscribe 继续可用 | ## Implementation Order 建议按这个顺序做: 1. 修正创建房间身份来源:gateway 不接收 body 的 owner/host,room-service 从 `RequestMeta.actor_user_id` 收敛 owner/default host。 2. 补齐 JoinRoom 幂等刷新 `last_seen_at_ms`。 3. 补齐 LeaveRoom 后对订阅守卫的拒绝行为测试。 4. 接入腾讯云 IM UserSig 签发、CreateRoom 建群和房间系统消息发布。 5. 增加 stale presence 清理 worker。 6. 增加重连集成测试:JoinRoom -> UserSig -> IM login/join group -> disconnect -> reconnect -> history。 7. 增加 room-service 重启恢复测试。