176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
|
|
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)
|
|||
|
|
}
|