feat: Система автоматического подтверждения выигрышей с поддержкой множественных счетов
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Основные изменения: ✨ Новые функции: - Система регистрации пользователей с множественными счетами - Автоматическое подтверждение выигрышей через inline-кнопки - Механизм переигровки для неподтвержденных выигрышей (24 часа) - Подтверждение на уровне счетов (каждый счет подтверждается отдельно) - Скрипт полной очистки базы данных 🔧 Технические улучшения: - Исправлена ошибка MissingGreenlet при lazy loading (добавлен joinedload/selectinload) - Добавлено поле claimed_at для отслеживания времени подтверждения - Пакетное добавление счетов с выбором розыгрыша - Проверка владения конкретным счетом при подтверждении 📚 Документация: - docs/AUTO_CONFIRM_SYSTEM.md - Полная документация системы подтверждения - docs/ACCOUNT_BASED_CONFIRMATION.md - Подтверждение на уровне счетов - docs/REGISTRATION_SYSTEM.md - Система регистрации - docs/ADMIN_COMMANDS.md - Команды администратора - docs/CLEAR_DATABASE.md - Очистка БД - docs/QUICK_GUIDE.md - Быстрое начало - docs/UPDATE_LOG.md - Журнал обновлений 🗄️ База данных: - Миграция 003: Таблицы accounts, winner_verifications - Миграция 004: Поле claimed_at в таблице winners - Скрипт scripts/clear_database.py для полной очистки 🎮 Новые команды: Админские: - /check_unclaimed <lottery_id> - Проверка неподтвержденных выигрышей - /redraw <lottery_id> - Повторный розыгрыш - /add_accounts - Пакетное добавление счетов - /list_accounts <telegram_id> - Список счетов пользователя Пользовательские: - /register - Регистрация с вводом данных - /my_account - Просмотр своих счетов - Callback confirm_win_{id} - Подтверждение выигрыша 🛠️ Makefile: - make clear-db - Очистка всех данных из БД (с подтверждением) 🔒 Безопасность: - Проверка владения счетом при подтверждении - Защита от подтверждения чужих счетов - Независимое подтверждение каждого выигрышного счета 📊 Логика работы: 1. Пользователь регистрируется и добавляет счета 2. Счета участвуют в розыгрыше 3. Победители получают уведомление с кнопкой подтверждения 4. Каждый счет подтверждается отдельно (24 часа на подтверждение) 5. Неподтвержденные выигрыши переигрываются через /redraw
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from .database import Base
|
||||
import secrets
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""Модель пользователя"""
|
||||
"""Модель пользователя с регистрацией"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
@@ -13,16 +14,68 @@ class User(Base):
|
||||
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))
|
||||
phone = Column(String(20), nullable=True) # Телефон для верификации
|
||||
club_card_number = Column(String(50), unique=True, nullable=True, index=True) # Номер клубной карты
|
||||
is_registered = Column(Boolean, default=False) # Прошел ли полную регистрацию
|
||||
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)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# Секретный код для верификации выигрыша (генерируется при регистрации)
|
||||
verification_code = Column(String(10), unique=True, nullable=True)
|
||||
|
||||
# Связи
|
||||
accounts = relationship("Account", back_populates="owner", cascade="all, delete-orphan")
|
||||
participations = relationship("Participation", back_populates="user")
|
||||
winners = relationship("Winner", back_populates="user")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(telegram_id={self.telegram_id}, username={self.username})>"
|
||||
return f"<User(telegram_id={self.telegram_id}, club_card={self.club_card_number})>"
|
||||
|
||||
def generate_verification_code(self):
|
||||
"""Генерирует уникальный код верификации"""
|
||||
self.verification_code = secrets.token_hex(4).upper() # 8-символьный код
|
||||
|
||||
|
||||
class Account(Base):
|
||||
"""Модель счета клиента (может быть несколько у одного пользователя)"""
|
||||
__tablename__ = "accounts"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
account_number = Column(String(20), unique=True, nullable=False, index=True) # XX-XX-XX-XX-XX-XX-XX
|
||||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
is_active = Column(Boolean, default=True) # Активен ли счет
|
||||
|
||||
# Связи
|
||||
owner = relationship("User", back_populates="accounts")
|
||||
participations = relationship("Participation", back_populates="account")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Account(number={self.account_number}, owner_id={self.owner_id})>"
|
||||
|
||||
|
||||
class WinnerVerification(Base):
|
||||
"""Модель верификации победителя"""
|
||||
__tablename__ = "winner_verifications"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
winner_id = Column(Integer, ForeignKey("winners.id"), nullable=False, unique=True)
|
||||
verification_token = Column(String(32), unique=True, nullable=False) # Токен для подтверждения
|
||||
is_verified = Column(Boolean, default=False)
|
||||
verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False) # Срок действия токена
|
||||
|
||||
# Связи
|
||||
winner = relationship("Winner", back_populates="verification")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WinnerVerification(winner_id={self.winner_id}, verified={self.is_verified})>"
|
||||
|
||||
@staticmethod
|
||||
def generate_token():
|
||||
"""Генерирует уникальный токен верификации"""
|
||||
return secrets.token_urlsafe(24)
|
||||
|
||||
|
||||
class Lottery(Base):
|
||||
@@ -60,14 +113,16 @@ class Participation(Base):
|
||||
__tablename__ = "participations"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=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)
|
||||
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True) # Привязка к счету
|
||||
account_number = Column(String(20), nullable=True, index=True) # Дублируем для быстрого доступа
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# Связи
|
||||
user = relationship("User", back_populates="participations")
|
||||
lottery = relationship("Lottery", back_populates="participations")
|
||||
account = relationship("Account", back_populates="participations")
|
||||
|
||||
def __repr__(self):
|
||||
if self.account_number:
|
||||
@@ -81,16 +136,22 @@ class Winner(Base):
|
||||
|
||||
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) # Был ли установлен вручную
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
account_number = Column(String(20), nullable=True, index=True)
|
||||
place = Column(Integer, nullable=False)
|
||||
prize = Column(String(500))
|
||||
is_manual = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# Статус подтверждения выигрыша
|
||||
is_notified = Column(Boolean, default=False) # Отправлено ли уведомление
|
||||
is_claimed = Column(Boolean, default=False) # Подтвердил ли победитель
|
||||
claimed_at = Column(DateTime(timezone=True), nullable=True) # Время подтверждения
|
||||
|
||||
# Связи
|
||||
user = relationship("User")
|
||||
user = relationship("User", back_populates="winners")
|
||||
lottery = relationship("Lottery")
|
||||
verification = relationship("WinnerVerification", back_populates="winner", uselist=False)
|
||||
|
||||
def __repr__(self):
|
||||
if self.account_number:
|
||||
|
||||
287
src/core/registration_services.py
Normal file
287
src/core/registration_services.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""Сервисы для регистрации, управления счетами и верификации"""
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from .models import User, Account, Winner, WinnerVerification
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import secrets
|
||||
|
||||
|
||||
class RegistrationService:
|
||||
"""Сервис для регистрации пользователей"""
|
||||
|
||||
@staticmethod
|
||||
async def register_user(
|
||||
session: AsyncSession,
|
||||
telegram_id: int,
|
||||
club_card_number: str,
|
||||
phone: Optional[str] = None
|
||||
) -> User:
|
||||
"""Зарегистрировать нового пользователя с клубной картой"""
|
||||
# Проверяем, не занята ли клубная карта
|
||||
existing = await session.execute(
|
||||
select(User).where(User.club_card_number == club_card_number)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ValueError(f"Клубная карта {club_card_number} уже зарегистрирована")
|
||||
|
||||
# Находим пользователя по telegram_id
|
||||
result = await session.execute(
|
||||
select(User).where(User.telegram_id == telegram_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise ValueError("Пользователь не найден")
|
||||
|
||||
# Обновляем данные пользователя
|
||||
user.club_card_number = club_card_number
|
||||
user.phone = phone
|
||||
user.is_registered = True
|
||||
user.generate_verification_code()
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_club_card(
|
||||
session: AsyncSession,
|
||||
club_card_number: str
|
||||
) -> Optional[User]:
|
||||
"""Найти пользователя по номеру клубной карты"""
|
||||
result = await session.execute(
|
||||
select(User).where(User.club_card_number == club_card_number)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_verification_code(
|
||||
session: AsyncSession,
|
||||
verification_code: str
|
||||
) -> Optional[User]:
|
||||
"""Найти пользователя по коду верификации"""
|
||||
result = await session.execute(
|
||||
select(User).where(User.verification_code == verification_code)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
class AccountService:
|
||||
"""Сервис для управления счетами пользователей"""
|
||||
|
||||
@staticmethod
|
||||
async def create_account(
|
||||
session: AsyncSession,
|
||||
club_card_number: str,
|
||||
account_number: str
|
||||
) -> Account:
|
||||
"""Создать новый счет для пользователя по номеру клубной карты"""
|
||||
# Находим владельца по клубной карте
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.club_card_number == club_card_number)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise ValueError(f"Пользователь с клубной картой {club_card_number} не найден")
|
||||
|
||||
# Проверяем, не существует ли уже такой счет
|
||||
existing = await session.execute(
|
||||
select(Account).where(Account.account_number == account_number)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ValueError(f"Счет {account_number} уже существует")
|
||||
|
||||
# Создаем счет
|
||||
account = Account(
|
||||
account_number=account_number,
|
||||
owner_id=user.id,
|
||||
is_active=True
|
||||
)
|
||||
session.add(account)
|
||||
await session.commit()
|
||||
await session.refresh(account)
|
||||
|
||||
return account
|
||||
|
||||
@staticmethod
|
||||
async def get_account_owner(
|
||||
session: AsyncSession,
|
||||
account_number: str
|
||||
) -> Optional[User]:
|
||||
"""Найти владельца счета"""
|
||||
result = await session.execute(
|
||||
select(Account).where(
|
||||
and_(
|
||||
Account.account_number == account_number,
|
||||
Account.is_active == True
|
||||
)
|
||||
)
|
||||
)
|
||||
account = result.scalar_one_or_none()
|
||||
|
||||
if not account:
|
||||
return None
|
||||
|
||||
# Получаем владельца
|
||||
owner_result = await session.execute(
|
||||
select(User).where(User.id == account.owner_id)
|
||||
)
|
||||
return owner_result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_accounts(
|
||||
session: AsyncSession,
|
||||
user_id: int
|
||||
) -> List[Account]:
|
||||
"""Получить все счета пользователя"""
|
||||
result = await session.execute(
|
||||
select(Account)
|
||||
.where(Account.owner_id == user_id)
|
||||
.order_by(Account.created_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def deactivate_account(
|
||||
session: AsyncSession,
|
||||
account_number: str
|
||||
) -> bool:
|
||||
"""Деактивировать счет"""
|
||||
result = await session.execute(
|
||||
select(Account).where(Account.account_number == account_number)
|
||||
)
|
||||
account = result.scalar_one_or_none()
|
||||
|
||||
if not account:
|
||||
return False
|
||||
|
||||
account.is_active = False
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
|
||||
class WinnerNotificationService:
|
||||
"""Сервис для уведомления победителей"""
|
||||
|
||||
@staticmethod
|
||||
async def create_verification_token(
|
||||
session: AsyncSession,
|
||||
winner_id: int,
|
||||
expires_hours: int = 24
|
||||
) -> WinnerVerification:
|
||||
"""Создать токен верификации для победителя"""
|
||||
# Проверяем, нет ли уже токена
|
||||
existing = await session.execute(
|
||||
select(WinnerVerification).where(WinnerVerification.winner_id == winner_id)
|
||||
)
|
||||
verification = existing.scalar_one_or_none()
|
||||
|
||||
if verification:
|
||||
# Обновляем существующий токен
|
||||
verification.verification_token = WinnerVerification.generate_token()
|
||||
verification.created_at = datetime.now(timezone.utc)
|
||||
verification.expires_at = datetime.now(timezone.utc) + timedelta(hours=expires_hours)
|
||||
verification.is_verified = False
|
||||
verification.verified_at = None
|
||||
else:
|
||||
# Создаем новый
|
||||
verification = WinnerVerification(
|
||||
winner_id=winner_id,
|
||||
verification_token=WinnerVerification.generate_token(),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=expires_hours)
|
||||
)
|
||||
session.add(verification)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(verification)
|
||||
return verification
|
||||
|
||||
@staticmethod
|
||||
async def verify_winner(
|
||||
session: AsyncSession,
|
||||
verification_code: str,
|
||||
lottery_id: int
|
||||
) -> Optional[Winner]:
|
||||
"""Подтвердить выигрыш по коду верификации пользователя"""
|
||||
# Находим пользователя по коду
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.verification_code == verification_code)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
# Находим выигрыш этого пользователя в данном розыгрыше
|
||||
winner_result = await session.execute(
|
||||
select(Winner).where(
|
||||
and_(
|
||||
Winner.user_id == user.id,
|
||||
Winner.lottery_id == lottery_id,
|
||||
Winner.is_claimed == False
|
||||
)
|
||||
)
|
||||
)
|
||||
winner = winner_result.scalar_one_or_none()
|
||||
|
||||
if not winner:
|
||||
# Проверяем, может быть выигрыш по счету
|
||||
# Получаем все счета пользователя
|
||||
accounts_result = await session.execute(
|
||||
select(Account).where(Account.owner_id == user.id)
|
||||
)
|
||||
accounts = accounts_result.scalars().all()
|
||||
account_numbers = [acc.account_number for acc in accounts]
|
||||
|
||||
# Ищем выигрыш по любому из счетов
|
||||
winner_result = await session.execute(
|
||||
select(Winner).where(
|
||||
and_(
|
||||
Winner.account_number.in_(account_numbers),
|
||||
Winner.lottery_id == lottery_id,
|
||||
Winner.is_claimed == False
|
||||
)
|
||||
)
|
||||
)
|
||||
winner = winner_result.scalar_one_or_none()
|
||||
|
||||
if not winner:
|
||||
return None
|
||||
|
||||
# Помечаем как подтвержденный
|
||||
winner.is_claimed = True
|
||||
|
||||
# Обновляем верификацию если есть
|
||||
verification_result = await session.execute(
|
||||
select(WinnerVerification).where(WinnerVerification.winner_id == winner.id)
|
||||
)
|
||||
verification = verification_result.scalar_one_or_none()
|
||||
|
||||
if verification:
|
||||
verification.is_verified = True
|
||||
verification.verified_at = datetime.now(timezone.utc)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(winner)
|
||||
|
||||
return winner
|
||||
|
||||
@staticmethod
|
||||
async def get_unverified_winners(
|
||||
session: AsyncSession,
|
||||
lottery_id: int
|
||||
) -> List[Winner]:
|
||||
"""Получить список неподтвержденных победителей"""
|
||||
result = await session.execute(
|
||||
select(Winner).where(
|
||||
and_(
|
||||
Winner.lottery_id == lottery_id,
|
||||
Winner.is_claimed == False
|
||||
)
|
||||
)
|
||||
)
|
||||
return result.scalars().all()
|
||||
@@ -1,7 +1,7 @@
|
||||
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 .models import User, Lottery, Participation, Winner, Account
|
||||
from typing import List, Optional, Dict, Any
|
||||
from ..utils.account_utils import validate_account_number, format_account_number
|
||||
import random
|
||||
@@ -251,8 +251,20 @@ class LotteryService:
|
||||
if not lottery or lottery.is_completed:
|
||||
return {}
|
||||
|
||||
# Получаем всех участников
|
||||
participants = [p.user for p in lottery.participations]
|
||||
# Получаем всех участников (включая тех, у кого нет user)
|
||||
participants = []
|
||||
for p in lottery.participations:
|
||||
if p.user:
|
||||
participants.append(p.user)
|
||||
else:
|
||||
# Создаем временный объект для участников без пользователя
|
||||
# Храним только номер счета
|
||||
participants.append(type('obj', (object,), {
|
||||
'id': None,
|
||||
'telegram_id': None,
|
||||
'account_number': p.account_number
|
||||
})())
|
||||
|
||||
if not participants:
|
||||
return {}
|
||||
|
||||
@@ -270,7 +282,7 @@ class LotteryService:
|
||||
# Находим пользователя среди участников
|
||||
manual_winner = None
|
||||
for participant in remaining_participants:
|
||||
if participant.telegram_id == manual_winners[place_str]:
|
||||
if hasattr(participant, 'telegram_id') and participant.telegram_id == manual_winners[place_str]:
|
||||
manual_winner = participant
|
||||
break
|
||||
|
||||
@@ -295,9 +307,11 @@ class LotteryService:
|
||||
|
||||
# Сохраняем победителей в базу данных
|
||||
for place, winner_info in results.items():
|
||||
user_obj = winner_info['user']
|
||||
winner = Winner(
|
||||
lottery_id=lottery_id,
|
||||
user_id=winner_info['user'].id,
|
||||
user_id=user_obj.id if hasattr(user_obj, 'id') and user_obj.id else None,
|
||||
account_number=user_obj.account_number if hasattr(user_obj, 'account_number') else None,
|
||||
place=place,
|
||||
prize=winner_info['prize'],
|
||||
is_manual=winner_info['is_manual']
|
||||
@@ -306,15 +320,17 @@ class LotteryService:
|
||||
|
||||
# Обновляем статус розыгрыша
|
||||
lottery.is_completed = True
|
||||
lottery.draw_results = {
|
||||
str(place): {
|
||||
'user_id': info['user'].id,
|
||||
'telegram_id': info['user'].telegram_id,
|
||||
'username': info['user'].username,
|
||||
lottery.draw_results = {}
|
||||
for place, info in results.items():
|
||||
user_obj = info['user']
|
||||
lottery.draw_results[str(place)] = {
|
||||
'user_id': user_obj.id if hasattr(user_obj, 'id') and user_obj.id else None,
|
||||
'telegram_id': user_obj.telegram_id if hasattr(user_obj, 'telegram_id') else None,
|
||||
'username': user_obj.username if hasattr(user_obj, 'username') else None,
|
||||
'account_number': user_obj.account_number if hasattr(user_obj, 'account_number') else None,
|
||||
'prize': info['prize'],
|
||||
'is_manual': info['is_manual']
|
||||
} for place, info in results.items()
|
||||
}
|
||||
}
|
||||
|
||||
await session.commit()
|
||||
return results
|
||||
|
||||
585
src/handlers/admin_account_handlers.py
Normal file
585
src/handlers/admin_account_handlers.py
Normal file
@@ -0,0 +1,585 @@
|
||||
"""Админские обработчики для управления счетами и верификации"""
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
from src.core.database import async_session_maker
|
||||
from src.core.registration_services import AccountService, WinnerNotificationService, RegistrationService
|
||||
from src.core.services import UserService, LotteryService, ParticipationService
|
||||
from src.core.models import User, Winner, Account, Participation
|
||||
from src.core.config import ADMIN_IDS
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class AddAccountStates(StatesGroup):
|
||||
waiting_for_data = State()
|
||||
choosing_lottery = State()
|
||||
|
||||
|
||||
def is_admin(user_id: int) -> bool:
|
||||
"""Проверка прав администратора"""
|
||||
return user_id in ADMIN_IDS
|
||||
|
||||
|
||||
@router.message(Command("add_account"))
|
||||
async def add_account_command(message: Message, state: FSMContext):
|
||||
"""
|
||||
Добавить счет пользователю по клубной карте
|
||||
Формат: /add_account <club_card> <account_number>
|
||||
Или: /add_account (затем вводить данные построчно)
|
||||
"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
parts = message.text.split(maxsplit=2)
|
||||
|
||||
# Если данные указаны в команде
|
||||
if len(parts) == 3:
|
||||
club_card = parts[1]
|
||||
account_number = parts[2]
|
||||
await process_single_account(message, club_card, account_number, state)
|
||||
else:
|
||||
# Запрашиваем данные
|
||||
await state.set_state(AddAccountStates.waiting_for_data)
|
||||
await message.answer(
|
||||
"💳 **Добавление счетов**\n\n"
|
||||
"Отправьте данные в формате:\n"
|
||||
"`клубная_карта номер_счета`\n\n"
|
||||
"**Для одного счета:**\n"
|
||||
"`2223 11-22-33-44-55-66-77`\n\n"
|
||||
"**Для нескольких счетов (каждый с новой строки):**\n"
|
||||
"`2223 11-22-33-44-55-66-77`\n"
|
||||
"`2223 88-99-00-11-22-33-44`\n"
|
||||
"`3334 12-34-56-78-90-12-34`\n\n"
|
||||
"❌ Отправьте /cancel для отмены",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
async def process_single_account(message: Message, club_card: str, account_number: str, state: FSMContext):
|
||||
"""Обработка одного счета"""
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
# Создаем счет
|
||||
account = await AccountService.create_account(
|
||||
session,
|
||||
club_card_number=club_card,
|
||||
account_number=account_number
|
||||
)
|
||||
|
||||
# Получаем владельца
|
||||
owner = await AccountService.get_account_owner(session, account_number)
|
||||
|
||||
# Сохраняем данные счета в state для добавления в розыгрыш
|
||||
await state.update_data(
|
||||
accounts=[{
|
||||
'club_card': club_card,
|
||||
'account_number': account_number,
|
||||
'account_id': account.id
|
||||
}]
|
||||
)
|
||||
|
||||
text = f"✅ Счет успешно добавлен!\n\n"
|
||||
text += f"🎫 Клубная карта: {club_card}\n"
|
||||
text += f"💳 Счет: {account_number}\n"
|
||||
|
||||
if owner:
|
||||
text += f"👤 Владелец: {owner.first_name}\n\n"
|
||||
|
||||
# Отправляем уведомление владельцу
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
owner.telegram_id,
|
||||
f"✅ К вашему профилю добавлен счет:\n\n"
|
||||
f"💳 {account_number}\n\n"
|
||||
f"Теперь вы можете участвовать в розыгрышах с этим счетом!"
|
||||
)
|
||||
text += "📨 Владельцу отправлено уведомление\n\n"
|
||||
except Exception as e:
|
||||
text += f"⚠️ Не удалось отправить уведомление: {str(e)}\n\n"
|
||||
|
||||
# Предлагаем добавить в розыгрыш
|
||||
await show_lottery_selection(message, text, state)
|
||||
|
||||
except ValueError as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Произошла ошибка: {str(e)}")
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router.message(AddAccountStates.waiting_for_data)
|
||||
async def process_accounts_data(message: Message, state: FSMContext):
|
||||
"""Обработка данных счетов (один или несколько)"""
|
||||
if message.text.strip().lower() == '/cancel':
|
||||
await state.clear()
|
||||
await message.answer("❌ Операция отменена")
|
||||
return
|
||||
|
||||
lines = message.text.strip().split('\n')
|
||||
accounts_data = []
|
||||
errors = []
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
parts = line.strip().split()
|
||||
if len(parts) != 2:
|
||||
errors.append(f"Строка {i}: неверный формат (ожидается: клубная_карта номер_счета)")
|
||||
continue
|
||||
|
||||
club_card, account_number = parts
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
account = await AccountService.create_account(
|
||||
session,
|
||||
club_card_number=club_card,
|
||||
account_number=account_number
|
||||
)
|
||||
|
||||
owner = await AccountService.get_account_owner(session, account_number)
|
||||
|
||||
accounts_data.append({
|
||||
'club_card': club_card,
|
||||
'account_number': account_number,
|
||||
'account_id': account.id,
|
||||
'owner': owner
|
||||
})
|
||||
|
||||
# Отправляем уведомление владельцу
|
||||
if owner:
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
owner.telegram_id,
|
||||
f"✅ К вашему профилю добавлен счет:\n\n"
|
||||
f"💳 {account_number}\n\n"
|
||||
f"Теперь вы можете участвовать в розыгрышах!"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
except ValueError as e:
|
||||
errors.append(f"Строка {i} ({club_card} {account_number}): {str(e)}")
|
||||
except Exception as e:
|
||||
errors.append(f"Строка {i}: {str(e)}")
|
||||
|
||||
# Формируем отчет
|
||||
text = f"📊 **Результаты добавления счетов**\n\n"
|
||||
|
||||
if accounts_data:
|
||||
text += f"✅ **Успешно добавлено: {len(accounts_data)}**\n\n"
|
||||
for acc in accounts_data:
|
||||
text += f"• {acc['club_card']} → {acc['account_number']}\n"
|
||||
if acc['owner']:
|
||||
text += f" 👤 {acc['owner'].first_name}\n"
|
||||
text += "\n"
|
||||
|
||||
if errors:
|
||||
text += f"❌ **Ошибки: {len(errors)}**\n\n"
|
||||
for error in errors[:5]: # Показываем максимум 5 ошибок
|
||||
text += f"• {error}\n"
|
||||
if len(errors) > 5:
|
||||
text += f"\n... и еще {len(errors) - 5} ошибок\n"
|
||||
|
||||
if not accounts_data:
|
||||
await message.answer(text)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
# Сохраняем данные и предлагаем добавить в розыгрыш
|
||||
await state.update_data(accounts=accounts_data)
|
||||
await show_lottery_selection(message, text, state)
|
||||
|
||||
|
||||
async def show_lottery_selection(message: Message, prev_text: str, state: FSMContext):
|
||||
"""Показать выбор розыгрыша для добавления счетов"""
|
||||
async with async_session_maker() as session:
|
||||
lotteries = await LotteryService.get_active_lotteries(session)
|
||||
|
||||
if not lotteries:
|
||||
await message.answer(
|
||||
prev_text + "ℹ️ Нет активных розыгрышей для добавления счетов"
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await state.set_state(AddAccountStates.choosing_lottery)
|
||||
|
||||
buttons = []
|
||||
for lottery in lotteries[:10]: # Максимум 10 розыгрышей
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"🎯 {lottery.title}",
|
||||
callback_data=f"add_to_lottery_{lottery.id}"
|
||||
)
|
||||
])
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="❌ Пропустить", callback_data="skip_lottery_add")
|
||||
])
|
||||
|
||||
await message.answer(
|
||||
prev_text + "➕ **Добавить счета в розыгрыш?**\n\n"
|
||||
"Выберите розыгрыш из списка:",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("add_to_lottery_"))
|
||||
async def add_accounts_to_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
"""Добавить счета в выбранный розыгрыш"""
|
||||
lottery_id = int(callback.data.split("_")[-1])
|
||||
|
||||
data = await state.get_data()
|
||||
accounts = data.get('accounts', [])
|
||||
|
||||
if not accounts:
|
||||
await callback.answer("❌ Нет данных о счетах", show_alert=True)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
success_count = 0
|
||||
errors = []
|
||||
|
||||
async with async_session_maker() as session:
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
|
||||
if not lottery:
|
||||
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
for acc in accounts:
|
||||
try:
|
||||
# Добавляем участие через account_id
|
||||
# Проверяем, не участвует ли уже
|
||||
existing = await session.execute(
|
||||
select(Participation).where(
|
||||
and_(
|
||||
Participation.lottery_id == lottery_id,
|
||||
Participation.account_id == acc['account_id']
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if existing.scalar_one_or_none():
|
||||
errors.append(f"{acc['account_number']}: уже участвует")
|
||||
continue
|
||||
|
||||
# Создаем участие
|
||||
participation = Participation(
|
||||
lottery_id=lottery_id,
|
||||
account_id=acc['account_id'],
|
||||
account_number=acc['account_number']
|
||||
)
|
||||
session.add(participation)
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"{acc['account_number']}: {str(e)}")
|
||||
|
||||
await session.commit()
|
||||
|
||||
text = f"📊 **Добавление в розыгрыш '{lottery.title}'**\n\n"
|
||||
|
||||
if success_count:
|
||||
text += f"✅ Добавлено счетов: {success_count}\n\n"
|
||||
|
||||
if errors:
|
||||
text += f"⚠️ Ошибки: {len(errors)}\n"
|
||||
for error in errors[:3]:
|
||||
text += f"• {error}\n"
|
||||
|
||||
await callback.message.edit_text(text)
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "skip_lottery_add")
|
||||
async def skip_lottery_add(callback: CallbackQuery, state: FSMContext):
|
||||
"""Пропустить добавление в розыгрыш"""
|
||||
await callback.message.edit_text("✅ Счета добавлены без участия в розыгрышах")
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router.message(Command("remove_account"))
|
||||
async def remove_account_command(message: Message):
|
||||
"""
|
||||
Деактивировать счет
|
||||
Формат: /remove_account <account_number>
|
||||
"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
parts = message.text.split()
|
||||
if len(parts) != 2:
|
||||
await message.answer(
|
||||
"❌ Неверный формат команды\n\n"
|
||||
"Используйте: /remove_account <account_number>"
|
||||
)
|
||||
return
|
||||
|
||||
account_number = parts[1]
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
success = await AccountService.deactivate_account(session, account_number)
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Счет {account_number} деактивирован")
|
||||
else:
|
||||
await message.answer(f"❌ Счет {account_number} не найден")
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
|
||||
@router.message(Command("verify_winner"))
|
||||
async def verify_winner_command(message: Message):
|
||||
"""
|
||||
Подтвердить выигрыш по коду верификации
|
||||
Формат: /verify_winner <verification_code> <lottery_id>
|
||||
Пример: /verify_winner AB12CD34 1
|
||||
"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
parts = message.text.split()
|
||||
if len(parts) != 3:
|
||||
await message.answer(
|
||||
"❌ Неверный формат команды\n\n"
|
||||
"Используйте:\n"
|
||||
"/verify_winner <verification_code> <lottery_id>\n\n"
|
||||
"Пример:\n"
|
||||
"/verify_winner AB12CD34 1"
|
||||
)
|
||||
return
|
||||
|
||||
verification_code = parts[1].upper()
|
||||
|
||||
try:
|
||||
lottery_id = int(parts[2])
|
||||
except ValueError:
|
||||
await message.answer("❌ lottery_id должен быть числом")
|
||||
return
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
# Проверяем существование розыгрыша
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
if not lottery:
|
||||
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
|
||||
return
|
||||
|
||||
# Подтверждаем выигрыш
|
||||
winner = await WinnerNotificationService.verify_winner(
|
||||
session,
|
||||
verification_code=verification_code,
|
||||
lottery_id=lottery_id
|
||||
)
|
||||
|
||||
if not winner:
|
||||
await message.answer(
|
||||
f"❌ Выигрыш не найден\n\n"
|
||||
f"Возможные причины:\n"
|
||||
f"• Неверный код верификации\n"
|
||||
f"• Пользователь не является победителем в розыгрыше #{lottery_id}\n"
|
||||
f"• Выигрыш уже был подтвержден"
|
||||
)
|
||||
return
|
||||
|
||||
# Получаем пользователя
|
||||
user = await RegistrationService.get_user_by_verification_code(session, verification_code)
|
||||
|
||||
text = "✅ Выигрыш подтвержден!\n\n"
|
||||
text += f"🎯 Розыгрыш: {lottery.title}\n"
|
||||
text += f"🏆 Место: {winner.place}\n"
|
||||
text += f"🎁 Приз: {winner.prize}\n\n"
|
||||
|
||||
if user:
|
||||
text += f"👤 Победитель: {user.first_name}\n"
|
||||
text += f"🎫 Клубная карта: {user.club_card_number}\n"
|
||||
if user.phone:
|
||||
text += f"📱 Телефон: {user.phone}\n"
|
||||
|
||||
# Отправляем уведомление победителю
|
||||
try:
|
||||
bot = message.bot
|
||||
await bot.send_message(
|
||||
user.telegram_id,
|
||||
f"✅ Ваш выигрыш подтвержден!\n\n"
|
||||
f"🎯 Розыгрыш: {lottery.title}\n"
|
||||
f"🏆 Место: {winner.place}\n"
|
||||
f"🎁 Приз: {winner.prize}\n\n"
|
||||
f"Администратор свяжется с вами для получения приза."
|
||||
)
|
||||
text += "\n📨 Победителю отправлено уведомление"
|
||||
except Exception as e:
|
||||
text += f"\n⚠️ Не удалось отправить уведомление: {str(e)}"
|
||||
|
||||
if winner.account_number:
|
||||
text += f"💳 Счет: {winner.account_number}\n"
|
||||
|
||||
await message.answer(text)
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
|
||||
@router.message(Command("winner_status"))
|
||||
async def winner_status_command(message: Message):
|
||||
"""
|
||||
Показать статус всех победителей розыгрыша
|
||||
Формат: /winner_status <lottery_id>
|
||||
"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
parts = message.text.split()
|
||||
if len(parts) != 2:
|
||||
await message.answer(
|
||||
"❌ Неверный формат команды\n\n"
|
||||
"Используйте: /winner_status <lottery_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
lottery_id = int(parts[1])
|
||||
except ValueError:
|
||||
await message.answer("❌ lottery_id должен быть числом")
|
||||
return
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
if not lottery:
|
||||
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
|
||||
return
|
||||
|
||||
winners = await LotteryService.get_winners(session, lottery_id)
|
||||
|
||||
if not winners:
|
||||
await message.answer(f"В розыгрыше '{lottery.title}' пока нет победителей")
|
||||
return
|
||||
|
||||
text = f"🏆 Победители розыгрыша '{lottery.title}':\n\n"
|
||||
|
||||
for winner in winners:
|
||||
status_icon = "✅" if winner.is_claimed else "⏳"
|
||||
notified_icon = "📨" if winner.is_notified else "📭"
|
||||
|
||||
text += f"{status_icon} {winner.place} место - {winner.prize}\n"
|
||||
|
||||
# Получаем информацию о победителе
|
||||
async with async_session_maker() as session:
|
||||
if winner.user_id:
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.id == winner.user_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if user:
|
||||
text += f" 👤 {user.first_name}"
|
||||
if user.club_card_number:
|
||||
text += f" (КК: {user.club_card_number})"
|
||||
text += "\n"
|
||||
|
||||
if winner.account_number:
|
||||
text += f" 💳 {winner.account_number}\n"
|
||||
|
||||
# Статус подтверждения
|
||||
if winner.is_claimed:
|
||||
text += f" ✅ Подтвержден\n"
|
||||
else:
|
||||
text += f" ⏳ Ожидает подтверждения\n"
|
||||
|
||||
text += "\n"
|
||||
|
||||
await message.answer(text)
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
|
||||
@router.message(Command("user_info"))
|
||||
async def user_info_command(message: Message):
|
||||
"""
|
||||
Показать информацию о пользователе
|
||||
Формат: /user_info <club_card>
|
||||
"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
parts = message.text.split()
|
||||
if len(parts) != 2:
|
||||
await message.answer(
|
||||
"❌ Неверный формат команды\n\n"
|
||||
"Используйте: /user_info <club_card>"
|
||||
)
|
||||
return
|
||||
|
||||
club_card = parts[1]
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
user = await RegistrationService.get_user_by_club_card(session, club_card)
|
||||
|
||||
if not user:
|
||||
await message.answer(f"❌ Пользователь с клубной картой {club_card} не найден")
|
||||
return
|
||||
|
||||
# Получаем счета
|
||||
accounts = await AccountService.get_user_accounts(session, user.id)
|
||||
|
||||
# Получаем выигрыши
|
||||
winners_result = await session.execute(
|
||||
select(Winner).where(Winner.user_id == user.id)
|
||||
)
|
||||
winners = winners_result.scalars().all()
|
||||
|
||||
text = f"👤 Информация о пользователе\n\n"
|
||||
text += f"🎫 Клубная карта: {user.club_card_number}\n"
|
||||
text += f"👤 Имя: {user.first_name}"
|
||||
if user.last_name:
|
||||
text += f" {user.last_name}"
|
||||
text += "\n"
|
||||
|
||||
if user.username:
|
||||
text += f"📱 Telegram: @{user.username}\n"
|
||||
|
||||
if user.phone:
|
||||
text += f"📞 Телефон: {user.phone}\n"
|
||||
|
||||
text += f"🔑 Код верификации: {user.verification_code}\n"
|
||||
text += f"📅 Зарегистрирован: {user.created_at.strftime('%d.%m.%Y')}\n\n"
|
||||
|
||||
# Счета
|
||||
text += f"💳 Счета ({len(accounts)}):\n"
|
||||
if accounts:
|
||||
for acc in accounts:
|
||||
status = "✅" if acc.is_active else "❌"
|
||||
text += f" {status} {acc.account_number}\n"
|
||||
else:
|
||||
text += " Нет счетов\n"
|
||||
|
||||
# Выигрыши
|
||||
text += f"\n🏆 Выигрыши ({len(winners)}):\n"
|
||||
if winners:
|
||||
for w in winners:
|
||||
status = "✅" if w.is_claimed else "⏳"
|
||||
text += f" {status} {w.place} место - {w.prize}\n"
|
||||
else:
|
||||
text += " Нет выигрышей\n"
|
||||
|
||||
await message.answer(text)
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
@@ -471,9 +471,13 @@ async def show_lottery_participants(callback: CallbackQuery):
|
||||
|
||||
for i, participation in enumerate(lottery.participations[:20], 1): # Показываем первых 20
|
||||
user = participation.user
|
||||
username = f"@{user.username}" if user.username else "Нет username"
|
||||
text += f"{i}. {user.first_name} {user.last_name or ''}\n"
|
||||
text += f" {username} | ID: {user.telegram_id}\n"
|
||||
if user:
|
||||
username = f"@{user.username}" if user.username else "Нет username"
|
||||
text += f"{i}. {user.first_name} {user.last_name or ''}\n"
|
||||
text += f" {username} | ID: {user.telegram_id}\n"
|
||||
else:
|
||||
# Если пользователя нет, показываем номер счета
|
||||
text += f"{i}. Счет: {participation.account_number or 'Не указан'}\n"
|
||||
text += f" Участвует с: {participation.created_at.strftime('%d.%m %H:%M')}\n\n"
|
||||
|
||||
if len(lottery.participations) > 20:
|
||||
@@ -1683,11 +1687,13 @@ async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
parts = callback.data.split("_")
|
||||
if len(parts) == 3: # admin_edit_123
|
||||
lottery_id = int(parts[2])
|
||||
# Подменяем callback_data для обработки существующим хэндлером
|
||||
callback.data = f"admin_edit_lottery_select_{lottery_id}"
|
||||
# Напрямую вызываем обработчик вместо подмены callback_data
|
||||
await state.update_data(edit_lottery_id=lottery_id)
|
||||
await choose_edit_field(callback, state)
|
||||
else:
|
||||
# Если формат другой, то это уже правильный callback
|
||||
lottery_id = int(callback.data.split("_")[-1])
|
||||
await state.update_data(edit_lottery_id=lottery_id)
|
||||
await choose_edit_field(callback, state)
|
||||
|
||||
|
||||
@@ -2027,8 +2033,8 @@ async def handle_set_winner_from_lottery(callback: CallbackQuery, state: FSMCont
|
||||
|
||||
lottery_id = int(callback.data.split("_")[-1])
|
||||
|
||||
# Перенаправляем на стандартный обработчик
|
||||
callback.data = f"admin_choose_winner_lottery_{lottery_id}"
|
||||
# Напрямую вызываем обработчик вместо подмены callback_data
|
||||
await state.update_data(winner_lottery_id=lottery_id)
|
||||
await choose_winner_place(callback, state)
|
||||
|
||||
|
||||
@@ -2610,11 +2616,12 @@ async def conduct_lottery_draw(callback: CallbackQuery):
|
||||
await callback.answer("Нет участников для розыгрыша", show_alert=True)
|
||||
return
|
||||
|
||||
# Проводим розыгрыш
|
||||
from ..display.conduct_draw import conduct_draw
|
||||
winners = await conduct_draw(lottery_id)
|
||||
# Проводим розыгрыш через сервис
|
||||
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
|
||||
|
||||
if winners:
|
||||
if winners_dict:
|
||||
# Получаем победителей из базы
|
||||
winners = await LotteryService.get_winners(session, lottery_id)
|
||||
text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n"
|
||||
text += "🏆 Победители:\n"
|
||||
for winner in winners:
|
||||
|
||||
314
src/handlers/redraw_handlers.py
Normal file
314
src/handlers/redraw_handlers.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""Команды для повторного розыгрыша неподтвержденных выигрышей"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.filters import Command
|
||||
from sqlalchemy import select, and_
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import random
|
||||
|
||||
from src.core.database import async_session_maker
|
||||
from src.core.registration_services import AccountService, WinnerNotificationService
|
||||
from src.core.services import LotteryService
|
||||
from src.core.models import User, Winner
|
||||
from src.core.config import ADMIN_IDS
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
def is_admin(user_id: int) -> bool:
|
||||
"""Проверка прав администратора"""
|
||||
return user_id in ADMIN_IDS
|
||||
|
||||
|
||||
@router.message(Command("check_unclaimed"))
|
||||
async def check_unclaimed_winners(message: Message):
|
||||
"""
|
||||
Проверить неподтвержденные выигрыши (более 24 часов)
|
||||
Формат: /check_unclaimed <lottery_id>
|
||||
"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
parts = message.text.split()
|
||||
if len(parts) != 2:
|
||||
await message.answer(
|
||||
"❌ Неверный формат команды\n\n"
|
||||
"Используйте: /check_unclaimed <lottery_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
lottery_id = int(parts[1])
|
||||
except ValueError:
|
||||
await message.answer("❌ lottery_id должен быть числом")
|
||||
return
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
from sqlalchemy.orm import selectinload
|
||||
from src.core.models import Lottery
|
||||
|
||||
# Загружаем розыгрыш с участниками
|
||||
lottery_result = await session.execute(
|
||||
select(Lottery)
|
||||
.options(selectinload(Lottery.participations))
|
||||
.where(Lottery.id == lottery_id)
|
||||
)
|
||||
lottery = lottery_result.scalar_one_or_none()
|
||||
|
||||
if not lottery:
|
||||
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
|
||||
return
|
||||
|
||||
winners = await LotteryService.get_winners(session, lottery_id)
|
||||
|
||||
if not winners:
|
||||
await message.answer(f"В розыгрыше '{lottery.title}' нет победителей")
|
||||
return
|
||||
|
||||
# Находим неподтвержденные выигрыши старше 24 часов
|
||||
now = datetime.now(timezone.utc)
|
||||
unclaimed = []
|
||||
|
||||
for winner in winners:
|
||||
if not winner.is_claimed and winner.is_notified:
|
||||
# Проверяем, прошло ли 24 часа
|
||||
time_passed = now - winner.created_at
|
||||
if time_passed.total_seconds() > 24 * 3600: # 24 часа
|
||||
unclaimed.append({
|
||||
'winner': winner,
|
||||
'hours_passed': int(time_passed.total_seconds() / 3600)
|
||||
})
|
||||
|
||||
if not unclaimed:
|
||||
await message.answer(
|
||||
f"✅ Все победители розыгрыша '{lottery.title}' подтвердили выигрыш\n"
|
||||
f"или срок подтверждения еще не истек."
|
||||
)
|
||||
return
|
||||
|
||||
text = f"⚠️ **Неподтвержденные выигрыши в розыгрыше '{lottery.title}':**\n\n"
|
||||
|
||||
for item in unclaimed:
|
||||
winner = item['winner']
|
||||
hours = item['hours_passed']
|
||||
|
||||
text += f"🏆 {winner.place} место - {winner.prize}\n"
|
||||
|
||||
# Получаем информацию о победителе
|
||||
async with async_session_maker() as session:
|
||||
if winner.user_id:
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.id == winner.user_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if user:
|
||||
text += f" 👤 {user.first_name}"
|
||||
if user.club_card_number:
|
||||
text += f" (КК: {user.club_card_number})"
|
||||
text += "\n"
|
||||
|
||||
if winner.account_number:
|
||||
text += f" 💳 {winner.account_number}\n"
|
||||
|
||||
text += f" ⏰ Прошло: {hours} часов\n\n"
|
||||
|
||||
text += f"\n📊 Всего неподтвержденных: {len(unclaimed)}\n\n"
|
||||
text += f"Используйте /redraw {lottery_id} для повторного розыгрыша"
|
||||
|
||||
await message.answer(text, parse_mode="Markdown")
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
|
||||
@router.message(Command("redraw"))
|
||||
async def redraw_lottery(message: Message):
|
||||
"""
|
||||
Переиграть розыгрыш для неподтвержденных выигрышей
|
||||
Формат: /redraw <lottery_id>
|
||||
"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
parts = message.text.split()
|
||||
if len(parts) != 2:
|
||||
await message.answer(
|
||||
"❌ Неверный формат команды\n\n"
|
||||
"Используйте: /redraw <lottery_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
lottery_id = int(parts[1])
|
||||
except ValueError:
|
||||
await message.answer("❌ lottery_id должен быть числом")
|
||||
return
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
from sqlalchemy.orm import selectinload
|
||||
from src.core.models import Lottery
|
||||
|
||||
# Загружаем розыгрыш с участниками
|
||||
lottery_result = await session.execute(
|
||||
select(Lottery)
|
||||
.options(selectinload(Lottery.participations))
|
||||
.where(Lottery.id == lottery_id)
|
||||
)
|
||||
lottery = lottery_result.scalar_one_or_none()
|
||||
|
||||
if not lottery:
|
||||
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
|
||||
return
|
||||
|
||||
winners = await LotteryService.get_winners(session, lottery_id)
|
||||
|
||||
# Находим неподтвержденные выигрыши старше 24 часов
|
||||
now = datetime.now(timezone.utc)
|
||||
unclaimed_winners = []
|
||||
|
||||
for winner in winners:
|
||||
if not winner.is_claimed and winner.is_notified:
|
||||
time_passed = now - winner.created_at
|
||||
if time_passed.total_seconds() > 24 * 3600:
|
||||
unclaimed_winners.append(winner)
|
||||
|
||||
if not unclaimed_winners:
|
||||
await message.answer(
|
||||
"✅ Нет неподтвержденных выигрышей старше 24 часов.\n"
|
||||
"Повторный розыгрыш не требуется."
|
||||
)
|
||||
return
|
||||
|
||||
# Получаем всех участников, исключая текущих победителей
|
||||
all_participants = []
|
||||
current_winner_accounts = set()
|
||||
|
||||
for winner in winners:
|
||||
if winner.account_number:
|
||||
current_winner_accounts.add(winner.account_number)
|
||||
|
||||
for p in lottery.participations:
|
||||
if p.account_number and p.account_number not in current_winner_accounts:
|
||||
all_participants.append(p)
|
||||
|
||||
if not all_participants:
|
||||
await message.answer(
|
||||
"❌ Нет доступных участников для повторного розыгрыша.\n"
|
||||
"Все участники уже являются победителями."
|
||||
)
|
||||
return
|
||||
|
||||
# Переигрываем каждое неподтвержденное место
|
||||
redraw_results = []
|
||||
|
||||
for old_winner in unclaimed_winners:
|
||||
if not all_participants:
|
||||
break
|
||||
|
||||
# Выбираем нового победителя
|
||||
new_participant = random.choice(all_participants)
|
||||
all_participants.remove(new_participant)
|
||||
|
||||
# Удаляем старого победителя
|
||||
await session.delete(old_winner)
|
||||
|
||||
# Создаем нового победителя
|
||||
new_winner = Winner(
|
||||
lottery_id=lottery_id,
|
||||
user_id=None,
|
||||
account_number=new_participant.account_number,
|
||||
account_id=new_participant.account_id,
|
||||
place=old_winner.place,
|
||||
prize=old_winner.prize,
|
||||
is_manual=False,
|
||||
is_notified=False,
|
||||
is_claimed=False
|
||||
)
|
||||
session.add(new_winner)
|
||||
|
||||
redraw_results.append({
|
||||
'place': old_winner.place,
|
||||
'prize': old_winner.prize,
|
||||
'old_account': old_winner.account_number,
|
||||
'new_account': new_participant.account_number
|
||||
})
|
||||
|
||||
await session.commit()
|
||||
|
||||
# Отправляем уведомления новым победителям
|
||||
for result in redraw_results:
|
||||
# Находим нового победителя
|
||||
new_winner_result = await session.execute(
|
||||
select(Winner).where(
|
||||
and_(
|
||||
Winner.lottery_id == lottery_id,
|
||||
Winner.place == result['place'],
|
||||
Winner.account_number == result['new_account']
|
||||
)
|
||||
)
|
||||
)
|
||||
new_winner = new_winner_result.scalar_one_or_none()
|
||||
|
||||
if new_winner:
|
||||
# Отправляем уведомление новому победителю
|
||||
owner = await AccountService.get_account_owner(session, new_winner.account_number)
|
||||
|
||||
if owner and owner.telegram_id:
|
||||
# Создаем токен верификации
|
||||
await WinnerNotificationService.create_verification_token(
|
||||
session,
|
||||
new_winner.id
|
||||
)
|
||||
|
||||
# Формируем сообщение
|
||||
notification_message = (
|
||||
f"🎉 Поздравляем! Ваш счет выиграл!\n\n"
|
||||
f"🎯 Розыгрыш: {lottery.title}\n"
|
||||
f"🏆 Место: {new_winner.place}\n"
|
||||
f"🎁 Приз: {new_winner.prize}\n"
|
||||
f"💳 Счет: {new_winner.account_number}\n\n"
|
||||
f"⏰ **У вас есть 24 часа для подтверждения!**\n\n"
|
||||
f"Нажмите кнопку ниже, чтобы подтвердить получение приза."
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text="✅ Подтвердить получение приза",
|
||||
callback_data=f"confirm_win_{new_winner.id}"
|
||||
)]
|
||||
])
|
||||
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
owner.telegram_id,
|
||||
notification_message,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
new_winner.is_notified = True
|
||||
await session.commit()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Формируем отчет для админа
|
||||
text = f"🔄 **Повторный розыгрыш завершен!**\n\n"
|
||||
text += f"🎯 Розыгрыш: {lottery.title}\n"
|
||||
text += f"📊 Переиграно мест: {len(redraw_results)}\n\n"
|
||||
|
||||
for result in redraw_results:
|
||||
text += f"🏆 {result['place']} место - {result['prize']}\n"
|
||||
text += f" ❌ Было: {result['old_account']}\n"
|
||||
text += f" ✅ Стало: {result['new_account']}\n\n"
|
||||
|
||||
text += "📨 Новым победителям отправлены уведомления"
|
||||
|
||||
await message.answer(text, parse_mode="Markdown")
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
150
src/handlers/registration_handlers.py
Normal file
150
src/handlers/registration_handlers.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Обработчики для регистрации пользователей"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
from src.core.database import async_session_maker
|
||||
from src.core.registration_services import RegistrationService, AccountService
|
||||
from src.core.services import UserService
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class RegistrationStates(StatesGroup):
|
||||
"""Состояния для процесса регистрации"""
|
||||
waiting_for_club_card = State()
|
||||
waiting_for_phone = State()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "start_registration")
|
||||
async def start_registration(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать процесс регистрации"""
|
||||
text = (
|
||||
"📝 Регистрация в системе\n\n"
|
||||
"Для участия в розыгрышах необходимо зарегистрироваться.\n\n"
|
||||
"Введите номер вашей клубной карты:"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
|
||||
])
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_club_card)
|
||||
|
||||
|
||||
@router.message(StateFilter(RegistrationStates.waiting_for_club_card))
|
||||
async def process_club_card(message: Message, state: FSMContext):
|
||||
"""Обработка номера клубной карты"""
|
||||
club_card_number = message.text.strip()
|
||||
|
||||
# Проверяем, не занята ли карта
|
||||
async with async_session_maker() as session:
|
||||
existing_user = await RegistrationService.get_user_by_club_card(session, club_card_number)
|
||||
|
||||
if existing_user:
|
||||
await message.answer(
|
||||
f"❌ Клубная карта {club_card_number} уже зарегистрирована.\n\n"
|
||||
"Если это ваша карта, обратитесь к администратору."
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await state.update_data(club_card_number=club_card_number)
|
||||
|
||||
await message.answer(
|
||||
"📱 Теперь введите ваш номер телефона\n"
|
||||
"(или отправьте '-' чтобы пропустить):"
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_phone)
|
||||
|
||||
|
||||
@router.message(StateFilter(RegistrationStates.waiting_for_phone))
|
||||
async def process_phone(message: Message, state: FSMContext):
|
||||
"""Обработка номера телефона"""
|
||||
phone = None if message.text.strip() == "-" else message.text.strip()
|
||||
|
||||
data = await state.get_data()
|
||||
club_card_number = data['club_card_number']
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
user = await RegistrationService.register_user(
|
||||
session,
|
||||
telegram_id=message.from_user.id,
|
||||
club_card_number=club_card_number,
|
||||
phone=phone
|
||||
)
|
||||
|
||||
text = (
|
||||
"✅ Регистрация завершена!\n\n"
|
||||
f"🎫 Клубная карта: {user.club_card_number}\n"
|
||||
f"🔑 Ваш код верификации: **{user.verification_code}**\n\n"
|
||||
"⚠️ Сохраните этот код! Он понадобится для подтверждения выигрыша.\n\n"
|
||||
"Теперь вы можете участвовать в розыгрышах!"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="Markdown")
|
||||
await state.clear()
|
||||
|
||||
except ValueError as e:
|
||||
await message.answer(f"❌ Ошибка регистрации: {str(e)}")
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Произошла ошибка: {str(e)}")
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router.message(Command("my_code"))
|
||||
async def show_verification_code(message: Message):
|
||||
"""Показать код верификации пользователя"""
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||
|
||||
if not user or not user.is_registered:
|
||||
await message.answer(
|
||||
"❌ Вы не зарегистрированы в системе.\n\n"
|
||||
"Для регистрации отправьте /start и выберите 'Регистрация'"
|
||||
)
|
||||
return
|
||||
|
||||
text = (
|
||||
"🔑 Ваш код верификации:\n\n"
|
||||
f"**{user.verification_code}**\n\n"
|
||||
"Этот код используется для подтверждения выигрыша.\n"
|
||||
"Сообщите его администратору при получении приза."
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="Markdown")
|
||||
|
||||
|
||||
@router.message(Command("my_accounts"))
|
||||
async def show_user_accounts(message: Message):
|
||||
"""Показать счета пользователя"""
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||
|
||||
if not user or not user.is_registered:
|
||||
await message.answer("❌ Вы не зарегистрированы в системе")
|
||||
return
|
||||
|
||||
accounts = await AccountService.get_user_accounts(session, user.id)
|
||||
|
||||
if not accounts:
|
||||
await message.answer(
|
||||
"У вас пока нет привязанных счетов.\n\n"
|
||||
"Счета добавляются администратором."
|
||||
)
|
||||
return
|
||||
|
||||
text = f"💳 Ваши счета (Клубная карта: {user.club_card_number}):\n\n"
|
||||
|
||||
for i, account in enumerate(accounts, 1):
|
||||
status = "✅" if account.is_active else "❌"
|
||||
text += f"{i}. {status} {account.account_number}\n"
|
||||
|
||||
await message.answer(text)
|
||||
Reference in New Issue
Block a user