feat: Implement user preferences (saved cities, search history) with Redis storage and API handlers
This commit is contained in:
268
internal/cache/preferences.go
vendored
Normal file
268
internal/cache/preferences.go
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PreferenceKey defines the structure for preference cache keys.
|
||||
type PreferenceKey struct {
|
||||
UserID string // user identifier
|
||||
Kind string // "saved_city" or "search_history"
|
||||
CityCode string // city code for saved_city
|
||||
}
|
||||
|
||||
// PreferenceSavedCity represents a user's saved city preference.
|
||||
type PreferenceSavedCity struct {
|
||||
CityCode string `json:"city_code"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// PreferenceSearchHistory represents a user's search history entry.
|
||||
type PreferenceSearchHistory struct {
|
||||
Query string `json:"query"`
|
||||
FromCity string `json:"from_city"`
|
||||
ToCity string `json:"to_city"`
|
||||
Date string `json:"date"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// Preferences represents user preferences storage.
|
||||
// It provides methods for managing saved cities and search history.
|
||||
type Preferences struct {
|
||||
store Cache
|
||||
}
|
||||
|
||||
// NewPreferences creates a new Preferences instance with the given cache store.
|
||||
func NewPreferences(store Cache) *Preferences {
|
||||
return &Preferences{store: store}
|
||||
}
|
||||
|
||||
// GetSavedCities returns the user's saved cities.
|
||||
func (p *Preferences) GetSavedCities(ctx context.Context, userID string) ([]PreferenceSavedCity, error) {
|
||||
data, err := p.store.Get(ctx, &CacheKey{
|
||||
Kind: "prefs:saved_city:" + userID,
|
||||
Code: userID,
|
||||
From: "",
|
||||
To: "",
|
||||
Date: "",
|
||||
Request: "",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return []PreferenceSavedCity{}, nil
|
||||
}
|
||||
|
||||
var cities []PreferenceSavedCity
|
||||
if err := json.Unmarshal(data, &cities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cities, nil
|
||||
}
|
||||
|
||||
// AddSavedCity adds a city to the user's saved cities.
|
||||
func (p *Preferences) AddSavedCity(ctx context.Context, userID, cityCode, cityName string) error {
|
||||
|
||||
// Load existing cities
|
||||
cities, err := p.GetSavedCities(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if city already exists
|
||||
exists := false
|
||||
for i, c := range cities {
|
||||
if c.CityCode == cityCode {
|
||||
cities[i].Name = cityName
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Add new city
|
||||
cities = append(cities, PreferenceSavedCity{
|
||||
CityCode: cityCode,
|
||||
Name: cityName,
|
||||
})
|
||||
}
|
||||
|
||||
// Store back to cache
|
||||
data, err := json.Marshal(cities)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheKey := &CacheKey{
|
||||
Kind: "prefs:saved_city:" + userID,
|
||||
Code: userID,
|
||||
From: "",
|
||||
To: "",
|
||||
Date: "",
|
||||
Request: "",
|
||||
}
|
||||
if err := p.store.Set(ctx, cacheKey, data, CityTTL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSavedCity removes a city from the user's saved cities.
|
||||
func (p *Preferences) RemoveSavedCity(ctx context.Context, userID, cityCode string) error {
|
||||
key := &CacheKey{
|
||||
Kind: "prefs:saved_city:" + userID,
|
||||
Code: userID,
|
||||
From: "",
|
||||
To: "",
|
||||
Date: "",
|
||||
Request: "",
|
||||
}
|
||||
|
||||
// Load existing cities
|
||||
cities, err := p.GetSavedCities(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the city
|
||||
var result []PreferenceSavedCity
|
||||
for _, c := range cities {
|
||||
if c.CityCode != cityCode {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
// If no cities left, delete the key
|
||||
return p.store.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// Store back
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.store.Set(ctx, key, data, CityTTL)
|
||||
}
|
||||
|
||||
// GetSearchHistory returns the user's search history.
|
||||
func (p *Preferences) GetSearchHistory(ctx context.Context, userID string) ([]PreferenceSearchHistory, error) {
|
||||
key := &CacheKey{
|
||||
Kind: "prefs:search_history:" + userID,
|
||||
Code: userID,
|
||||
From: "",
|
||||
To: "",
|
||||
Date: "",
|
||||
Request: "",
|
||||
}
|
||||
|
||||
data, err := p.store.Get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return []PreferenceSearchHistory{}, nil
|
||||
}
|
||||
|
||||
var history []PreferenceSearchHistory
|
||||
if err := json.Unmarshal(data, &history); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return history, nil
|
||||
}
|
||||
|
||||
// AddSearchHistory adds a search to the user's history.
|
||||
func (p *Preferences) AddSearchHistory(ctx context.Context, userID, fromCity, toCity, date string) error {
|
||||
key := &CacheKey{
|
||||
Kind: "prefs:search_history:" + userID,
|
||||
Code: userID,
|
||||
From: "",
|
||||
To: "",
|
||||
Date: "",
|
||||
Request: "",
|
||||
}
|
||||
|
||||
// Load existing history
|
||||
history, err := p.GetSearchHistory(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add new entry at the beginning (most recent first)
|
||||
history = append([]PreferenceSearchHistory{
|
||||
{
|
||||
Query: fromCity + "→" + toCity,
|
||||
FromCity: fromCity,
|
||||
ToCity: toCity,
|
||||
Date: date,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
},
|
||||
}, history...)
|
||||
|
||||
// Keep only last 50 searches
|
||||
if len(history) > 50 {
|
||||
history = history[:50]
|
||||
}
|
||||
|
||||
// Store back to cache
|
||||
data, err := json.Marshal(history)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.store.Set(ctx, key, data, CityTTL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveOldSearchHistory removes search entries older than the given age.
|
||||
func (p *Preferences) RemoveOldSearchHistory(ctx context.Context, userID string, maxAgeSeconds int64) error {
|
||||
key := &CacheKey{
|
||||
Kind: "prefs:search_history:" + userID,
|
||||
Code: userID,
|
||||
From: "",
|
||||
To: "",
|
||||
Date: "",
|
||||
Request: "",
|
||||
}
|
||||
|
||||
history, err := p.GetSearchHistory(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Filter out old entries
|
||||
var recent []PreferenceSearchHistory
|
||||
now := time.Now().Unix()
|
||||
for _, entry := range history {
|
||||
if entry.CreatedAt >= now-maxAgeSeconds {
|
||||
recent = append(recent, entry)
|
||||
}
|
||||
}
|
||||
|
||||
if len(recent) == len(history) {
|
||||
// No entries removed
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store back
|
||||
data, err := json.Marshal(recent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.store.Set(ctx, key, data, CityTTL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user