package service import ( "context" "strings" "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" ) const defaultSystemEvictReason = "platform_moderation" // SystemEvictUser removes a platform-banned or risk-blocked user from their current Room Cell. // It deliberately bypasses room manager role checks, but still serializes through the Room Cell command log. func (s *Service) SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUserRequest) (*roomv1.SystemEvictUserResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) targetUserID := req.GetTargetUserId() if targetUserID <= 0 { return nil, xerr.New(xerr.InvalidArgument, "target_user_id is required") } roomID := strings.TrimSpace(req.GetMeta().GetRoomId()) if roomID == "" { presence, exists, err := s.repository.GetCurrentRoomPresence(ctx, targetUserID) if err != nil { return nil, err } if !exists { return &roomv1.SystemEvictUserResponse{ HadCurrentRoom: false, Result: commandResult(false, 0, s.clock.Now()), }, nil } roomID = presence.RoomID } base := baseFromMeta(req.GetMeta()) base.Room = roomID if base.ActorID <= 0 { // mutateRoom requires a non-zero actor for idempotency metadata; system eviction does not use it for authorization. base.ActorID = targetUserID } cmd := command.SystemEvictUser{ Base: base, TargetUserID: targetUserID, OperatorUserID: req.GetOperatorUserId(), Reason: normalizeSystemEvictReason(req.GetReason()), BanFromRoom: req.GetBanFromRoom(), } shouldKickRTC := false 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 } _, inRoom := current.OnlineUsers[cmd.TargetUserID] _, onSeat := current.SeatByUser(cmd.TargetUserID) alreadyBanned := current.BanUsers[cmd.TargetUserID] if !inRoom && !onSeat && !current.AdminUsers[cmd.TargetUserID] && (!cmd.BanFromRoom || alreadyBanned) { // 重复系统驱逐保持幂等;没有残留状态时只返回当前快照。 return mutationResult{snapshot: current.ToProto()}, nil, nil } shouldKickRTC = inRoom || onSeat var releasedSeatNo int32 var releasedMicSessionID string if seat, exists := current.SeatByUser(cmd.TargetUserID); exists { index := current.SeatIndex(seat.SeatNo) releasedSeatNo = seat.SeatNo releasedMicSessionID = current.MicSeats[index].MicSessionID current.ClearMicSession(index) } delete(current.OnlineUsers, cmd.TargetUserID) delete(current.AdminUsers, cmd.TargetUserID) if cmd.BanFromRoom { current.BanUsers[cmd.TargetUserID] = true } current.Version++ evictEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{ ActorUserId: 0, TargetUserId: cmd.TargetUserID, }) if err != nil { return mutationResult{}, nil, err } records := make([]outbox.Record, 0, 2) if releasedMicSessionID != "" { micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, 0, cmd.TargetUserID, releasedSeatNo, releasedMicSessionID, cmd.Reason) if err != nil { return mutationResult{}, nil, err } records = append(records, micEvent) } records = append(records, evictEvent) return mutationResult{ snapshot: current.ToProto(), syncEvent: &tencentim.RoomEvent{ EventID: evictEvent.EventID, RoomID: current.RoomID, EventType: "room_user_kicked", TargetUserID: cmd.TargetUserID, RoomVersion: current.Version, Attributes: map[string]string{"reason": cmd.Reason}, }, }, records, nil }) if err != nil { return nil, err } response := &roomv1.SystemEvictUserResponse{ HadCurrentRoom: true, Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, RoomId: roomID, } if result.applied && shouldKickRTC && s.rtcUserRemover != nil { if err := s.rtcUserRemover.RemoveUserByStrRoomID(ctx, roomID, targetUserID); err != nil { response.RtcKickError = err.Error() } else { response.RtcKicked = true } } return response, nil } func normalizeSystemEvictReason(reason string) string { reason = strings.TrimSpace(reason) if reason == "" { return defaultSystemEvictReason } return reason }