feat: Implement user preferences (saved cities, search history) with Redis storage and API handlers

This commit is contained in:
2026-08-17 14:30:54 +03:00
parent 2907ed3e0d
commit 5842667fff
8 changed files with 1073 additions and 81 deletions

View File

@@ -19,6 +19,13 @@ type StationMonitor struct {
// ScheduleFunc is the function used to check a station's schedule.
// Defaults to checkStationSchedule if not set.
ScheduleFunc func(context.Context, string) (int, error)
// ZeroSince is the timestamp when the current zero-trip streak began.
// Zero if the station is not in a zero-trip streak.
ZeroSince time.Time
// LastSeenFlight is the timestamp of the last successful schedule check.
LastSeenFlight time.Time
}
// Status represents the current status of a station.
@@ -47,6 +54,22 @@ func zeroDaysKey(id string) *cache.CacheKey {
}
}
// zeroSinceKey returns the Redis key for tracking the zero-trip streak start timestamp.
func zeroSinceKey(id string) *cache.CacheKey {
return &cache.CacheKey{
Kind: "station_zero_since",
Code: id,
}
}
// lastSeenFlightKey returns the Redis key for tracking the last seen flight timestamp.
func lastSeenFlightKey(id string) *cache.CacheKey {
return &cache.CacheKey{
Kind: "station_last_seen_flight",
Code: id,
}
}
// checkStationSchedule queries the Yandex /schedule endpoint for a station
// and returns the number of trips found.
func checkStationSchedule(ctx context.Context, yc *yandex.Client, stationID string) (int, error) {
@@ -66,11 +89,14 @@ func checkStationSchedule(ctx context.Context, yc *yandex.Client, stationID stri
}
// updateStationStatus updates the station's status in cache based on trip count.
// It returns the new status. Writes status and zero-days count separately;
// partial failures may leave cache inconsistent but do not lose the core state.
// It returns the new status. Writes status, zero-days count, zero-since timestamp,
// and last-seen-flight timestamp separately; partial failures may leave cache
// inconsistent but do not lose the core state.
func (sm *StationMonitor) updateStationStatus(ctx context.Context, tripCount int) (Status, error) {
cacheKey := stationStatusKey(sm.ID)
zeroDaysKey := zeroDaysKey(sm.ID)
zeroSinceKey := zeroSinceKey(sm.ID)
lastSeenFlightKey := lastSeenFlightKey(sm.ID)
// Get current status from cache
data, err := sm.Cache.Get(ctx, cacheKey)
@@ -101,14 +127,39 @@ func (sm *StationMonitor) updateStationStatus(ctx context.Context, tripCount int
}
}
// Get current zero-since timestamp
zeroSinceData, err := sm.Cache.Get(ctx, zeroSinceKey)
var zeroSince time.Time
if err == nil && zeroSinceData != nil {
_, err := fmt.Sscanf(string(zeroSinceData), "%d", (&zeroSince).Unix())
if err != nil {
zeroSince = time.Time{}
}
}
// Get current last-seen-flight timestamp
lastSeenFlightData, err := sm.Cache.Get(ctx, lastSeenFlightKey)
var lastSeenFlight time.Time
if err == nil && lastSeenFlightData != nil {
_, err := fmt.Sscanf(string(lastSeenFlightData), "%d", (&lastSeenFlight).Unix())
if err != nil {
lastSeenFlight = time.Time{}
}
}
// Update status based on trip count
var newStatus Status
if tripCount > 0 {
newStatus = StatusActive
zeroDays = 0
zeroSince = time.Time{}
lastSeenFlight = time.Now()
} else {
zeroDays++
if zeroSince.IsZero() {
zeroSince = time.Now()
}
if zeroDays >= 3 {
newStatus = StatusClosed
} else {
@@ -126,6 +177,16 @@ func (sm *StationMonitor) updateStationStatus(ctx context.Context, tripCount int
return "", fmt.Errorf("cache set zero days: %w", err)
}
// Write updated zero-since timestamp to cache with 24h TTL
if err := sm.Cache.Set(ctx, zeroSinceKey, []byte(fmt.Sprintf("%d", zeroSince.Unix())), 24*time.Hour); err != nil {
return "", fmt.Errorf("cache set zero since: %w", err)
}
// Write updated last-seen-flight timestamp to cache with 24h TTL
if err := sm.Cache.Set(ctx, lastSeenFlightKey, []byte(fmt.Sprintf("%d", lastSeenFlight.Unix())), 24*time.Hour); err != nil {
return "", fmt.Errorf("cache set last seen flight: %w", err)
}
return newStatus, nil
}