This commit is contained in:
2026-01-25 21:59:00 +08:00
parent 7fd537bef3
commit 4cad3f0250
118 changed files with 30473 additions and 0 deletions

192
internal/cache/exchange_rate_cache.go vendored Normal file
View 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
}