from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update, delete from sqlalchemy.orm import selectinload from models import User, Lottery, Participation, Winner from typing import List, Optional, Dict, Any from account_utils import validate_account_number, format_account_number import random class UserService: """Сервис для работы с пользователями""" @staticmethod async def get_or_create_user(session: AsyncSession, telegram_id: int, username: str = None, first_name: str = None, last_name: str = None) -> User: """Получить или создать пользователя""" # Пробуем найти существующего пользователя result = await session.execute( select(User).where(User.telegram_id == telegram_id) ) user = result.scalar_one_or_none() if user: # Обновляем информацию о пользователе user.username = username user.first_name = first_name user.last_name = last_name await session.commit() return user # Создаем нового пользователя user = User( telegram_id=telegram_id, username=username, first_name=first_name, last_name=last_name ) session.add(user) await session.commit() await session.refresh(user) return user @staticmethod async def get_user_by_telegram_id(session: AsyncSession, telegram_id: int) -> Optional[User]: """Получить пользователя по Telegram ID""" result = await session.execute( select(User).where(User.telegram_id == telegram_id) ) return result.scalar_one_or_none() @staticmethod async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]: """Получить пользователя по username""" result = await session.execute(select(User).where(User.username == username)) return result.scalar_one_or_none() @staticmethod async def get_all_users(session: AsyncSession, limit: int = None, offset: int = 0) -> List[User]: """Получить всех пользователей""" query = select(User).order_by(User.created_at.desc()) if limit: query = query.offset(offset).limit(limit) result = await session.execute(query) return result.scalars().all() @staticmethod async def search_users(session: AsyncSession, search_term: str, limit: int = 20) -> List[User]: """Поиск пользователей по имени или username""" from sqlalchemy import or_, func search_pattern = f"%{search_term.lower()}%" result = await session.execute( select(User).where( or_( func.lower(User.first_name).contains(search_pattern), func.lower(User.last_name).contains(search_pattern) if User.last_name else False, func.lower(User.username).contains(search_pattern) if User.username else False ) ).limit(limit) ) return result.scalars().all() @staticmethod async def delete_user(session: AsyncSession, user_id: int) -> bool: """Удалить пользователя и все связанные данные""" user = await session.get(User, user_id) if not user: return False # Удаляем все участия await session.execute( delete(Participation).where(Participation.user_id == user_id) ) # Удаляем все победы await session.execute( delete(Winner).where(Winner.user_id == user_id) ) # Удаляем пользователя await session.delete(user) await session.commit() return True @staticmethod async def set_admin(session: AsyncSession, telegram_id: int, is_admin: bool = True) -> bool: """Установить/снять права администратора""" result = await session.execute( update(User) .where(User.telegram_id == telegram_id) .values(is_admin=is_admin) ) await session.commit() return result.rowcount > 0 @staticmethod async def set_account_number(session: AsyncSession, telegram_id: int, account_number: str) -> bool: """Установить номер клиентского счета пользователю""" # Валидируем и форматируем номер formatted_number = format_account_number(account_number) if not formatted_number: return False # Проверяем уникальность номера existing = await session.execute( select(User).where(User.account_number == formatted_number) ) if existing.scalar_one_or_none(): return False # Номер уже занят # Обновляем пользователя result = await session.execute( update(User) .where(User.telegram_id == telegram_id) .values(account_number=formatted_number) ) await session.commit() return result.rowcount > 0 @staticmethod async def get_user_by_account(session: AsyncSession, account_number: str) -> Optional[User]: """Получить пользователя по номеру счета""" formatted_number = format_account_number(account_number) if not formatted_number: return None result = await session.execute( select(User).where(User.account_number == formatted_number) ) return result.scalar_one_or_none() @staticmethod async def search_by_account(session: AsyncSession, account_pattern: str) -> List[User]: """Поиск пользователей по части номера счета""" # Убираем все кроме цифр и дефисов clean_pattern = ''.join(c for c in account_pattern if c.isdigit() or c == '-') if not clean_pattern: return [] result = await session.execute( select(User).where( User.account_number.like(f'%{clean_pattern}%') ).limit(20) ) return result.scalars().all() class LotteryService: """Сервис для работы с розыгрышами""" @staticmethod async def create_lottery(session: AsyncSession, title: str, description: str, prizes: List[str], creator_id: int) -> Lottery: """Создать новый розыгрыш""" lottery = Lottery( title=title, description=description, prizes=prizes, creator_id=creator_id ) session.add(lottery) await session.commit() await session.refresh(lottery) return lottery @staticmethod async def get_lottery(session: AsyncSession, lottery_id: int) -> Optional[Lottery]: """Получить розыгрыш по ID""" result = await session.execute( select(Lottery) .options(selectinload(Lottery.participations).selectinload(Participation.user)) .where(Lottery.id == lottery_id) ) return result.scalar_one_or_none() @staticmethod async def get_active_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]: """Получить список активных розыгрышей""" query = select(Lottery).where( Lottery.is_active == True, Lottery.is_completed == False ).order_by(Lottery.created_at.desc()) if limit: query = query.limit(limit) result = await session.execute(query) return result.scalars().all() @staticmethod async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]: """Получить список всех розыгрышей""" query = select(Lottery).order_by(Lottery.created_at.desc()) if limit: query = query.limit(limit) result = await session.execute(query) return result.scalars().all() @staticmethod async def set_manual_winner(session: AsyncSession, lottery_id: int, place: int, telegram_id: int) -> bool: """Установить ручного победителя для определенного места""" # Получаем пользователя user = await UserService.get_user_by_telegram_id(session, telegram_id) if not user: return False # Получаем розыгрыш lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: return False # Обновляем ручных победителей if not lottery.manual_winners: lottery.manual_winners = {} lottery.manual_winners[str(place)] = telegram_id await session.commit() return True @staticmethod async def conduct_draw(session: AsyncSession, lottery_id: int) -> Dict[int, Dict[str, Any]]: """Провести розыгрыш с учетом ручных победителей""" lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery or lottery.is_completed: return {} # Получаем всех участников participants = [p.user for p in lottery.participations] if not participants: return {} # Определяем количество призовых мест num_prizes = len(lottery.prizes) if lottery.prizes else 1 results = {} remaining_participants = participants.copy() manual_winners = lottery.manual_winners or {} # Сначала обрабатываем ручных победителей for place in range(1, num_prizes + 1): place_str = str(place) if place_str in manual_winners: # Находим пользователя среди участников manual_winner = None for participant in remaining_participants: if participant.telegram_id == manual_winners[place_str]: manual_winner = participant break if manual_winner: results[place] = { 'user': manual_winner, 'prize': lottery.prizes[place - 1] if lottery.prizes and place <= len(lottery.prizes) else f"Приз {place} места", 'is_manual': True } remaining_participants.remove(manual_winner) # Заполняем оставшиеся места случайными участниками for place in range(1, num_prizes + 1): if place not in results and remaining_participants: winner = random.choice(remaining_participants) results[place] = { 'user': winner, 'prize': lottery.prizes[place - 1] if lottery.prizes and place <= len(lottery.prizes) else f"Приз {place} места", 'is_manual': False } remaining_participants.remove(winner) # Сохраняем победителей в базу данных for place, winner_info in results.items(): winner = Winner( lottery_id=lottery_id, user_id=winner_info['user'].id, place=place, prize=winner_info['prize'], is_manual=winner_info['is_manual'] ) session.add(winner) # Обновляем статус розыгрыша lottery.is_completed = True lottery.draw_results = { str(place): { 'user_id': info['user'].id, 'telegram_id': info['user'].telegram_id, 'username': info['user'].username, 'prize': info['prize'], 'is_manual': info['is_manual'] } for place, info in results.items() } await session.commit() return results @staticmethod async def get_winners(session: AsyncSession, lottery_id: int) -> List[Winner]: """Получить победителей розыгрыша""" result = await session.execute( select(Winner) .options(selectinload(Winner.user)) .where(Winner.lottery_id == lottery_id) .order_by(Winner.place) ) return result.scalars().all() @staticmethod async def set_winner_display_type(session: AsyncSession, lottery_id: int, display_type: str) -> bool: """Установить тип отображения победителей для розыгрыша""" from winner_display import validate_display_type if not validate_display_type(display_type): return False result = await session.execute( update(Lottery) .where(Lottery.id == lottery_id) .values(winner_display_type=display_type) ) await session.commit() return result.rowcount > 0 @staticmethod async def set_lottery_active(session: AsyncSession, lottery_id: int, is_active: bool) -> bool: """Установить статус активности розыгрыша""" result = await session.execute( update(Lottery) .where(Lottery.id == lottery_id) .values(is_active=is_active) ) await session.commit() return result.rowcount > 0 @staticmethod async def complete_lottery(session: AsyncSession, lottery_id: int) -> bool: """Завершить розыгрыш (сделать неактивным и завершенным)""" from datetime import datetime result = await session.execute( update(Lottery) .where(Lottery.id == lottery_id) .values(is_active=False, is_completed=True, end_date=datetime.now()) ) await session.commit() return result.rowcount > 0 @staticmethod async def delete_lottery(session: AsyncSession, lottery_id: int) -> bool: """Удалить розыгрыш и все связанные данные""" # Сначала удаляем все связанные данные # Удаляем победителей await session.execute( delete(Winner).where(Winner.lottery_id == lottery_id) ) # Удаляем участников await session.execute( delete(Participation).where(Participation.lottery_id == lottery_id) ) # Удаляем сам розыгрыш result = await session.execute( delete(Lottery).where(Lottery.id == lottery_id) ) await session.commit() return result.rowcount > 0 class ParticipationService: """Сервис для работы с участием в розыгрышах""" @staticmethod async def add_participant(session: AsyncSession, lottery_id: int, user_id: int) -> bool: """Добавить участника в розыгрыш""" # Проверяем, не участвует ли уже пользователь existing = await session.execute( select(Participation) .where(Participation.lottery_id == lottery_id, Participation.user_id == user_id) ) if existing.scalar_one_or_none(): return False participation = Participation(lottery_id=lottery_id, user_id=user_id) session.add(participation) await session.commit() return True @staticmethod async def remove_participant(session: AsyncSession, lottery_id: int, user_id: int) -> bool: """Удалить участника из розыгрыша""" participation = await session.execute( select(Participation) .where(Participation.lottery_id == lottery_id, Participation.user_id == user_id) ) participation = participation.scalar_one_or_none() if not participation: return False await session.delete(participation) await session.commit() return True @staticmethod async def get_participants(session: AsyncSession, lottery_id: int, limit: Optional[int] = None, offset: int = 0) -> List[User]: """Получить участников розыгрыша""" query = select(User).join(Participation).where(Participation.lottery_id == lottery_id) if limit: query = query.offset(offset).limit(limit) result = await session.execute(query) return list(result.scalars().all()) @staticmethod async def get_user_participations(session: AsyncSession, user_id: int) -> List[Participation]: """Получить участие пользователя в розыгрышах""" result = await session.execute( select(Participation) .options(selectinload(Participation.lottery)) .where(Participation.user_id == user_id) .order_by(Participation.created_at.desc()) ) return list(result.scalars().all()) @staticmethod async def get_participants_count(session: AsyncSession, lottery_id: int) -> int: """Получить количество участников в розыгрыше""" result = await session.execute( select(Participation) .where(Participation.lottery_id == lottery_id) ) return len(result.scalars().all()) @staticmethod async def add_participants_bulk(session: AsyncSession, lottery_id: int, telegram_ids: List[int]) -> Dict[str, Any]: """Массовое добавление участников""" results = { "added": 0, "skipped": 0, "errors": [], "details": [] } for telegram_id in telegram_ids: try: # Проверяем, существует ли пользователь user = await UserService.get_user_by_telegram_id(session, telegram_id) if not user: results["errors"].append(f"Пользователь {telegram_id} не найден") continue # Пробуем добавить if await ParticipationService.add_participant(session, lottery_id, user.id): results["added"] += 1 results["details"].append(f"Добавлен: {user.first_name} (@{user.username or 'no_username'})") else: results["skipped"] += 1 results["details"].append(f"Уже участвует: {user.first_name}") except Exception as e: results["errors"].append(f"Ошибка с {telegram_id}: {str(e)}") return results @staticmethod async def remove_participants_bulk(session: AsyncSession, lottery_id: int, telegram_ids: List[int]) -> Dict[str, Any]: """Массовое удаление участников""" results = { "removed": 0, "not_found": 0, "errors": [], "details": [] } for telegram_id in telegram_ids: try: user = await UserService.get_user_by_telegram_id(session, telegram_id) if not user: results["not_found"] += 1 results["details"].append(f"Не найден: {telegram_id}") continue if await ParticipationService.remove_participant(session, lottery_id, user.id): results["removed"] += 1 results["details"].append(f"Удален: {user.first_name}") else: results["not_found"] += 1 results["details"].append(f"Не участвовал: {user.first_name}") except Exception as e: results["errors"].append(f"Ошибка с {telegram_id}: {str(e)}") return results @staticmethod async def add_participants_by_accounts_bulk(session: AsyncSession, lottery_id: int, account_numbers: List[str]) -> Dict[str, Any]: """Массовое добавление участников по номерам счетов""" results = { "added": 0, "skipped": 0, "errors": [], "details": [], "invalid_accounts": [] } for account_number in account_numbers: account_number = account_number.strip() if not account_number: continue try: # Валидируем и форматируем номер formatted_account = format_account_number(account_number) if not formatted_account: results["invalid_accounts"].append(account_number) results["errors"].append(f"Неверный формат: {account_number}") continue # Ищем пользователя по номеру счёта user = await UserService.get_user_by_account(session, formatted_account) if not user: results["errors"].append(f"Пользователь с счётом {formatted_account} не найден") continue # Пробуем добавить в розыгрыш if await ParticipationService.add_participant(session, lottery_id, user.id): results["added"] += 1 results["details"].append(f"Добавлен: {user.first_name} ({formatted_account})") else: results["skipped"] += 1 results["details"].append(f"Уже участвует: {user.first_name} ({formatted_account})") except Exception as e: results["errors"].append(f"Ошибка с {account_number}: {str(e)}") return results @staticmethod async def remove_participants_by_accounts_bulk(session: AsyncSession, lottery_id: int, account_numbers: List[str]) -> Dict[str, Any]: """Массовое удаление участников по номерам счетов""" results = { "removed": 0, "not_found": 0, "errors": [], "details": [], "invalid_accounts": [] } for account_number in account_numbers: account_number = account_number.strip() if not account_number: continue try: # Валидируем и форматируем номер formatted_account = format_account_number(account_number) if not formatted_account: results["invalid_accounts"].append(account_number) results["errors"].append(f"Неверный формат: {account_number}") continue # Ищем пользователя по номеру счёта user = await UserService.get_user_by_account(session, formatted_account) if not user: results["not_found"] += 1 results["details"].append(f"Не найден: {formatted_account}") continue # Пробуем удалить из розыгрыша if await ParticipationService.remove_participant(session, lottery_id, user.id): results["removed"] += 1 results["details"].append(f"Удалён: {user.first_name} ({formatted_account})") else: results["not_found"] += 1 results["details"].append(f"Не участвовал: {user.first_name} ({formatted_account})") except Exception as e: results["errors"].append(f"Ошибка с {account_number}: {str(e)}") return results @staticmethod async def get_participant_stats(session: AsyncSession, user_id: int) -> Dict[str, Any]: """Статистика участника""" from sqlalchemy import func # Количество участий participations_count = await session.scalar( select(func.count(Participation.id)).where(Participation.user_id == user_id) ) # Количество побед wins_count = await session.scalar( select(func.count(Winner.id)).where(Winner.user_id == user_id) ) # Последнее участие last_participation = await session.execute( select(Participation).where(Participation.user_id == user_id) .order_by(Participation.created_at.desc()).limit(1) ) last_participation = last_participation.scalar_one_or_none() return { "participations_count": participations_count, "wins_count": wins_count, "last_participation": last_participation.created_at if last_participation else None }