backend/pkg/library/centrifugo/online.go

176 lines
4.9 KiB
Go
Raw Normal View History

2026-01-06 07:15:18 +00:00
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)
}