import asyncio from typing import Dict, Optional from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from database.models import Card as DBCard from services.database_service_base import DatabaseServiceBase from utils.logger import session_logger class Card(BaseModel): card_id: Optional[int] = None card_uuid: str device_id: Optional[str] = None card_name: Optional[str] = None status: int = 0 total_swaps: int = 0 created_at: Optional[float] = None updated_at: Optional[float] = None @classmethod def from_db_model(cls, db_model: DBCard): return cls( card_id=db_model.card_id, card_uuid=db_model.card_uuid, device_id=db_model.device_id, card_name=db_model.card_name, status=db_model.status, total_swaps=db_model.total_swaps, created_at=db_model.created_at.timestamp() if db_model.created_at else None, updated_at=db_model.updated_at.timestamp() if db_model.updated_at else None, ) class CardService(DatabaseServiceBase): def __init__(self): super().__init__(service_name="card") self.cards: Dict[str, Card] = {} self.lock = asyncio.Lock() async def _cache_card(self, card: Card) -> None: async with self.lock: self.cards[card.card_uuid] = card async def _load_card_from_db(self, card_uuid: str, async_session: AsyncSession) -> Optional[Card]: try: query = select(DBCard).where(DBCard.card_uuid == card_uuid) result = await async_session.execute(query) db_card = result.scalar_one_or_none() if not db_card: return None card = Card.from_db_model(db_card) await self._cache_card(card) return card except Exception as exc: session_logger.error("card", "service", f"load card failed: {exc}") return None async def _save_card_to_db(self, card: Card, async_session: AsyncSession, commit: bool = True) -> None: try: query = select(DBCard).where(DBCard.card_uuid == card.card_uuid) result = await async_session.execute(query) existing_card = result.scalar_one_or_none() if existing_card: existing_card.device_id = card.device_id existing_card.card_name = card.card_name existing_card.status = card.status existing_card.total_swaps = card.total_swaps db_card = existing_card else: db_card = DBCard( card_uuid=card.card_uuid, device_id=card.device_id, card_name=card.card_name, status=card.status, total_swaps=card.total_swaps, ) async_session.add(db_card) await async_session.flush() if commit: await async_session.commit() card.card_id = db_card.card_id await self._cache_card(card) session_logger.info("card", "service", f"card saved: {card.card_uuid}") except Exception as exc: if commit: await async_session.rollback() session_logger.error("card", "service", f"save card failed: {exc}") raise async def get_card_by_uuid( self, card_uuid: str, force_refresh: bool = False, db_session: Optional[AsyncSession] = None, ) -> Optional[Card]: await self._init_database() card = None if not force_refresh: async with self.lock: card = self.cards.get(card_uuid) if card: return card if db_session is not None: return await self._load_card_from_db(card_uuid, db_session) session = await self.db_manager.get_session() try: return await self._load_card_from_db(card_uuid, session) finally: await session.close() async def get_card_by_device_id(self, device_id: str) -> list[Card]: await self._init_database() db_session = await self.db_manager.get_session() try: query = select(DBCard).where(DBCard.device_id == device_id) result = await db_session.execute(query) db_cards = result.scalars().all() cards = [] for db_card in db_cards: card = Card.from_db_model(db_card) await self._cache_card(card) cards.append(card) return cards except Exception as exc: session_logger.error(device_id, "card", f"load device cards failed: {exc}") return [] finally: await db_session.close() async def activate_card( self, card_uuid: str, device_id: str, card_name: Optional[str] = None, db_session: Optional[AsyncSession] = None, ) -> Card: await self._init_database() owns_session = db_session is None if db_session is None: db_session = await self.db_manager.get_session() try: existing_card = await self.get_card_by_uuid(card_uuid, db_session=db_session) if existing_card: if existing_card.device_id and existing_card.device_id != device_id: raise ValueError("card is already bound to another device") existing_card.device_id = device_id existing_card.card_name = card_name existing_card.status = 1 await self._save_card_to_db(existing_card, db_session, commit=owns_session) session_logger.info(device_id, "card", f"card activated: {card_uuid}") return existing_card new_card = Card( card_uuid=card_uuid, device_id=device_id, card_name=card_name, status=1, total_swaps=0, ) await self._save_card_to_db(new_card, db_session, commit=owns_session) session_logger.info(device_id, "card", f"new card activated: {card_uuid}") return new_card except Exception: if owns_session: await db_session.rollback() raise finally: if owns_session: await db_session.close() async def increment_swap_count(self, card_uuid: str) -> Optional[Card]: await self._init_database() card = await self.get_card_by_uuid(card_uuid) if not card: return None card.total_swaps += 1 db_session = await self.db_manager.get_session() try: await self._save_card_to_db(card, db_session) session_logger.info("card", "service", f"swap count incremented: {card_uuid}, total={card.total_swaps}") return card finally: await db_session.close() async def check_card_ownership(self, card_uuid: str, device_id: str) -> bool: card = await self.get_card_by_uuid(card_uuid) return bool(card and card.device_id == device_id) async def is_active_for_device(self, card_uuid: str, device_id: str) -> bool: card = await self.get_card_by_uuid(card_uuid) return bool(card and card.device_id == device_id and int(card.status) == 1) card_service = CardService()