feat(presence): migration 088 user_presence (P1.1)
This commit is contained in:
parent
3e280b66f5
commit
4d37311b79
3 changed files with 110 additions and 0 deletions
21
veza-backend-api/internal/models/user_presence.go
Normal file
21
veza-backend-api/internal/models/user_presence.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserPresence represents a user's online status (v0.301 Lot P1)
|
||||
type UserPresence struct {
|
||||
UserID uuid.UUID `gorm:"type:uuid;primaryKey" json:"user_id"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'offline'" json:"status"` // online, away, busy, offline
|
||||
LastSeenAt time.Time `gorm:"not null" json:"last_seen_at"`
|
||||
StatusMsg string `gorm:"type:text" json:"status_message,omitempty"`
|
||||
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName overrides the table name
|
||||
func (UserPresence) TableName() string {
|
||||
return "user_presence"
|
||||
}
|
||||
76
veza-backend-api/internal/services/presence_service.go
Normal file
76
veza-backend-api/internal/services/presence_service.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"veza-backend-api/internal/models"
|
||||
)
|
||||
|
||||
// PresenceService manages user presence (v0.301 Lot P1)
|
||||
type PresenceService struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewPresenceService creates a new PresenceService
|
||||
func NewPresenceService(db *gorm.DB, logger *zap.Logger) *PresenceService {
|
||||
return &PresenceService{db: db, logger: logger}
|
||||
}
|
||||
|
||||
// UpdatePresence updates or creates presence for the current user (call on each authenticated request)
|
||||
func (s *PresenceService) UpdatePresence(ctx context.Context, userID uuid.UUID, status string) error {
|
||||
now := time.Now()
|
||||
var p models.UserPresence
|
||||
err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&p).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
p = models.UserPresence{
|
||||
UserID: userID,
|
||||
Status: status,
|
||||
LastSeenAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
return s.db.WithContext(ctx).Create(&p).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Status = status
|
||||
p.LastSeenAt = now
|
||||
p.UpdatedAt = now
|
||||
return s.db.WithContext(ctx).Save(&p).Error
|
||||
}
|
||||
|
||||
// GetPresence returns presence for a user, or nil if not found
|
||||
func (s *PresenceService) GetPresence(ctx context.Context, userID uuid.UUID) (*models.UserPresence, error) {
|
||||
var p models.UserPresence
|
||||
err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&p).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetOnlineUsers returns user IDs with status online (for list in chat/sidebar)
|
||||
func (s *PresenceService) GetOnlineUsers(ctx context.Context, limit int) ([]uuid.UUID, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
var presences []models.UserPresence
|
||||
err := s.db.WithContext(ctx).Where("status = ?", "online").Order("updated_at DESC").Limit(limit).Find(&presences).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]uuid.UUID, 0, len(presences))
|
||||
for _, p := range presences {
|
||||
ids = append(ids, p.UserID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
13
veza-backend-api/migrations/088_user_presence.sql
Normal file
13
veza-backend-api/migrations/088_user_presence.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- Migration 088: User presence (online/away/offline, last_seen)
|
||||
-- v0.301 Lot P1
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_presence (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'offline' CHECK (status IN ('online', 'away', 'busy', 'offline')),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
status_message TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_presence_status ON user_presence(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen_at DESC);
|
||||
Loading…
Reference in a new issue