package centrifugo import ( "context" "fmt" "time" ) // OnlineStatus 在線狀態 type OnlineStatus struct { UserID string `json:"user_id"` IsOnline bool `json:"is_online"` LastSeenAt time.Time `json:"last_seen_at,omitempty"` Clients int `json:"clients,omitempty"` // 連線數(可能多個設備) } // OnlineStore 在線狀態存儲介面 // 可以用 Redis、Memory 或其他存儲實作 type OnlineStore interface { // SetOnline 設置用戶在線 SetOnline(ctx context.Context, userID string, ttl time.Duration) error // SetOffline 設置用戶離線 SetOffline(ctx context.Context, userID string) error // IsOnline 檢查用戶是否在線 IsOnline(ctx context.Context, userID string) (bool, error) // GetOnlineUsers 獲取在線用戶列表 GetOnlineUsers(ctx context.Context, userIDs []string) (map[string]bool, error) // IncrClient 增加用戶連線數 IncrClient(ctx context.Context, userID string) (int64, error) // DecrClient 減少用戶連線數 DecrClient(ctx context.Context, userID string) (int64, error) } // OnlineManager 在線狀態管理器 // 結合 Redis 存儲和 Centrifugo Presence API 提供在線狀態追蹤 type OnlineManager struct { client *Client store OnlineStore ttl time.Duration } // NewOnlineManager 創建在線狀態管理器 // store 可以為 nil,此時只使用 Centrifugo Presence API func NewOnlineManager(client *Client, store OnlineStore) *OnlineManager { return &OnlineManager{ client: client, store: store, ttl: 5 * time.Minute, // 預設 5 分鐘過期 } } // NewOnlineManagerWithTTL 創建帶 TTL 的在線狀態管理器 func NewOnlineManagerWithTTL(client *Client, store OnlineStore, ttl time.Duration) *OnlineManager { return &OnlineManager{ client: client, store: store, ttl: ttl, } } // ==================== 連線事件處理 ==================== // HandleConnect 處理用戶連線事件(用於 Centrifugo Connect Proxy) func (m *OnlineManager) HandleConnect(ctx context.Context, userID string) error { if m.store == nil { return nil } // 增加連線數 count, err := m.store.IncrClient(ctx, userID) if err != nil { return fmt.Errorf("failed to incr client: %w", err) } // 如果是第一個連線,設置在線狀態 if count == 1 { if err := m.store.SetOnline(ctx, userID, m.ttl); err != nil { return fmt.Errorf("failed to set online: %w", err) } } return nil } // HandleDisconnect 處理用戶斷線事件(用於 Centrifugo Disconnect Proxy) func (m *OnlineManager) HandleDisconnect(ctx context.Context, userID string) error { if m.store == nil { return nil } // 減少連線數 count, err := m.store.DecrClient(ctx, userID) if err != nil { return fmt.Errorf("failed to decr client: %w", err) } // 如果沒有連線了,設置離線狀態 if count <= 0 { if err := m.store.SetOffline(ctx, userID); err != nil { return fmt.Errorf("failed to set offline: %w", err) } } return nil } // ==================== 在線狀態查詢 ==================== // IsUserOnline 檢查用戶是否在線(使用 Store) func (m *OnlineManager) IsUserOnline(ctx context.Context, userID string) (bool, error) { if m.store == nil { return false, ErrOnlineStoreNotConfigured } return m.store.IsOnline(ctx, userID) } // GetUsersOnlineStatus 批量獲取用戶在線狀態 func (m *OnlineManager) GetUsersOnlineStatus(ctx context.Context, userIDs []string) (map[string]bool, error) { if m.store == nil { return nil, ErrOnlineStoreNotConfigured } return m.store.GetOnlineUsers(ctx, userIDs) } // RefreshOnline 刷新用戶在線狀態(用於心跳) func (m *OnlineManager) RefreshOnline(ctx context.Context, userID string) error { if m.store == nil { return nil } return m.store.SetOnline(ctx, userID, m.ttl) } // ==================== Centrifugo Presence API ==================== // IsUserInChannel 檢查用戶是否在指定頻道中(使用 Centrifugo Presence) func (m *OnlineManager) IsUserInChannel(ctx context.Context, userID, channel string) (bool, error) { presence, err := m.client.Presence(ctx, channel) if err != nil { return false, err } for _, info := range presence.Presence { if info.User == userID { return true, nil } } return false, nil } // GetChannelOnlineUsers 獲取頻道中的在線用戶(使用 Centrifugo Presence) func (m *OnlineManager) GetChannelOnlineUsers(ctx context.Context, channel string) ([]string, error) { presence, err := m.client.Presence(ctx, channel) if err != nil { return nil, err } // 去重(一個用戶可能有多個連線) userMap := make(map[string]bool) for _, info := range presence.Presence { userMap[info.User] = true } users := make([]string, 0, len(userMap)) for userID := range userMap { users = append(users, userID) } return users, nil } // GetChannelStats 獲取頻道在線統計 func (m *OnlineManager) GetChannelStats(ctx context.Context, channel string) (*PresenceStatsResult, error) { return m.client.PresenceStats(ctx, channel) }