package service import ( "context" "time" roomeventsv1 "hyapp.local/api/proto/events/room/v1" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/tencentim" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/command" "hyapp/services/room-service/internal/room/outbox" "hyapp/services/room-service/internal/room/rank" "hyapp/services/room-service/internal/room/state" ) // SetMicSeatLock 锁定或解锁指定麦位;首版只允许锁空麦位。 func (s *Service) SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) // 锁麦是房间管理命令,客户端不能通过麦位字段直接绕过 Room Cell 权限矩阵。 cmd := command.SetMicSeatLock{ Base: baseFromMeta(req.GetMeta()), SeatNo: req.GetSeatNo(), Locked: req.GetLocked(), } result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { if err := requireActiveRoom(current); err != nil { return mutationResult{}, nil, err } if err := requireManagerPresent(current, cmd.ActorUserID()); err != nil { return mutationResult{}, nil, err } index := current.SeatIndex(cmd.SeatNo) if index < 0 { // 只能锁定房间初始化时存在的麦位编号。 return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "seat does not exist") } if cmd.Locked && current.MicSeats[index].UserID != 0 { // 首版不把“下麦”和“锁麦”合并成一个命令,避免客户端处理复合事件。 return mutationResult{}, nil, xerr.New(xerr.Conflict, "seat is occupied") } if current.MicSeats[index].Locked == cmd.Locked { // 重复锁定或重复解锁是 no-op,只写 command log 做幂等保护,不广播事件。 return mutationResult{snapshot: current.ToProto()}, nil, nil } // 锁状态属于 Room Cell 核心态,必须和版本递增一起提交。 current.MicSeats[index].Locked = cmd.Locked current.Version++ // 锁麦事件进入 outbox,客户端通过系统消息修正麦位 UI。 lockEvent, err := outbox.Build(current.RoomID, "RoomMicSeatLocked", current.Version, now, &roomeventsv1.RoomMicSeatLocked{ ActorUserId: cmd.ActorUserID(), SeatNo: cmd.SeatNo, Locked: cmd.Locked, }) if err != nil { return mutationResult{}, nil, err } return mutationResult{ snapshot: current.ToProto(), syncEvent: &tencentim.RoomEvent{ EventID: lockEvent.EventID, RoomID: current.RoomID, EventType: "room_mic_seat_locked", ActorUserID: cmd.ActorUserID(), SeatNo: cmd.SeatNo, RoomVersion: current.Version, Attributes: map[string]string{ "locked": boolString(cmd.Locked), }, }, }, []outbox.Record{lockEvent}, nil }) if err != nil { return nil, err } return &roomv1.SetMicSeatLockResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, }, nil } // SetChatEnabled 开启或关闭公屏发言守卫。 func (s *Service) SetChatEnabled(ctx context.Context, req *roomv1.SetChatEnabledRequest) (*roomv1.SetChatEnabledResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) // 公屏开关只影响普通客户端发言守卫,不阻断 room-service 发送系统消息。 cmd := command.SetChatEnabled{ Base: baseFromMeta(req.GetMeta()), Enabled: req.GetEnabled(), } result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { if err := requireActiveRoom(current); err != nil { return mutationResult{}, nil, err } if err := requireManagerPresent(current, cmd.ActorUserID()); err != nil { return mutationResult{}, nil, err } if current.ChatEnabled == cmd.Enabled { // 重复设置同一个开关值不产生事件,避免客户端收到无意义的状态变更。 return mutationResult{snapshot: current.ToProto()}, nil, nil } // CheckSpeakPermission 后续会以该字段作为 chat_disabled 的权威来源。 current.ChatEnabled = cmd.Enabled current.Version++ // 公屏开关变化需要进入 outbox,便于 IM 系统消息和审计消费。 chatEvent, err := outbox.Build(current.RoomID, "RoomChatEnabledChanged", current.Version, now, &roomeventsv1.RoomChatEnabledChanged{ ActorUserId: cmd.ActorUserID(), Enabled: cmd.Enabled, }) if err != nil { return mutationResult{}, nil, err } return mutationResult{ snapshot: current.ToProto(), syncEvent: &tencentim.RoomEvent{ EventID: chatEvent.EventID, RoomID: current.RoomID, EventType: "room_chat_enabled_changed", ActorUserID: cmd.ActorUserID(), RoomVersion: current.Version, Attributes: map[string]string{ "enabled": boolString(cmd.Enabled), }, }, }, []outbox.Record{chatEvent}, nil }) if err != nil { return nil, err } return &roomv1.SetChatEnabledResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, }, nil } // SetRoomAdmin 添加或移除房间管理员;只有 owner 可以执行。 func (s *Service) SetRoomAdmin(ctx context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) // 管理员身份是 RoomState 持久态,离房后保留,重新进房后继续生效。 cmd := command.SetRoomAdmin{ Base: baseFromMeta(req.GetMeta()), TargetUserID: req.GetTargetUserId(), Enabled: req.GetEnabled(), } result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { if err := requireActiveRoom(current); err != nil { return mutationResult{}, nil, err } if cmd.TargetUserID <= 0 { return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required") } if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil { // 只有 owner 可以增删 admin,避免管理员扩张自己的权限。 return mutationResult{}, nil, err } if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists { // 首版不支持预设管理员,避免把不存在或已离房用户加入管理集合。 return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room") } if cmd.TargetUserID == current.OwnerUserID { // owner 不是管理员,不能对 owner 执行设置或取消管理员。 return mutationResult{}, nil, xerr.New(xerr.Conflict, "owner cannot be room admin target") } if current.AdminUsers[cmd.TargetUserID] == cmd.Enabled { // 重复添加或重复移除是 no-op,避免重复发送 RoomAdminChanged。 return mutationResult{snapshot: current.ToProto()}, nil, nil } if cmd.Enabled { // map set 是管理员集合的恢复来源,快照会导出为 admin_user_ids。 current.AdminUsers[cmd.TargetUserID] = true } else { delete(current.AdminUsers, cmd.TargetUserID) } current.Version++ // 管理员变更事件同时服务客户端提示和后续房间审计。 adminEvent, err := outbox.Build(current.RoomID, "RoomAdminChanged", current.Version, now, &roomeventsv1.RoomAdminChanged{ ActorUserId: cmd.ActorUserID(), TargetUserId: cmd.TargetUserID, Enabled: cmd.Enabled, }) if err != nil { return mutationResult{}, nil, err } return mutationResult{ snapshot: current.ToProto(), syncEvent: &tencentim.RoomEvent{ EventID: adminEvent.EventID, RoomID: current.RoomID, EventType: "room_admin_changed", ActorUserID: cmd.ActorUserID(), TargetUserID: cmd.TargetUserID, RoomVersion: current.Version, Attributes: map[string]string{ "enabled": boolString(cmd.Enabled), }, }, }, []outbox.Record{adminEvent}, nil }) if err != nil { return nil, err } return &roomv1.SetRoomAdminResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, }, nil }