feat(presence): migration 088 user_presence (P1.1)

This commit is contained in:
senke 2026-02-21 05:22:33 +01:00
parent 3e280b66f5
commit 4d37311b79
3 changed files with 110 additions and 0 deletions

View 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"
}

View 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
}

View 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);