hyapp-server/services/room-service/internal/room/service/user_voice_room_presence.go
2026-07-17 16:55:22 +08:00

168 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}