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
|
||||
}
|
||||
69
internal/cache/redis.go
vendored
Normal file
69
internal/cache/redis.go
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"accounting-app/internal/config"
|
||||
)
|
||||
|
||||
// RedisClient wraps the Redis client with additional functionality
|
||||
type RedisClient struct {
|
||||
client *redis.Client
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewRedisClient creates a new Redis client from the configuration
|
||||
func NewRedisClient(cfg *config.Config) (*RedisClient, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.RedisAddr,
|
||||
Password: cfg.RedisPassword,
|
||||
DB: cfg.RedisDB,
|
||||
})
|
||||
|
||||
rc := &RedisClient{
|
||||
client: client,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
// Test the connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := rc.Ping(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// Ping checks if the Redis connection is healthy
|
||||
func (rc *RedisClient) Ping(ctx context.Context) error {
|
||||
_, err := rc.client.Ping(ctx).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
// HealthCheck performs a health check on the Redis connection
|
||||
func (rc *RedisClient) HealthCheck() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return rc.Ping(ctx)
|
||||
}
|
||||
|
||||
// Close closes the Redis connection
|
||||
func (rc *RedisClient) Close() error {
|
||||
return rc.client.Close()
|
||||
}
|
||||
|
||||
// Client returns the underlying Redis client for direct access
|
||||
func (rc *RedisClient) Client() *redis.Client {
|
||||
return rc.client
|
||||
}
|
||||
|
||||
// GetConfig returns the configuration used by this client
|
||||
func (rc *RedisClient) GetConfig() *config.Config {
|
||||
return rc.cfg
|
||||
}
|
||||
Reference in New Issue
Block a user