package service import ( "fmt" "sort" "strings" "time" "xboard-go/internal/database" "xboard-go/internal/model" ) const deviceStateTTL = 10 * time.Minute type userDevicesSnapshot map[string][]string func SaveUserNodeDevices(userID, nodeID int, ips []string) error { unique := make([]string, 0, len(ips)) seen := make(map[string]struct{}) for _, ip := range ips { trimmed := strings.TrimSpace(ip) if trimmed == "" { continue } if _, ok := seen[trimmed]; ok { continue } seen[trimmed] = struct{}{} unique = append(unique, trimmed) } sort.Strings(unique) if err := database.CacheSet(deviceStateKey(userID, nodeID), unique, deviceStateTTL); err != nil { return err } return syncUserOnlineDevices(userID, nodeID, unique) } func GetUsersDevices(userIDs []int) map[int][]string { result := make(map[int][]string, len(userIDs)) for _, userID := range userIDs { snapshot, ok := database.CacheGetJSON[userDevicesSnapshot](deviceStateUserIndexKey(userID)) if !ok { result[userID] = []string{} continue } merged := make([]string, 0) seen := make(map[string]struct{}) for _, ips := range snapshot { for _, ip := range ips { if _, exists := seen[ip]; exists { continue } seen[ip] = struct{}{} merged = append(merged, ip) } } sort.Strings(merged) if len(merged) == 0 { merged = loadUserOnlineDevicesFromDB(userID) } result[userID] = merged } return result } func SetDevices(userID, nodeID int, ips []string) error { if err := SaveUserNodeDevices(userID, nodeID, ips); err != nil { return err } indexKey := deviceStateUserIndexKey(userID) indexSnapshot, _ := database.CacheGetJSON[userDevicesSnapshot](indexKey) if indexSnapshot == nil { indexSnapshot = make(userDevicesSnapshot) } indexSnapshot[fmt.Sprintf("%d", nodeID)] = normalizeIPs(ips) return database.CacheSet(indexKey, indexSnapshot, deviceStateTTL) } func normalizeIPs(ips []string) []string { seen := make(map[string]struct{}) result := make([]string, 0, len(ips)) for _, ip := range ips { ip = strings.TrimSpace(ip) if ip == "" { continue } if _, ok := seen[ip]; ok { continue } seen[ip] = struct{}{} result = append(result, ip) } sort.Strings(result) return result } func deviceStateKey(userID, nodeID int) string { return fmt.Sprintf("device_state:user:%d:node:%d", userID, nodeID) } func deviceStateUserIndexKey(userID int) string { return fmt.Sprintf("device_state:user:%d:index", userID) } func syncUserOnlineDevices(userID, nodeID int, ips []string) error { now := time.Now().Unix() expiresAt := now + int64(deviceStateTTL.Seconds()) if err := database.DB.Where("user_id = ? AND node_id = ? AND expires_at <= ?", userID, nodeID, now). Delete(&model.UserOnlineDevice{}).Error; err != nil { return err } existing := make([]model.UserOnlineDevice, 0) if err := database.DB.Where("user_id = ? AND node_id = ?", userID, nodeID).Find(&existing).Error; err != nil { return err } existingByIP := make(map[string]model.UserOnlineDevice, len(existing)) for _, item := range existing { existingByIP[item.IP] = item } seen := make(map[string]struct{}, len(ips)) for _, ip := range ips { seen[ip] = struct{}{} if current, ok := existingByIP[ip]; ok { if err := database.DB.Model(&model.UserOnlineDevice{}). Where("id = ?", current.ID). Updates(map[string]any{ "last_seen_at": now, "expires_at": expiresAt, "updated_at": now, }).Error; err != nil { return err } continue } record := model.UserOnlineDevice{ UserID: userID, NodeID: nodeID, IP: ip, FirstSeenAt: now, LastSeenAt: now, ExpiresAt: expiresAt, CreatedAt: now, UpdatedAt: now, } if err := database.DB.Create(&record).Error; err != nil { return err } } if len(existing) > 0 { staleIDs := make([]uint64, 0) for _, item := range existing { if _, ok := seen[item.IP]; !ok { staleIDs = append(staleIDs, item.ID) } } if len(staleIDs) > 0 { if err := database.DB.Where("id IN ?", staleIDs).Delete(&model.UserOnlineDevice{}).Error; err != nil { return err } } } var onlineCount int64 if err := database.DB.Model(&model.UserOnlineDevice{}). Where("user_id = ? AND expires_at > ?", userID, now). Distinct("ip"). Count(&onlineCount).Error; err == nil { count := int(onlineCount) _ = database.DB.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]any{ "online_count": count, "last_online_at": time.Unix(now, 0), "updated_at": now, }).Error } return nil } func loadUserOnlineDevicesFromDB(userID int) []string { now := time.Now().Unix() var records []model.UserOnlineDevice if err := database.DB. Select("ip"). Where("user_id = ? AND expires_at > ?", userID, now). Order("ip ASC"). Find(&records).Error; err != nil { return []string{} } seen := make(map[string]struct{}, len(records)) ips := make([]string, 0, len(records)) for _, item := range records { if item.IP == "" { continue } if _, ok := seen[item.IP]; ok { continue } seen[item.IP] = struct{}{} ips = append(ips, item.IP) } return ips }