package db import ( "context" "database/sql" "encoding/json" "time" ) type UserInterestsData struct { UserID string `json:"userId"` Topics json.RawMessage `json:"topics"` Sources json.RawMessage `json:"sources"` Keywords json.RawMessage `json:"keywords"` ViewHistory json.RawMessage `json:"viewHistory"` SavedArticles json.RawMessage `json:"savedArticles"` BlockedSources json.RawMessage `json:"blockedSources"` BlockedTopics json.RawMessage `json:"blockedTopics"` PreferredLang string `json:"preferredLang"` Region string `json:"region"` ReadingLevel string `json:"readingLevel"` Notifications json.RawMessage `json:"notifications"` CustomCategories json.RawMessage `json:"customCategories"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type UserInterestsRepository struct { db *PostgresDB } func NewUserInterestsRepository(db *PostgresDB) *UserInterestsRepository { return &UserInterestsRepository{db: db} } func (r *UserInterestsRepository) createTable(ctx context.Context) error { query := ` CREATE TABLE IF NOT EXISTS user_interests ( user_id VARCHAR(255) PRIMARY KEY, topics JSONB DEFAULT '{}', sources JSONB DEFAULT '{}', keywords JSONB DEFAULT '{}', view_history JSONB DEFAULT '[]', saved_articles JSONB DEFAULT '[]', blocked_sources JSONB DEFAULT '[]', blocked_topics JSONB DEFAULT '[]', preferred_lang VARCHAR(10) DEFAULT 'ru', region VARCHAR(50) DEFAULT 'russia', reading_level VARCHAR(20) DEFAULT 'general', notifications JSONB DEFAULT '{}', custom_categories JSONB DEFAULT '[]', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_user_interests_updated ON user_interests(updated_at); CREATE INDEX IF NOT EXISTS idx_user_interests_region ON user_interests(region); ` _, err := r.db.DB().ExecContext(ctx, query) return err } func (r *UserInterestsRepository) Get(ctx context.Context, userID string) (*UserInterestsData, error) { query := ` SELECT user_id, topics, sources, keywords, view_history, saved_articles, blocked_sources, blocked_topics, preferred_lang, region, reading_level, notifications, custom_categories, created_at, updated_at FROM user_interests WHERE user_id = $1 ` row := r.db.DB().QueryRowContext(ctx, query, userID) var data UserInterestsData err := row.Scan( &data.UserID, &data.Topics, &data.Sources, &data.Keywords, &data.ViewHistory, &data.SavedArticles, &data.BlockedSources, &data.BlockedTopics, &data.PreferredLang, &data.Region, &data.ReadingLevel, &data.Notifications, &data.CustomCategories, &data.CreatedAt, &data.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &data, nil } func (r *UserInterestsRepository) Save(ctx context.Context, data *UserInterestsData) error { query := ` INSERT INTO user_interests ( user_id, topics, sources, keywords, view_history, saved_articles, blocked_sources, blocked_topics, preferred_lang, region, reading_level, notifications, custom_categories, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) ON CONFLICT (user_id) DO UPDATE SET topics = EXCLUDED.topics, sources = EXCLUDED.sources, keywords = EXCLUDED.keywords, view_history = EXCLUDED.view_history, saved_articles = EXCLUDED.saved_articles, blocked_sources = EXCLUDED.blocked_sources, blocked_topics = EXCLUDED.blocked_topics, preferred_lang = EXCLUDED.preferred_lang, region = EXCLUDED.region, reading_level = EXCLUDED.reading_level, notifications = EXCLUDED.notifications, custom_categories = EXCLUDED.custom_categories, updated_at = NOW() ` now := time.Now() if data.CreatedAt.IsZero() { data.CreatedAt = now } data.UpdatedAt = now if data.Topics == nil { data.Topics = json.RawMessage("{}") } if data.Sources == nil { data.Sources = json.RawMessage("{}") } if data.Keywords == nil { data.Keywords = json.RawMessage("{}") } if data.ViewHistory == nil { data.ViewHistory = json.RawMessage("[]") } if data.SavedArticles == nil { data.SavedArticles = json.RawMessage("[]") } if data.BlockedSources == nil { data.BlockedSources = json.RawMessage("[]") } if data.BlockedTopics == nil { data.BlockedTopics = json.RawMessage("[]") } if data.Notifications == nil { data.Notifications = json.RawMessage("{}") } if data.CustomCategories == nil { data.CustomCategories = json.RawMessage("[]") } _, err := r.db.DB().ExecContext(ctx, query, data.UserID, data.Topics, data.Sources, data.Keywords, data.ViewHistory, data.SavedArticles, data.BlockedSources, data.BlockedTopics, data.PreferredLang, data.Region, data.ReadingLevel, data.Notifications, data.CustomCategories, data.CreatedAt, data.UpdatedAt, ) return err } func (r *UserInterestsRepository) Delete(ctx context.Context, userID string) error { query := `DELETE FROM user_interests WHERE user_id = $1` _, err := r.db.DB().ExecContext(ctx, query, userID) return err } func (r *UserInterestsRepository) AddViewEvent(ctx context.Context, userID string, event json.RawMessage) error { query := ` UPDATE user_interests SET view_history = CASE WHEN jsonb_array_length(view_history) >= 500 THEN jsonb_build_array($2) || view_history[0:499] ELSE jsonb_build_array($2) || view_history END, updated_at = NOW() WHERE user_id = $1 ` result, err := r.db.DB().ExecContext(ctx, query, userID, event) if err != nil { return err } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { insertQuery := ` INSERT INTO user_interests (user_id, view_history, updated_at) VALUES ($1, jsonb_build_array($2), NOW()) ` _, err = r.db.DB().ExecContext(ctx, insertQuery, userID, event) } return err } func (r *UserInterestsRepository) UpdateTopicScore(ctx context.Context, userID, topic string, delta float64) error { query := ` UPDATE user_interests SET topics = topics || jsonb_build_object($2, COALESCE((topics->>$2)::float, 0) + $3), updated_at = NOW() WHERE user_id = $1 ` result, err := r.db.DB().ExecContext(ctx, query, userID, topic, delta) if err != nil { return err } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { insertQuery := ` INSERT INTO user_interests (user_id, topics, updated_at) VALUES ($1, jsonb_build_object($2, $3), NOW()) ` _, err = r.db.DB().ExecContext(ctx, insertQuery, userID, topic, delta) } return err } func (r *UserInterestsRepository) SaveArticle(ctx context.Context, userID, articleURL string) error { query := ` UPDATE user_interests SET saved_articles = CASE WHEN NOT saved_articles ? $2 THEN saved_articles || jsonb_build_array($2) ELSE saved_articles END, updated_at = NOW() WHERE user_id = $1 ` _, err := r.db.DB().ExecContext(ctx, query, userID, articleURL) return err } func (r *UserInterestsRepository) UnsaveArticle(ctx context.Context, userID, articleURL string) error { query := ` UPDATE user_interests SET saved_articles = saved_articles - $2, updated_at = NOW() WHERE user_id = $1 ` _, err := r.db.DB().ExecContext(ctx, query, userID, articleURL) return err } func (r *UserInterestsRepository) BlockSource(ctx context.Context, userID, source string) error { query := ` UPDATE user_interests SET blocked_sources = CASE WHEN NOT blocked_sources ? $2 THEN blocked_sources || jsonb_build_array($2) ELSE blocked_sources END, updated_at = NOW() WHERE user_id = $1 ` _, err := r.db.DB().ExecContext(ctx, query, userID, source) return err } func (r *UserInterestsRepository) UnblockSource(ctx context.Context, userID, source string) error { query := ` UPDATE user_interests SET blocked_sources = blocked_sources - $2, updated_at = NOW() WHERE user_id = $1 ` _, err := r.db.DB().ExecContext(ctx, query, userID, source) return err } func (r *UserInterestsRepository) GetTopUsers(ctx context.Context, limit int) ([]string, error) { query := ` SELECT user_id FROM user_interests ORDER BY updated_at DESC LIMIT $1 ` rows, err := r.db.DB().QueryContext(ctx, query, limit) if err != nil { return nil, err } defer rows.Close() var userIDs []string for rows.Next() { var userID string if err := rows.Scan(&userID); err != nil { return nil, err } userIDs = append(userIDs, userID) } return userIDs, rows.Err() } func (r *UserInterestsRepository) DecayAllInterests(ctx context.Context, decayFactor float64) error { query := ` UPDATE user_interests SET topics = ( SELECT jsonb_object_agg(key, (value::text::float * $1)) FROM jsonb_each(topics) WHERE (value::text::float * $1) > 0.01 ), sources = ( SELECT jsonb_object_agg(key, (value::text::float * $1)) FROM jsonb_each(sources) WHERE (value::text::float * $1) > 0.01 ), keywords = ( SELECT jsonb_object_agg(key, (value::text::float * $1)) FROM jsonb_each(keywords) WHERE (value::text::float * $1) > 0.01 ), updated_at = NOW() ` _, err := r.db.DB().ExecContext(ctx, query, decayFactor) return err }