29 KiB
Voice Room Mic And Room Management Development Guide
本文档定义语音房“麦位与房间管理”下一阶段开发设计。目标是在现有 MicUp/MicDown/ChangeMicSeat/MuteUser/KickUser 基础上补齐权限矩阵、锁麦、聊天开关、管理员管理、解封、时长型管控和 RTC 推流联动。
Scope
| Capability | Owner | Required Result |
|---|---|---|
| 麦位状态 | room-service |
Room Cell 串行维护麦位占用、锁定和换位 |
| 房间角色 | room-service |
owner、host、admin、audience 权限可判定、可恢复 |
| 管理命令入口 | gateway-service |
HTTP 只做鉴权和协议转换,actor 只能来自 access token |
| IM 发言守卫 | gateway-service + room-service |
腾讯 IM 发言前回查禁言、封禁、聊天开关和 presence |
| RTC 推流策略 | Client + Tencent RTC | 服务端给出麦位事实,客户端按麦位事实切换 TRTC role 或推流状态 |
| 房间系统事件 | room-service + Tencent IM |
管理动作进入 outbox,并同步推送房间系统消息 |
Non-Goals
| Non-goal | Reason |
|---|---|
| 不在 gateway 判断房间权限 | gateway 不持有 Room Cell,不能复制权限逻辑 |
| 不把 RTC 在线态写进 Room Cell | RTC 连接态属于腾讯 RTC,Room Cell 只保存业务麦位事实 |
| 不做客户端 socket 管理 | 长连接和群消息继续由腾讯 IM 承担 |
| 不把全局封禁放进 room-service | 全局用户状态属于 user-service 或后台风控服务 |
| 不在本阶段做复杂主持排班 | 当前只补房间内即时管理能力 |
Current State
现有能力:
| Feature | Status | Current Behavior |
|---|---|---|
| 上麦 | DONE |
MicUp 校验用户在 presence、目标麦位存在、未锁、未占用 |
| 下麦 | DONE |
MicDown 可把指定 target 从麦位移下 |
| 换麦 | DONE |
ChangeMicSeat 原子清空源麦位并占用目标麦位 |
| 禁言 | DONE |
MuteUser 写 MuteUsers 集合,CheckSpeakPermission 拒绝发言 |
| 踢人 | DONE |
KickUser 移除 presence、释放麦位、写 BanUsers 集合 |
| 麦位锁定字段 | PARTIAL |
SeatState.locked 和 RoomState.MicSeat.Locked 已存在,但没有命令 |
| 管理员集合 | PARTIAL |
RoomState.AdminUsers 和 snapshot admin_user_ids 已存在,但没有管理命令 |
| 聊天开关字段 | PARTIAL |
chat_enabled 已影响 CheckSpeakPermission,但没有命令 |
| RTC 发流确认 | DONE |
MicUp 进入 pending_publish,客户端或 RTC webhook 确认后进入 publishing,超时自动下麦 |
现有主要缺口:
- 管理命令没有统一权限矩阵。
- 管理员增删、主持转移、owner 转让未实现。
- 锁麦、聊天开关、解封没有命令。
- 禁言/封禁没有过期时间、原因、操作者审计字段。
- RTC token 仍未按麦位事实动态返回
anchor/audience;当前由客户端在MicUp成功后切换角色并确认发流。
Target State
本阶段完成后,房间管理应该满足:
- 普通用户只能自助上麦、主动下麦、主动离房和送礼。
- owner/host/admin 可以管理麦位、禁言、踢人和聊天开关。
- 只有 owner 可以增删 admin、转 host;转 owner 推迟到后续房间资产和后台审计阶段。
- 锁麦位后任何角色都不能上麦或换入,必须先解锁。
- 被禁言用户不能发公屏,但仍可收消息、上麦规则由权限策略决定。
- 被踢/ban 用户不能 JoinRoom、不能 IM 进群、不能发言、不能拿 RTC token;已在腾讯 IM 群内的用户必须通过 IM bridge 触发踢出群副作用。
- 所有管理动作写 command log、snapshot、outbox,并可恢复。
- 客户端通过房间系统消息和最新 snapshot 修正本地 UI。
Role Model
Roles
| Role | Meaning | Source |
|---|---|---|
owner |
房间所有者,最高权限 | RoomState.OwnerUserID |
host |
主持人,负责房间现场管理 | RoomState.HostUserID |
admin |
房间管理员 | RoomState.AdminUsers 中除 owner/host 外的用户 |
audience |
普通观众 | OnlineUsers[user].Role 或默认值 |
角色判定不要只信 RoomUser.role。权威顺序:
if actor == owner_user_id -> owner
else if actor == host_user_id -> host
else if admin_users[actor] -> admin
else -> audience
JoinRoom.role 只能作为展示和默认入房角色,不能让客户端自报 admin 获得权限。
Permission Matrix
| Action | Owner | Host | Admin | Audience |
|---|---|---|---|---|
自己上麦 MicUp |
allow | allow | allow | allow |
自己下麦 MicDown(self) |
allow | allow | allow | allow |
下别人麦 MicDown(target) |
allow | allow | allow | deny |
换别人麦 ChangeMicSeat(target) |
allow | allow | allow | deny |
| 锁/解锁麦位 | allow | allow | allow | deny |
| 禁言/解禁 | allow | allow | allow | deny |
| 踢人/ban | allow | allow | allow | deny |
| 解封/unban | allow | allow | deny | deny |
| 开关公屏 | allow | allow | allow | deny |
| 增删 admin | allow | deny | deny | deny |
| 转 host | allow | deny | deny | deny |
| 转 owner(后续批次) | allow | deny | deny | deny |
| 关房(后续批次) | allow | deny by default | deny | deny |
补充约束:
- owner 不能被 host/admin 踢、禁言、下麦或移出管理员集合。
- host 不能被普通 admin 踢、禁言或降级。
- actor 对自己执行
MuteUser和KickUser默认拒绝,避免错误操作造成不可恢复状态。 - 非 presence 用户不能被设为 host/admin,除非后续明确支持预设管理员。
- 锁麦是硬锁,owner/host/admin 也不能直接上锁定麦位;管理员需要先解锁,或先下麦再锁麦。
- 管理命令 actor 必须在当前房间 presence 中;离房后的 host/admin 不能继续远程管理,重新 JoinRoom 后恢复其持久角色。
Command Design
现有命令保留:
MicUpMicDownChangeMicSeatMuteUserKickUser
本阶段新增命令:
| Command | Purpose | Notes |
|---|---|---|
SetMicSeatLock |
锁定或解锁指定麦位 | 首版只允许锁空麦位 |
SetChatEnabled |
开启或关闭公屏 | 影响 CheckSpeakPermission |
SetRoomAdmin |
添加或移除管理员 | 只有 owner 可执行 |
TransferRoomHost |
转移主持人 | 只有 owner 可执行 |
UnbanUser |
解除房间 ban | 用户可重新 JoinRoom |
后续批次命令:
| Command | Reason To Defer |
|---|---|
TransferRoomOwner |
影响房间资产、收益、后台审计和纠纷处理,本阶段只做现场主持转移 |
CloseRoom |
属于房间生命周期管理,需要和房间列表、搜索、推荐和恢复策略一起设计 |
SetMicSeatLock
语义:
locked=true锁麦。locked=false解锁。- 首版锁麦只允许目标麦位为空;如果麦位有人,返回
CONFLICT。 - 解锁空麦位或已解锁麦位幂等返回当前快照,不重复写事件。
拒绝锁 occupied seat 的原因:
- 避免一个命令同时产生“下麦”和“锁麦”两个 UI 事件。
- 如果管理端要清空并锁定,应先
MicDown(target),再SetMicSeatLock(seat_no,true)。
SetChatEnabled
语义:
enabled=false后CheckSpeakPermission返回chat_disabled。- 关闭公屏对所有用户自发公屏消息生效,包括 owner/host/admin。
- 系统消息和管理消息不受影响,因为它们由 room-service 通过腾讯 IM REST 发布。
- 重复设置相同值幂等,不重复写事件。
SetRoomAdmin
语义:
enabled=true添加管理员。enabled=false移除管理员。- owner/host 永远属于管理集合,不能通过该命令移除。
- target 必须在当前房间 presence 中,避免后台误把不存在用户加入房间管理。
- admin 离房后保留 admin 身份,重新 JoinRoom 后继续具备 admin 权限。
- admin 被 KickUser/ban 后必须从
AdminUsers删除;Unban 后不自动恢复 admin,必须 owner 重新添加。
TransferRoomHost
语义:
- 新 host 必须在 room presence 中。
- 新 host 自动进入
AdminUsers。 - 旧 host 是否仍为 admin:首版保留为 admin,避免主持交接后失去管理权限造成现场中断。
- host 主动离房后
HostUserID不自动变化,但离房期间不能执行管理命令。 - 当前 host 不能被 KickUser 直接踢出;owner 必须先
TransferRoomHost,再按普通 admin/audience 规则管理旧 host。
Deferred TransferRoomOwner
语义:
- 本阶段不实现 HTTP/RPC,只保留后续语义边界。
- 新 owner 必须在 room presence 中。
- 新 owner 自动成为 owner、host 和 admin。
- 旧 owner 是否降级:首版降级为 admin,不继续是 host,避免双 owner。
- 转 owner 必须写强审计事件。
UnbanUser
语义:
- 从
BanUsers删除 target。 - 不自动 JoinRoom;用户必须重新调用
JoinRoom。 - 重复 unban 已不在 ban 集合的用户幂等返回当前快照。
- 不恢复被 KickUser 时移除的 admin 身份、麦位或 presence。
KickUser / Room Ban Extension
KickUser 表达“踢出房间并写入房间 ban 集合”。如果本阶段要支持限时 ban,直接把请求和快照契约调整为:
message KickUserRequest {
RequestMeta meta = 1;
int64 target_user_id = 2;
int64 duration_ms = 3;
string reason = 4;
}
语义:
duration_ms=0表示永久 ban,直到UnbanUser。duration_ms>0表示 ban 到now + duration_ms;过期后 JoinRoom、IM 进群和 RTC token 可以重新通过。- KickUser 成功后必须原子移除 target 的 presence、麦位和 admin 身份,并写
BanUsers[target]。 - KickUser 成功提交后,outbox/IM bridge 必须触发腾讯 IM 群成员踢出副作用;副作用失败不回滚 Room Cell,但必须重试。
- 在 IM 踢出副作用成功前,
CheckSpeakPermission和VerifyRoomPresence仍必须拒绝 target,避免其继续发言或重新进群。
MuteUser Extension
如果需要限时禁言,MuteUser 请求和快照契约直接调整为:
message MuteUserRequest {
RequestMeta meta = 1;
int64 target_user_id = 2;
bool muted = 3;
int64 duration_ms = 4;
string reason = 5;
}
语义:
muted=true,duration_ms=0表示永久禁言,直到手动解除。muted=true,duration_ms>0表示禁言到now + duration_ms。muted=false表示解除禁言,忽略duration_ms。- 当前 snapshot 仍保留
mute_user_ids,后续可追加mute_states暴露过期时间。 - MuteUser 只阻断公屏发言,不移除 presence、不影响收消息、不改变 RTC 麦位;如果需要离房必须使用 KickUser。
Proto Changes
只允许追加字段、消息和 RPC,不能改已有字段含义或编号。
本阶段固定追加:
message SetMicSeatLockRequest {
RequestMeta meta = 1;
int32 seat_no = 2;
bool locked = 3;
}
message SetMicSeatLockResponse {
CommandResult result = 1;
RoomSnapshot room = 2;
}
message SetChatEnabledRequest {
RequestMeta meta = 1;
bool enabled = 2;
}
message SetChatEnabledResponse {
CommandResult result = 1;
RoomSnapshot room = 2;
}
message SetRoomAdminRequest {
RequestMeta meta = 1;
int64 target_user_id = 2;
bool enabled = 3;
}
message SetRoomAdminResponse {
CommandResult result = 1;
RoomSnapshot room = 2;
}
message TransferRoomHostRequest {
RequestMeta meta = 1;
int64 target_user_id = 2;
}
message TransferRoomHostResponse {
CommandResult result = 1;
RoomSnapshot room = 2;
}
message UnbanUserRequest {
RequestMeta meta = 1;
int64 target_user_id = 2;
}
message UnbanUserResponse {
CommandResult result = 1;
RoomSnapshot room = 2;
}
如果本阶段启用限时 mute/ban,再追加 moderation state 和 RoomSnapshot 字段:
message ModerationState {
int64 user_id = 1;
int64 expires_at_ms = 2;
int64 operator_user_id = 3;
string reason = 4;
}
message RoomSnapshot {
...
repeated ModerationState mute_states = 16;
repeated ModerationState ban_states = 17;
}
字段规则:
mute_user_ids/ban_user_ids输出当前仍有效的 user_id,供只关心布尔状态的客户端使用。mute_states/ban_states输出过期时间、原因和操作者,用于完整管理面展示。- 快照和恢复链路必须使用同一套 state 结构,不能同时维护两套来源。
RoomCommandService 追加:
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
rpc SetChatEnabled(SetChatEnabledRequest) returns (SetChatEnabledResponse);
rpc SetRoomAdmin(SetRoomAdminRequest) returns (SetRoomAdminResponse);
rpc TransferRoomHost(TransferRoomHostRequest) returns (TransferRoomHostResponse);
rpc UnbanUser(UnbanUserRequest) returns (UnbanUserResponse);
TransferRoomOwner 和 CloseRoom 可以放到下一批,如果本阶段只做现场管理。
HTTP API Design
gateway 追加 HTTP 路由:
POST /api/v1/rooms/mic/lock
POST /api/v1/rooms/chat/enabled
POST /api/v1/rooms/admin/set
POST /api/v1/rooms/host/transfer
POST /api/v1/rooms/user/unban
所有请求都必须支持 command_id:
{
"room_id": "room_1001",
"command_id": "cmd_room_1001_lock_seat_1_10001_1"
}
Command Idempotency
command_id 是房间命令幂等键,不是链路追踪 ID。
规则:
- 幂等作用域为
(app_code, room_id, command_id)。 - room-service 持久化 command log 时必须保存
command_type和 payload hash。 - 重复提交相同
(app_code, room_id, command_id, command_type, payload_hash)返回幂等成功,不重复执行 mutate、不重复写 outbox。 - 重复提交相同
(app_code, room_id, command_id)但command_type或 payload hash 不一致,返回CONFLICT。 - 幂等返回可以携带当前最新 snapshot,但
CommandResult.applied=false,客户端按 snapshotversion修正 UI。 - 业务 no-op 也必须写入幂等记录或 command log;例如重复
SetChatEnabled(false)不能因为未递增版本而丢失幂等保护。 request_id只用于追踪,可以每次重试不同;不能参与幂等判定。
Lock Mic
{
"room_id": "room_1001",
"command_id": "cmd_lock_1",
"seat_no": 3,
"locked": true
}
Chat Enabled
{
"room_id": "room_1001",
"command_id": "cmd_chat_off_1",
"enabled": false
}
Set Admin
{
"room_id": "room_1001",
"command_id": "cmd_admin_add_10002",
"target_user_id": 10002,
"enabled": true
}
Transfer Host
{
"room_id": "room_1001",
"command_id": "cmd_host_to_10002",
"target_user_id": 10002
}
Unban
{
"room_id": "room_1001",
"command_id": "cmd_unban_10002",
"target_user_id": 10002
}
RoomState Changes
短期最小变更:
type RoomState struct {
...
HostUserID int64
ChatEnabled bool
AdminUsers map[int64]bool
BanUsers map[int64]bool
MuteUsers map[int64]bool
}
这些字段已存在。
如果本阶段启用时长型禁言和封禁,内部状态替换为:
type UserModerationState struct {
UserID int64
ExpiresAtMS int64
OperatorUserID int64
Reason string
}
MuteUsers map[int64]UserModerationState
BanUsers map[int64]UserModerationState
开发策略:
- snapshot 输出
mute_user_ids、ban_user_ids、mute_states和ban_states时必须来自同一份状态。 - 请求里没有
duration_ms时按永久处理,不能依赖旧命令语义分支。 CheckSpeakPermission、VerifyRoomPresence、JoinRoom和/api/v1/rtc/token都只看未过期的 ban/mute。- 如果本阶段不做限时 ban/mute,就不要追加
duration_ms、mute_states、ban_states;保留 bool set,避免半套协议。
Authorization Design
在 room-service 内新增统一权限 helper:
type roomRole string
const (
roleOwner roomRole = "owner"
roleHost roomRole = "host"
roleAdmin roomRole = "admin"
roleAudience roomRole = "audience"
)
func actorRole(state *RoomState, actorUserID int64) roomRole
func canManageMic(state *RoomState, actorUserID int64, targetUserID int64) bool
func canManageModeration(state *RoomState, actorUserID int64, targetUserID int64) bool
func canManageAdmins(state *RoomState, actorUserID int64) bool
func canTransferHost(state *RoomState, actorUserID int64) bool
权限失败统一返回:
PERMISSION_DENIED
不要在错误信息里暴露过多角色细节,避免客户端据此枚举房间管理结构。服务端日志可以记录:
request_id, command_id, room_id, actor_user_id, target_user_id, action, actor_role, denied_reason
RTC Push Linkage
服务端不直接控制 TRTC 推流,但必须区分“业务占麦成功”和“RTC 已确认发布音频”:
| Room State | Client RTC Behavior |
|---|---|
| 用户不在麦位 | TRTC role audience,不能发布音频 |
MicUp 成功且 SeatState.user_id == self、publish_state=pending_publish |
客户端切 anchor 并 startLocalAudio,UI 展示为等待发流确认 |
SDK 本地发流成功后调用 ConfirmMicPublishing(mic_session_id, room_version, event_time_ms) |
服务端把当前麦位切到 publish_state=publishing |
pending_publish 超过 publish_deadline_ms 未确认 |
服务端自动 MicDown(reason=publish_timeout),客户端停止上行并切回 audience |
MicDown/KickUser/LeaveRoom 后麦位释放 |
客户端停止本地音频上行并切回 audience |
ChangeMicSeat |
客户端只更新 UI,不需要重进 RTC |
SetMicSeatLock |
客户端更新麦位 UI,不影响当前 RTC 连接 |
后续接 RTC webhook 时,payload 必须带 mic_session_id、room_version、event_time_ms。room-service 只接受当前 session 且事件时间不旧于已接受事件的数据,旧 audience/leave 事件不能清理新的上麦会话。
/api/v1/rtc/token 首版仍返回 audience。后续如果要服务端返回 anchor,必须在签 token 时读取 Room Cell 麦位事实:
if user is on mic -> role=anchor
else -> role=audience
但不要把 RTC 在线态写入 Room Cell。
Event Design
新增或复用 room outbox 事件:
| Event Type | Trigger | IM Event Type |
|---|---|---|
RoomMicChanged |
MicUp/MicDown/ChangeMicSeat | room_mic_up/down/change |
RoomMicSeatLocked |
SetMicSeatLock | room_mic_seat_locked |
RoomChatEnabledChanged |
SetChatEnabled | room_chat_enabled_changed |
RoomAdminChanged |
SetRoomAdmin | room_admin_changed |
RoomHostTransferred |
TransferRoomHost | room_host_transferred |
RoomUserMuted |
MuteUser | room_user_muted |
RoomUserKicked |
KickUser | room_user_kicked |
RoomUserUnbanned |
UnbanUser | room_user_unbanned |
Event Payload Contract
所有 room outbox 事件和腾讯 IM 系统消息必须使用同一套字段语义,避免客户端按不同来源写两套解析。
| Event Type | Required Payload |
|---|---|
RoomMicChanged |
event_id、room_id、room_version、actor_user_id、target_user_id、from_seat、to_seat、action、mic_session_id、publish_state、reason、publish_deadline_ms、publish_event_time_ms |
RoomMicSeatLocked |
event_id、room_id、room_version、actor_user_id、seat_no、locked |
RoomChatEnabledChanged |
event_id、room_id、room_version、actor_user_id、enabled |
RoomAdminChanged |
event_id、room_id、room_version、actor_user_id、target_user_id、enabled |
RoomHostTransferred |
event_id、room_id、room_version、actor_user_id、old_host_user_id、new_host_user_id |
RoomUserMuted |
event_id、room_id、room_version、actor_user_id、target_user_id、muted、expires_at_ms、reason |
RoomUserKicked |
event_id、room_id、room_version、actor_user_id、target_user_id、expires_at_ms、reason |
RoomUserUnbanned |
event_id、room_id、room_version、actor_user_id、target_user_id |
事件要求:
event_id由 outbox 生成。room_version必须等于命令提交后的版本。- 同步 IM publish 失败不回滚命令。
- outbox worker 后续补投。
- 客户端按
event_id去重,按room_version判断是否需要拉最新 snapshot。 - 客户端不能把
room_mic_up当成音频已发布,只能在publish_state=publishing后展示为真正发流中。 - IM 系统消息只是 UI 通知;KickUser 的群成员踢出是独立 side effect,必须由 outbox/IM bridge 可重试执行。
IM Side Effects
KickUser/ban 成功提交后,需要两个层面的外部效果:
| Side Effect | Required Behavior |
|---|---|
| 房间系统消息 | 发布 room_user_kicked,让客户端立即关闭麦位、弹出提示、停止 RTC 上行 |
| 腾讯 IM 群成员移除 | 调用腾讯 IM REST 把 target 从房间群移出,避免继续收公屏消息 |
| 腾讯 IM 失败补偿 | 不回滚 Room Cell;outbox 保留待处理记录,worker 重试直到成功或进入人工告警 |
| 失败窗口守卫 | IM 移除成功前,CheckSpeakPermission 继续拒绝发言,VerifyRoomPresence 继续拒绝进群 |
首版不在 room-service 管理客户端 socket;如果腾讯 IM REST 持续失败,服务端安全边界是“不能发言、不能重新进群、不能拿 RTC token”,但可能短时间继续收到群消息,需要告警和补偿。
Recovery Replay
每个新增命令都必须加入:
command模型。command.Serialize/Deserialize。replay分支。- command log 测试。
恢复语义:
| Command | Replay Behavior |
|---|---|
SetMicSeatLock |
设置 MicSeats[index].Locked |
SetChatEnabled |
设置 ChatEnabled |
SetRoomAdmin |
增删 AdminUsers[target],但 owner/host 保持 admin |
TransferRoomHost |
更新 HostUserID,新 host 加入 AdminUsers |
UnbanUser |
删除 BanUsers[target] |
MuteUser with duration |
如果恢复时已过期,清理;否则保留 |
KickUser with duration |
恢复 presence/mic/admin 移除结果;ban 未过期时保留,已过期时不恢复 ban |
如果支持时长型禁言/封禁,需要有清理 worker 或在 guard 查询时懒清理过期状态。首版建议 guard 查询时懒判断:
if expires_at_ms > 0 && now >= expires_at_ms -> treat as not muted/banned
是否立即写回清理结果可以后续由 moderation sweep worker 完成。
恢复时不要重新执行腾讯 IM 踢人副作用;IM side effect 只从 outbox 待投递记录补偿,不从 command replay 直接调用外部服务。
Validation Rules
| Input | Rule | Error |
|---|---|---|
seat_no |
必须存在于当前房间麦位 | INVALID_ARGUMENT |
target_user_id |
必须大于 0 | INVALID_ARGUMENT |
| actor | 必须来自 RequestMeta.actor_user_id |
UNAUTHENTICATED or INVALID_ARGUMENT |
| room | 必须 active | FAILED_PRECONDITION |
| locked occupied seat | 首版拒绝 | CONFLICT |
| permission denied | actor 权限不足 | PERMISSION_DENIED |
| target is owner | host/admin 不能管理 owner | PERMISSION_DENIED |
| self kick/mute | 默认拒绝 | INVALID_ARGUMENT or PERMISSION_DENIED |
command_id duplicate with different payload |
同房间同 command_id 不能复用不同 payload | CONFLICT |
duration_ms |
>=0,上限按配置限制,首版建议不超过 30 天 |
INVALID_ARGUMENT |
reason |
trim 后最多 128 字符,不写入客户端不可见敏感信息 | INVALID_ARGUMENT |
| target is current host | 首版不能直接 KickUser,owner 需先转 host | PERMISSION_DENIED |
Implementation Order
- 新增 room-service 权限 helper 和单测,先接入现有
MicDown/ChangeMicSeat/MuteUser/KickUser。 - 补齐 command_id payload hash 校验,确保重复 command_id 不会静默吞掉不同请求。
- 追加 proto:
SetMicSeatLock、SetChatEnabled、SetRoomAdmin、TransferRoomHost、UnbanUser;如果本阶段启用限时管控,再追加 moderation state 和 duration 字段。 - 运行
make proto,提交生成的.pb.go和_grpc.pb.go。 - 扩展 gateway room client、HTTP handler 和 OpenAPI。
- 新增 command 模型和
Deserialize分支。 - 在 room-service 实现新增命令,全部走
mutateRoom。 - 新增 room events,接入 outbox 和腾讯 IM system event。
- 为 KickUser 接入腾讯 IM 群成员移除 side effect,失败不回滚命令但必须由 outbox worker 补偿。
- 扩展 recovery replay,确保 replay 不直接调用外部 IM。
- 补全单元测试、gateway transport 测试和 outbox/IM bridge 补偿测试。
- 客户端联调麦位 UI、系统消息和 RTC 推流切换。
Test Plan
Permission Tests
| Case | Expected |
|---|---|
| audience 下别人麦 | PERMISSION_DENIED |
| audience 自己下麦 | success |
| admin 下 audience 麦 | success |
| admin 下 owner 麦 | PERMISSION_DENIED |
| host 禁言 audience | success |
| admin 禁言 host | PERMISSION_DENIED |
| owner 添加 admin | success |
| host 添加 admin | PERMISSION_DENIED |
| admin 解封用户 | PERMISSION_DENIED |
| host 解封用户 | success |
Mic Tests
| Case | Expected |
|---|---|
| 锁空麦 | SeatState.locked=true,写事件 |
| 重复锁同一空麦 | 幂等返回,不重复事件 |
| 锁 occupied 麦 | CONFLICT |
| 解锁已锁麦 | SeatState.locked=false |
| locked seat MicUp | PERMISSION_DENIED |
| locked seat ChangeMicSeat | PERMISSION_DENIED |
| owner 上 locked seat | PERMISSION_DENIED,必须先解锁 |
Chat Tests
| Case | Expected |
|---|---|
| SetChatEnabled false | snapshot chat_enabled=false |
| chat disabled 后发言守卫 | allowed=false, reason=chat_disabled |
| SetChatEnabled true | 发言守卫恢复正常 |
Admin And Host Tests
| Case | Expected |
|---|---|
| owner 添加 admin | admin_user_ids 包含 target |
| owner 移除 admin | admin_user_ids 不包含 target |
| 移除 owner admin | 拒绝 |
| TransferRoomHost | host_user_id 更新,新 host 在 admin 集合 |
| 新 host 管理麦位 | success |
| admin 离房后重进 | admin 权限保留 |
| Kick admin | admin 身份被移除,Unban 后不自动恢复 |
| Kick current host | PERMISSION_DENIED,必须先转 host |
Ban And Unban Tests
| Case | Expected |
|---|---|
| KickUser 后 JoinRoom | PERMISSION_DENIED |
| KickUser 后 VerifyRoomPresence | present=false, reason=user_banned |
| KickUser 后 CheckSpeakPermission | allowed=false, reason=user_banned |
| KickUser 后 IM side effect | outbox/IM bridge 触发群成员移除 |
| IM side effect 失败 | Room Cell 不回滚,outbox 保留重试 |
| 限时 KickUser 过期后 JoinRoom | success |
| UnbanUser 后 JoinRoom | success |
| 重复 UnbanUser | 幂等 |
Idempotency Tests
| Case | Expected |
|---|---|
| 相同 command_id 相同 payload 重试 | 不重复 mutate,不重复 outbox,返回幂等结果 |
| 相同 command_id 不同 payload | CONFLICT |
| no-op 命令重试 | 不因未递增版本而丢失幂等保护 |
Event Payload Tests
| Case | Expected |
|---|---|
| 每类 room outbox event | 包含文档 payload 矩阵要求字段 |
| IM system event | 与 outbox payload 字段语义一致 |
| 客户端重复收到同一 event_id | 去重后 UI 不重复变更 |
Recovery Tests
| Case | Expected |
|---|---|
| snapshot 后回放 SetMicSeatLock | locked 状态恢复 |
| snapshot 后回放 SetChatEnabled | chat_enabled 恢复 |
| snapshot 后回放 SetRoomAdmin | admin 集合恢复 |
| snapshot 后回放 TransferRoomHost | host 恢复 |
| snapshot 后回放 UnbanUser | ban 集合恢复 |
Acceptance Criteria
- 权限矩阵覆盖所有麦位和房间管理命令。
- 普通用户不能管理他人麦位、禁言、踢人、锁麦或开关公屏。
- owner 可以添加管理员、转 host、管理所有麦位。
- 锁麦命令可用,locked 麦位对所有角色都不能上麦或换入。
- 关闭公屏后腾讯 IM 发言回调被拒绝,打开后恢复。
- 踢人后用户不能 JoinRoom、不能 IM 进群、不能发言、不能拿 RTC token。
- 踢人后 outbox/IM bridge 会补偿执行腾讯 IM 群成员移除;副作用失败不回滚 Room Cell。
- 解封后用户可以重新 JoinRoom,但不会自动恢复 presence。
- 解封后不会自动恢复 admin、host、麦位或 RTC 推流状态。
- 相同 command_id 不同 payload 会被拒绝,no-op 管理命令也具备幂等保护。
- 新增管理动作全部写 command log、snapshot、outbox,并能恢复。
- 同步 IM 失败不回滚房间状态,outbox worker 后续补投。
- 客户端可通过系统消息和 snapshot 正确更新麦位、禁言、踢人、聊天开关和管理员 UI。
Remaining Decisions
- 是否本阶段启用限时 mute/ban;如果不开启,就不要追加
duration_ms和mute_states/ban_states。 - 上麦后 RTC role 首版由客户端自行切换;gateway RTC token 返回
anchor需要后续读取 Room Cell 麦位事实后再做。