168 lines
5.4 KiB
Go
168 lines
5.4 KiB
Go
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
|
||
}
|