630 lines
26 KiB
Python
630 lines
26 KiB
Python
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) -> List[Lottery]:
|
||
"""Получить список активных розыгрышей"""
|
||
result = await session.execute(
|
||
select(Lottery)
|
||
.where(Lottery.is_active == True, Lottery.is_completed == False)
|
||
.order_by(Lottery.created_at.desc())
|
||
)
|
||
return result.scalars().all()
|
||
|
||
@staticmethod
|
||
async def get_all_lotteries(session: AsyncSession) -> List[Lottery]:
|
||
"""Получить список всех розыгрышей"""
|
||
result = await session.execute(
|
||
select(Lottery)
|
||
.order_by(Lottery.created_at.desc())
|
||
)
|
||
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
|
||
} |