refactor
This commit is contained in:
3
src/core/__init__.py
Normal file
3
src/core/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Основные компоненты системы: конфигурация, база данных, модели и сервисы.
|
||||
"""
|
||||
29
src/core/config.py
Normal file
29
src/core/config.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Загружаем переменные окружения
|
||||
load_dotenv()
|
||||
|
||||
# Telegram Bot
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||
if not BOT_TOKEN:
|
||||
raise ValueError("BOT_TOKEN не найден в переменных окружения")
|
||||
|
||||
# База данных
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./lottery_bot.db")
|
||||
|
||||
# Администраторы
|
||||
ADMIN_IDS = []
|
||||
admin_ids_str = os.getenv("ADMIN_IDS", "")
|
||||
if admin_ids_str:
|
||||
try:
|
||||
ADMIN_IDS = [int(id_str.strip()) for id_str in admin_ids_str.split(",") if id_str.strip()]
|
||||
except ValueError:
|
||||
print("Предупреждение: Некорректные ID администраторов в ADMIN_IDS")
|
||||
|
||||
# Логирование
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||
|
||||
# Настройки бота
|
||||
MAX_PARTICIPANTS_PER_LOTTERY = 10000 # Максимальное количество участников в розыгрыше
|
||||
MAX_ACTIVE_LOTTERIES = 10 # Максимальное количество активных розыгрышей
|
||||
41
src/core/database.py
Normal file
41
src/core/database.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Загружаем переменные окружения
|
||||
load_dotenv()
|
||||
|
||||
# Конфигурация базы данных
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./lottery_bot.db")
|
||||
|
||||
# Создаем асинхронный движок
|
||||
engine = create_async_engine(
|
||||
DATABASE_URL,
|
||||
echo=True, # Логирование SQL запросов
|
||||
future=True,
|
||||
)
|
||||
|
||||
# Создаем фабрику сессий
|
||||
async_session_maker = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
# Базовый класс для моделей
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_session() -> AsyncSession:
|
||||
"""Получить асинхронную сессию базы данных"""
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
async def init_db():
|
||||
"""Инициализация базы данных"""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async def close_db():
|
||||
"""Закрытие соединения с базой данных"""
|
||||
await engine.dispose()
|
||||
98
src/core/models.py
Normal file
98
src/core/models.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from .database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""Модель пользователя"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
telegram_id = Column(Integer, unique=True, nullable=False, index=True)
|
||||
username = Column(String(255))
|
||||
first_name = Column(String(255))
|
||||
last_name = Column(String(255))
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
is_admin = Column(Boolean, default=False)
|
||||
# Клиентский счет в формате: XX-XX-XX-XX-XX-XX-XX (7 пар цифр через дефис)
|
||||
account_number = Column(String(20), unique=True, nullable=True, index=True)
|
||||
|
||||
# Связи
|
||||
participations = relationship("Participation", back_populates="user")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(telegram_id={self.telegram_id}, username={self.username})>"
|
||||
|
||||
|
||||
class Lottery(Base):
|
||||
"""Модель розыгрыша"""
|
||||
__tablename__ = "lotteries"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(String(500), nullable=False)
|
||||
description = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
start_date = Column(DateTime(timezone=True))
|
||||
end_date = Column(DateTime(timezone=True))
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_completed = Column(Boolean, default=False)
|
||||
prizes = Column(JSON) # Список призов в формате JSON
|
||||
creator_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Настройки для ручного управления победителями
|
||||
manual_winners = Column(JSON, default=lambda: {}) # {место: telegram_id}
|
||||
draw_results = Column(JSON) # Результаты розыгрыша
|
||||
|
||||
# Тип отображения победителей: "username", "chat_id", "account_number"
|
||||
winner_display_type = Column(String(20), default="username")
|
||||
|
||||
# Связи
|
||||
creator = relationship("User")
|
||||
participations = relationship("Participation", back_populates="lottery")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Lottery(id={self.id}, title={self.title})>"
|
||||
|
||||
|
||||
class Participation(Base):
|
||||
"""Модель участия в розыгрыше"""
|
||||
__tablename__ = "participations"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Опционально
|
||||
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
|
||||
account_number = Column(String(20), nullable=True, index=True) # Счет участника (XX-XX-XX-XX-XX-XX-XX)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# Связи
|
||||
user = relationship("User", back_populates="participations")
|
||||
lottery = relationship("Lottery", back_populates="participations")
|
||||
|
||||
def __repr__(self):
|
||||
if self.account_number:
|
||||
return f"<Participation(account={self.account_number}, lottery_id={self.lottery_id})>"
|
||||
return f"<Participation(user_id={self.user_id}, lottery_id={self.lottery_id})>"
|
||||
|
||||
|
||||
class Winner(Base):
|
||||
"""Модель победителя розыгрыша"""
|
||||
__tablename__ = "winners"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Опционально
|
||||
account_number = Column(String(20), nullable=True, index=True) # Счет победителя
|
||||
place = Column(Integer, nullable=False) # Место (1, 2, 3...)
|
||||
prize = Column(String(500)) # Описание приза
|
||||
is_manual = Column(Boolean, default=False) # Был ли установлен вручную
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# Связи
|
||||
user = relationship("User")
|
||||
lottery = relationship("Lottery")
|
||||
|
||||
def __repr__(self):
|
||||
if self.account_number:
|
||||
return f"<Winner(lottery_id={self.lottery_id}, account={self.account_number}, place={self.place})>"
|
||||
return f"<Winner(lottery_id={self.lottery_id}, user_id={self.user_id}, place={self.place})>"
|
||||
636
src/core/services.py
Normal file
636
src/core/services.py
Normal file
@@ -0,0 +1,636 @@
|
||||
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 ..utils.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 ..display.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
|
||||
}
|
||||
Reference in New Issue
Block a user