193 lines
5.3 KiB
Go
193 lines
5.3 KiB
Go
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
|
|
}
|