Files
gooseek/backend/internal/db/user_interests_repo.go
home 06fe57c765 feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
2026-02-27 04:15:32 +03:00

324 lines
9.0 KiB
Go

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
}