hyapp-server/docs/语音房麦位房管开发.md

29 KiB
Raw Permalink Blame History

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 连接态属于腾讯 RTCRoom Cell 只保存业务麦位事实
不做客户端 socket 管理 长连接和群消息继续由腾讯 IM 承担
不把全局封禁放进 room-service 全局用户状态属于 user-service 或后台风控服务
不在本阶段做复杂主持排班 当前只补房间内即时管理能力

Current State

现有能力:

Feature Status Current Behavior
上麦 DONE MicUp 校验用户在 presence、目标麦位存在、未锁、未占用
下麦 DONE MicDown 可把指定 target 从麦位移下
换麦 DONE ChangeMicSeat 原子清空源麦位并占用目标麦位
禁言 DONE MuteUserMuteUsers 集合,CheckSpeakPermission 拒绝发言
踢人 DONE KickUser 移除 presence、释放麦位、写 BanUsers 集合
麦位锁定字段 PARTIAL SeatState.lockedRoomState.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 对自己执行 MuteUserKickUser 默认拒绝,避免错误操作造成不可恢复状态。
  • 非 presence 用户不能被设为 host/admin除非后续明确支持预设管理员。
  • 锁麦是硬锁owner/host/admin 也不能直接上锁定麦位;管理员需要先解锁,或先下麦再锁麦。
  • 管理命令 actor 必须在当前房间 presence 中;离房后的 host/admin 不能继续远程管理,重新 JoinRoom 后恢复其持久角色。

Command Design

现有命令保留:

  • MicUp
  • MicDown
  • ChangeMicSeat
  • MuteUser
  • KickUser

本阶段新增命令:

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=falseCheckSpeakPermission 返回 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 踢出副作用成功前,CheckSpeakPermissionVerifyRoomPresence 仍必须拒绝 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);

TransferRoomOwnerCloseRoom 可以放到下一批,如果本阶段只做现场管理。

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
  • 幂等返回可以携带当前最新 snapshotCommandResult.applied=false,客户端按 snapshot version 修正 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_idsban_user_idsmute_statesban_states 时必须来自同一份状态。
  • 请求里没有 duration_ms 时按永久处理,不能依赖旧命令语义分支。
  • CheckSpeakPermissionVerifyRoomPresenceJoinRoom/api/v1/rtc/token 都只看未过期的 ban/mute。
  • 如果本阶段不做限时 ban/mute就不要追加 duration_msmute_statesban_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 == selfpublish_state=pending_publish 客户端切 anchorstartLocalAudioUI 展示为等待发流确认
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_idroom_versionevent_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_idroom_idroom_versionactor_user_idtarget_user_idfrom_seatto_seatactionmic_session_idpublish_statereasonpublish_deadline_mspublish_event_time_ms
RoomMicSeatLocked event_idroom_idroom_versionactor_user_idseat_nolocked
RoomChatEnabledChanged event_idroom_idroom_versionactor_user_idenabled
RoomAdminChanged event_idroom_idroom_versionactor_user_idtarget_user_idenabled
RoomHostTransferred event_idroom_idroom_versionactor_user_idold_host_user_idnew_host_user_id
RoomUserMuted event_idroom_idroom_versionactor_user_idtarget_user_idmutedexpires_at_msreason
RoomUserKicked event_idroom_idroom_versionactor_user_idtarget_user_idexpires_at_msreason
RoomUserUnbanned event_idroom_idroom_versionactor_user_idtarget_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 Celloutbox 保留待处理记录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 首版不能直接 KickUserowner 需先转 host PERMISSION_DENIED

Implementation Order

  1. 新增 room-service 权限 helper 和单测,先接入现有 MicDown/ChangeMicSeat/MuteUser/KickUser
  2. 补齐 command_id payload hash 校验,确保重复 command_id 不会静默吞掉不同请求。
  3. 追加 protoSetMicSeatLockSetChatEnabledSetRoomAdminTransferRoomHostUnbanUser;如果本阶段启用限时管控,再追加 moderation state 和 duration 字段。
  4. 运行 make proto,提交生成的 .pb.go_grpc.pb.go
  5. 扩展 gateway room client、HTTP handler 和 OpenAPI。
  6. 新增 command 模型和 Deserialize 分支。
  7. 在 room-service 实现新增命令,全部走 mutateRoom
  8. 新增 room events接入 outbox 和腾讯 IM system event。
  9. 为 KickUser 接入腾讯 IM 群成员移除 side effect失败不回滚命令但必须由 outbox worker 补偿。
  10. 扩展 recovery replay确保 replay 不直接调用外部 IM。
  11. 补全单元测试、gateway transport 测试和 outbox/IM bridge 补偿测试。
  12. 客户端联调麦位 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_msmute_states/ban_states
  • 上麦后 RTC role 首版由客户端自行切换gateway RTC token 返回 anchor 需要后续读取 Room Cell 麦位事实后再做。