package voiceroomrocket import ( "context" "fmt" "sort" "strconv" "strings" "time" ) type roomAudienceUser struct { UserID int64 LastSeenTime time.Time } func (s *Service) listOnlineRoomUsers(ctx context.Context, sysOrigin string, roomID int64, now time.Time) ([]roomAudienceUser, error) { if s.repo.Redis == nil { return nil, nil } members, err := s.repo.Redis.SMembers(ctx, onlineRoomKey(sysOrigin, roomID)).Result() if err != nil { return nil, err } users := make([]roomAudienceUser, 0, len(members)) seen := make(map[int64]struct{}, len(members)) for _, raw := range members { userID, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) if err != nil || userID <= 0 { continue } if _, exists := seen[userID]; exists { continue } seen[userID] = struct{}{} seenAt, active := s.loadActiveRoomSeenTime(ctx, sysOrigin, roomID, userID, now) if !active { _ = s.repo.Redis.SRem(ctx, onlineRoomKey(sysOrigin, roomID), userID).Err() continue } users = append(users, roomAudienceUser{ UserID: userID, LastSeenTime: seenAt, }) } sort.Slice(users, func(i, j int) bool { return users[i].UserID < users[j].UserID }) return users, nil } func (s *Service) loadActiveRoomSeenTime(ctx context.Context, sysOrigin string, roomID, userID int64, now time.Time) (time.Time, bool) { if s.repo.Redis == nil { return time.Time{}, false } rawRoomID, err := s.repo.Redis.Get(ctx, userRoomKey(sysOrigin, userID)).Result() if err != nil { return time.Time{}, false } currentRoomID, err := strconv.ParseInt(strings.TrimSpace(rawRoomID), 10, 64) if err != nil || currentRoomID != roomID { return time.Time{}, false } seenAt, ok := s.loadUserSeenTimeStrict(ctx, sysOrigin, userID, now) if !ok { return time.Time{}, false } if ttl := s.roomPresenceTTL(); ttl > 0 && now.Sub(seenAt) > ttl { return time.Time{}, false } return seenAt, true } func (s *Service) loadUserSeenTimeStrict(ctx context.Context, sysOrigin string, userID int64, fallback time.Time) (time.Time, bool) { if s.repo.Redis == nil { return time.Time{}, false } raw, err := s.repo.Redis.Get(ctx, userSeenKey(sysOrigin, userID)).Result() if err != nil { return time.Time{}, false } millis, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) if err != nil || millis <= 0 { return time.Time{}, false } return resolveMillisTime(millis, fallback), true } func (s *Service) loadUserSeenTime(ctx context.Context, sysOrigin string, userID int64, fallback time.Time) time.Time { if s.repo.Redis == nil { return fallback } raw, err := s.repo.Redis.Get(ctx, userSeenKey(sysOrigin, userID)).Result() if err != nil { return fallback } millis, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) if err != nil || millis <= 0 { return fallback } return resolveMillisTime(millis, fallback) } func (s *Service) roomPresenceTTL() time.Duration { seconds := s.cfg.VoiceRoomRedPacket.PresenceTTLSeconds if seconds <= 0 { seconds = defaultPresenceTTLSeconds } return time.Duration(seconds) * time.Second } func onlineRoomKey(sysOrigin string, roomID int64) string { return fmt.Sprintf("voice_room:online:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), roomID) } func userRoomKey(sysOrigin string, userID int64) string { return fmt.Sprintf("voice_room:user_room:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), userID) } func userSeenKey(sysOrigin string, userID int64) string { return fmt.Sprintf("voice_room:user_seen:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), userID) }