Files
new_lottery_bot/main_old.py
Andrey K. Choi 4a741715f5
Some checks reported errors
continuous-integration/drone/push Build encountered an error
feat: Полный рефакторинг с модульной архитектурой
- Исправлены критические ошибки callback обработки
- Реализована модульная архитектура с применением SOLID принципов
- Добавлена система dependency injection
- Создана новая структура: interfaces, repositories, components, controllers
- Исправлены проблемы с базой данных (добавлены отсутствующие столбцы)
- Заменены заглушки на полную функциональность управления розыгрышами
- Добавлены отчеты о проделанной работе и документация

Архитектура готова для production и легко масштабируется
2025-11-17 05:34:08 +09:00

1427 lines
64 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import (
Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
BotCommand
)
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
import asyncio
import logging
import signal
import sys
from src.core.config import BOT_TOKEN, ADMIN_IDS
from src.core.database import async_session_maker, init_db
from src.core.services import UserService, LotteryService, ParticipationService
from src.core.models import User
from src.core.permissions import is_admin, format_commands_help
# Роутеры будут импортированы в main() для избежания циклических зависимостей
from src.utils.async_decorators import (
async_user_action, admin_async_action, db_operation,
TaskManagerMiddleware, shutdown_task_manager,
format_task_stats, TaskPriority
)
from src.utils.account_utils import validate_account_number, format_account_number
from src.display.winner_display import format_winner_display
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Состояния для FSM
class CreateLotteryStates(StatesGroup):
waiting_for_title = State()
waiting_for_description = State()
waiting_for_prizes = State()
class SetWinnerStates(StatesGroup):
waiting_for_lottery_id = State()
waiting_for_place = State()
waiting_for_user_id = State()
class AccountStates(StatesGroup):
waiting_for_account_number = State()
# Инициализация бота
bot = Bot(token=BOT_TOKEN)
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
router = Router()
# Подключаем middleware для управления задачами
dp.message.middleware(TaskManagerMiddleware())
dp.callback_query.middleware(TaskManagerMiddleware())
def get_main_keyboard(is_admin_user: bool = False) -> InlineKeyboardMarkup:
"""Главная клавиатура"""
buttons = [
[InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="list_lotteries")]
]
# Для всех пользователей (включая админов) показываем базовые функции
buttons.extend([
[InlineKeyboardButton(text="📝 Мои участия", callback_data="my_participations")],
[InlineKeyboardButton(text="💳 Мой счёт", callback_data="my_account")]
])
# Дополнительные кнопки только для админов
if is_admin_user:
buttons.extend([
[InlineKeyboardButton(text="🔧 Админ-панель", callback_data="admin_panel")],
[InlineKeyboardButton(text=" Создать розыгрыш", callback_data="create_lottery")],
[InlineKeyboardButton(text="📊 Статистика задач", callback_data="task_stats")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@router.message(Command("start"))
async def cmd_start(message: Message):
"""Обработчик команды /start"""
if not message.from_user:
return
logger.info(f"Получена команда /start от пользователя {message.from_user.id}")
try:
async with async_session_maker() as session:
user = await UserService.get_or_create_user(
session,
telegram_id=message.from_user.id,
username=message.from_user.username or "",
first_name=message.from_user.first_name or "",
last_name=message.from_user.last_name or ""
)
# Устанавливаем права администратора, если пользователь в списке
if message.from_user.id in ADMIN_IDS:
await UserService.set_admin(session, message.from_user.id, True)
is_registered = user.is_registered
is_admin_user = is_admin(message.from_user.id)
welcome_text = f"Добро пожаловать, {message.from_user.first_name or 'пользователь'}! 🎉\n\n"
welcome_text += "Это бот для проведения розыгрышей.\n\n"
# Для обычных пользователей - проверяем регистрацию
if not is_admin_user and not bool(is_registered):
welcome_text += "⚠️ Для участия в розыгрышах необходимо пройти регистрацию.\n\n"
buttons = [
[InlineKeyboardButton(text="🧪 ТЕСТ КОЛБЭК", callback_data="test_callback")],
[InlineKeyboardButton(text="📝 Зарегистрироваться", callback_data="start_registration")],
[InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="list_lotteries")]
]
await message.answer(
welcome_text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
return
welcome_text += "Выберите действие из меню ниже:"
if is_admin_user:
welcome_text += "\n\n👑 У вас есть права администратора!"
await message.answer(
welcome_text,
reply_markup=get_main_keyboard(is_admin_user)
)
except Exception as e:
logger.error(f"Ошибка в обработчике /start: {e}")
await message.answer("Произошла ошибка. Попробуйте позже.")
@router.message(Command("help"))
async def cmd_help(message: Message):
"""Показать список доступных команд с учетом прав пользователя"""
help_text = format_commands_help(message.from_user.id)
await message.answer(help_text, parse_mode="HTML")
@router.message(Command("admin"))
async def cmd_admin(message: Message):
"""Команда для быстрого доступа к админ-панели (только для админов)"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
# Создаем полноценную админ-панель
admin_text = (
"🔧 <b>Административная панель</b>\n\n"
f"👑 Добро пожаловать, {message.from_user.first_name}!\n\n"
"Выберите раздел для управления:"
)
admin_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="👥 Управление пользователями", callback_data="admin_users"),
InlineKeyboardButton(text="💳 Управление счетами", callback_data="admin_accounts")
],
[
InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries"),
InlineKeyboardButton(text="🔄 Повторные розыгрыши", callback_data="admin_redraw")
],
[
InlineKeyboardButton(text="💬 Управление чатом", callback_data="admin_chat"),
InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")
],
[
InlineKeyboardButton(text=" Создать розыгрыш", callback_data="create_lottery"),
InlineKeyboardButton(text="<EFBFBD> Задачи", callback_data="task_stats")
],
[
InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_main")
]
])
await message.answer(
admin_text,
parse_mode="HTML",
reply_markup=admin_keyboard
)
@router.callback_query(F.data == "list_lotteries")
async def show_active_lotteries(callback: CallbackQuery):
"""Показать активные розыгрыши"""
async with async_session_maker() as session:
lotteries = await LotteryService.get_active_lotteries(session)
if not lotteries:
await callback.message.edit_text(
"🔍 Активных розыгрышей нет",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
])
)
return
text = "🎲 Активные розыгрыши:\n\n"
buttons = []
for lottery in lotteries:
async with async_session_maker() as session:
participants_count = await ParticipationService.get_participants_count(
session, lottery.id
)
text += f"🎯 {lottery.title}\n"
text += f"👥 Участников: {participants_count}\n"
text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
buttons.append([
InlineKeyboardButton(
text=f"🎲 {lottery.title}",
callback_data=f"lottery_{lottery.id}"
)
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
@router.callback_query(F.data.startswith("lottery_"))
async def show_lottery_details(callback: CallbackQuery):
"""Показать детали розыгрыша"""
lottery_id = int(callback.data.split("_")[1])
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not lottery:
await callback.answer("Розыгрыш не найден", show_alert=True)
return
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
# Проверяем, участвует ли пользователь
is_participating = any(
p.user_id == user.id for p in lottery.participations
) if user else False
text = f"🎯 {lottery.title}\n\n"
text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n"
if lottery.prizes:
text += "🏆 Призы:\n"
for i, prize in enumerate(lottery.prizes, 1):
text += f"{i}. {prize}\n"
text += "\n"
text += f"👥 Участников: {participants_count}\n"
text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n"
if lottery.is_completed:
text += "\n✅ Розыгрыш завершен"
# Показываем победителей
async with async_session_maker() as session:
winners = await LotteryService.get_winners(session, lottery_id)
if winners:
text += "\n\n🏆 Победители:\n"
for winner in winners:
# Безопасное отображение победителя
if winner.user:
if winner.user.username:
winner_display = f"@{winner.user.username}"
else:
winner_display = f"{winner.user.first_name}"
elif winner.account_number:
winner_display = f"Счет: {winner.account_number}"
else:
winner_display = "Участник"
text += f"{winner.place}. {winner_display} - {winner.prize}\n"
else:
text += f"\n🟢 Статус: {'Активен' if lottery.is_active else 'Неактивен'}"
if is_participating:
text += "\n✅ Вы участвуете в розыгрыше"
buttons = []
if not lottery.is_completed and lottery.is_active and not is_participating:
buttons.append([
InlineKeyboardButton(
text="🎫 Участвовать",
callback_data=f"join_{lottery_id}"
)
])
if is_admin(callback.from_user.id) and not lottery.is_completed:
buttons.append([
InlineKeyboardButton(
text="🎲 Провести розыгрыш",
callback_data=f"conduct_{lottery_id}"
)
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="list_lotteries")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
@router.callback_query(F.data.startswith("join_"))
async def join_lottery(callback: CallbackQuery):
"""Присоединиться к розыгрышу"""
lottery_id = int(callback.data.split("_")[1])
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer("Ошибка получения данных пользователя", show_alert=True)
return
# Используем правильный метод ParticipationService
success = await ParticipationService.add_participant(session, lottery_id, user.id)
if success:
await callback.answer("✅ Вы успешно присоединились к розыгрышу!", show_alert=True)
else:
await callback.answer("❌ Вы уже участвуете в этом розыгрыше", show_alert=True)
# Обновляем информацию о розыгрыше
await show_lottery_details(callback)
async def notify_winners_async(bot: Bot, lottery_id: int, results: dict):
"""
Асинхронно отправить уведомления победителям с кнопкой подтверждения
Вызывается после проведения розыгрыша
"""
async with async_session_maker() as session:
from src.core.registration_services import AccountService, WinnerNotificationService
from src.core.models import Winner
from sqlalchemy import select
# Получаем информацию о розыгрыше
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
return
# Получаем всех победителей из БД
winners_result = await session.execute(
select(Winner).where(Winner.lottery_id == lottery_id)
)
winners = winners_result.scalars().all()
for winner in winners:
try:
# Если у победителя есть account_number, ищем владельца
if winner.account_number:
owner = await AccountService.get_account_owner(session, winner.account_number)
if owner and owner.telegram_id:
# Создаем токен верификации
verification = await WinnerNotificationService.create_verification_token(
session,
winner.id
)
# Формируем сообщение с кнопкой подтверждения
message = (
f"🎉 **Поздравляем! Ваш счет выиграл!**\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
f"💳 **Выигрышный счет: {winner.account_number}**\n\n"
f"⏰ **У вас есть 24 часа для подтверждения!**\n\n"
f"Нажмите кнопку ниже, чтобы подтвердить получение приза по этому счету.\n"
f"Если вы не подтвердите в течение 24 часов, "
f"приз будет разыгран заново.\n\n"
f" Если у вас несколько выигрышных счетов, "
f"подтвердите каждый из них отдельно."
)
# Создаем кнопку подтверждения с указанием счета
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"✅ Подтвердить счет {winner.account_number}",
callback_data=f"confirm_win_{winner.id}"
)],
[InlineKeyboardButton(
text="📞 Связаться с администратором",
url=f"tg://user?id={ADMIN_IDS[0]}"
)]
])
# Отправляем уведомление с кнопкой
await bot.send_message(
owner.telegram_id,
message,
reply_markup=keyboard,
parse_mode="Markdown"
)
# Отмечаем, что уведомление отправлено
winner.is_notified = True
await session.commit()
logger.info(f"Отправлено уведомление победителю {owner.telegram_id} за счет {winner.account_number}")
# Если победитель - обычный пользователь (старая система)
elif 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 and user.telegram_id:
message = (
f"🎉 Поздравляем! Вы выиграли!\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n\n"
f"⏰ **У вас есть 24 часа для подтверждения!**\n\n"
f"Нажмите кнопку ниже, чтобы подтвердить получение приза."
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="✅ Подтвердить получение приза",
callback_data=f"confirm_win_{winner.id}"
)]
])
await bot.send_message(
user.telegram_id,
message,
reply_markup=keyboard,
parse_mode="Markdown"
)
winner.is_notified = True
await session.commit()
logger.info(f"Отправлено уведомление победителю {user.telegram_id}")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления победителю: {e}")
@router.callback_query(F.data.startswith("confirm_win_"))
async def confirm_winner_response(callback: CallbackQuery):
"""Обработка подтверждения выигрыша победителем"""
winner_id = int(callback.data.split("_")[2])
async with async_session_maker() as session:
from src.core.models import Winner
from sqlalchemy import select
from sqlalchemy.orm import joinedload
# Получаем выигрыш с загрузкой связанного розыгрыша
winner_result = await session.execute(
select(Winner)
.options(joinedload(Winner.lottery))
.where(Winner.id == winner_id)
)
winner = winner_result.scalar_one_or_none()
if not winner:
await callback.answer("❌ Выигрыш не найден", show_alert=True)
return
# Проверяем, не подтвержден ли уже этот конкретный счет
if winner.is_claimed:
await callback.message.edit_text(
"✅ **Выигрыш этого счета уже подтвержден!**\n\n"
f"🎯 Розыгрыш: {winner.lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
f"💳 Счет: {winner.account_number}\n\n"
"Администратор свяжется с вами для передачи приза.",
parse_mode="Markdown"
)
return
# Проверяем, что подтверждает владелец именно ЭТОГО счета
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if winner.account_number:
# Проверяем что счет принадлежит текущему пользователю
from src.core.registration_services import AccountService
owner = await AccountService.get_account_owner(session, winner.account_number)
if not owner or owner.telegram_id != callback.from_user.id:
await callback.answer(
f"❌ Счет {winner.account_number} вам не принадлежит",
show_alert=True
)
return
elif winner.user_id:
# Старая логика для выигрышей без счета
if not user or user.id != winner.user_id:
await callback.answer("❌ Это не ваш выигрыш", show_alert=True)
return
# Подтверждаем выигрыш ЭТОГО конкретного счета
from datetime import datetime, timezone
winner.is_claimed = True
winner.claimed_at = datetime.now(timezone.utc)
await session.commit()
# Обновляем сообщение с указанием счета
confirmation_text = (
"✅ **Выигрыш успешно подтвержден!**\n\n"
f"🎯 Розыгрыш: {winner.lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
)
if winner.account_number:
confirmation_text += f"💳 Счет: {winner.account_number}\n"
confirmation_text += (
"\n🎊 Поздравляем! Администратор свяжется с вами "
"для передачи приза в ближайшее время.\n\n"
"Спасибо за участие!"
)
await callback.message.edit_text(
confirmation_text,
parse_mode="Markdown"
)
# Уведомляем администраторов о подтверждении конкретного счета
for admin_id in ADMIN_IDS:
try:
admin_msg = (
f"✅ **Победитель подтвердил получение приза!**\n\n"
f"🎯 Розыгрыш: {winner.lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
)
# Обязательно показываем счет
if winner.account_number:
admin_msg += f"<EFBFBD> **Подтвержденный счет: {winner.account_number}**\n\n"
if user:
admin_msg += f"👤 Владелец: {user.first_name}"
if user.username:
admin_msg += f" (@{user.username})"
admin_msg += f"\n🎫 Клубная карта: {user.club_card_number}\n"
if user.phone:
admin_msg += f"📱 Телефон: {user.phone}\n"
await callback.bot.send_message(admin_id, admin_msg, parse_mode="Markdown")
except:
pass
logger.info(
f"Победитель {callback.from_user.id} подтвердил выигрыш {winner_id} "
f"(счет: {winner.account_number})"
)
await callback.answer("✅ Выигрыш подтвержден!", show_alert=True)
@router.callback_query(F.data.startswith("conduct_") & ~F.data.in_(["conduct_lottery_admin"]))
async def conduct_lottery(callback: CallbackQuery):
"""Провести розыгрыш по ID"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
try:
lottery_id = int(callback.data.split("_")[1])
except (ValueError, IndexError):
await callback.answer("❌ Неверный формат данных", show_alert=True)
return
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
return
results = await LotteryService.conduct_draw(session, lottery_id)
if not results:
await callback.answer("Не удалось провести розыгрыш", show_alert=True)
return
text = "🎉 Розыгрыш завершен!\n\n🏆 Победители:\n\n"
for place, winner_info in results.items():
user_obj = winner_info['user']
prize = winner_info['prize']
# Безопасное отображение победителя
if hasattr(user_obj, 'username') and user_obj.username:
winner_display = f"@{user_obj.username}"
elif hasattr(user_obj, 'first_name'):
winner_display = f"{user_obj.first_name}"
elif hasattr(user_obj, 'account_number'):
winner_display = f"Счет: {user_obj.account_number}"
else:
winner_display = "Участник"
text += f"{place}. {winner_display}\n"
text += f" 🎁 {prize}\n\n"
# Отправляем уведомления победителям асинхронно
asyncio.create_task(notify_winners_async(callback.bot, lottery_id, results))
text += "📨 Уведомления отправляются победителям...\n"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К розыгрышам", callback_data="list_lotteries")]
])
)
# Создание розыгрыша
@router.callback_query(F.data == "create_lottery")
async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
"""Начать создание розыгрыша"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
await callback.message.edit_text(
"📝 Создание нового розыгрыша\n\n"
"Введите название розыгрыша:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
])
)
await state.set_state(CreateLotteryStates.waiting_for_title)
@router.message(StateFilter(CreateLotteryStates.waiting_for_title))
async def process_lottery_title(message: Message, state: FSMContext):
"""Обработка названия розыгрыша"""
await state.update_data(title=message.text)
await message.answer(
"📋 Введите описание розыгрыша (или отправьте '-' для пропуска):"
)
await state.set_state(CreateLotteryStates.waiting_for_description)
@router.message(StateFilter(CreateLotteryStates.waiting_for_description))
async def process_lottery_description(message: Message, state: FSMContext):
"""Обработка описания розыгрыша"""
description = None if message.text == "-" else message.text
await state.update_data(description=description)
await message.answer(
"🏆 Введите призы через новую строку:\n\n"
"Пример:\n"
"1000 рублей\n"
"iPhone 15\n"
"Подарочный сертификат"
)
await state.set_state(CreateLotteryStates.waiting_for_prizes)
@router.message(StateFilter(CreateLotteryStates.waiting_for_prizes))
async def process_lottery_prizes(message: Message, state: FSMContext):
"""Обработка призов розыгрыша"""
prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()]
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
await message.answer("❌ Ошибка получения данных пользователя")
await state.clear()
return
data = await state.get_data()
lottery = await LotteryService.create_lottery(
session,
title=data['title'],
description=data['description'],
prizes=prizes,
creator_id=user.id
)
await state.clear()
text = f"✅ Розыгрыш успешно создан!\n\n"
text += f"🎯 Название: {lottery.title}\n"
text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n"
text += f"🏆 Призы:\n"
for i, prize in enumerate(prizes, 1):
text += f"{i}. {prize}\n"
await message.answer(
text,
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
)
# Установка ручного победителя
@router.callback_query(F.data == "set_winner")
async def start_set_winner(callback: CallbackQuery, state: FSMContext):
"""Начать установку ручного победителя"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
async with async_session_maker() as session:
lotteries = await LotteryService.get_active_lotteries(session)
if not lotteries:
await callback.message.edit_text(
"❌ Нет активных розыгрышей",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
])
)
return
text = "👑 Установка ручного победителя\n\n"
text += "Выберите розыгрыш:\n\n"
buttons = []
for lottery in lotteries:
text += f"🎯 {lottery.title} (ID: {lottery.id})\n"
buttons.append([
InlineKeyboardButton(
text=f"{lottery.title}",
callback_data=f"setwinner_{lottery.id}"
)
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
@router.callback_query(F.data.startswith("setwinner_"))
async def select_winner_place(callback: CallbackQuery, state: FSMContext):
"""Выбор места для ручного победителя"""
lottery_id = int(callback.data.split("_")[1])
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.answer("Розыгрыш не найден", show_alert=True)
return
await state.update_data(lottery_id=lottery_id)
num_prizes = len(lottery.prizes) if lottery.prizes else 3
text = f"👑 Установка ручного победителя для розыгрыша:\n"
text += f"🎯 {lottery.title}\n\n"
text += f"Введите номер места (1-{num_prizes}):"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="set_winner")]
])
)
await state.set_state(SetWinnerStates.waiting_for_place)
@router.message(StateFilter(SetWinnerStates.waiting_for_place))
async def process_winner_place(message: Message, state: FSMContext):
"""Обработка места победителя"""
try:
place = int(message.text)
if place < 1:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректный номер места (положительное число)")
return
await state.update_data(place=place)
await message.answer(
f"👑 Установка ручного победителя на {place} место\n\n"
"Введите Telegram ID пользователя:"
)
await state.set_state(SetWinnerStates.waiting_for_user_id)
@router.message(StateFilter(SetWinnerStates.waiting_for_user_id))
async def process_winner_user_id(message: Message, state: FSMContext):
"""Обработка ID пользователя-победителя"""
try:
telegram_id = int(message.text)
except ValueError:
await message.answer("❌ Введите корректный Telegram ID (число)")
return
data = await state.get_data()
async with async_session_maker() as session:
success = await LotteryService.set_manual_winner(
session,
data['lottery_id'],
data['place'],
telegram_id
)
await state.clear()
if success:
await message.answer(
f"✅ Ручной победитель установлен!\n\n"
f"🏆 Место: {data['place']}\n"
f"👤 Telegram ID: {telegram_id}",
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
)
else:
await message.answer(
"Не удалось установить ручного победителя.\n"
"Проверьте, что пользователь существует в системе.",
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
)
@router.callback_query(F.data == "my_participations")
async def show_my_participations(callback: CallbackQuery):
"""Показать участие пользователя в розыгрышах"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer("Ошибка получения данных пользователя", show_alert=True)
return
participations = await ParticipationService.get_user_participations(session, user.id)
if not participations:
await callback.message.edit_text(
"📝 Вы пока не участвуете в розыгрышах",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
])
)
return
text = "📝 Ваши участия в розыгрышах:\n\n"
for participation in participations:
lottery = participation.lottery
status = "✅ Завершен" if lottery.is_completed else "🟢 Активен"
text += f"🎯 {lottery.title}\n"
text += f"📊 Статус: {status}\n"
text += f"📅 Участие с: {participation.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
])
)
# Хэндлеры для работы с номерами счетов
@router.callback_query(F.data == "my_account")
@db_operation()
async def show_my_account(callback: CallbackQuery):
"""Показать информацию о счетах пользователя"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer("Пользователь не найден", show_alert=True)
return
# Проверяем регистрацию
if not user.is_registered:
text = "❌ **Вы не зарегистрированы**\n\n"
text += "Пройдите регистрацию для доступа к счетам"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📝 Зарегистрироваться", callback_data="start_registration")],
[InlineKeyboardButton(text="🔙 Главное меню", callback_data="back_to_main")]
]),
parse_mode="Markdown"
)
return
# Получаем счета пользователя
from src.core.registration_services import AccountService
accounts = await AccountService.get_user_accounts(session, user.id)
text = "💳 **Ваши счета**\n\n"
if accounts:
text += f"🎫 Клубная карта: `{user.club_card_number}`\n"
text += f"<EFBFBD> Код верификации: `{user.verification_code}`\n\n"
text += f"**Счета ({len(accounts)}):**\n\n"
for i, acc in enumerate(accounts, 1):
status = "✅ Активен" if acc.is_active else "❌ Неактивен"
text += f"{i}. `{acc.account_number}`\n"
text += f" {status}\n\n"
text += " Счета используются для участия в розыгрышах"
else:
text += f"🎫 Клубная карта: `{user.club_card_number}`\n\n"
text += "У вас нет счетов\n\n"
text += "Обратитесь к администратору для добавления счетов"
buttons = [[InlineKeyboardButton(text="🔙 Главное меню", callback_data="back_to_main")]]
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
parse_mode="Markdown"
)
@router.callback_query(F.data.in_(["add_account", "change_account"]))
@db_operation()
async def start_account_setup(callback: CallbackQuery, state: FSMContext):
"""Начало процесса привязки/изменения счёта"""
await state.set_state(AccountStates.waiting_for_account_number)
action = "привязки" if callback.data == "add_account" else "изменения"
text = f"💳 **Процедура {action} счёта**\n\n"
text += "Введите номер вашего клиентского счёта в формате:\n"
text += "`12-34-56-78-90-12-34`\n\n"
text += "📝 **Требования:**\n"
text += "• Ровно 14 цифр\n"
text += "• Разделены дефисами через каждые 2 цифры\n"
text += "• Номер должен быть уникальным\n\n"
text += "✉️ Отправьте номер счёта в ответном сообщении"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="my_account")]
]),
parse_mode="Markdown"
)
@router.message(StateFilter(AccountStates.waiting_for_account_number))
@db_operation()
async def process_account_number(message: Message, state: FSMContext):
"""Обработка введённого номера счёта"""
account_input = message.text.strip()
# Форматируем и валидируем номер
formatted_number = format_account_number(account_input)
if not formatted_number:
await message.answer(
"❌ **Некорректный формат номера счёта**\n\n"
"Номер должен содержать ровно 14 цифр.\n"
"Пример правильного формата: `12-34-56-78-90-12-34`\n\n"
"Попробуйте ещё раз:",
parse_mode="Markdown"
)
return
async with async_session_maker() as session:
# Проверяем уникальность
existing_user = await UserService.get_user_by_account(session, formatted_number)
if existing_user and existing_user.telegram_id != message.from_user.id:
await message.answer(
"❌ **Номер счёта уже используется**\n\n"
"Данный номер счёта уже привязан к другому пользователю.\n"
"Убедитесь, что вы вводите правильный номер.\n\n"
"Попробуйте ещё раз:"
)
return
# Обновляем номер счёта
success = await UserService.set_account_number(
session, message.from_user.id, formatted_number
)
if success:
await state.clear()
await message.answer(
f"✅ **Счёт успешно привязан!**\n\n"
f"💳 Номер счёта: `{formatted_number}`\n\n"
f"Теперь вы можете участвовать в розыгрышах.\n"
f"Ваш номер счёта будет использоваться для идентификации.",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_main")]
])
)
else:
await message.answer(
"❌ **Ошибка привязки счёта**\n\n"
"Произошла ошибка при сохранении номера счёта.\n"
"Попробуйте ещё раз или обратитесь к администратору.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="my_account")]
])
)
@router.callback_query(F.data == "task_stats")
@admin_async_action()
async def show_task_stats(callback: CallbackQuery):
"""Показать статистику задач (только для админов)"""
if not is_admin(callback.from_user.id):
await callback.answer("Доступ запрещён", show_alert=True)
return
stats_text = await format_task_stats()
await callback.message.edit_text(
stats_text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="task_stats")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
]),
parse_mode="Markdown"
)
@router.callback_query(F.data == "back_to_main")
async def back_to_main(callback: CallbackQuery, state: FSMContext):
"""Вернуться в главное меню"""
await state.clear()
is_admin_user = is_admin(callback.from_user.id)
await callback.message.edit_text(
"🏠 Главное меню\n\nВыберите действие:",
reply_markup=get_main_keyboard(is_admin_user)
)
# ==================== АДМИНСКИЕ ОБРАБОТЧИКИ ====================
@router.callback_query(F.data == "admin_panel")
async def admin_panel(callback: CallbackQuery):
"""Административная панель"""
if not is_admin(callback.from_user.id):
await callback.answer("У вас нет прав доступа", show_alert=True)
return
admin_text = (
"🔧 <b>Административная панель</b>\n\n"
f"👑 Добро пожаловать, {callback.from_user.first_name}!\n\n"
"Выберите раздел для управления:"
)
admin_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="👥 Управление пользователями", callback_data="admin_users"),
InlineKeyboardButton(text="💳 Управление счетами", callback_data="admin_accounts")
],
[
InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries"),
InlineKeyboardButton(text="🔄 Повторные розыгрыши", callback_data="admin_redraw")
],
[
InlineKeyboardButton(text="💬 Управление чатом", callback_data="admin_chat"),
InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")
],
[
InlineKeyboardButton(text=" Создать розыгрыш", callback_data="create_lottery"),
InlineKeyboardButton(text="⚙️ Задачи", callback_data="task_stats")
],
[InlineKeyboardButton(text="🔙 Главное меню", callback_data="back_to_main")]
])
await callback.message.edit_text(admin_text, reply_markup=admin_keyboard, parse_mode="HTML")
@router.callback_query(F.data == "admin_users")
async def admin_users(callback: CallbackQuery):
"""Управление пользователями"""
if not is_admin(callback.from_user.id):
await callback.answer("У вас нет прав доступа", show_alert=True)
return
async with async_session_maker() as session:
# Получаем статистику пользователей
from sqlalchemy import func
total_users = await session.scalar(
select(func.count(User.id))
)
registered_users = await session.scalar(
select(func.count(User.id)).where(User.is_registered == True)
)
admin_users_count = await session.scalar(
select(func.count(User.id)).where(User.is_admin == True)
)
text = (
"👥 <b>Управление пользователями</b>\n\n"
f"📊 <b>Статистика:</b>\n"
f"👤 Всего пользователей: {total_users or 0}\n"
f"✅ Зарегистрированных: {registered_users or 0}\n"
f"👑 Администраторов: {admin_users_count or 0}\n\n"
"Выберите действие:"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="👤 Список пользователей", callback_data="user_list"),
InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="user_search")
],
[
InlineKeyboardButton(text="🚫 Заблокированные", callback_data="banned_users"),
InlineKeyboardButton(text="👑 Администраторы", callback_data="admin_list")
],
[InlineKeyboardButton(text="🔙 Админ-панель", callback_data="admin_panel")]
])
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
@router.callback_query(F.data == "admin_accounts")
async def admin_accounts(callback: CallbackQuery):
"""Управление счетами"""
if not is_admin(callback.from_user.id):
await callback.answer("У вас нет прав доступа", show_alert=True)
return
text = (
"💳 <b>Управление счетами</b>\n\n"
"Управление игровыми счетами пользователей:\n\n"
"Выберите действие:"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="💰 Пополнить счет", callback_data="admin_add_balance"),
InlineKeyboardButton(text="💸 Списать со счета", callback_data="admin_deduct_balance")
],
[
InlineKeyboardButton(text="📊 Статистика счетов", callback_data="accounts_stats"),
InlineKeyboardButton(text="🔍 Поиск по счету", callback_data="search_account")
],
[
InlineKeyboardButton(text="📋 Все счета", callback_data="all_accounts"),
InlineKeyboardButton(text="⚡ Массовые операции", callback_data="bulk_operations")
],
[InlineKeyboardButton(text="🔙 Админ-панель", callback_data="admin_panel")]
])
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
@router.callback_query(F.data == "admin_lotteries")
async def admin_lotteries(callback: CallbackQuery):
"""Управление розыгрышами"""
if not is_admin(callback.from_user.id):
await callback.answer("У вас нет прав доступа", show_alert=True)
return
text = (
"🎲 <b>Управление розыгрышами</b>\n\n"
"Управление всеми розыгрышами в системе:\n\n"
"Выберите действие:"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=" Создать розыгрыш", callback_data="create_lottery"),
InlineKeyboardButton(text="📋 Все розыгрыши", callback_data="all_lotteries")
],
[
InlineKeyboardButton(text="✅ Активные", callback_data="active_lotteries"),
InlineKeyboardButton(text="🏁 Завершенные", callback_data="completed_lotteries")
],
[
InlineKeyboardButton(text="🎯 Провести розыгрыш", callback_data="conduct_lottery_admin"),
InlineKeyboardButton(text="🔄 Повторный розыгрыш", callback_data="admin_redraw")
],
[InlineKeyboardButton(text="🔙 Админ-панель", callback_data="admin_panel")]
])
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
@router.callback_query(F.data == "admin_chat")
async def admin_chat(callback: CallbackQuery):
"""Управление чатом"""
if not is_admin(callback.from_user.id):
await callback.answer("У вас нет прав доступа", show_alert=True)
return
text = (
"💬 <b>Управление чатом</b>\n\n"
"Модерация и управление чатом:\n\n"
"Выберите действие:"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🚫 Заблокировать пользователя", callback_data="ban_user"),
InlineKeyboardButton(text="✅ Разблокировать", callback_data="unban_user")
],
[
InlineKeyboardButton(text="🗂 Список заблокированных", callback_data="banned_users"),
InlineKeyboardButton(text="💬 Настройки чата", callback_data="chat_settings")
],
[
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="broadcast"),
InlineKeyboardButton(text="📨 Сообщения чата", callback_data="chat_messages")
],
[InlineKeyboardButton(text="🔙 Админ-панель", callback_data="admin_panel")]
])
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
@router.callback_query(F.data == "admin_stats")
async def admin_stats(callback: CallbackQuery):
"""Статистика системы"""
if not is_admin(callback.from_user.id):
await callback.answer("У вас нет прав доступа", show_alert=True)
return
async with async_session_maker() as session:
# Получаем общую статистику
from sqlalchemy import func
from src.core.models import Lottery, Participation, Account, Winner
# Пользователи
total_users = await session.scalar(select(func.count(User.id)))
registered_users = await session.scalar(select(func.count(User.id)).where(User.is_registered == True))
# Розыгрыши
total_lotteries = await session.scalar(select(func.count(Lottery.id)))
active_lotteries = await session.scalar(select(func.count(Lottery.id)).where(Lottery.is_active == True))
completed_lotteries = await session.scalar(select(func.count(Lottery.id)).where(Lottery.is_completed == True))
# Участия
total_participations = await session.scalar(select(func.count(Participation.id)))
# Счета
total_accounts = await session.scalar(select(func.count(Account.id)))
# Победители
total_winners = await session.scalar(select(func.count(Winner.id)))
text = (
"📊 <b>Статистика системы</b>\n\n"
f"👥 <b>Пользователи:</b>\n"
f"├─ Всего: {total_users or 0}\n"
f"└─ Зарегистрированных: {registered_users or 0}\n\n"
f"🎲 <b>Розыгрыши:</b>\n"
f"├─ Всего: {total_lotteries or 0}\n"
f"├─ Активных: {active_lotteries or 0}\n"
f"└─ Завершенных: {completed_lotteries or 0}\n\n"
f"📝 <b>Участия:</b> {total_participations or 0}\n"
f"💳 <b>Счетов:</b> {total_accounts or 0}\n"
f"🏆 <b>Победителей:</b> {total_winners or 0}\n"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="📈 Подробная статистика", callback_data="detailed_stats"),
InlineKeyboardButton(text="📊 Экспорт данных", callback_data="export_data")
],
[InlineKeyboardButton(text="🔙 Админ-панель", callback_data="admin_panel")]
])
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
# ================= ЗАГЛУШКИ ДЛЯ ОСТАЛЬНЫХ КНОПОК =================
@router.callback_query(F.data.in_(["user_list", "user_search", "banned_users", "admin_list"]))
async def user_management_stub(callback: CallbackQuery):
"""Заглушка для управления пользователями"""
await callback.answer("🚧 Раздел в разработке", show_alert=True)
@router.callback_query(F.data.in_(["admin_add_balance", "admin_deduct_balance", "accounts_stats", "search_account", "all_accounts", "bulk_operations"]))
async def account_management_stub(callback: CallbackQuery):
"""Заглушка для управления счетами"""
await callback.answer("🚧 Раздел в разработке", show_alert=True)
@router.callback_query(F.data.in_(["all_lotteries", "active_lotteries", "completed_lotteries", "conduct_lottery_admin", "admin_redraw"]))
async def lottery_management_stub(callback: CallbackQuery):
"""Заглушка для управления розыгрышами"""
await callback.answer("🚧 Раздел в разработке", show_alert=True)
@router.callback_query(F.data.in_(["ban_user", "unban_user", "chat_settings", "broadcast", "chat_messages"]))
async def chat_management_stub(callback: CallbackQuery):
"""Заглушка для управления чатом"""
await callback.answer("🚧 Раздел в разработке", show_alert=True)
@router.callback_query(F.data.in_(["detailed_stats", "export_data"]))
async def stats_stub(callback: CallbackQuery):
"""Заглушка для статистики"""
await callback.answer("🚧 Раздел в разработке", show_alert=True)
@router.callback_query(F.data == "reg_start")
async def registration_start_stub(callback: CallbackQuery):
"""Заглушка для регистрации"""
await callback.answer("🚧 Регистрация временно недоступна", show_alert=True)
# ТЕСТ КОЛБЭКОВ
@router.callback_query(F.data == "test_callback")
async def test_callback(callback: CallbackQuery):
"""Тестовый колбэк для диагностики"""
logger.info(f"Тестовый колбэк сработал! От пользователя: {callback.from_user.id}")
await callback.answer("✅ Тестовый колбэк работает!", show_alert=True)
async def set_commands():
"""Установка команд бота"""
# Команды для обычных пользователей
user_commands = [
BotCommand(command="start", description="🚀 Начать работу с ботом"),
BotCommand(command="help", description="📋 Показать список команд"),
BotCommand(command="my_code", description="🔑 Мой реферальный код"),
BotCommand(command="my_accounts", description="💳 Мои счета"),
]
# Команды для администраторов (добавляются к пользовательским)
admin_commands = user_commands + [
BotCommand(command="add_account", description=" Добавить счет"),
BotCommand(command="remove_account", description=" Удалить счет"),
BotCommand(command="verify_winner", description="✅ Верифицировать победителя"),
BotCommand(command="check_unclaimed", description="🔍 Проверить невостребованные"),
BotCommand(command="redraw", description="🎲 Повторный розыгрыш"),
BotCommand(command="chat_mode", description="💬 Режим чата"),
BotCommand(command="ban", description="🚫 Забанить пользователя"),
BotCommand(command="unban", description="✅ Разбанить"),
BotCommand(command="banlist", description="📋 Список банов"),
BotCommand(command="chat_stats", description="📊 Статистика чата"),
]
# Устанавливаем команды для обычных пользователей
await bot.set_my_commands(user_commands)
# Для админов устанавливаем расширенный набор команд
from aiogram.types import BotCommandScopeChat
for admin_id in ADMIN_IDS:
try:
await bot.set_my_commands(
admin_commands,
scope=BotCommandScopeChat(chat_id=admin_id)
)
except Exception as e:
logging.warning(f"Не удалось установить команды для админа {admin_id}: {e}")
async def main():
"""Главная функция"""
# Импорт роутеров (для избежания циклических зависимостей)
from src.handlers.admin_panel import admin_router
from src.handlers.account_handlers import account_router
from src.handlers.registration_handlers import router as registration_router
from src.handlers.admin_account_handlers import router as admin_account_router
from src.handlers.redraw_handlers import router as redraw_router
from src.handlers.chat_handlers import router as chat_router
from src.handlers.admin_chat_handlers import router as admin_chat_router
from src.handlers.test_handlers import test_router # Тестовый роутер
# Инициализация базы данных
await init_db()
# Установка команд
await set_commands()
# Подключение роутеров
dp.include_router(router) # Основной роутер с командой /start (ПЕРВЫМ!)
dp.include_router(registration_router) # Роутер регистрации
dp.include_router(admin_account_router) # Роутер админских команд для счетов
dp.include_router(admin_chat_router) # Роутер админских команд чата
dp.include_router(redraw_router) # Роутер повторного розыгрыша
dp.include_router(account_router) # Роутер для работы со счетами
dp.include_router(admin_router) # Админский роутер
dp.include_router(chat_router) # Роутер чата пользователей (ПОСЛЕДНИМ!)
# Обработка сигналов для graceful shutdown
def signal_handler():
logger.info("Получен сигнал завершения, остановка бота...")
asyncio.create_task(shutdown_task_manager())
# Настройка обработчиков сигналов
if sys.platform != "win32":
for sig in (signal.SIGTERM, signal.SIGINT):
asyncio.get_event_loop().add_signal_handler(sig, signal_handler)
# Запуск бота
logger.info("Бот запущен")
try:
await dp.start_polling(bot)
finally:
# Остановка менеджера задач при завершении
await shutdown_task_manager()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Бот остановлен пользователем")
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
finally:
logger.info("Завершение работы")