diff --git a/app/config.py b/app/config.py index 899fe4a7..9bb8ffb8 100644 --- a/app/config.py +++ b/app/config.py @@ -79,7 +79,8 @@ class Settings(BaseSettings): DATABASE_MODE: str = "auto" REDIS_URL: str = "redis://localhost:6379/0" - + CART_TTL_SECONDS: int = 3600 # Время жизни корзины пользователя в Redis (1 час) + REMNAWAVE_API_URL: Optional[str] = None REMNAWAVE_API_KEY: Optional[str] = None REMNAWAVE_SECRET_KEY: Optional[str] = None diff --git a/app/services/user_cart_service.py b/app/services/user_cart_service.py index 9024d98a..613ff1c8 100644 --- a/app/services/user_cart_service.py +++ b/app/services/user_cart_service.py @@ -1,7 +1,6 @@ import json import logging from typing import Optional, Dict, Any -from datetime import timedelta import redis.asyncio as redis @@ -9,104 +8,136 @@ from app.config import settings logger = logging.getLogger(__name__) + class UserCartService: """ - Сервис для работы с корзиной пользователя через Redis + Сервис для работы с корзиной пользователя через Redis. + + Использует ленивую инициализацию Redis-клиента для graceful fallback + при недоступности Redis. """ - + def __init__(self): - self.redis_client = None - self._setup_redis() - - def _setup_redis(self): - """Инициализация Redis клиента""" + self._redis_client: Optional[redis.Redis] = None + self._initialized: bool = False + + def _get_redis_client(self) -> Optional[redis.Redis]: + """Ленивая инициализация Redis клиента.""" + if self._initialized: + return self._redis_client + try: - self.redis_client = redis.from_url(settings.REDIS_URL) + self._redis_client = redis.from_url(settings.REDIS_URL) + self._initialized = True + logger.debug("Redis клиент для корзины инициализирован") except Exception as e: - logger.error(f"Ошибка подключения к Redis: {e}") - raise - - async def save_user_cart(self, user_id: int, cart_data: Dict[str, Any], ttl: int = 3600) -> bool: + logger.warning(f"Не удалось подключиться к Redis для корзины: {e}") + self._redis_client = None + self._initialized = True + + return self._redis_client + + async def save_user_cart( + self, user_id: int, cart_data: Dict[str, Any], ttl: Optional[int] = None + ) -> bool: """ - Сохранить корзину пользователя в Redis - + Сохранить корзину пользователя в Redis. + Args: user_id: ID пользователя cart_data: Данные корзины (параметры подписки) - ttl: Время жизни ключа в секундах (по умолчанию 1 час) - + ttl: Время жизни ключа в секундах (по умолчанию из settings.CART_TTL_SECONDS) + Returns: bool: Успешность сохранения """ + client = self._get_redis_client() + if client is None: + return False + try: key = f"user_cart:{user_id}" json_data = json.dumps(cart_data, ensure_ascii=False) - await self.redis_client.setex(key, ttl, json_data) - logger.info(f"Корзина пользователя {user_id} сохранена в Redis") + effective_ttl = ttl if ttl is not None else settings.CART_TTL_SECONDS + await client.setex(key, effective_ttl, json_data) + logger.debug(f"Корзина пользователя {user_id} сохранена в Redis") return True except Exception as e: logger.error(f"Ошибка сохранения корзины пользователя {user_id}: {e}") return False - + async def get_user_cart(self, user_id: int) -> Optional[Dict[str, Any]]: """ - Получить корзину пользователя из Redis - + Получить корзину пользователя из Redis. + Args: user_id: ID пользователя - + Returns: dict: Данные корзины или None """ + client = self._get_redis_client() + if client is None: + return None + try: key = f"user_cart:{user_id}" - json_data = await self.redis_client.get(key) + json_data = await client.get(key) if json_data: cart_data = json.loads(json_data) - logger.info(f"Корзина пользователя {user_id} загружена из Redis") + logger.debug(f"Корзина пользователя {user_id} загружена из Redis") return cart_data return None except Exception as e: logger.error(f"Ошибка получения корзины пользователя {user_id}: {e}") return None - + async def delete_user_cart(self, user_id: int) -> bool: """ - Удалить корзину пользователя из Redis - + Удалить корзину пользователя из Redis. + Args: user_id: ID пользователя - + Returns: bool: Успешность удаления """ + client = self._get_redis_client() + if client is None: + return False + try: key = f"user_cart:{user_id}" - result = await self.redis_client.delete(key) + result = await client.delete(key) if result: - logger.info(f"Корзина пользователя {user_id} удалена из Redis") + logger.debug(f"Корзина пользователя {user_id} удалена из Redis") return bool(result) except Exception as e: logger.error(f"Ошибка удаления корзины пользователя {user_id}: {e}") return False - + async def has_user_cart(self, user_id: int) -> bool: """ - Проверить наличие корзины у пользователя - + Проверить наличие корзины у пользователя. + Args: user_id: ID пользователя - + Returns: bool: Наличие корзины """ + client = self._get_redis_client() + if client is None: + return False + try: key = f"user_cart:{user_id}" - exists = await self.redis_client.exists(key) + exists = await client.exists(key) return bool(exists) except Exception as e: logger.error(f"Ошибка проверки наличия корзины пользователя {user_id}: {e}") return False -# Глобальный экземпляр сервиса -user_cart_service = UserCartService() \ No newline at end of file + +# Глобальный экземпляр сервиса (инициализация Redis отложена) +user_cart_service = UserCartService() diff --git a/tests/test_user_cart_service.py b/tests/test_user_cart_service.py index 56fe8543..1531bc8c 100644 --- a/tests/test_user_cart_service.py +++ b/tests/test_user_cart_service.py @@ -31,7 +31,8 @@ def mock_redis(): @pytest.fixture def user_cart_service(mock_redis): service = UserCartService() - service.redis_client = mock_redis + service._redis_client = mock_redis + service._initialized = True return service async def test_save_user_cart(user_cart_service, mock_redis):