Files
SingBox-Gopanel/internal/service/device_state.go
CN-JS-HuiBai b3435e5ef8
Some checks failed
build / build (api, amd64, linux) (push) Has been cancelled
build / build (api, arm64, linux) (push) Has been cancelled
build / build (api.exe, amd64, windows) (push) Has been cancelled
基本功能已初步完善
2026-04-17 20:41:47 +08:00

214 lines
5.1 KiB
Go

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
}