init
This commit is contained in:
192
internal/cache/exchange_rate_cache.go
vendored
Normal file
192
internal/cache/exchange_rate_cache.go
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user