package service import ( "context" "log/slog" "strings" "time" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" "hyapp/pkg/logx" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/state" ) const ( maxUserVoiceRoomPresenceBatchSize = 100 userVoiceRoomStateOnMic = "on_mic" userVoiceRoomStateNotOnMic = "not_on_mic" ) // BatchGetUserVoiceRoomPresences 返回个人资料页可以安全展示的实时上麦入口。 // room_user_presence 只负责把 user_id 定位到候选 Room Cell;上麦、房间生命周期和 viewer 权限最终都以最新快照为准。 func (s *Service) BatchGetUserVoiceRoomPresences(ctx context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) { startedAt := time.Now() if req == nil || req.GetViewerUserId() <= 0 { return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required") } userIDs, ok := normalizedVoiceRoomPresenceUserIDs(req.GetUserIds()) if !ok { return nil, xerr.New(xerr.InvalidArgument, "user_ids must contain 1 to 100 positive ids") } ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) presences, err := s.repository.BatchGetCurrentRoomPresences(ctx, userIDs) if err != nil { return nil, err } // 一个 batch 可能包含同房间多个麦上用户;每个候选房间只恢复/读取一次快照,避免重复触发 Room Cell 或 MySQL 恢复链路。 snapshots := make(map[string]*roomv1.RoomSnapshot) for userID, presence := range presences { if userID == req.GetViewerUserId() || strings.TrimSpace(presence.RoomID) == "" { continue } if _, loaded := snapshots[presence.RoomID]; loaded { continue } snapshot, err := s.currentSnapshot(ctx, presence.RoomID) if err != nil { return nil, err } snapshots[presence.RoomID] = snapshot } nowMS := s.clock.Now().UnixMilli() resolved := make(map[int64]*roomv1.UserVoiceRoomPresence, len(userIDs)) onMicCount := 0 hiddenCount := 0 orphanCount := 0 for _, userID := range userIDs { item := &roomv1.UserVoiceRoomPresence{ UserId: userID, State: userVoiceRoomStateNotOnMic, ObservedAtMs: nowMS, } resolved[userID] = item // 当前产品只在客态资料页展示 Live;查询本人时按不可见结果收敛,不泄露多余房间入口。 if userID == req.GetViewerUserId() { hiddenCount++ continue } presence, exists := presences[userID] if !exists { continue } snapshot := snapshots[presence.RoomID] if snapshot == nil || snapshot.GetStatus() != state.RoomStatusActive || snapshot.GetAppCode() != appcode.FromContext(ctx) { // presence 投影允许短暂滞后;孤儿索引只能降级为 not_on_mic,不能返回旧 room_id。 orphanCount++ continue } if snapshotUserBanned(snapshot, req.GetViewerUserId(), nowMS) { // viewer 被房间封禁时正式进房必然失败,因此这里隐藏全部房间定位字段。 hiddenCount++ continue } seat := protoSeatByUser(snapshot, userID) if seat == nil { // 仅在房间内但没有占用麦位不是上麦状态。 continue } if findProtoUser(snapshot, userID) == nil { // 麦位用户不在当前 online_users 说明快照内部事实不完整,按异常数据隐藏并留日志计数。 orphanCount++ continue } ext := snapshot.GetRoomExt() roomShortID := snapshot.GetRoomShortId() if roomShortID == "" { roomShortID = ext["room_short_id"] } onlineCount := snapshot.GetOnlineCount() if onlineCount <= 0 && len(snapshot.GetOnlineUsers()) > 0 { onlineCount = int32(len(snapshot.GetOnlineUsers())) } item.State = userVoiceRoomStateOnMic item.SeatNo = seat.GetSeatNo() item.Joinable = true item.Room = &roomv1.UserVoiceRoomPresenceRoom{ RoomId: snapshot.GetRoomId(), RoomShortId: roomShortID, OwnerUserId: snapshot.GetOwnerUserId(), HostUserId: snapshot.GetOwnerUserId(), Title: ext["title"], CoverUrl: ext["cover_url"], BackgroundUrl: ext["background_url"], Mode: snapshot.GetMode(), OnlineCount: onlineCount, SeatCount: int32(len(snapshot.GetMicSeats())), Locked: snapshot.GetLocked(), } onMicCount++ } items := make([]*roomv1.UserVoiceRoomPresence, 0, len(userIDs)) for _, userID := range userIDs { items = append(items, resolved[userID]) } logx.Info(ctx, "user_voice_room_presence_batch_queried", slog.String("request_id", req.GetMeta().GetRequestId()), slog.String("app_code", appcode.FromContext(ctx)), slog.Int64("viewer_user_id", req.GetViewerUserId()), slog.Int("user_count", len(userIDs)), slog.Int("active_presence_count", len(presences)), slog.Int("on_mic_count", onMicCount), slog.Int("hidden_count", hiddenCount), slog.Int("orphan_count", orphanCount), slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()), ) return &roomv1.BatchGetUserVoiceRoomPresencesResponse{Items: items, ServerTimeMs: nowMS}, nil } func normalizedVoiceRoomPresenceUserIDs(values []int64) ([]int64, bool) { if len(values) == 0 { return nil, false } seen := make(map[int64]struct{}, len(values)) result := make([]int64, 0, len(values)) for _, userID := range values { if userID <= 0 { return nil, false } if _, exists := seen[userID]; exists { continue } seen[userID] = struct{}{} result = append(result, userID) if len(result) > maxUserVoiceRoomPresenceBatchSize { return nil, false } } return result, len(result) > 0 }