package cache import ( "context" "encoding/json" "errors" "fmt" "strconv" "time" "github.com/redis/go-redis/v9" "accounting-app/internal/config" ) // Redis key constants for exchange rate cache const ( RatesCacheKey = "exchange_rates:all" RateCacheKey = "exchange_rates:rate:" SyncStatusKey = "exchange_rates:sync_status" ) // SyncStatus represents the synchronization status of exchange rates type SyncStatus struct { LastSyncTime time.Time `json:"last_sync_time"` LastSyncStatus string `json:"last_sync_status"` // success, failed NextSyncTime time.Time `json:"next_sync_time"` RatesCount int `json:"rates_count"` ErrorMessage string `json:"error_message,omitempty"` } // ExchangeRateCache provides Redis caching for exchange rates type ExchangeRateCache struct { client *redis.Client keyPrefix string expiration time.Duration } // NewExchangeRateCache creates a new ExchangeRateCache instance func NewExchangeRateCache(redisClient *RedisClient, cfg *config.Config) *ExchangeRateCache { expiration := cfg.CacheExpiration if expiration == 0 { expiration = 10 * time.Minute // Default to 10 minutes } return &ExchangeRateCache{ client: redisClient.Client(), keyPrefix: "", expiration: expiration, } } // GetAll retrieves all exchange rates from the cache // Returns a map of currency code to rate (1 Currency = Rate CNY) func (c *ExchangeRateCache) GetAll(ctx context.Context) (map[string]float64, error) { result, err := c.client.HGetAll(ctx, RatesCacheKey).Result() if err != nil { return nil, fmt.Errorf("failed to get all rates from cache: %w", err) } if len(result) == 0 { return nil, nil // Cache miss - no data } rates := make(map[string]float64, len(result)) for currency, rateStr := range result { rate, err := strconv.ParseFloat(rateStr, 64) if err != nil { // Skip invalid rate values continue } rates[currency] = rate } return rates, nil } // Get retrieves a single currency's exchange rate from the cache // Returns the rate (1 Currency = Rate CNY) or an error if not found func (c *ExchangeRateCache) Get(ctx context.Context, currency string) (float64, error) { rateStr, err := c.client.HGet(ctx, RatesCacheKey, currency).Result() if err != nil { if errors.Is(err, redis.Nil) { return 0, fmt.Errorf("rate for currency %s not found in cache", currency) } return 0, fmt.Errorf("failed to get rate for %s from cache: %w", currency, err) } rate, err := strconv.ParseFloat(rateStr, 64) if err != nil { return 0, fmt.Errorf("invalid rate value for %s in cache: %w", currency, err) } return rate, nil } // SetAll stores all exchange rates in the cache with TTL // rates is a map of currency code to rate (1 Currency = Rate CNY) func (c *ExchangeRateCache) SetAll(ctx context.Context, rates map[string]float64) error { if len(rates) == 0 { return nil } // Convert rates to string map for Redis Hash rateStrings := make(map[string]interface{}, len(rates)) for currency, rate := range rates { rateStrings[currency] = strconv.FormatFloat(rate, 'f', 6, 64) } // Use pipeline for atomic operation pipe := c.client.Pipeline() // Delete existing key to ensure clean state pipe.Del(ctx, RatesCacheKey) // Set all rates in hash pipe.HSet(ctx, RatesCacheKey, rateStrings) // Set TTL pipe.Expire(ctx, RatesCacheKey, c.expiration) _, err := pipe.Exec(ctx) if err != nil { return fmt.Errorf("failed to set rates in cache: %w", err) } return nil } // GetSyncStatus retrieves the synchronization status from the cache func (c *ExchangeRateCache) GetSyncStatus(ctx context.Context) (*SyncStatus, error) { data, err := c.client.Get(ctx, SyncStatusKey).Bytes() if err != nil { if errors.Is(err, redis.Nil) { return nil, nil // No sync status stored yet } return nil, fmt.Errorf("failed to get sync status from cache: %w", err) } var status SyncStatus if err := json.Unmarshal(data, &status); err != nil { return nil, fmt.Errorf("failed to unmarshal sync status: %w", err) } return &status, nil } // SetSyncStatus stores the synchronization status in the cache func (c *ExchangeRateCache) SetSyncStatus(ctx context.Context, status *SyncStatus) error { if status == nil { return errors.New("sync status cannot be nil") } data, err := json.Marshal(status) if err != nil { return fmt.Errorf("failed to marshal sync status: %w", err) } // Sync status doesn't expire - it's always relevant err = c.client.Set(ctx, SyncStatusKey, data, 0).Err() if err != nil { return fmt.Errorf("failed to set sync status in cache: %w", err) } return nil } // Exists checks if the rates cache exists and is not expired func (c *ExchangeRateCache) Exists(ctx context.Context) (bool, error) { exists, err := c.client.Exists(ctx, RatesCacheKey).Result() if err != nil { return false, fmt.Errorf("failed to check cache existence: %w", err) } return exists > 0, nil } // Delete removes all exchange rate data from the cache func (c *ExchangeRateCache) Delete(ctx context.Context) error { pipe := c.client.Pipeline() pipe.Del(ctx, RatesCacheKey) pipe.Del(ctx, SyncStatusKey) _, err := pipe.Exec(ctx) if err != nil { return fmt.Errorf("failed to delete cache: %w", err) } return nil } // GetExpiration returns the cache expiration duration func (c *ExchangeRateCache) GetExpiration() time.Duration { return c.expiration }