""" Расширенная админ-панель для управления розыгрышами """ import logging from aiogram import Router, F from aiogram.types import ( CallbackQuery, Message, InlineKeyboardButton, InlineKeyboardMarkup ) from aiogram.exceptions import TelegramBadRequest from aiogram.filters import StateFilter from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from sqlalchemy.ext.asyncio import AsyncSession from datetime import datetime, timedelta import json from ..core.database import async_session_maker from ..core.services import UserService, LotteryService, ParticipationService from ..core.chat_services import ChatMessageService from ..core.broadcast_services import broadcast_service from ..core.config import ADMIN_IDS from ..core.models import User, Lottery, Participation, Account, ChatMessage, Winner, BroadcastChannel, BlockedUser logger = logging.getLogger(__name__) async def safe_edit_message( callback: CallbackQuery, text: str, reply_markup: InlineKeyboardMarkup | None = None, parse_mode: str = "Markdown" ) -> bool: """ Безопасное редактирование сообщения с обработкой ошибки 'message is not modified' Returns: bool: True если сообщение отредактировано, False если не изменилось """ try: await callback.message.edit_text( text, reply_markup=reply_markup, parse_mode=parse_mode ) return True except TelegramBadRequest as e: if "message is not modified" in str(e): await callback.answer("Сообщение уже актуально", show_alert=False) return False raise # Состояния для админки class AdminStates(StatesGroup): # Создание розыгрыша lottery_title = State() lottery_description = State() lottery_prizes = State() lottery_confirm = State() # Управление участниками add_participant_lottery = State() add_participant_user = State() add_participant_bulk = State() add_participant_bulk_accounts = State() remove_participant_lottery = State() remove_participant_user = State() remove_participant_bulk = State() remove_participant_bulk_accounts = State() participant_search = State() # Добавление/удаление участников в конкретном розыгрыше add_to_lottery_user = State() remove_from_lottery_user = State() # Установка победителей set_winner_lottery = State() set_winner_place = State() set_winner_user = State() # Редактирование розыгрыша edit_lottery_select = State() edit_lottery_field = State() edit_lottery_value = State() # Настройки отображения победителей lottery_display_type_select = State() lottery_display_type_set = State() # Массовая рассылка broadcast_message = State() broadcast_type_select = State() # Выбор типа рассылки (ЛС/канал/группа) broadcast_channel_select = State() # Выбор канала/группы broadcast_add_channel_id = State() # Добавление нового канала broadcast_add_channel_title = State() # Название канала # Импорт/экспорт пользователей import_users_json = State() # Управление пользователями user_management_search = State() # Поиск пользователей user_management_view = State() # Просмотр пользователя # Управление админами admin_management_action = State() # Выбор действия (добавить/удалить) admin_add_search = State() # Поиск пользователя для назначения админом admin_add_confirm = State() # Подтверждение назначения admin_remove_select = State() # Выбор админа для удаления admin_remove_confirm = State() # Подтверждение удаления admin_router = Router() def is_admin(user_id: int) -> bool: """Проверка прав администратора (быстрая проверка только .env)""" return user_id in ADMIN_IDS def is_super_admin(user_id: int) -> bool: """Проверка, является ли пользователь главным администратором (из ADMIN_IDS)""" return user_id in ADMIN_IDS async def check_admin_access(user_id: int) -> bool: """ Асинхронная проверка доступа администратора. Проверяет как главных администраторов (.env), так и назначенных (БД) """ # Сначала проверяем главных администраторов if user_id in ADMIN_IDS: return True # Затем проверяем назначенных администраторов в БД async with async_session_maker() as session: from sqlalchemy import select result = await session.execute( select(User).where(User.telegram_id == user_id, User.is_admin == True) ) user = result.scalar_one_or_none() return user is not None def get_admin_main_keyboard() -> InlineKeyboardMarkup: """Главная админ-панель""" buttons = [ [InlineKeyboardButton(text="🎰 Розыгрыши", callback_data="admin_lotteries"), InlineKeyboardButton(text="🏆 Победители", callback_data="admin_winners")], [InlineKeyboardButton(text="📋 Участники", callback_data="admin_participants"), InlineKeyboardButton(text="👥 Пользователи", callback_data="admin_users")], [InlineKeyboardButton(text="📢 Рассылки", callback_data="admin_broadcast"), InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")], [InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")], [InlineKeyboardButton(text="◀️ Назад", callback_data="back_to_main")] ] return InlineKeyboardMarkup(inline_keyboard=buttons) def get_lottery_management_keyboard() -> InlineKeyboardMarkup: """Клавиатура управления розыгрышами""" buttons = [ [InlineKeyboardButton(text="✨ Создать", callback_data="admin_create_lottery"), InlineKeyboardButton(text="✏️ Редактировать", callback_data="admin_edit_lottery")], [InlineKeyboardButton(text="📜 Список всех", callback_data="admin_list_all_lotteries")], [InlineKeyboardButton(text="🎰 Провести розыгрыш", callback_data="admin_conduct_draw")], [InlineKeyboardButton(text="✅ Завершить", callback_data="admin_finish_lottery"), InlineKeyboardButton(text="🗑️ Удалить", callback_data="admin_delete_lottery")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ] return InlineKeyboardMarkup(inline_keyboard=buttons) def get_participant_management_keyboard() -> InlineKeyboardMarkup: """Клавиатура управления участниками""" buttons = [ [InlineKeyboardButton(text="➕ Добавить", callback_data="admin_add_participant"), InlineKeyboardButton(text="➖ Удалить", callback_data="admin_remove_participant")], [InlineKeyboardButton(text="📥 Массовые операции", callback_data="admin_bulk_operations")], [InlineKeyboardButton(text="📋 Список всех", callback_data="admin_list_all_participants"), InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_search_participants")], [InlineKeyboardButton(text="📊 По розыгрышам", callback_data="admin_participants_by_lottery")], [InlineKeyboardButton(text="📄 Отчет", callback_data="admin_participants_report")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ] return InlineKeyboardMarkup(inline_keyboard=buttons) def get_winner_management_keyboard() -> InlineKeyboardMarkup: """Клавиатура управления победителями""" buttons = [ [InlineKeyboardButton(text="🏆 Установить вручную", callback_data="admin_set_manual_winner")], [InlineKeyboardButton(text="✏️ Изменить", callback_data="admin_edit_winner"), InlineKeyboardButton(text="❌ Удалить", callback_data="admin_remove_winner")], [InlineKeyboardButton(text="📜 Список победителей", callback_data="admin_list_winners")], [InlineKeyboardButton(text="👁️ Настройка отображения", callback_data="admin_winner_display_settings")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ] return InlineKeyboardMarkup(inline_keyboard=buttons) @admin_router.callback_query(F.data == "admin_panel") async def show_admin_panel(callback: CallbackQuery): """Показать админ-панель""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: # Быстрая статистика from sqlalchemy import select, func from ..core.models import User, Lottery, Participation users_count = await session.scalar(select(func.count(User.id))) lotteries_count = await session.scalar(select(func.count(Lottery.id))) active_lotteries = await session.scalar( select(func.count(Lottery.id)) .where(Lottery.is_active == True, Lottery.is_completed == False) ) total_participations = await session.scalar(select(func.count(Participation.id))) text = f"🔧 Админ-панель\n\n" text += f"📊 Быстрая статистика:\n" text += f"👥 Пользователей: {users_count}\n" text += f"🎲 Всего розыгрышей: {lotteries_count}\n" text += f"🟢 Активных: {active_lotteries}\n" text += f"🎫 Участий: {total_participations}\n\n" text += "Выберите раздел для управления:" await callback.message.edit_text(text, reply_markup=get_admin_main_keyboard()) # ====================== # УПРАВЛЕНИЕ РОЗЫГРЫШАМИ # ====================== @admin_router.callback_query(F.data == "admin_lotteries") async def show_lottery_management(callback: CallbackQuery): """Управление розыгрышами""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = "🎲 Управление розыгрышами\n\n" text += "Здесь вы можете создавать, редактировать и управлять розыгрышами.\n\n" text += "Выберите действие:" await callback.message.edit_text(text, reply_markup=get_lottery_management_keyboard()) @admin_router.callback_query(F.data == "admin_create_lottery") async def start_create_lottery(callback: CallbackQuery, state: FSMContext): """Начать создание розыгрыша""" logging.info(f"🎯 Callback admin_create_lottery получен от пользователя {callback.from_user.id}") # Сразу отвечаем на callback await callback.answer() if not await check_admin_access(callback.from_user.id): logging.warning(f"⚠️ Пользователь {callback.from_user.id} не является админом") await callback.message.answer("❌ Недостаточно прав") return logging.info(f"✅ Админ {callback.from_user.id} начинает создание розыгрыша") text = "📝 Создание нового розыгрыша\n\n" text += "Шаг 1 из 4\n\n" text += "Введите название розыгрыша:" try: await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")] ]) ) await state.set_state(AdminStates.lottery_title) logging.info(f"✅ Состояние установлено: AdminStates.lottery_title") except Exception as e: logging.error(f"❌ Ошибка при создании розыгрыша: {e}") await callback.message.answer(f"❌ Ошибка: {str(e)}") @admin_router.message(StateFilter(AdminStates.lottery_title)) async def process_lottery_title(message: Message, state: FSMContext): """Обработка названия розыгрыша (создание или редактирование)""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() edit_lottery_id = data.get('edit_lottery_id') # Если это редактирование существующего розыгрыша if edit_lottery_id: async with async_session_maker() as session: success = await LotteryService.update_lottery( session, edit_lottery_id, title=message.text ) if success: await message.answer(f"✅ Название изменено на: {message.text}") await state.clear() # Возвращаемся к выбору полей from aiogram.types import CallbackQuery fake_callback = CallbackQuery( id="fake", from_user=message.from_user, chat_instance="fake", data=f"admin_edit_lottery_select_{edit_lottery_id}", message=message ) await choose_edit_field(fake_callback, state) else: await message.answer("❌ Ошибка при изменении названия") return # Если это создание нового розыгрыша await state.update_data(title=message.text) text = f"📝 Создание нового розыгрыша\n\n" text += f"Шаг 2 из 4\n\n" text += f"✅ Название: {message.text}\n\n" text += f"Введите описание розыгрыша (или '-' для пропуска):" await message.answer(text) await state.set_state(AdminStates.lottery_description) @admin_router.message(StateFilter(AdminStates.lottery_description)) async def process_lottery_description(message: Message, state: FSMContext): """Обработка описания розыгрыша (создание или редактирование)""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() edit_lottery_id = data.get('edit_lottery_id') # Если это редактирование существующего розыгрыша if edit_lottery_id: description = None if message.text == "-" else message.text async with async_session_maker() as session: success = await LotteryService.update_lottery( session, edit_lottery_id, description=description ) if success: await message.answer(f"✅ Описание изменено") await state.clear() # Возвращаемся к выбору полей from aiogram.types import CallbackQuery fake_callback = CallbackQuery( id="fake", from_user=message.from_user, chat_instance="fake", data=f"admin_edit_lottery_select_{edit_lottery_id}", message=message ) await choose_edit_field(fake_callback, state) else: await message.answer("❌ Ошибка при изменении описания") return # Если это создание нового розыгрыша description = None if message.text == "-" else message.text await state.update_data(description=description) data = await state.get_data() text = f"📝 Создание нового розыгрыша\n\n" text += f"Шаг 3 из 4\n\n" text += f"✅ Название: {data['title']}\n" text += f"✅ Описание: {description or 'Не указано'}\n\n" text += f"Введите призы (каждый с новой строки):\n\n" text += f"Пример:\n" text += f"🥇 iPhone 15 Pro\n" text += f"🥈 MacBook Air\n" text += f"🥉 AirPods Pro\n" text += f"🏆 10,000 рублей" await message.answer(text) await state.set_state(AdminStates.lottery_prizes) @admin_router.message(StateFilter(AdminStates.lottery_prizes)) async def process_lottery_prizes(message: Message, state: FSMContext): """Обработка призов розыгрыша (создание или редактирование)""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() edit_lottery_id = data.get('edit_lottery_id') prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()] # Если это редактирование существующего розыгрыша if edit_lottery_id: async with async_session_maker() as session: success = await LotteryService.update_lottery( session, edit_lottery_id, prizes=prizes ) if success: await message.answer(f"✅ Призы изменены") await state.clear() # Возвращаемся к выбору полей from aiogram.types import CallbackQuery fake_callback = CallbackQuery( id="fake", from_user=message.from_user, chat_instance="fake", data=f"admin_edit_lottery_select_{edit_lottery_id}", message=message ) await choose_edit_field(fake_callback, state) else: await message.answer("❌ Ошибка при изменении призов") return # Если это создание нового розыгрыша await state.update_data(prizes=prizes) data = await state.get_data() text = f"📝 Создание нового розыгрыша\n\n" text += f"Шаг 4 из 4 - Подтверждение\n\n" text += f"🎯 Название: {data['title']}\n" text += f"📋 Описание: {data['description'] or 'Не указано'}\n\n" text += f"🏆 Призы:\n" for i, prize in enumerate(prizes, 1): text += f"{i}. {prize}\n" text += f"\n✅ Подтвердите создание розыгрыша:" await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="✅ Создать", callback_data="confirm_create_lottery")], [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")] ]) ) await state.set_state(AdminStates.lottery_confirm) @admin_router.callback_query(F.data == "confirm_create_lottery", StateFilter(AdminStates.lottery_confirm)) async def confirm_create_lottery(callback: CallbackQuery, state: FSMContext): """Подтверждение создания розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return data = await state.get_data() async with async_session_maker() as session: user = await UserService.get_or_create_user( session, callback.from_user.id, username=callback.from_user.username, first_name=callback.from_user.first_name, last_name=callback.from_user.last_name ) lottery = await LotteryService.create_lottery( session, title=data['title'], description=data['description'], prizes=data['prizes'], creator_id=user.id ) await state.clear() text = f"✅ Розыгрыш успешно создан!\n\n" text += f"🆔 ID: {lottery.id}\n" text += f"🎯 Название: {lottery.title}\n" text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" text += f"Розыгрыш доступен для участников." await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🎰 К управлению розыгрышами", callback_data="admin_lotteries")], [InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")] ]) ) @admin_router.callback_query(F.data == "admin_list_all_lotteries") async def list_all_lotteries(callback: CallbackQuery): """Список всех розыгрышей""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import select from ..core.models import Lottery result = await session.execute( select(Lottery).order_by(Lottery.created_at.desc()) ) lotteries = result.scalars().all() if not lotteries: text = "📋 Розыгрышей пока нет" buttons = [[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")]] else: text = f"📋 Все розыгрыши ({len(lotteries)}):\n\n" buttons = [] for lottery in lotteries[:10]: # Показываем первые 10 status = "🟢" if lottery.is_active and not lottery.is_completed else "✅" if lottery.is_completed else "🔴" async with async_session_maker() as session: participants_count = await ParticipationService.get_participants_count( session, lottery.id ) text += f"{status} {lottery.title}\n" text += f" ID: {lottery.id} | Участников: {participants_count}\n" text += f" Создан: {lottery.created_at.strftime('%d.%m %H:%M')}\n\n" buttons.append([ InlineKeyboardButton( text=f"📝 {lottery.title[:25]}..." if len(lottery.title) > 25 else lottery.title, callback_data=f"admin_lottery_detail_{lottery.id}" ) ]) if len(lotteries) > 10: text += f"... и еще {len(lotteries) - 10} розыгрышей" buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_lottery_detail_")) async def show_lottery_detail(callback: CallbackQuery): """Детальная информация о розыгрыше""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 participants_count = await ParticipationService.get_participants_count(session, lottery_id) winners = await LotteryService.get_winners(session, lottery_id) if lottery.is_completed else [] status_emoji = "🟢" if lottery.is_active and not lottery.is_completed else "✅" if lottery.is_completed else "🔴" status_text = "Активен" if lottery.is_active and not lottery.is_completed else "Завершен" if lottery.is_completed else "Неактивен" text = f"🎲 Детали розыгрыша\n\n" text += f"🆔 ID: {lottery.id}\n" text += f"🎯 Название: {lottery.title}\n" text += f"📋 Описание: {lottery.description or 'Не указано'}\n" text += f"{status_emoji} Статус: {status_text}\n" text += f"👥 Участников: {participants_count}\n" text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" if lottery.prizes: text += f"🏆 Призы:\n" for i, prize in enumerate(lottery.prizes, 1): text += f"{i}. {prize}\n" text += "\n" # Ручные победители if lottery.manual_winners: text += f"👑 Предустановленные победители:\n" for place, telegram_id in lottery.manual_winners.items(): async with async_session_maker() as session: winner_user = await UserService.get_user_by_telegram_id(session, telegram_id) name = winner_user.username if winner_user and winner_user.username else str(telegram_id) text += f"{place} место: @{name}\n" text += "\n" # Результаты розыгрыша if lottery.is_completed and winners: text += f"🏆 Результаты:\n" for winner in winners: manual_mark = " 👑" if winner.is_manual else "" # Безопасная обработка победителя - может быть без user_id if winner.user: username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name else: # Победитель по номеру счета без связанного пользователя username = f"Счет: {winner.account_number}" text += f"{winner.place}. {username}{manual_mark}\n" buttons = [] if not lottery.is_completed: # Розыгрыш ещё не проведён buttons.extend([ [InlineKeyboardButton(text="🏆 Установить победителя", callback_data=f"admin_set_winner_{lottery_id}")], [InlineKeyboardButton(text="🎰 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")], ]) buttons.extend([ [InlineKeyboardButton(text="📝 Редактировать", callback_data=f"admin_edit_{lottery_id}")], [InlineKeyboardButton(text="👥 Участники", callback_data=f"admin_participants_{lottery_id}")], [InlineKeyboardButton(text="◀️ К списку", callback_data="admin_list_all_lotteries")] ]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) # ====================== # УПРАВЛЕНИЕ УЧАСТНИКАМИ # ====================== @admin_router.callback_query(F.data == "admin_participants") async def show_participant_management(callback: CallbackQuery): """Управление участниками""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = "👥 Управление участниками\n\n" text += "Здесь вы можете добавлять и удалять участников розыгрышей.\n\n" text += "Выберите действие:" await callback.message.edit_text(text, reply_markup=get_participant_management_keyboard()) @admin_router.callback_query(F.data.startswith("admin_participants_")) async def show_lottery_participants(callback: CallbackQuery): """Показать участников конкретного розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 text = f"👥 Участники розыгрыша\n" text += f"🎯 {lottery.title}\n\n" if not lottery.participations: text += "Участников пока нет" buttons = [[InlineKeyboardButton(text="◀️ Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]] else: text += f"Всего участников: {len(lottery.participations)}\n\n" for i, participation in enumerate(lottery.participations[:20], 1): # Показываем первых 20 user = participation.user 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: text += f"... и еще {len(lottery.participations) - 20} участников" buttons = [ [InlineKeyboardButton(text="✨ Добавить участника", callback_data=f"admin_add_to_{lottery_id}")], [InlineKeyboardButton(text="➖ Удалить участника", callback_data=f"admin_remove_from_{lottery_id}")], [InlineKeyboardButton(text="◀️ Назад", callback_data=f"admin_lottery_detail_{lottery_id}")] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) # ====================== # НОВЫЕ ХЭНДЛЕРЫ ДЛЯ УПРАВЛЕНИЯ УЧАСТНИКАМИ # ====================== @admin_router.callback_query(F.data == "admin_bulk_operations") async def show_bulk_operations_menu(callback: CallbackQuery): """Подменю массовых операций""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = ( "📥 Массовые операции\n\n" "Выберите тип операции:" ) buttons = [ [InlineKeyboardButton(text="⬇️ Добавление по ID", callback_data="admin_bulk_add_participant"), InlineKeyboardButton(text="💳 Добавление по счетам", callback_data="admin_bulk_add_accounts")], [InlineKeyboardButton(text="⬆️ Удаление по ID", callback_data="admin_bulk_remove_participant"), InlineKeyboardButton(text="💳 Удаление по счетам", callback_data="admin_bulk_remove_accounts")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_add_participant") async def start_add_participant(callback: CallbackQuery, state: FSMContext): """Начать добавление участника""" if not await check_admin_access(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="admin_participants")] ]) ) return text = "➕ Добавление участника\n\n" text += "Выберите розыгрыш:\n\n" buttons = [] for lottery in lotteries: async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) text += f"🎯 {lottery.title} (участников: {count})\n" buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title[:35]}...", callback_data=f"admin_add_part_to_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_add_part_to_")) async def choose_user_to_add(callback: CallbackQuery, state: FSMContext): """Выбор пользователя для добавления""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(add_participant_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) text = f"➕ Добавление в: {lottery.title}\n\n" text += "Введите Telegram ID или username пользователя:\n\n" text += "Примеры:\n" text += "• @username\n" text += "• 123456789" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_add_participant")] ]) ) await state.set_state(AdminStates.add_participant_user) @admin_router.message(StateFilter(AdminStates.add_participant_user)) async def process_add_participant(message: Message, state: FSMContext): """Обработка добавления участника""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data['add_participant_lottery_id'] user_input = message.text.strip() async with async_session_maker() as session: # Ищем пользователя user = None if user_input.startswith('@'): username = user_input[1:] user = await UserService.get_user_by_username(session, username) elif user_input.isdigit(): telegram_id = int(user_input) user = await UserService.get_user_by_telegram_id(session, telegram_id) if not user: await message.answer( "❌ Пользователь не найден в системе.\n" "Пользователь должен сначала запустить бота командой /start" ) return # Добавляем участника success = await ParticipationService.add_participant(session, lottery_id, user.id) lottery = await LotteryService.get_lottery(session, lottery_id) await state.clear() if success: username = f"@{user.username}" if user.username else "Нет username" await message.answer( f"✅ Участник добавлен!\n\n" f"👤 Пользователь: {user.first_name} {user.last_name or ''}\n" f"📱 Username: {username}\n" f"🆔 ID: {user.telegram_id}\n" f"🎯 Розыгрыш: {lottery.title}", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) else: await message.answer( f"⚠️ Пользователь {user.first_name} уже участвует в этом розыгрыше", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_remove_participant") async def remove_participant_start(callback: CallbackQuery): """Начало процесса удаления участника""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_all_lotteries(session, limit=20) if not lotteries: await callback.message.edit_text( "❌ Нет розыгрышей в системе", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) return buttons = [] for lottery in lotteries: buttons.append([ InlineKeyboardButton( text=f"{'✅' if lottery.is_active else '🔴'} {lottery.title}", callback_data=f"admin_remove_part_from_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]) await callback.message.edit_text( "➖ Удалить участника из розыгрыша\n\nВыберите розыгрыш:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons) ) @admin_router.callback_query(F.data.startswith("admin_remove_part_from_")) async def remove_participant_select_lottery(callback: CallbackQuery, state: FSMContext): """Выбор розыгрыша для удаления участника""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 participant_count = await ParticipationService.get_participants_count(session, lottery_id) await state.update_data(remove_participant_lottery_id=lottery_id) await state.set_state(AdminStates.remove_participant_user) await callback.message.edit_text( f"➖ Удалить участника из розыгрыша\n\n" f"🎯 Розыгрыш: {lottery.title}\n" f"👥 Участников: {participant_count}\n\n" f"Отправьте Telegram ID пользователя для удаления:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_remove_participant")] ]) ) @admin_router.message(StateFilter(AdminStates.remove_participant_user)) async def process_remove_participant(message: Message, state: FSMContext): """Обработка удаления участника""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data.get("remove_participant_lottery_id") try: telegram_id = int(message.text.strip()) except ValueError: await message.answer( "❌ Неверный формат. Отправьте числовой Telegram ID.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_remove_participant")] ]) ) return async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, telegram_id) if not user: await message.answer( f"❌ Пользователь с ID {telegram_id} не найден в системе", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) await state.clear() return lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await message.answer("❌ Розыгрыш не найден") await state.clear() return removed = await ParticipationService.remove_participant(session, lottery_id, user.id) await state.clear() username = f"@{user.username}" if user.username else "Нет username" if removed: await message.answer( f"✅ Участник удалён из розыгрыша!\n\n" f"👤 {user.first_name} {user.last_name or ''}\n" f"📱 Username: {username}\n" f"🆔 ID: {user.telegram_id}\n" f"🎯 Розыгрыш: {lottery.title}", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) else: await message.answer( f"⚠️ Пользователь {user.first_name} не участвует в этом розыгрыше", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_list_all_participants") async def list_all_participants(callback: CallbackQuery): """Список всех участников""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: users = await UserService.get_all_users(session, limit=50) # Получаем статистику для каждого пользователя user_stats = [] for user in users: stats = await ParticipationService.get_participant_stats(session, user.id) user_stats.append((user, stats)) if not user_stats: await callback.message.edit_text( "❌ В системе нет пользователей", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) return text = "👥 Все участники системы\n\n" text += f"Всего пользователей: {len(users)}\n\n" for i, (user, stats) in enumerate(user_stats[:20], 1): 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" text += f" 🎫 Участий: {stats['participations_count']} | 🏆 Побед: {stats['wins_count']}\n" if stats['last_participation']: text += f" 📅 Последнее участие: {stats['last_participation'].strftime('%d.%m.%Y')}\n" text += "\n" if len(users) > 20: text += f"... и еще {len(users) - 20} пользователей" buttons = [ [InlineKeyboardButton(text="📄 Подробный отчет", callback_data="admin_participants_report")], [InlineKeyboardButton(text="🔃 Обновить", callback_data="admin_list_all_participants")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data == "admin_participants_report") async def generate_participants_report(callback: CallbackQuery): """Генерация отчета по участникам""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import func, select from ..core.models import User, Participation, Winner # Общие статистики total_users = await session.scalar(select(func.count(User.id))) total_participations = await session.scalar(select(func.count(Participation.id))) total_winners = await session.scalar(select(func.count(Winner.id))) # Топ участников по количеству участий top_participants = await session.execute( select(User.first_name, User.username, func.count(Participation.id).label('count')) .join(Participation) .group_by(User.id) .order_by(func.count(Participation.id).desc()) .limit(10) ) top_participants = top_participants.fetchall() # Топ победителей top_winners = await session.execute( select(User.first_name, User.username, func.count(Winner.id).label('wins')) .join(Winner) .group_by(User.id) .order_by(func.count(Winner.id).desc()) .limit(5) ) top_winners = top_winners.fetchall() # Недавняя активность recent_users = await session.execute( select(User.first_name, User.username, User.created_at) .order_by(User.created_at.desc()) .limit(5) ) recent_users = recent_users.fetchall() text = "📈 Подробный отчет по участникам\n\n" text += "📊 ОБЩАЯ СТАТИСТИКА\n" text += f"👥 Всего пользователей: {total_users}\n" text += f"🎫 Всего участий: {total_participations}\n" text += f"🏆 Всего побед: {total_winners}\n" if total_users > 0: avg_participations = total_participations / total_users text += f"📈 Среднее участий на пользователя: {avg_participations:.1f}\n" text += "\n" if top_participants: text += "🔥 ТОП УЧАСТНИКИ (по количеству участий)\n" for i, (first_name, username, count) in enumerate(top_participants, 1): name = f"@{username}" if username else first_name text += f"{i}. {name} - {count} участий\n" text += "\n" if top_winners: text += "👑 ТОП ПОБЕДИТЕЛИ\n" for i, (first_name, username, wins) in enumerate(top_winners, 1): name = f"@{username}" if username else first_name text += f"{i}. {name} - {wins} побед\n" text += "\n" if recent_users: text += "🆕 НЕДАВНИЕ РЕГИСТРАЦИИ\n" for first_name, username, created_at in recent_users: name = f"@{username}" if username else first_name text += f"• {name} - {created_at.strftime('%d.%m.%Y %H:%M')}\n" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="💿 Экспорт данных", callback_data="admin_export_participants")], [InlineKeyboardButton(text="🔃 Обновить отчет", callback_data="admin_participants_report")], [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_export_participants") async def export_participants_data(callback: CallbackQuery): """Экспорт данных участников""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return await callback.answer("📊 Генерируем отчет...", show_alert=False) async with async_session_maker() as session: users = await UserService.get_all_users(session) export_data = { "timestamp": datetime.now().isoformat(), "total_users": len(users), "users": [] } for user in users: stats = await ParticipationService.get_participant_stats(session, user.id) user_data = { "id": user.id, "telegram_id": user.telegram_id, "first_name": user.first_name, "last_name": user.last_name, "username": user.username, "created_at": user.created_at.isoformat() if user.created_at else None, "participations_count": stats["participations_count"], "wins_count": stats["wins_count"], "last_participation": stats["last_participation"].isoformat() if stats["last_participation"] else None } export_data["users"].append(user_data) # Формируем JSON для вывода import json json_data = json.dumps(export_data, ensure_ascii=False, indent=2) # Отправляем JSON как текст (в реальном боте можно отправить как файл) text = f"📊 Экспорт данных участников\n\n" text += f"Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n" text += f"Всего пользователей: {len(users)}\n\n" text += "Данные готовы к экспорту (JSON формат)\n" text += f"Размер: {len(json_data)} символов" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="📄 К отчету", callback_data="admin_participants_report")], [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_search_participants") async def start_search_participants(callback: CallbackQuery, state: FSMContext): """Начать поиск участников""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = "🔍 Поиск участников\n\n" text += "Введите имя, фамилию или username для поиска:\n\n" text += "Примеры:\n" text += "• Иван\n" text += "• username\n" text += "• Петров" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_participants")] ]) ) await state.set_state(AdminStates.participant_search) @admin_router.message(StateFilter(AdminStates.participant_search)) async def process_search_participants(message: Message, state: FSMContext): """Обработка поиска участников""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return search_term = message.text.strip() async with async_session_maker() as session: users = await UserService.search_users(session, search_term) # Получаем статистику для найденных пользователей user_stats = [] for user in users: stats = await ParticipationService.get_participant_stats(session, user.id) user_stats.append((user, stats)) await state.clear() if not user_stats: await message.answer( f"❌ Пользователи с поисковым запросом '{search_term}' не найдены", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) return text = f"🔍 Результаты поиска: '{search_term}'\n\n" text += f"Найдено: {len(users)} пользователей\n\n" for i, (user, stats) in enumerate(user_stats[:15], 1): 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" text += f" 🎫 Участий: {stats['participations_count']} | 🏆 Побед: {stats['wins_count']}\n" text += "\n" if len(users) > 15: text += f"... и еще {len(users) - 15} найденных пользователей" await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔍 Новый поиск", callback_data="admin_search_participants")], [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_bulk_add_participant") async def start_bulk_add_participant(callback: CallbackQuery, state: FSMContext): """Начать массовое добавление участников""" if not await check_admin_access(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="admin_participants")] ]) ) return text = "📥 Массовое добавление участников\n\n" text += "Выберите розыгрыш:\n\n" buttons = [] for lottery in lotteries: async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) text += f"🎯 {lottery.title} (участников: {count})\n" buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title[:35]}...", callback_data=f"admin_bulk_add_to_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_bulk_add_to_")) async def choose_users_bulk_add(callback: CallbackQuery, state: FSMContext): """Выбор пользователей для массового добавления""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(bulk_add_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) text = f"📥 Массовое добавление в: {lottery.title}\n\n" text += "Введите список Telegram ID или username через запятую:\n\n" text += "Примеры:\n" text += "• @user1, @user2, @user3\n" text += "• 123456789, 987654321, 555444333\n" text += "• @user1, 123456789, @user3" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_bulk_add_participant")] ]) ) await state.set_state(AdminStates.add_participant_bulk) @admin_router.message(StateFilter(AdminStates.add_participant_bulk)) async def process_bulk_add_participant(message: Message, state: FSMContext): """Обработка массового добавления участников""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data['bulk_add_lottery_id'] # Парсим входные данные user_inputs = [x.strip() for x in message.text.split(',') if x.strip()] telegram_ids = [] async with async_session_maker() as session: for user_input in user_inputs: try: if user_input.startswith('@'): username = user_input[1:] user = await UserService.get_user_by_username(session, username) if user: telegram_ids.append(user.telegram_id) elif user_input.isdigit(): telegram_ids.append(int(user_input)) except: continue # Массовое добавление results = await ParticipationService.add_participants_bulk(session, lottery_id, telegram_ids) lottery = await LotteryService.get_lottery(session, lottery_id) await state.clear() text = f"📥 Результат массового добавления\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += f"✅ Добавлено: {results['added']}\n" text += f"⚠️ Уже участвуют: {results['skipped']}\n" text += f"❌ Ошибок: {len(results['errors'])}\n\n" if results['details']: text += "Детали:\n" for detail in results['details'][:10]: # Первые 10 text += f"• {detail}\n" if len(results['details']) > 10: text += f"... и еще {len(results['details']) - 10} записей" await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_bulk_remove_participant") async def start_bulk_remove_participant(callback: CallbackQuery, state: FSMContext): """Начать массовое удаление участников""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_all_lotteries(session) if not lotteries: await callback.message.edit_text( "❌ Нет розыгрышей", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) return text = "📤 Массовое удаление участников\n\n" text += "Выберите розыгрыш:\n\n" buttons = [] for lottery in lotteries: async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) if count > 0: text += f"🎯 {lottery.title} (участников: {count})\n" buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title[:35]}...", callback_data=f"admin_bulk_remove_from_{lottery.id}" ) ]) if not buttons: await callback.message.edit_text( "❌ Нет розыгрышей с участниками", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) return buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_bulk_remove_from_")) async def choose_users_bulk_remove(callback: CallbackQuery, state: FSMContext): """Выбор пользователей для массового удаления""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(bulk_remove_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) text = f"📤 Массовое удаление из: {lottery.title}\n\n" text += "Введите список Telegram ID или username через запятую:\n\n" text += "Примеры:\n" text += "• @user1, @user2, @user3\n" text += "• 123456789, 987654321, 555444333\n" text += "• @user1, 123456789, @user3" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_bulk_remove_participant")] ]) ) await state.set_state(AdminStates.remove_participant_bulk) @admin_router.message(StateFilter(AdminStates.remove_participant_bulk)) async def process_bulk_remove_participant(message: Message, state: FSMContext): """Обработка массового удаления участников""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data['bulk_remove_lottery_id'] # Парсим входные данные user_inputs = [x.strip() for x in message.text.split(',') if x.strip()] telegram_ids = [] async with async_session_maker() as session: for user_input in user_inputs: try: if user_input.startswith('@'): username = user_input[1:] user = await UserService.get_user_by_username(session, username) if user: telegram_ids.append(user.telegram_id) elif user_input.isdigit(): telegram_ids.append(int(user_input)) except: continue # Массовое удаление results = await ParticipationService.remove_participants_bulk(session, lottery_id, telegram_ids) lottery = await LotteryService.get_lottery(session, lottery_id) await state.clear() text = f"📤 Результат массового удаления\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += f"✅ Удалено: {results['removed']}\n" text += f"⚠️ Не найдено: {results['not_found']}\n" text += f"❌ Ошибок: {len(results['errors'])}\n\n" if results['details']: text += "Детали:\n" for detail in results['details'][:10]: # Первые 10 text += f"• {detail}\n" if len(results['details']) > 10: text += f"... и еще {len(results['details']) - 10} записей" await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) # ====================== # ДОБАВЛЕНИЕ/УДАЛЕНИЕ УЧАСТНИКОВ В КОНКРЕТНОМ РОЗЫГРЫШЕ # ====================== @admin_router.callback_query(F.data.startswith("admin_add_to_")) async def add_participant_to_lottery(callback: CallbackQuery, state: FSMContext): """Добавление участника в конкретный розыгрыш""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(add_to_lottery_id=lottery_id) 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 text = f"➕ Добавление участника\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += "Введите данные участника:\n" text += "• Telegram ID (число)\n" text += "• @username\n" text += "• Номер счета (XX-XX-XX-XX-XX-XX-XX)" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_participants_{lottery_id}")] ]) ) await state.set_state(AdminStates.add_to_lottery_user) @admin_router.message(StateFilter(AdminStates.add_to_lottery_user)) async def process_add_to_lottery(message: Message, state: FSMContext): """Обработка добавления участника в конкретный розыгрыш""" if not is_admin(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data.get('add_to_lottery_id') user_input = message.text.strip() async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await message.answer("❌ Розыгрыш не найден") await state.clear() return # Определяем тип ввода user = None account_number = None if user_input.startswith('@'): # Username username = user_input[1:] user = await UserService.get_user_by_username(session, username) elif user_input.isdigit(): # Telegram ID telegram_id = int(user_input) user = await UserService.get_user_by_telegram_id(session, telegram_id) elif '-' in user_input: # Номер счета from src.utils.account_utils import parse_accounts_from_message accounts = parse_accounts_from_message(user_input) if accounts: account_number = accounts[0] if not user and not account_number: await message.answer( "❌ Не удалось найти пользователя или распознать счет.\n" "Пользователь должен запустить бота командой /start", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")] ]) ) await state.clear() return # Добавляем участника if user: success = await ParticipationService.add_participant(session, lottery_id, user.id) name = f"@{user.username}" if user.username else f"{user.first_name} (ID: {user.telegram_id})" else: # Добавление по номеру счета from sqlalchemy import select from ..core.models import Participation # Проверяем, не добавлен ли уже этот счет existing = await session.execute( select(Participation).where( Participation.lottery_id == lottery_id, Participation.account_number == account_number ) ) if existing.scalar_one_or_none(): await message.answer( f"⚠️ Счет {account_number} уже участвует в этом розыгрыше", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")] ]) ) await state.clear() return participation = Participation(lottery_id=lottery_id, account_number=account_number) session.add(participation) await session.commit() success = True name = f"Счет: {account_number}" await state.clear() if success: await message.answer( f"✅ Участник добавлен!\n\n" f"👤 {name}\n" f"🎯 Розыгрыш: {lottery.title}", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="➕ Добавить ещё", callback_data=f"admin_add_to_{lottery_id}")], [InlineKeyboardButton(text="👥 К участникам", callback_data=f"admin_participants_{lottery_id}")] ]) ) else: await message.answer( f"⚠️ Участник уже добавлен в этот розыгрыш", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")] ]) ) @admin_router.callback_query(F.data.startswith("admin_remove_from_")) async def remove_participant_from_lottery(callback: CallbackQuery, state: FSMContext): """Удаление участника из конкретного розыгрыша""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(remove_from_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) participants_count = await ParticipationService.get_participants_count(session, lottery_id) if not lottery: await callback.answer("❌ Розыгрыш не найден", show_alert=True) return text = f"➖ Удаление участника\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n" text += f"👥 Участников: {participants_count}\n\n" text += "Введите данные участника для удаления:\n" text += "• Telegram ID (число)\n" text += "• @username\n" text += "• Номер счета (XX-XX-XX-XX-XX-XX-XX)" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_participants_{lottery_id}")] ]) ) await state.set_state(AdminStates.remove_from_lottery_user) @admin_router.message(StateFilter(AdminStates.remove_from_lottery_user)) async def process_remove_from_lottery(message: Message, state: FSMContext): """Обработка удаления участника из конкретного розыгрыша""" if not is_admin(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data.get('remove_from_lottery_id') user_input = message.text.strip() async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await message.answer("❌ Розыгрыш не найден") await state.clear() return removed = False name = user_input if user_input.startswith('@'): # Username username = user_input[1:] user = await UserService.get_user_by_username(session, username) if user: removed = await ParticipationService.remove_participant(session, lottery_id, user.id) name = f"@{user.username}" if user.username else f"{user.first_name}" elif user_input.isdigit(): # Telegram ID telegram_id = int(user_input) user = await UserService.get_user_by_telegram_id(session, telegram_id) if user: removed = await ParticipationService.remove_participant(session, lottery_id, user.id) name = f"@{user.username}" if user and user.username else f"ID: {telegram_id}" elif '-' in user_input: # Номер счета from sqlalchemy import select, delete from ..core.models import Participation from src.utils.account_utils import parse_accounts_from_message accounts = parse_accounts_from_message(user_input) if accounts: account_number = accounts[0] result = await session.execute( select(Participation).where( Participation.lottery_id == lottery_id, Participation.account_number == account_number ) ) participation = result.scalar_one_or_none() if participation: await session.delete(participation) await session.commit() removed = True name = f"Счет: {account_number}" await state.clear() if removed: await message.answer( f"✅ Участник удалён!\n\n" f"👤 {name}\n" f"🎯 Розыгрыш: {lottery.title}", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="➖ Удалить ещё", callback_data=f"admin_remove_from_{lottery_id}")], [InlineKeyboardButton(text="👥 К участникам", callback_data=f"admin_participants_{lottery_id}")] ]) ) else: await message.answer( f"⚠️ Участник не найден в этом розыгрыше", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")] ]) ) # ====================== # ПРОВЕРКА ПОБЕДИТЕЛЕЙ И ПОВТОРНЫЙ РОЗЫГРЫШ # ====================== @admin_router.callback_query(F.data.startswith("admin_check_winners_")) async def check_winners(callback: CallbackQuery): """Проверка подтверждения победителей""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 winners = await LotteryService.get_winners(session, lottery_id) if not winners: await callback.message.edit_text( f"🏆 Проверка победителей\n\n" f"🎯 Розыгрыш: {lottery.title}\n\n" f"❌ Победители не определены. Сначала проведите розыгрыш.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")] ]) ) return text = f"🏆 Проверка победителей\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" confirmed_count = 0 unconfirmed_count = 0 for winner in winners: status = "✅" if winner.is_claimed else "⏳" if winner.is_claimed: confirmed_count += 1 else: unconfirmed_count += 1 # Определяем имя победителя if winner.account_number: name = f"Счет: {winner.account_number}" elif winner.user: name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name else: name = f"ID: {winner.user_id}" # Приз prize = lottery.prizes[winner.place - 1] if lottery.prizes and len(lottery.prizes) >= winner.place else "Не указан" text += f"{status} {winner.place} место: {name}\n" text += f" 🎁 Приз: {prize}\n" if winner.is_claimed and winner.claimed_at: text += f" 📅 Подтверждено: {winner.claimed_at.strftime('%d.%m.%Y %H:%M')}\n" text += "\n" text += f"📊 Итого: {confirmed_count} подтверждено, {unconfirmed_count} ожидает\n" buttons = [] if unconfirmed_count > 0: buttons.append([InlineKeyboardButton(text="🔄 Переиграть неподтверждённые", callback_data=f"admin_redraw_{lottery_id}")]) buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_redraw_")) async def redraw_lottery(callback: CallbackQuery): """Повторный розыгрыш для неподтверждённых призов""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 winners = await LotteryService.get_winners(session, lottery_id) # Находим неподтверждённых победителей unconfirmed = [w for w in winners if not w.is_claimed] if not unconfirmed: await callback.answer("✅ Все победители подтверждены!", show_alert=True) return # Показываем подтверждение text = f"⚠️ Повторный розыгрыш\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += f"Будут переиграны {len(unconfirmed)} неподтверждённых мест:\n\n" for winner in unconfirmed: if winner.account_number: name = f"Счет: {winner.account_number}" elif winner.user: name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name else: name = f"ID: {winner.user_id}" prize = lottery.prizes[winner.place - 1] if lottery.prizes and len(lottery.prizes) >= winner.place else "Не указан" text += f"• {winner.place} место: {name} → {prize}\n" text += "\n❗️ Эти счета будут исключены из повторного розыгрыша." await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="✅ Подтвердить переигровку", callback_data=f"admin_redraw_confirm_{lottery_id}")], [InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_check_winners_{lottery_id}")] ]) ) @admin_router.callback_query(F.data.startswith("admin_redraw_confirm_")) async def confirm_redraw(callback: CallbackQuery): """Подтверждение и выполнение повторного розыгрыша""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await callback.answer("⏳ Проводится повторный розыгрыш...", show_alert=True) async with async_session_maker() as session: from sqlalchemy import select, delete from ..core.models import Winner, Participation import random lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await callback.message.edit_text("❌ Розыгрыш не найден") return winners = await LotteryService.get_winners(session, lottery_id) # Собираем подтверждённые и неподтверждённые confirmed_winners = [w for w in winners if w.is_claimed] unconfirmed_winners = [w for w in winners if not w.is_claimed] if not unconfirmed_winners: await callback.message.edit_text( "✅ Все победители уже подтверждены!", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")] ]) ) return # Собираем исключённые счета (подтверждённые победители + бывшие неподтверждённые) excluded_accounts = set() for w in winners: if w.account_number: excluded_accounts.add(w.account_number) # Получаем всех участников, исключая уже выигравших result = await session.execute( select(Participation) .where(Participation.lottery_id == lottery_id) ) all_participations = result.scalars().all() # Фильтруем участников available_participations = [ p for p in all_participations if p.account_number not in excluded_accounts ] if not available_participations: await callback.message.edit_text( "❌ Нет доступных участников для переигровки.\n" "Все участники уже выиграли или были исключены.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")] ]) ) return # Удаляем неподтверждённых победителей for winner in unconfirmed_winners: await session.delete(winner) # Проводим розыгрыш для неподтверждённых мест new_winners_text = "" random.shuffle(available_participations) for i, old_winner in enumerate(unconfirmed_winners): if i >= len(available_participations): break new_participation = available_participations[i] # Создаём нового победителя new_winner = Winner( lottery_id=lottery_id, user_id=new_participation.user_id, account_number=new_participation.account_number, place=old_winner.place, is_manual=False, is_claimed=False ) session.add(new_winner) # Исключаем из следующих итераций if new_participation.account_number: excluded_accounts.add(new_participation.account_number) prize = lottery.prizes[old_winner.place - 1] if lottery.prizes and len(lottery.prizes) >= old_winner.place else "Приз" name = new_participation.account_number or f"ID: {new_participation.user_id}" new_winners_text += f"🏆 {old_winner.place} место: {name} → {prize}\n" await session.commit() # Отправляем уведомления новым победителям from ..utils.notifications import notify_winners_async try: await notify_winners_async(callback.bot, session, lottery_id) except Exception as e: logger.error(f"Ошибка при отправке уведомлений: {e}") text = f"🎉 Повторный розыгрыш завершён!\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += f"Новые победители:\n{new_winners_text}\n" text += "✅ Уведомления отправлены новым победителям" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="✅ Проверить победителей", callback_data=f"admin_check_winners_{lottery_id}")], [InlineKeyboardButton(text="🔙 К розыгрышу", callback_data=f"admin_lottery_detail_{lottery_id}")] ]) ) # ====================== # УДАЛЕНИЕ РОЗЫГРЫША # ====================== @admin_router.callback_query(F.data.startswith("admin_del_lottery_")) async def delete_lottery_confirm(callback: CallbackQuery): """Подтверждение удаления розыгрыша""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 participants_count = await ParticipationService.get_participants_count(session, lottery_id) winners = await LotteryService.get_winners(session, lottery_id) text = f"⚠️ Удаление розыгрыша\n\n" text += f"🎯 Название: {lottery.title}\n" text += f"👥 Участников: {participants_count}\n" text += f"🏆 Победителей: {len(winners)}\n\n" text += "❗️ Это действие необратимо!\n" text += "Все данные об участниках и победителях будут удалены." await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🗑️ Да, удалить", callback_data=f"admin_del_lottery_yes_{lottery_id}")], [InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_lottery_detail_{lottery_id}")] ]) ) @admin_router.callback_query(F.data.startswith("admin_del_lottery_yes_")) async def delete_lottery_execute(callback: CallbackQuery): """Выполнение удаления розыгрыша""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: from sqlalchemy import delete as sql_delete from ..core.models import Winner, Participation lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await callback.answer("❌ Розыгрыш не найден", show_alert=True) return lottery_title = lottery.title # Удаляем победителей await session.execute(sql_delete(Winner).where(Winner.lottery_id == lottery_id)) # Удаляем участников await session.execute(sql_delete(Participation).where(Participation.lottery_id == lottery_id)) # Удаляем розыгрыш await session.delete(lottery) await session.commit() await callback.message.edit_text( f"✅ Розыгрыш удалён\n\n" f"🗑️ {lottery_title}", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="📋 К списку розыгрышей", callback_data="admin_list_all_lotteries")] ]) ) # ====================== # МАССОВОЕ УПРАВЛЕНИЕ УЧАСТНИКАМИ ПО СЧЕТАМ # ====================== @admin_router.callback_query(F.data == "admin_bulk_add_accounts") async def start_bulk_add_accounts(callback: CallbackQuery, state: FSMContext): """Начать массовое добавление участников по номерам счетов""" if not await check_admin_access(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="admin_participants")] ]) ) return text = "🏦 Массовое добавление по номерам счетов\n\n" text += "Выберите розыгрыш:\n\n" buttons = [] for lottery in lotteries: async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) text += f"🎯 {lottery.title} (участников: {count})\n" buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title[:35]}...", callback_data=f"admin_bulk_add_accounts_to_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_bulk_add_accounts_to_")) async def choose_accounts_bulk_add(callback: CallbackQuery, state: FSMContext): """Выбор номеров счетов для массового добавления""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(bulk_add_accounts_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) text = f"🏦 Массовое добавление в: {lottery.title}\n\n" text += "Введите список номеров счетов через запятую или новую строку:\n\n" text += "Примеры:\n" text += "• 12-34-56-78-90-12-34\n" text += "• 98-76-54-32-10-98-76, 11-22-33-44-55-66-77\n" text += "• 12345678901234 (будет отформатирован)\n\n" text += "Формат: XX-XX-XX-XX-XX-XX-XX\n" text += "Всего 7 пар цифр разделенных дефисами" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_bulk_add_accounts")] ]) ) await state.set_state(AdminStates.add_participant_bulk_accounts) @admin_router.message(StateFilter(AdminStates.add_participant_bulk_accounts)) async def process_bulk_add_accounts(message: Message, state: FSMContext): """Обработка массового добавления участников по номерам счетов""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data['bulk_add_accounts_lottery_id'] # Используем функцию парсинга из account_utils для корректной обработки формата "КАРТА СЧЕТ" from ..utils.account_utils import parse_accounts_from_message account_inputs = parse_accounts_from_message(message.text) async with async_session_maker() as session: # Массовое добавление по номерам счетов results = await ParticipationService.add_participants_by_accounts_bulk(session, lottery_id, account_inputs) lottery = await LotteryService.get_lottery(session, lottery_id) await state.clear() text = f"🏦 Результат массового добавления по счетам\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += f"✅ Добавлено: {results['added']}\n" text += f"⚠️ Уже участвуют: {results['skipped']}\n" text += f"🚫 Неверных форматов: {len(results['invalid_accounts'])}\n" text += f"❌ Ошибок: {len(results['errors'])}\n\n" if results['details']: text += "✅ Успешно добавлены:\n" for detail in results['details'][:7]: # Первые 7 text += f"• {detail}\n" if len(results['details']) > 7: text += f"... и еще {len(results['details']) - 7} записей\n\n" if results['invalid_accounts']: text += "\n🚫 Неверные форматы:\n" for invalid in results['invalid_accounts'][:5]: text += f"• {invalid}\n" if len(results['invalid_accounts']) > 5: text += f"... и еще {len(results['invalid_accounts']) - 5} номеров\n" if results['errors']: text += "\n❌ Ошибки:\n" for error in results['errors'][:3]: text += f"• {error}\n" if len(results['errors']) > 3: text += f"... и еще {len(results['errors']) - 3} ошибок\n" await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_bulk_remove_accounts") async def start_bulk_remove_accounts(callback: CallbackQuery, state: FSMContext): """Начать массовое удаление участников по номерам счетов""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_all_lotteries(session) if not lotteries: await callback.message.edit_text( "❌ Нет розыгрышей", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) return text = "🏦 Массовое удаление по номерам счетов\n\n" text += "Выберите розыгрыш:\n\n" buttons = [] for lottery in lotteries: async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) text += f"🎯 {lottery.title} (участников: {count})\n" buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title[:35]}...", callback_data=f"admin_bulk_remove_accounts_from_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_bulk_remove_accounts_from_")) async def choose_accounts_bulk_remove(callback: CallbackQuery, state: FSMContext): """Выбор номеров счетов для массового удаления""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(bulk_remove_accounts_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) text = f"🏦 Массовое удаление из: {lottery.title}\n\n" text += "Введите список номеров счетов через запятую или новую строку:\n\n" text += "Примеры:\n" text += "• 12-34-56-78-90-12-34\n" text += "• 98-76-54-32-10-98-76, 11-22-33-44-55-66-77\n" text += "• 12345678901234 (будет отформатирован)\n\n" text += "Формат: XX-XX-XX-XX-XX-XX-XX" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_bulk_remove_accounts")] ]) ) await state.set_state(AdminStates.remove_participant_bulk_accounts) @admin_router.message(StateFilter(AdminStates.remove_participant_bulk_accounts)) async def process_bulk_remove_accounts(message: Message, state: FSMContext): """Обработка массового удаления участников по номерам счетов""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return data = await state.get_data() lottery_id = data['bulk_remove_accounts_lottery_id'] # Используем функцию парсинга из account_utils для корректной обработки формата "КАРТА СЧЕТ" from ..utils.account_utils import parse_accounts_from_message account_inputs = parse_accounts_from_message(message.text) async with async_session_maker() as session: # Массовое удаление по номерам счетов results = await ParticipationService.remove_participants_by_accounts_bulk(session, lottery_id, account_inputs) lottery = await LotteryService.get_lottery(session, lottery_id) await state.clear() text = f"🏦 Результат массового удаления по счетам\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += f"✅ Удалено: {results['removed']}\n" text += f"⚠️ Не найдено: {results['not_found']}\n" text += f"🚫 Неверных форматов: {len(results['invalid_accounts'])}\n" text += f"❌ Ошибок: {len(results['errors'])}\n\n" if results['details']: text += "✅ Успешно удалены:\n" for detail in results['details'][:7]: # Первые 7 text += f"• {detail}\n" if len(results['details']) > 7: text += f"... и еще {len(results['details']) - 7} записей\n\n" if results['invalid_accounts']: text += "\n🚫 Неверные форматы:\n" for invalid in results['invalid_accounts'][:5]: text += f"• {invalid}\n" if len(results['invalid_accounts']) > 5: text += f"... и еще {len(results['invalid_accounts']) - 5} номеров\n" if results['errors']: text += "\n❌ Ошибки:\n" for error in results['errors'][:3]: text += f"• {error}\n" if len(results['errors']) > 3: text += f"... и еще {len(results['errors']) - 3} ошибок\n" await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👥 К управлению участниками", callback_data="admin_participants")] ]) ) # ====================== # ДОПОЛНИТЕЛЬНЫЕ ХЭНДЛЕРЫ УЧАСТНИКОВ # ====================== @admin_router.callback_query(F.data == "admin_participants_by_lottery") async def show_participants_by_lottery(callback: CallbackQuery): """Показать участников по розыгрышам""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_all_lotteries(session) if not lotteries: await callback.message.edit_text( "❌ Нет розыгрышей", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) return text = "📊 Участники по розыгрышам\n\n" for lottery in lotteries[:15]: # Показываем первые 15 async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) status = "🟢" if getattr(lottery, 'is_active', True) else "🔴" text += f"{status} {lottery.title}: {count} участников\n" if len(lotteries) > 15: text += f"\n... и еще {len(lotteries) - 15} розыгрышей" buttons = [] for lottery in lotteries[:10]: # Кнопки для первых 10 buttons.append([ InlineKeyboardButton( text=f"👥 {lottery.title[:30]}...", callback_data=f"admin_participants_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data == "admin_participants_report") async def show_participants_report(callback: CallbackQuery): """Отчет по участникам""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import func, select from ..core.models import User, Participation, Lottery # Общая статистика по участникам total_participants = await session.scalar( select(func.count(func.distinct(User.id))) .select_from(User) .join(Participation) ) total_participations = await session.scalar(select(func.count(Participation.id))) # Топ активных участников top_participants = await session.execute( select( User.first_name, User.username, User.account_number, func.count(Participation.id).label('participations') ) .join(Participation) .group_by(User.id) .order_by(func.count(Participation.id).desc()) .limit(10) ) top_participants = top_participants.fetchall() # Участники с аккаунтами vs без users_with_accounts = await session.scalar( select(func.count(User.id)).where(User.account_number.isnot(None)) ) users_without_accounts = await session.scalar( select(func.count(User.id)).where(User.account_number.is_(None)) ) text = "📈 Отчет по участникам\n\n" text += f"👥 Всего уникальных участников: {total_participants}\n" text += f"📊 Всего участий: {total_participations}\n" text += f"🏦 С номерами счетов: {users_with_accounts}\n" text += f"🆔 Только Telegram ID: {users_without_accounts}\n\n" if top_participants: text += "🏆 Топ-10 активных участников:\n" for i, (name, username, account, count) in enumerate(top_participants, 1): display_name = f"@{username}" if username else name if account: display_name += f" ({account[-7:]})" # Последние 7 символов счёта text += f"{i}. {display_name} - {count} участий\n" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔃 Обновить", callback_data="admin_participants_report")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")] ]) ) @admin_router.callback_query(F.data == "admin_edit_lottery") async def start_edit_lottery(callback: CallbackQuery, state: FSMContext): """Начать редактирование розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_all_lotteries(session) if not lotteries: await callback.message.edit_text( "❌ Нет розыгрышей для редактирования", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")] ]) ) return text = "📝 Редактирование розыгрыша\n\n" text += "Выберите розыгрыш для редактирования:\n\n" buttons = [] for lottery in lotteries[:10]: # Первые 10 розыгрышей status = "🟢" if getattr(lottery, 'is_active', True) else "🔴" text += f"{status} {lottery.title}\n" buttons.append([ InlineKeyboardButton( text=f"📝 {lottery.title[:30]}...", callback_data=f"admin_edit_lottery_select_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_edit_field_")) async def handle_edit_field(callback: CallbackQuery, state: FSMContext): """Обработка выбора поля для редактирования""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return # Парсим callback_data: admin_edit_field_{lottery_id}_{field_name} parts = callback.data.split("_") if len(parts) < 5: await callback.answer("❌ Неверный формат данных", show_alert=True) return lottery_id = int(parts[3]) # admin_edit_field_{lottery_id}_... field_name = "_".join(parts[4:]) # Всё после lottery_id это имя поля await state.update_data(edit_lottery_id=lottery_id, edit_field=field_name) # Определяем, что редактируем if field_name == "title": text = "📝 Введите новое название розыгрыша:" await state.set_state(AdminStates.lottery_title) elif field_name == "description": text = "📄 Введите новое описание розыгрыша:" await state.set_state(AdminStates.lottery_description) elif field_name == "prizes": text = "🎁 Введите новый список призов (каждый приз с новой строки):" await state.set_state(AdminStates.lottery_prizes) else: await callback.answer("❌ Неизвестное поле", show_alert=True) return await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")] ]) ) await callback.answer() @admin_router.callback_query(F.data.startswith("admin_edit_")) async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext): """Редирект на редактирование розыгрыша из детального просмотра""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return # Извлекаем lottery_id из callback_data (формат admin_edit_123) parts = callback.data.split("_") if len(parts) == 3: # admin_edit_123 lottery_id = int(parts[2]) # Напрямую вызываем обработчик вместо подмены 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) @admin_router.callback_query(F.data.startswith("admin_edit_lottery_select_")) async def choose_edit_field(callback: CallbackQuery, state: FSMContext): """Выбор поля для редактирования""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(edit_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) text = f"📝 Редактирование: {lottery.title}\n\n" text += "Выберите, что хотите изменить:\n\n" text += f"📝 Название: {lottery.title}\n" text += f"📄 Описание: {lottery.description[:50]}{'...' if len(lottery.description) > 50 else ''}\n" text += f"🎁 Призы: {len(getattr(lottery, 'prizes', []))} шт.\n" text += f"🎭 Отображение: {getattr(lottery, 'winner_display_type', 'username')}\n" text += f"🟢 Активен: {'Да' if getattr(lottery, 'is_active', True) else 'Нет'}" buttons = [ [InlineKeyboardButton(text="📝 Изменить название", callback_data=f"admin_edit_field_{lottery_id}_title")], [InlineKeyboardButton(text="📄 Изменить описание", callback_data=f"admin_edit_field_{lottery_id}_description")], [InlineKeyboardButton(text="🎁 Изменить призы", callback_data=f"admin_edit_field_{lottery_id}_prizes")], [ InlineKeyboardButton(text="⏸️ Деактивировать" if getattr(lottery, 'is_active', True) else "▶️ Активировать", callback_data=f"admin_toggle_active_{lottery_id}"), InlineKeyboardButton(text="👁️ Тип отображения", callback_data=f"admin_set_display_{lottery_id}") ], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_edit_lottery")] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_toggle_active_")) async def toggle_lottery_active(callback: CallbackQuery, state: FSMContext): """Переключить активность розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) current_active = getattr(lottery, 'is_active', True) # Переключаем статус success = await LotteryService.set_lottery_active(session, lottery_id, not current_active) if success: new_status = "активирован" if not current_active else "деактивирован" await callback.answer(f"✅ Розыгрыш {new_status}!", show_alert=True) else: await callback.answer("❌ Ошибка изменения статуса", show_alert=True) # Обновляем отображение await choose_edit_field(callback, state) @admin_router.callback_query(F.data == "admin_finish_lottery") async def start_finish_lottery(callback: CallbackQuery): """Завершить розыгрыш""" if not await check_admin_access(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="admin_lotteries")] ]) ) return text = "🏁 Завершение розыгрыша\n\n" text += "Выберите розыгрыш для завершения:\n\n" buttons = [] for lottery in lotteries: async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) text += f"🎯 {lottery.title} ({count} участников)\n" buttons.append([ InlineKeyboardButton( text=f"🏁 {lottery.title[:30]}...", callback_data=f"admin_confirm_finish_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_confirm_finish_")) async def confirm_finish_lottery(callback: CallbackQuery): """Подтвердить завершение розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) count = await ParticipationService.get_participants_count(session, lottery_id) text = f"🏁 Завершение розыгрыша\n\n" text += f"🎯 {lottery.title}\n" text += f"👥 Участников: {count}\n\n" text += "⚠️ После завершения розыгрыш станет неактивным и новые участники не смогут присоединиться.\n\n" text += "Вы уверены?" buttons = [ [ InlineKeyboardButton(text="✅ Да, завершить", callback_data=f"admin_do_finish_{lottery_id}"), InlineKeyboardButton(text="❌ Отмена", callback_data="admin_finish_lottery") ] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_do_finish_")) async def do_finish_lottery(callback: CallbackQuery): """Выполнить завершение розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: success = await LotteryService.complete_lottery(session, lottery_id) lottery = await LotteryService.get_lottery(session, lottery_id) if success: text = f"✅ Розыгрыш завершён!\n\n" text += f"🎯 {lottery.title}\n" text += f"📅 Завершён: {datetime.now().strftime('%d.%m.%Y %H:%M')}" await callback.answer("✅ Розыгрыш завершён!", show_alert=True) else: text = "❌ Ошибка завершения розыгрыша" await callback.answer("❌ Ошибка завершения", show_alert=True) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🎰 К управлению розыгрышами", callback_data="admin_lotteries")] ]) ) @admin_router.callback_query(F.data == "admin_delete_lottery") async def start_delete_lottery(callback: CallbackQuery): """Удаление розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_all_lotteries(session) if not lotteries: await callback.message.edit_text( "❌ Нет розыгрышей для удаления", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")] ]) ) return text = "🗑️ Удаление розыгрыша\n\n" text += "⚠️ ВНИМАНИЕ! Это действие нельзя отменить!\n\n" text += "Выберите розыгрыш для удаления:\n\n" buttons = [] for lottery in lotteries[:10]: status = "🟢" if getattr(lottery, 'is_active', True) else "🔴" async with async_session_maker() as session: count = await ParticipationService.get_participants_count(session, lottery.id) text += f"{status} {lottery.title} ({count} участников)\n" buttons.append([ InlineKeyboardButton( text=f"🗑️ {lottery.title[:25]}...", callback_data=f"admin_confirm_delete_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_confirm_delete_")) async def confirm_delete_lottery(callback: CallbackQuery): """Подтвердить удаление розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) count = await ParticipationService.get_participants_count(session, lottery_id) text = f"🗑️ Удаление розыгрыша\n\n" text += f"🎯 {lottery.title}\n" text += f"👥 Участников: {count}\n\n" text += "⚠️ ВНИМАНИЕ!\n" text += "• Все данные о розыгрыше будут удалены навсегда\n" text += "• Все участия в розыгрыше будут удалены\n" text += "• Это действие НЕЛЬЗЯ отменить!\n\n" text += "Вы ТОЧНО уверены?" buttons = [ [ InlineKeyboardButton(text="🗑️ ДА, УДАЛИТЬ", callback_data=f"admin_do_delete_{lottery_id}"), InlineKeyboardButton(text="❌ ОТМЕНА", callback_data="admin_delete_lottery") ] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_do_delete_")) async def do_delete_lottery(callback: CallbackQuery): """Выполнить удаление розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) lottery_title = lottery.title success = await LotteryService.delete_lottery(session, lottery_id) if success: text = f"✅ Розыгрыш удалён!\n\n" text += f"🎯 {lottery_title}\n" text += f"📅 Удалён: {datetime.now().strftime('%d.%m.%Y %H:%M')}" await callback.answer("✅ Розыгрыш удалён!", show_alert=True) else: text = "❌ Ошибка удаления розыгрыша" await callback.answer("❌ Ошибка удаления", show_alert=True) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🎰 К управлению розыгрышами", callback_data="admin_lotteries")] ]) ) # ====================== # УПРАВЛЕНИЕ ПОБЕДИТЕЛЯМИ # ====================== @admin_router.callback_query(F.data == "admin_winners") async def show_winner_management(callback: CallbackQuery): """Управление победителями""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = "👑 Управление победителями\n\n" text += "Здесь вы можете устанавливать предопределенных победителей и проводить розыгрыши.\n\n" text += "Выберите действие:" await callback.message.edit_text(text, reply_markup=get_winner_management_keyboard()) @admin_router.callback_query(F.data == "admin_set_manual_winner") async def start_set_manual_winner(callback: CallbackQuery, state: FSMContext): """Начать установку ручного победителя""" if not await check_admin_access(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="admin_winners")] ]) ) return text = "👑 Установка предопределенного победителя\n\n" text += "Выберите розыгрыш:\n\n" buttons = [] for lottery in lotteries: text += f"🎯 {lottery.title} (ID: {lottery.id})\n" # Показываем уже установленных ручных победителей if lottery.manual_winners: text += f" 👑 Установлены места: {', '.join(lottery.manual_winners.keys())}\n" text += "\n" buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title[:30]}...", callback_data=f"admin_choose_winner_lottery_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winners")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_set_winner_")) async def handle_set_winner_from_lottery(callback: CallbackQuery, state: FSMContext): """Обработчик для кнопки 'Установить победителя' из карточки розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) # Напрямую вызываем обработчик вместо подмены callback_data await state.update_data(winner_lottery_id=lottery_id) await choose_winner_place(callback, state) @admin_router.callback_query(F.data.startswith("admin_choose_winner_lottery_")) async def choose_winner_place(callback: CallbackQuery, state: FSMContext): """Выбор места для победителя""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 5 text = f"👑 Установка победителя\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" if lottery.manual_winners: text += f"Уже установлены места: {', '.join(lottery.manual_winners.keys())}\n\n" text += f"Введите номер места (1-{num_prizes}):" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_set_manual_winner")] ]) ) await state.set_state(AdminStates.set_winner_place) @admin_router.message(StateFilter(AdminStates.set_winner_place)) async def process_winner_place(message: Message, state: FSMContext): """Обработка места победителя""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return try: place = int(message.text) if place < 1: raise ValueError except ValueError: await message.answer("❌ Введите корректный номер места (положительное число)") return data = await state.get_data() lottery_id = data['lottery_id'] # Проверяем, не занято ли место async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) if lottery.manual_winners and str(place) in lottery.manual_winners: existing_id = lottery.manual_winners[str(place)] existing_user = await UserService.get_user_by_telegram_id(session, existing_id) name = existing_user.username if existing_user and existing_user.username else str(existing_id) await message.answer( f"⚠️ Место {place} уже занято пользователем @{name}\n" f"Введите другой номер места:" ) return await state.update_data(place=place) text = f"👑 Установка победителя на {place} место\n" text += f"🎯 Розыгрыш: {lottery.title}\n\n" text += ( "Введите один из вариантов:\n" "• Telegram ID (числовой ID)\n" "• Username (с @ или без)\n" "• Номер счета (формат: XX-XX-XX-XX-XX-XX-XX)" ) await message.answer(text) await state.set_state(AdminStates.set_winner_user) @admin_router.message(StateFilter(AdminStates.set_winner_user)) async def process_winner_user(message: Message, state: FSMContext): """Обработка пользователя-победителя (по ID, username или номеру счета)""" if not await check_admin_access(message.from_user.id): await message.answer("❌ Недостаточно прав") return user_input = message.text.strip() # Проверяем, это номер счета (формат XX-XX-XX-XX-XX-XX-XX) is_account = '-' in user_input and len(user_input.split('-')) >= 5 if is_account: # Обработка по номеру счета from ..core.registration_services import AccountService async with async_session_maker() as session: # Ищем владельца счета owner = await AccountService.get_account_owner(session, user_input) if not owner: await message.answer( f"❌ Счет {user_input} не найден в системе.\n" f"Проверьте правильность номера счета." ) return telegram_id = owner.telegram_id display_name = owner.nickname if owner.nickname else (f"@{owner.username}" if owner.username else owner.first_name) else: # Обработка по ID или username # Пробуем определить, это ID или username if user_input.startswith('@'): user_input = user_input[1:] # Убираем @ is_username = True elif user_input.isdigit(): is_username = False telegram_id = int(user_input) else: is_username = True async with async_session_maker() as session: if is_username: # Поиск по username from sqlalchemy import select from ..core.models import User result = await session.execute( select(User).where(User.username == user_input) ) user = result.scalar_one_or_none() if not user: await message.answer("❌ Пользователь с таким username не найден") return telegram_id = user.telegram_id display_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) else: user = await UserService.get_user_by_telegram_id(session, telegram_id) if not user: await message.answer("❌ Пользователь с таким ID не найден") return display_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) 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"👤 Пользователь: {display_name}\n" f"🆔 ID: {telegram_id}\n" + (f"💳 Счет: {user_input}\n" if is_account else "") + f"\n⚡ При проведении розыгрыша этот пользователь автоматически займет {data['place']} место.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🏆 К управлению победителями", callback_data="admin_winners")] ]) ) else: await message.answer( "❌ Не удалось установить победителя. Проверьте данные.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🏆 К управлению победителями", callback_data="admin_winners")] ]) ) @admin_router.callback_query(F.data == "admin_list_winners") async def list_all_winners(callback: CallbackQuery): """Список всех победителей""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: # Получаем все розыгрыши с победителями from sqlalchemy import select result = await session.execute( select(Winner) .options(selectinload(Winner.user), selectinload(Winner.lottery)) .order_by(Winner.created_at.desc()) .limit(50) ) winners = result.scalars().all() if not winners: await callback.message.edit_text( "📋 Список победителей пуст\n\nПока не было проведено ни одного розыгрыша.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winners")] ]) ) return text = "👑 Список победителей\n\n" # Группируем победителей по розыгрышам lotteries_dict = {} for winner in winners: lottery_id = winner.lottery_id if lottery_id not in lotteries_dict: lotteries_dict[lottery_id] = { 'lottery': winner.lottery, 'winners': [] } lotteries_dict[lottery_id]['winners'].append(winner) # Выводим информацию for lottery_id, data in list(lotteries_dict.items())[:10]: lottery = data['lottery'] winners_list = data['winners'] text += f"🎯 {lottery.title}\n" text += f"📅 {lottery.created_at.strftime('%d.%m.%Y')}\n" for winner in sorted(winners_list, key=lambda w: w.place): username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name manual_mark = "🔧" if winner.is_manual else "🎲" text += f" {manual_mark} {winner.place} место: {username} - {winner.prize}\n" text += "\n" if len(lotteries_dict) > 10: text += f"\n... и ещё {len(lotteries_dict) - 10} розыгрышей" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔃 Обновить", callback_data="admin_list_winners")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winners")] ]) ) @admin_router.callback_query(F.data == "admin_edit_winner") async def edit_winner_start(callback: CallbackQuery): """Начало редактирования победителя""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import select result = await session.execute( select(Lottery) .join(Winner) .distinct() .order_by(Lottery.created_at.desc()) .limit(20) ) lotteries_with_winners = result.scalars().all() if not lotteries_with_winners: await callback.message.edit_text( "❌ Нет розыгрышей с победителями для редактирования", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winners")] ]) ) return text = "📝 Редактировать победителя\n\nВыберите розыгрыш:\n\n" buttons = [] for lottery in lotteries_with_winners: buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title}", callback_data=f"admin_edit_winner_lottery_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winners")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons) ) @admin_router.callback_query(F.data.startswith("admin_edit_winner_lottery_")) async def edit_winner_select_place(callback: CallbackQuery, state: FSMContext): """Выбор места победителя для редактирования""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) winners = await LotteryService.get_winners(session, lottery_id) if not winners: await callback.answer("❌ Нет победителей для редактирования", show_alert=True) return text = f"📝 Редактировать победителя\n\n🎯 {lottery.title}\n\nВыберите место:\n\n" buttons = [] for winner in winners: username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name buttons.append([ InlineKeyboardButton( text=f"🏆 {winner.place} место: {username} - {winner.prize}", callback_data=f"admin_edit_winner_id_{winner.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_edit_winner")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_edit_winner_id_")) async def edit_winner_details(callback: CallbackQuery): """Показать детали победителя (пока просто информационное сообщение)""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return winner_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: from sqlalchemy import select result = await session.execute( select(Winner) .options(selectinload(Winner.user), selectinload(Winner.lottery)) .where(Winner.id == winner_id) ) winner = result.scalar_one_or_none() if not winner: await callback.answer("❌ Победитель не найден", show_alert=True) return username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name manual_mark = "🔧 Установлен вручную" if winner.is_manual else "🎲 Выбран случайно" text = f"📝 Информация о победителе\n\n" text += f"🎯 Розыгрыш: {winner.lottery.title}\n" text += f"🏆 Место: {winner.place}\n" text += f"💰 Приз: {winner.prize}\n" text += f"👤 Пользователь: {username}\n" text += f"🆔 ID: {winner.user.telegram_id}\n" text += f"📊 Тип: {manual_mark}\n" text += f"📅 Дата: {winner.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" text += "ℹ️ Редактирование победителей доступно через удаление и повторное добавление." await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data=f"admin_edit_winner_lottery_{winner.lottery_id}")] ]) ) @admin_router.callback_query(F.data == "admin_remove_winner") async def remove_winner_start(callback: CallbackQuery): """Начало удаления победителя""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import select result = await session.execute( select(Lottery) .join(Winner) .distinct() .order_by(Lottery.created_at.desc()) .limit(20) ) lotteries_with_winners = result.scalars().all() if not lotteries_with_winners: await callback.message.edit_text( "❌ Нет розыгрышей с победителями для удаления", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winners")] ]) ) return text = "❌ Удалить победителя\n\nВыберите розыгрыш:\n\n" buttons = [] for lottery in lotteries_with_winners: buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title}", callback_data=f"admin_remove_winner_lottery_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winners")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons) ) @admin_router.callback_query(F.data.startswith("admin_remove_winner_lottery_")) async def remove_winner_select_place(callback: CallbackQuery): """Выбор победителя для удаления""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) winners = await LotteryService.get_winners(session, lottery_id) if not winners: await callback.answer("❌ Нет победителей для удаления", show_alert=True) return text = f"❌ Удалить победителя\n\n🎯 {lottery.title}\n\nВыберите победителя для удаления:\n\n" buttons = [] for winner in winners: username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name buttons.append([ InlineKeyboardButton( text=f"🏆 {winner.place} место: {username} - {winner.prize}", callback_data=f"admin_confirm_remove_winner_{winner.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_remove_winner")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_confirm_remove_winner_")) async def confirm_remove_winner(callback: CallbackQuery): """Подтверждение удаления победителя""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return winner_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: from sqlalchemy import select result = await session.execute( select(Winner) .options(selectinload(Winner.user), selectinload(Winner.lottery)) .where(Winner.id == winner_id) ) winner = result.scalar_one_or_none() if not winner: await callback.answer("❌ Победитель не найден", show_alert=True) return username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name text = f"⚠️ Подтверждение удаления\n\n" text += f"Вы действительно хотите удалить победителя?\n\n" text += f"🎯 Розыгрыш: {winner.lottery.title}\n" text += f"🏆 Место: {winner.place}\n" text += f"👤 Пользователь: {username}\n" text += f"💰 Приз: {winner.prize}\n\n" text += "⚠️ Это действие необратимо!" buttons = [ [ InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"admin_do_remove_winner_{winner_id}"), InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_remove_winner_lottery_{winner.lottery_id}") ] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_do_remove_winner_")) async def do_remove_winner(callback: CallbackQuery): """Выполнение удаления победителя""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return winner_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: from sqlalchemy import select, delete # Получаем информацию о победителе перед удалением result = await session.execute( select(Winner) .options(selectinload(Winner.user), selectinload(Winner.lottery)) .where(Winner.id == winner_id) ) winner = result.scalar_one_or_none() if not winner: await callback.answer("❌ Победитель не найден", show_alert=True) return lottery_id = winner.lottery_id username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name # Удаляем победителя await session.execute(delete(Winner).where(Winner.id == winner_id)) await session.commit() await callback.message.edit_text( f"✅ Победитель удалён!\n\n" f"👤 {username}\n" f"🏆 Место: {winner.place}\n" f"💰 Приз: {winner.prize}", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🏆 К управлению победителями", callback_data="admin_winners")] ]) ) # ====================== # ПРОВЕДЕНИЕ РОЗЫГРЫША # ====================== @admin_router.callback_query(F.data == "admin_conduct_draw") async def choose_lottery_for_draw(callback: CallbackQuery): """Выбор розыгрыша для проведения""" if not await check_admin_access(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, limit=100) if not lotteries: await callback.answer("Нет активных розыгрышей", show_alert=True) 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" if lottery.is_completed: text += f" ✅ Завершён\n" text += "\n" buttons.append([ InlineKeyboardButton( text=f"🎲 {lottery.title[:30]}...", callback_data=f"admin_conduct_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_draws")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.regexp(r"^admin_conduct_\d+$")) async def conduct_lottery_draw_confirm(callback: CallbackQuery): """Запрос подтверждения проведения розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return 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 if lottery.is_completed: await callback.answer("Розыгрыш уже завершён", show_alert=True) return participants_count = await ParticipationService.get_participants_count(session, lottery_id) if participants_count == 0: await callback.answer("Нет участников для розыгрыша", show_alert=True) return # Подсчёт призов prizes_count = len(lottery.prizes) if lottery.prizes else 0 # Формируем сообщение с подтверждением text = f"⚠️ *Подтверждение проведения розыгрыша*\n\n" text += f"🎲 *Розыгрыш:* {lottery.title}\n" text += f"👥 *Участников:* {participants_count}\n" text += f"🏆 *Призов:* {prizes_count}\n\n" if lottery.prizes: text += "*Призы:*\n" for i, prize in enumerate(lottery.prizes, 1): text += f"{i}. {prize}\n" text += "\n" text += "❗️ *Внимание:* После проведения розыгрыша результаты нельзя будет изменить!\n\n" text += "Продолжить?" confirm_callback = f"admin_conduct_confirmed_{lottery_id}" logger.info(f"Создаём кнопку подтверждения с callback_data='{confirm_callback}'") buttons = [ [InlineKeyboardButton(text="✅ Да, провести розыгрыш", callback_data=confirm_callback)], [InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_lottery_{lottery_id}")] ] await safe_edit_message(callback, text, InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_conduct_confirmed_")) async def conduct_lottery_draw(callback: CallbackQuery): """Проведение розыгрыша после подтверждения""" logger.info(f"🎯 conduct_lottery_draw HANDLER TRIGGERED! data={callback.data}, user={callback.from_user.id}") logger.info(f"conduct_lottery_draw вызван: callback.data={callback.data}, user_id={callback.from_user.id}") if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) logger.info(f"Извлечен lottery_id={lottery_id}") async with async_session_maker() as session: logger.info(f"Создана сессия БД") lottery = await LotteryService.get_lottery(session, lottery_id) logger.info(f"Получен lottery: {lottery.title if lottery else None}, is_completed={lottery.is_completed if lottery else None}") if not lottery: await callback.answer("Розыгрыш не найден", show_alert=True) return if lottery.is_completed: await callback.answer("Розыгрыш уже завершён", show_alert=True) return participants_count = await ParticipationService.get_participants_count(session, lottery_id) if participants_count == 0: await callback.answer("Нет участников для розыгрыша", show_alert=True) return # Показываем индикатор загрузки await callback.answer("⏳ Проводится розыгрыш...", show_alert=True) # Проводим розыгрыш через сервис logger.info(f"Начинаем проведение розыгрыша {lottery_id}") try: winners_dict = await LotteryService.conduct_draw(session, lottery_id) logger.info(f"Розыгрыш {lottery_id} проведён, победителей: {len(winners_dict)}") except Exception as e: logger.error(f"Ошибка при проведении розыгрыша {lottery_id}: {e}", exc_info=True) await session.rollback() await callback.answer(f"❌ Ошибка: {e}", show_alert=True) return if winners_dict: # Коммитим изменения в БД await session.commit() logger.info(f"Изменения закоммичены для розыгрыша {lottery_id}") # Отправляем уведомления победителям from ..utils.notifications import notify_winners_async try: await notify_winners_async(callback.bot, session, lottery_id) logger.info(f"Уведомления отправлены для розыгрыша {lottery_id}") except Exception as e: logger.error(f"Ошибка при отправке уведомлений: {e}") # Отправляем результаты розыгрыша всем участникам (кроме победителей) try: await _notify_all_participants_about_results(callback.bot, session, lottery_id, winners_dict) logger.info(f"Результаты розыгрыша разосланы всем участникам {lottery_id}") except Exception as e: logger.error(f"Ошибка при рассылке результатов: {e}") # Получаем победителей из базы winners = await LotteryService.get_winners(session, lottery_id) text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n" text += "🏆 Победители:\n" for winner in winners: if winner.account_number: text += f"{winner.place} место: {winner.account_number}\n" elif winner.user: username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name text += f"{winner.place} место: {username}\n" else: text += f"{winner.place} место: ID {winner.user_id}\n" text += "\n✅ Уведомления отправлены победителям" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ К розыгрышам", callback_data="admin_draws")] ]) ) else: await callback.answer("Ошибка при проведении розыгрыша", show_alert=True) # ====================== # СТАТИСТИКА # ====================== @admin_router.callback_query(F.data == "admin_stats") async def show_detailed_stats(callback: CallbackQuery): """Подробная статистика""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import select, func from ..core.models import User, Lottery, Participation, Winner # Общая статистика total_users = await session.scalar(select(func.count(User.id))) 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, Lottery.is_completed == False) ) 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_winners = await session.scalar(select(func.count(Winner.id))) manual_winners = await session.scalar( select(func.count(Winner.id)).where(Winner.is_manual == True) ) # Топ активных пользователей top_users = await session.execute( select(User.first_name, User.username, func.count(Participation.id).label('count')) .join(Participation) .group_by(User.id) .order_by(func.count(Participation.id).desc()) .limit(5) ) top_users = top_users.fetchall() text = "📊 Детальная статистика\n\n" text += "👥 ПОЛЬЗОВАТЕЛИ\n" text += f"Всего зарегистрировано: {total_users}\n\n" text += "🎲 РОЗЫГРЫШИ\n" text += f"Всего создано: {total_lotteries}\n" text += f"🟢 Активных: {active_lotteries}\n" text += f"✅ Завершенных: {completed_lotteries}\n\n" text += "🎫 УЧАСТИЕ\n" text += f"Всего участий: {total_participations}\n" if total_lotteries > 0: avg_participation = total_participations / total_lotteries text += f"Среднее участие на розыгрыш: {avg_participation:.1f}\n\n" text += "🏆 ПОБЕДИТЕЛИ\n" text += f"Всего победителей: {total_winners}\n" text += f"👑 Предустановленных: {manual_winners}\n" text += f"🎲 Случайных: {total_winners - manual_winners}\n\n" if top_users: text += "🔥 САМЫЕ АКТИВНЫЕ УЧАСТНИКИ\n" for i, (first_name, username, count) in enumerate(top_users, 1): name = f"@{username}" if username else first_name text += f"{i}. {name} - {count} участий\n" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔃 Обновить", callback_data="admin_stats")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ]) ) # ====================== # НАСТРОЙКИ # ====================== @admin_router.callback_query(F.data == "admin_settings") async def show_admin_settings(callback: CallbackQuery): """Настройки админ-панели""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = "⚙️ Настройки системы\n\n" text += f"👑 Администраторы: {len(ADMIN_IDS)}\n" text += f"🗄️ База данных: SQLAlchemy ORM\n" text += f"📅 Сегодня: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n" text += "Доступные действия:" buttons = [] # Кнопка управления админами - только для главных админов if is_super_admin(callback.from_user.id): buttons.append([InlineKeyboardButton(text="👑 Управление админами", callback_data="admin_manage_admins")]) buttons.extend([ [InlineKeyboardButton(text="💿 Экспорт пользователей", callback_data="admin_export_users")], [InlineKeyboardButton(text="⬆️ Импорт пользователей", callback_data="admin_import_users")], [InlineKeyboardButton(text="🧹 Очистка старых данных", callback_data="admin_cleanup")], [InlineKeyboardButton(text="📜 Системная информация", callback_data="admin_system_info")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data == "admin_export_data") async def export_data(callback: CallbackQuery): """Экспорт данных из системы""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import func, select # Собираем статистику users_count = await session.scalar(select(func.count(User.id))) lotteries_count = await session.scalar(select(func.count(Lottery.id))) participations_count = await session.scalar(select(func.count(Participation.id))) winners_count = await session.scalar(select(func.count(Winner.id))) import json from datetime import datetime # Формируем данные для экспорта export_info = { "export_date": datetime.now().isoformat(), "statistics": { "users": users_count, "lotteries": lotteries_count, "participations": participations_count, "winners": winners_count } } # Сохраняем в файл filename = f"export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(filename, 'w', encoding='utf-8') as f: json.dump(export_info, f, ensure_ascii=False, indent=2) text = "💾 Экспорт данных\n\n" text += f"📊 Статистика:\n" text += f"👥 Пользователей: {users_count}\n" text += f"🎯 Розыгрышей: {lotteries_count}\n" text += f"🎫 Участий: {participations_count}\n" text += f"🏆 Победителей: {winners_count}\n\n" text += f"✅ Данные экспортированы в файл:\n{filename}" await safe_edit_message( callback, text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔃 Экспортировать снова", callback_data="admin_export_data")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_settings")] ]) ) @admin_router.callback_query(F.data == "admin_cleanup") async def cleanup_old_data(callback: CallbackQuery): """Очистка старых данных""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = "🧹 Очистка старых данных\n\n" text += "Выберите тип данных для очистки:\n\n" text += "⚠️ Внимание! Это действие необратимо!" buttons = [ [InlineKeyboardButton(text="🗑️ Завершённые розыгрыши (>30 дней)", callback_data="admin_cleanup_old_lotteries")], [InlineKeyboardButton(text="👻 Неактивные пользователи (>90 дней)", callback_data="admin_cleanup_inactive_users")], [InlineKeyboardButton(text="📜 Старые участия (>60 дней)", callback_data="admin_cleanup_old_participations")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_settings")] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data == "admin_cleanup_old_lotteries") async def cleanup_old_lotteries(callback: CallbackQuery): """Очистка старых завершённых розыгрышей""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return from datetime import timedelta cutoff_date = datetime.now() - timedelta(days=30) async with async_session_maker() as session: from sqlalchemy import select, delete # Находим старые завершённые розыгрыши result = await session.execute( select(Lottery) .where( Lottery.is_completed == True, Lottery.created_at < cutoff_date ) ) old_lotteries = result.scalars().all() count = len(old_lotteries) if count == 0: await callback.answer("✅ Нет старых розыгрышей для удаления", show_alert=True) return # Удаляем старые розыгрыши for lottery in old_lotteries: await session.delete(lottery) await session.commit() text = f"✅ Очистка завершена!\n\n" text += f"🗑️ Удалено розыгрышей: {count}\n" text += f"📅 Старше: {cutoff_date.strftime('%d.%m.%Y')}" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🧹 К очистке", callback_data="admin_cleanup")], [InlineKeyboardButton(text="⚙️ К настройкам", callback_data="admin_settings")] ]) ) @admin_router.callback_query(F.data == "admin_cleanup_inactive_users") async def cleanup_inactive_users(callback: CallbackQuery): """Очистка неактивных пользователей""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return from datetime import timedelta # Удаляем только незарегистрированных пользователей, которые не были активны более 30 дней cutoff_date = datetime.now() - timedelta(days=30) async with async_session_maker() as session: from sqlalchemy import select, delete, and_ # Находим неактивных незарегистрированных пользователей без участий и аккаунтов result = await session.execute( select(User) .where( and_( User.is_registered == False, User.created_at < cutoff_date ) ) ) inactive_users = result.scalars().all() # Проверяем, что у них нет связанных данных deleted_count = 0 for user in inactive_users: # Проверяем участия participations = await session.execute( select(Participation).where(Participation.user_id == user.id) ) if participations.scalars().first(): continue # Проверяем счета accounts = await session.execute( select(Account).where(Account.user_id == user.id) ) if accounts.scalars().first(): continue # Безопасно удаляем await session.delete(user) deleted_count += 1 await session.commit() await callback.message.edit_text( f"✅ Очистка завершена\n\n" f"Удалено неактивных пользователей: {deleted_count}\n" f"Критерий: незарегистрированные, неактивные более 30 дней, без данных", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🧹 К очистке", callback_data="admin_cleanup")], [InlineKeyboardButton(text="⚙️ К настройкам", callback_data="admin_settings")] ]) ) @admin_router.callback_query(F.data == "admin_cleanup_old_participations") async def cleanup_old_participations(callback: CallbackQuery): """Очистка старых участий""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return from datetime import timedelta cutoff_date = datetime.now() - timedelta(days=60) async with async_session_maker() as session: from sqlalchemy import select, delete # Находим старые участия в завершённых розыгрышах result = await session.execute( select(Participation) .join(Lottery) .where( Lottery.is_completed == True, Participation.created_at < cutoff_date ) ) old_participations = result.scalars().all() count = len(old_participations) if count == 0: await callback.answer("✅ Нет старых участий для удаления", show_alert=True) return # Удаляем старые участия for participation in old_participations: await session.delete(participation) await session.commit() text = f"✅ Очистка завершена!\n\n" text += f"🗑️ Удалено участий: {count}\n" text += f"📅 Старше: {cutoff_date.strftime('%d.%m.%Y')}" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🧹 К очистке", callback_data="admin_cleanup")], [InlineKeyboardButton(text="⚙️ К настройкам", callback_data="admin_settings")] ]) ) @admin_router.callback_query(F.data == "admin_system_info") async def show_system_info(callback: CallbackQuery): """Системная информация""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return import sys import platform from ..core.config import DATABASE_URL text = "💻 Системная информация\n\n" text += f"🐍 Python: {sys.version.split()[0]}\n" text += f"💾 Платформа: {platform.system()} {platform.release()}\n" text += f"🗄️ База данных: {DATABASE_URL.split('://')[0]}\n" text += f"👑 Админов: {len(ADMIN_IDS)}\n" text += f"🕐 Время работы: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_settings")] ]) ) # ====================== # НАСТРОЙКА ОТОБРАЖЕНИЯ ПОБЕДИТЕЛЕЙ # ====================== @admin_router.callback_query(F.data == "admin_winner_display_settings") async def show_winner_display_settings(callback: CallbackQuery, state: FSMContext): """Настройка отображения победителей для розыгрышей""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_all_lotteries(session) if not lotteries: await callback.message.edit_text( "❌ Нет розыгрышей", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")] ]) ) return text = "🎭 Настройка отображения победителей\n\n" text += "Выберите розыгрыш для настройки:\n\n" buttons = [] for lottery in lotteries[:10]: # Первые 10 розыгрышей display_type_emoji = { 'username': '👤', 'chat_id': '🆔', 'account_number': '🏦' }.get(getattr(lottery, 'winner_display_type', 'username'), '👤') text += f"{display_type_emoji} {lottery.title} - {getattr(lottery, 'winner_display_type', 'username')}\n" buttons.append([ InlineKeyboardButton( text=f"{display_type_emoji} {lottery.title[:30]}...", callback_data=f"admin_set_display_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")]) await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_set_display_")) async def choose_display_type(callback: CallbackQuery, state: FSMContext): """Выбор типа отображения для конкретного розыгрыша""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[-1]) await state.update_data(display_lottery_id=lottery_id) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) current_type = getattr(lottery, 'winner_display_type', 'username') text = f"🎭 Настройка отображения для:\n{lottery.title}\n\n" text += f"Текущий тип: {current_type}\n\n" text += "Выберите новый тип отображения:\n\n" text += "👤 Username - показывает @username или имя\n" text += "🆔 Chat ID - показывает Telegram ID пользователя\n" text += "🏦 Account Number - показывает номер клиентского счета" buttons = [ [ InlineKeyboardButton(text="👤 Username", callback_data=f"admin_apply_display_{lottery_id}_username"), InlineKeyboardButton(text="🆔 Chat ID", callback_data=f"admin_apply_display_{lottery_id}_chat_id") ], [InlineKeyboardButton(text="💳 Account Number", callback_data=f"admin_apply_display_{lottery_id}_account_number")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_winner_display_settings")] ] await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) @admin_router.callback_query(F.data.startswith("admin_apply_display_")) async def apply_display_type(callback: CallbackQuery, state: FSMContext): """Применить выбранный тип отображения""" import logging logger = logging.getLogger(__name__) logger.info(f"🎭 Попытка смены типа отображения. Callback data: {callback.data}") if not await check_admin_access(callback.from_user.id): logger.warning(f"🚫 Отказ в доступе пользователю {callback.from_user.id}") await callback.answer("❌ Недостаточно прав", show_alert=True) return try: parts = callback.data.split("_") logger.info(f"🔍 Разбор callback data: {parts}") # Format: admin_apply_display_{lottery_id}_{display_type} # Для account_number нужно склеить последние части lottery_id = int(parts[3]) display_type = "_".join(parts[4:]) # Склеиваем все остальные части logger.info(f"🎯 Розыгрыш ID: {lottery_id}, Новый тип: {display_type}") async with async_session_maker() as session: logger.info(f"📝 Вызов set_winner_display_type({lottery_id}, {display_type})") success = await LotteryService.set_winner_display_type(session, lottery_id, display_type) logger.info(f"💾 Результат сохранения: {success}") lottery = await LotteryService.get_lottery(session, lottery_id) logger.info(f"📋 Получен розыгрыш: {lottery.title if lottery else 'None'}") if success: display_type_name = { 'username': 'Username (@username или имя)', 'chat_id': 'Chat ID (Telegram ID)', 'account_number': 'Account Number (номер счета)' }.get(display_type, display_type) text = f"✅ Тип отображения изменен!\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n" text += f"🎭 Новый тип: {display_type_name}\n\n" text += "Теперь победители этого розыгрыша будут отображаться в выбранном формате." logger.info(f"✅ Успех! Тип изменен на {display_type}") await callback.answer("✅ Настройка сохранена!", show_alert=True) else: text = "❌ Ошибка при сохранении настройки" logger.error(f"❌ Ошибка сохранения для розыгрыша {lottery_id}, тип {display_type}") except Exception as e: logger.error(f"💥 Исключение при смене типа отображения: {e}") text = f"❌ Ошибка: {str(e)}" await callback.answer("❌ Ошибка при сохранении!", show_alert=True) return await callback.answer("❌ Ошибка сохранения", show_alert=True) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="👁️ К настройке отображения", callback_data="admin_winner_display_settings")], [InlineKeyboardButton(text="🎰 К управлению розыгрышами", callback_data="admin_lotteries")] ]) ) await state.clear() # ============= УПРАВЛЕНИЕ СООБЩЕНИЯМИ ПОЛЬЗОВАТЕЛЕЙ ============= @admin_router.callback_query(F.data == "admin_messages") async def show_messages_menu(callback: CallbackQuery): """Показать меню управления сообщениями""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return text = "💬 *Управление сообщениями пользователей*\n\n" text += "Здесь вы можете просматривать и удалять сообщения пользователей.\n\n" text += "Выберите действие:" buttons = [ [InlineKeyboardButton(text="📜 Последние сообщения", callback_data="admin_messages_recent")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="Markdown" ) @admin_router.callback_query(F.data == "admin_messages_recent") async def show_recent_messages(callback: CallbackQuery, page: int = 0): """Показать последние сообщения""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return limit = 10 offset = page * limit async with async_session_maker() as session: messages = await ChatMessageService.get_user_messages_all( session, limit=limit, offset=offset, include_deleted=False ) if not messages: text = "💬 Нет сообщений для отображения" buttons = [[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_messages")]] else: text = f"💬 *Последние сообщения*\n\n" # Добавляем кнопки для просмотра сообщений buttons = [] for msg in messages: sender = msg.sender username = f"@{sender.username}" if sender.username else f"ID{sender.telegram_id}" msg_preview = "" if msg.text: msg_preview = msg.text[:20] + "..." if len(msg.text) > 20 else msg.text else: msg_preview = msg.message_type buttons.append([InlineKeyboardButton( text=f"👁 {username}: {msg_preview}", callback_data=f"admin_message_view_{msg.id}" )]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_messages")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="Markdown" ) @admin_router.callback_query(F.data.startswith("admin_message_view_")) async def view_message(callback: CallbackQuery): """Просмотр конкретного сообщения""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return message_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: msg = await ChatMessageService.get_message(session, message_id) if not msg: await callback.answer("❌ Сообщение не найдено", show_alert=True) return sender = msg.sender username = f"@{sender.username}" if sender.username else f"ID: {sender.telegram_id}" text = f"💬 *Просмотр сообщения*\n\n" text += f"👤 Отправитель: {username}\n" text += f"🆔 Telegram ID: `{sender.telegram_id}`\n" text += f"📝 Тип: {msg.message_type}\n" text += f"📅 Дата: {msg.created_at.strftime('%d.%m.%Y %H:%M:%S')}\n\n" if msg.text: text += f"📄 *Текст:*\n{msg.text}\n\n" if msg.file_id: text += f"📎 File ID: `{msg.file_id}`\n\n" if msg.is_deleted: text += f"🗑 *Удалено:* Да\n" if msg.deleted_at: text += f" Дата: {msg.deleted_at.strftime('%d.%m.%Y %H:%M')}\n" buttons = [] # Кнопка удаления (если еще не удалено) if not msg.is_deleted: buttons.append([InlineKeyboardButton( text="🗑 Удалить сообщение", callback_data=f"admin_message_delete_{message_id}" )]) # Кнопка для просмотра всех сообщений пользователя buttons.append([InlineKeyboardButton( text="📜 Все сообщения пользователя", callback_data=f"admin_messages_user_{sender.id}" )]) buttons.append([InlineKeyboardButton(text="◀️ К списку", callback_data="admin_messages_recent")]) # Если сообщение содержит медиа, попробуем его показать if msg.file_id and msg.message_type in ['photo', 'video', 'document', 'animation']: try: if msg.message_type == 'photo': await callback.message.answer_photo( photo=msg.file_id, caption=text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="Markdown" ) await callback.message.delete() await callback.answer() return elif msg.message_type == 'video': await callback.message.answer_video( video=msg.file_id, caption=text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="Markdown" ) await callback.message.delete() await callback.answer() return except Exception as e: logger.error(f"Ошибка при отправке медиа: {e}") await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="Markdown" ) @admin_router.callback_query(F.data.startswith("admin_message_delete_")) async def delete_message(callback: CallbackQuery): """Удалить сообщение пользователя""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return message_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: msg = await ChatMessageService.get_message(session, message_id) if not msg: await callback.answer("❌ Сообщение не найдено", show_alert=True) return # Получаем админа admin = await UserService.get_or_create_user( session, callback.from_user.id, callback.from_user.username, callback.from_user.first_name, callback.from_user.last_name ) # Помечаем сообщение как удаленное success = await ChatMessageService.mark_as_deleted( session, message_id, admin.id ) if success: # Пытаемся удалить сообщение из чата пользователя try: if msg.forwarded_message_ids: # Удаляем пересланные копии у всех пользователей for user_tg_id, tg_msg_id in msg.forwarded_message_ids.items(): try: await callback.bot.delete_message( chat_id=int(user_tg_id), message_id=tg_msg_id ) except Exception as e: logger.warning(f"Не удалось удалить сообщение {tg_msg_id} у пользователя {user_tg_id}: {e}") # Удаляем оригинальное сообщение у отправителя try: await callback.bot.delete_message( chat_id=msg.sender.telegram_id, message_id=msg.telegram_message_id ) except Exception as e: logger.warning(f"Не удалось удалить оригинальное сообщение: {e}") await callback.answer("✅ Сообщение удалено!", show_alert=True) except Exception as e: logger.error(f"Ошибка при удалении сообщений: {e}") await callback.answer("⚠️ Помечено как удаленное", show_alert=True) else: await callback.answer("❌ Ошибка при удалении", show_alert=True) # Возвращаемся к списку await show_recent_messages(callback, 0) @admin_router.callback_query(F.data.startswith("admin_messages_user_")) async def show_user_messages(callback: CallbackQuery): """Показать все сообщения конкретного пользователя""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return user_id = int(callback.data.split("_")[-1]) async with async_session_maker() as session: user = await UserService.get_user_by_id(session, user_id) if not user: await callback.answer("❌ Пользователь не найден", show_alert=True) return messages = await ChatMessageService.get_user_messages( session, user_id, limit=20, include_deleted=True ) username = f"@{user.username}" if user.username else f"ID: {user.telegram_id}" text = f"💬 *Сообщения {username}*\n\n" if not messages: text += "Нет сообщений" buttons = [[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_messages_recent")]] else: # Кнопки для просмотра отдельных сообщений buttons = [] for msg in messages[:15]: status = "🗑" if msg.is_deleted else "✅" msg_preview = "" if msg.text: msg_preview = msg.text[:25] + "..." if len(msg.text) > 25 else msg.text else: msg_preview = msg.message_type buttons.append([InlineKeyboardButton( text=f"{status} {msg_preview} ({msg.created_at.strftime('%d.%m %H:%M')})", callback_data=f"admin_message_view_{msg.id}" )]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_messages_recent")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="Markdown" ) async def _notify_all_participants_about_results(bot, session: AsyncSession, lottery_id: int, winners_dict: dict): """ Рассылает результаты розыгрыша всем зарегистрированным пользователям (кроме победителей) Args: bot: Экземпляр бота session: Сессия БД lottery_id: ID розыгрыша winners_dict: Словарь с победителями {место: данные} """ import asyncio # Получаем розыгрыш lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: return # Получаем всех зарегистрированных пользователей all_users = await UserService.get_all_users(session) registered_users = [u for u in all_users if u.is_registered] # Получаем telegram_id всех победителей winners = await LotteryService.get_winners(session, lottery_id) winner_telegram_ids = set() for winner in winners: if winner.user and winner.user.telegram_id: winner_telegram_ids.add(winner.user.telegram_id) elif winner.account_number: # Ищем владельца счета from ..core.registration_services import AccountService owner = await AccountService.get_account_owner(session, winner.account_number) if owner and owner.telegram_id: winner_telegram_ids.add(owner.telegram_id) # Формируем сообщение с результатами message = ( f"📢 Результаты розыгрыша\n\n" f"🎯 {lottery.title}\n\n" f"🏆 Победители:\n" ) for winner in winners: nickname = None display_name = None # Определяем отображаемое имя победителя if winner.user: nickname = winner.user.nickname if not nickname: display_name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name elif winner.account_number: from ..core.registration_services import AccountService owner = await AccountService.get_account_owner(session, winner.account_number) if owner: nickname = owner.nickname if not nickname: display_name = f"@{owner.username}" if owner.username else owner.first_name # Формируем строку победителя winner_name = nickname if nickname else display_name if display_name else f"Счет {winner.account_number}" message += f"{winner.place} место: {winner_name}\n" message += ( f"\n🎁 Поздравляем победителей!\n" f"📌 Победители получат уведомления с инструкциями для получения призов." ) # Рассылаем всем кроме победителей success_count = 0 fail_count = 0 for user in registered_users: # Пропускаем победителей if user.telegram_id in winner_telegram_ids: continue try: await bot.send_message( user.telegram_id, message, parse_mode="HTML" ) success_count += 1 await asyncio.sleep(0.05) # Небольшая задержка между сообщениями except Exception as e: logger.warning(f"Не удалось отправить результаты пользователю {user.telegram_id}: {e}") fail_count += 1 logger.info(f"Результаты розыгрыша разосланы: {success_count} успешно, {fail_count} ошибок") # ============================================================================ # ЭКСПОРТ И ИМПОРТ ПОЛЬЗОВАТЕЛЕЙ # ============================================================================ @admin_router.callback_query(F.data == "admin_export_users") async def admin_export_users(callback: CallbackQuery): """Экспорт всех пользователей в XLSX""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return await callback.answer("⏳ Формирую файл...", show_alert=False) try: from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment from io import BytesIO from aiogram.types import BufferedInputFile async with async_session_maker() as session: # Получаем всех пользователей all_users = await UserService.get_all_users(session) # Создаем Excel файл wb = Workbook() ws = wb.active ws.title = "Пользователи" # Заголовки headers = [ 'Telegram ID', 'Username', 'Имя', 'Фамилия', 'Никнейм', 'Телефон', 'Клубная карта', 'Зарегистрирован', 'Админ', 'Код верификации', 'Дата создания', 'Последняя активность', 'Заблокирован в чате' ] # Стиль для заголовков header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") header_font = Font(bold=True, color="FFFFFF") for col_num, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_num, value=header) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal="center", vertical="center") # Данные пользователей for row_num, user in enumerate(all_users, 2): ws.cell(row=row_num, column=1, value=user.telegram_id) ws.cell(row=row_num, column=2, value=user.username or '') ws.cell(row=row_num, column=3, value=user.first_name or '') ws.cell(row=row_num, column=4, value=user.last_name or '') ws.cell(row=row_num, column=5, value=user.nickname or '') ws.cell(row=row_num, column=6, value=user.phone or '') ws.cell(row=row_num, column=7, value=user.club_card_number or '') ws.cell(row=row_num, column=8, value='Да' if user.is_registered else 'Нет') ws.cell(row=row_num, column=9, value='Да' if user.is_admin else 'Нет') ws.cell(row=row_num, column=10, value=user.verification_code or '') ws.cell(row=row_num, column=11, value=user.created_at.strftime('%d.%m.%Y %H:%M') if user.created_at else '') ws.cell(row=row_num, column=12, value=user.last_activity.strftime('%d.%m.%Y %H:%M') if user.last_activity else '') ws.cell(row=row_num, column=13, value='Да' if user.is_chat_banned else 'Нет') # Автоподбор ширины колонок for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) ws.column_dimensions[column_letter].width = adjusted_width # Сохраняем в BytesIO excel_file = BytesIO() wb.save(excel_file) excel_file.seek(0) # Отправляем файл filename = f"users_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" file = BufferedInputFile(excel_file.read(), filename=filename) registered_count = len([u for u in all_users if u.is_registered]) await callback.message.answer_document( document=file, caption=( f"📥 Экспорт пользователей\n\n" f"📊 Всего пользователей: {len(all_users)}\n" f"✅ Зарегистрировано: {registered_count}\n" f"📅 Дата экспорта: {datetime.now().strftime('%d.%m.%Y %H:%M')}" ), parse_mode="HTML" ) await callback.answer("✅ Файл отправлен", show_alert=False) except Exception as e: logger.error(f"Ошибка экспорта пользователей: {e}") await callback.answer("❌ Ошибка при создании файла", show_alert=True) @admin_router.callback_query(F.data == "admin_import_users") async def admin_import_users_start(callback: CallbackQuery, state: FSMContext): """Начать импорт пользователей""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return text = ( "📤 Импорт пользователей\n\n" "Отправьте XLSX файл с данными пользователей.\n\n" "📋 Формат файла:\n" "Первая строка - заголовки (как в экспорте)\n" "Обязательная колонка: Telegram ID\n\n" "⚠️ Внимание!\n" "• Будут обновлены существующие пользователи (по telegram_id)\n" "• Новые пользователи будут добавлены\n" "• Текущие данные не будут удалены\n\n" "Отправьте /cancel для отмены" ) buttons = [ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_settings")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await state.set_state(AdminStates.import_users_json) @admin_router.message(StateFilter(AdminStates.import_users_json), F.document) async def admin_import_users_process(message: Message, state: FSMContext): """Обработка импорта пользователей из XLSX""" if not await check_admin_access(message.from_user.id): return # Проверяем формат файла if not message.document.file_name.endswith('.xlsx'): await message.answer("❌ Неверный формат файла. Отправьте XLSX файл.") return status_msg = await message.answer("⏳ Загружаю файл...") try: from openpyxl import load_workbook from io import BytesIO # Скачиваем файл file = await message.bot.get_file(message.document.file_id) file_content = await message.bot.download_file(file.file_path) # Читаем Excel файл excel_file = BytesIO(file_content.read()) wb = load_workbook(excel_file, read_only=True) ws = wb.active # Читаем данные rows = list(ws.iter_rows(values_only=True)) if len(rows) < 2: await status_msg.edit_text("❌ Файл пуст или не содержит данных.") await state.clear() return # Первая строка - заголовки headers = [h if h else '' for h in rows[0]] # Находим индекс колонки Telegram ID try: telegram_id_idx = headers.index('Telegram ID') except ValueError: await status_msg.edit_text("❌ Не найдена обязательная колонка 'Telegram ID'.") await state.clear() return # Создаем маппинг индексов для других полей field_mapping = { 'Username': 'username', 'Имя': 'first_name', 'Фамилия': 'last_name', 'Никнейм': 'nickname', 'Телефон': 'phone', 'Клубная карта': 'club_card_number', 'Зарегистрирован': 'is_registered', 'Код верификации': 'verification_code' } users_data = [] for row in rows[1:]: # Пропускаем заголовки if not row or len(row) <= telegram_id_idx or not row[telegram_id_idx]: continue user_dict = {'telegram_id': row[telegram_id_idx]} for header_name, field_name in field_mapping.items(): try: idx = headers.index(header_name) if idx < len(row): value = row[idx] if field_name == 'is_registered': user_dict[field_name] = value in ['Да', 'Yes', 'True', True, 1] else: user_dict[field_name] = value if value else None except (ValueError, IndexError): user_dict[field_name] = None users_data.append(user_dict) await status_msg.edit_text( f"📊 Найдено пользователей в файле: {len(users_data)}\n" f"⏳ Импортирую..." ) # Импортируем пользователей async with async_session_maker() as session: added_count = 0 updated_count = 0 error_count = 0 for user_data in users_data: try: telegram_id = user_data.get('telegram_id') if not telegram_id: error_count += 1 continue # Преобразуем telegram_id в int если это строка try: telegram_id = int(telegram_id) except (ValueError, TypeError): error_count += 1 continue # Ищем существующего пользователя existing_user = await UserService.get_user_by_telegram_id(session, telegram_id) if existing_user: # Обновляем существующего if user_data.get('username') is not None: existing_user.username = user_data.get('username') if user_data.get('first_name') is not None: existing_user.first_name = user_data.get('first_name') if user_data.get('last_name') is not None: existing_user.last_name = user_data.get('last_name') if user_data.get('nickname') is not None: existing_user.nickname = user_data.get('nickname') if user_data.get('phone') is not None: existing_user.phone = user_data.get('phone') if user_data.get('club_card_number') is not None: existing_user.club_card_number = user_data.get('club_card_number') if user_data.get('is_registered') is not None: existing_user.is_registered = user_data.get('is_registered', False) if user_data.get('verification_code') is not None: existing_user.verification_code = user_data.get('verification_code') # is_admin не обновляем из соображений безопасности updated_count += 1 else: # Создаем нового new_user = User( telegram_id=telegram_id, username=user_data.get('username'), first_name=user_data.get('first_name'), last_name=user_data.get('last_name'), nickname=user_data.get('nickname'), phone=user_data.get('phone'), club_card_number=user_data.get('club_card_number'), is_registered=user_data.get('is_registered', False), is_admin=False, # Не импортируем админов из соображений безопасности verification_code=user_data.get('verification_code') ) session.add(new_user) added_count += 1 except Exception as e: logger.error(f"Ошибка импорта пользователя {user_data.get('telegram_id')}: {e}") error_count += 1 # Сохраняем изменения await session.commit() # Итоговый отчет await status_msg.edit_text( f"✅ Импорт завершен!\n\n" f"📊 Статистика:\n" f"➕ Добавлено: {added_count}\n" f"🔄 Обновлено: {updated_count}\n" f"❌ Ошибок: {error_count}\n" f"📝 Всего обработано: {added_count + updated_count}", parse_mode="HTML" ) await state.clear() except Exception as e: logger.error(f"Ошибка импорта пользователей: {e}") await status_msg.edit_text(f"❌ Ошибка импорта: {str(e)}\n\nПроверьте формат файла.") await state.clear() # ============================================================================ # МАССОВАЯ РАССЫЛКА # ============================================================================ @admin_router.callback_query(F.data == "admin_broadcast") async def admin_broadcast_menu(callback: CallbackQuery, state: FSMContext): """Меню массовой рассылки""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return async with async_session_maker() as session: # Получаем статистику пользователей all_users = await UserService.get_all_users(session) registered_users = [u for u in all_users if u.is_registered] # Получаем статистику заблокированных пользователей from sqlalchemy import select, func from ..core.models import BlockedUser blocked_stmt = select(func.count(BlockedUser.id)).where(BlockedUser.is_active == True) blocked_result = await session.execute(blocked_stmt) blocked_count = blocked_result.scalar() # Получаем количество каналов channels_stmt = select(func.count(BroadcastChannel.id)).where(BroadcastChannel.is_active == True) channels_result = await session.execute(channels_stmt) channels_count = channels_result.scalar() text = ( "📢 Массовая рассылка\n\n" f"👥 Всего пользователей: {len(all_users)}\n" f"✅ Зарегистрировано: {len(registered_users)}\n" f"🚫 Заблокировали бота: {blocked_count}\n" f"📱 Активных каналов/групп: {channels_count}\n\n" "Выберите действие:" ) buttons = [ [InlineKeyboardButton(text="✉️ Создать рассылку", callback_data="admin_broadcast_start")], [InlineKeyboardButton(text="📱 Управление каналами", callback_data="admin_broadcast_channels")], [InlineKeyboardButton(text="� Статистика рассылок", callback_data="admin_broadcast_stats")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_broadcast_start") async def admin_broadcast_start(callback: CallbackQuery, state: FSMContext): """Выбор типа рассылки""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return text = ( "📢 Выберите тип рассылки\n\n" "Доступные варианты:\n" "• ЛС пользователям - массовая рассылка всем зарегистрированным пользователям\n" "• В канал - отправка сообщения в выбранный канал\n" "• В группу - отправка сообщения в выбранную группу" ) buttons = [ [InlineKeyboardButton(text="👤 ЛС пользователям", callback_data="broadcast_type_direct")], [InlineKeyboardButton(text="📢 В канал", callback_data="broadcast_type_channel")], [InlineKeyboardButton(text="👥 В группу", callback_data="broadcast_type_group")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "broadcast_type_direct") async def broadcast_type_direct(callback: CallbackQuery, state: FSMContext): """Рассылка в ЛС - запрос сообщения""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return # Сохраняем тип рассылки await state.update_data(broadcast_type='direct') text = ( "✉️ Рассылка в личные сообщения\n\n" "Отправьте сообщение для рассылки.\n" "Вы можете отправить:\n" "• Текст (поддерживается Markdown)\n" "• Фото с подписью\n" "• Видео с подписью\n" "• Документ с подписью\n\n" "Отправьте /cancel для отмены" ) buttons = [ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_broadcast")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await state.set_state(AdminStates.broadcast_message) @admin_router.callback_query(F.data.startswith("broadcast_type_")) async def broadcast_type_channel_or_group(callback: CallbackQuery, state: FSMContext): """Выбор канала или группы для рассылки""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return broadcast_type = callback.data.replace("broadcast_type_", "") if broadcast_type == 'direct': return # Обрабатывается отдельно # Сохраняем тип рассылки await state.update_data(broadcast_type=broadcast_type) # Получаем список каналов/групп async with async_session_maker() as session: from sqlalchemy import select stmt = select(BroadcastChannel).where( BroadcastChannel.is_active == True, BroadcastChannel.chat_type == broadcast_type ) result = await session.execute(stmt) channels = result.scalars().all() if not channels: text = ( f"❌ Нет доступных {('каналов' if broadcast_type == 'channel' else 'групп')}\n\n" "Сначала добавьте канал или группу в разделе 'Управление каналами'" ) buttons = [ [InlineKeyboardButton(text="📱 Управление каналами", callback_data="admin_broadcast_channels")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast_start")] ] else: text = ( f"📢 Выберите {'канал' if broadcast_type == 'channel' else 'группу'}\n\n" f"Доступно: {len(channels)}" ) buttons = [] for channel in channels: title = channel.title[:30] + "..." if len(channel.title) > 30 else channel.title buttons.append([InlineKeyboardButton( text=f"📱 {title}", callback_data=f"broadcast_select_channel_{channel.id}" )]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast_start")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data.startswith("broadcast_select_channel_")) async def broadcast_select_channel(callback: CallbackQuery, state: FSMContext): """Выбран канал/группа - запрос сообщения""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return channel_id = int(callback.data.replace("broadcast_select_channel_", "")) # Сохраняем ID канала await state.update_data(channel_db_id=channel_id) # Получаем информацию о канале async with async_session_maker() as session: from sqlalchemy import select stmt = select(BroadcastChannel).where(BroadcastChannel.id == channel_id) result = await session.execute(stmt) channel = result.scalar_one() text = ( f"📢 Рассылка в {'канал' if channel.chat_type == 'channel' else 'группу'}\n\n" f"📱 Название: {channel.title}\n" f"🆔 ID: {channel.chat_id}\n\n" "Отправьте сообщение для отправки.\n" "Вы можете отправить:\n" "• Текст (поддерживается Markdown)\n" "• Фото с подписью\n" "• Видео с подписью\n" "• Документ с подписью\n\n" "Отправьте /cancel для отмены" ) buttons = [ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_broadcast")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await state.set_state(AdminStates.broadcast_message) @admin_router.message(StateFilter(AdminStates.broadcast_message), F.text | F.photo | F.video | F.document) async def admin_broadcast_send(message: Message, state: FSMContext): """Обработка и отправка рассылки""" if not await check_admin_access(message.from_user.id): return data = await state.get_data() broadcast_type = data.get('broadcast_type', 'direct') if broadcast_type == 'direct': # Рассылка в ЛС await _broadcast_direct(message, state) else: # Рассылка в канал/группу await _broadcast_channel(message, state, data) async def _broadcast_direct(message: Message, state: FSMContext): """Рассылка в личные сообщения""" # Отправляем уведомление о начале рассылки status_msg = await message.answer( "📤 Начинаю рассылку в ЛС...\n\n" "⏳ Подождите, это может занять некоторое время.\n" "💡 Используется Redis очередь и отслеживание заблокированных пользователей.", parse_mode="HTML" ) async with async_session_maker() as session: # Получаем или создаем пользователя-администратора admin_user = await UserService.get_or_create_user( session, telegram_id=message.from_user.id, username=message.from_user.username, first_name=message.from_user.first_name, last_name=message.from_user.last_name ) # Получаем всех зарегистрированных пользователей all_users = await UserService.get_all_users(session) registered_users = [u for u in all_users if u.is_registered] # Проверяем, есть ли пользователи для рассылки if not registered_users: await status_msg.edit_text( "⚠️ Нет зарегистрированных пользователей\n\n" "Рассылка невозможна, так как нет ни одного зарегистрированного пользователя.", parse_mode="HTML" ) await state.clear() return # Используем новый сервис рассылок stats = await broadcast_service.broadcast_to_users( bot=message.bot, message=message, admin_id=admin_user.id, users=registered_users ) # Рассчитываем процент доставки delivery_percent = (stats['success'] / stats['total'] * 100) if stats['total'] > 0 else 0 # Итоговый отчет await status_msg.edit_text( f"✅ Рассылка завершена!\n\n" f"📊 Статистика:\n" f"👥 Всего получателей: {stats['total']}\n" f"✅ Доставлено: {stats['success']}\n" f"❌ Не доставлено: {stats['failed']}\n" f"🚫 Заблокировали бота: {stats['blocked']}\n\n" f"📈 Процент доставки: {delivery_percent:.1f}%", parse_mode="HTML" ) await state.clear() async def _broadcast_channel(message: Message, state: FSMContext, data: dict): """Рассылка в канал или группу""" channel_db_id = data.get('channel_db_id') if not channel_db_id: await message.answer("❌ Ошибка: не выбран канал") await state.clear() return # Получаем информацию о канале и администратора async with async_session_maker() as session: # Получаем или создаем пользователя-администратора admin_user = await UserService.get_or_create_user( session, telegram_id=message.from_user.id, username=message.from_user.username, first_name=message.from_user.first_name, last_name=message.from_user.last_name ) from sqlalchemy import select stmt = select(BroadcastChannel).where(BroadcastChannel.id == channel_db_id) result = await session.execute(stmt) channel = result.scalar_one_or_none() if not channel: await message.answer("❌ Ошибка: канал не найден") await state.clear() return # Отправляем уведомление status_msg = await message.answer( f"📤 Отправляю в {'канал' if channel.chat_type == 'channel' else 'группу'}...\n\n" f"📱 {channel.title}", parse_mode="HTML" ) # Используем сервис рассылок success = await broadcast_service.broadcast_to_channel( bot=message.bot, message=message, channel_id=channel.chat_id, admin_id=admin_user.id ) if success: await status_msg.edit_text( f"✅ Сообщение отправлено!\n\n" f"📱 {'Канал' if channel.chat_type == 'channel' else 'Группа'}: {channel.title}", parse_mode="HTML" ) else: await status_msg.edit_text( f"❌ Ошибка отправки\n\n" f"Не удалось отправить сообщение в {'канал' if channel.chat_type == 'channel' else 'группу'} {channel.title}\n" f"Проверьте права бота и попробуйте снова.", parse_mode="HTML" ) await state.clear() # ============================================================================ # УПРАВЛЕНИЕ КАНАЛАМИ # ============================================================================ @admin_router.callback_query(F.data == "admin_broadcast_channels") async def admin_broadcast_channels_menu(callback: CallbackQuery, state: FSMContext): """Меню управления каналами""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return # Получаем список каналов async with async_session_maker() as session: from sqlalchemy import select stmt = select(BroadcastChannel).where(BroadcastChannel.is_active == True) result = await session.execute(stmt) channels = result.scalars().all() text = "📱 Управление каналами и группами\n\n" if channels: text += f"📊 Всего: {len(channels)}\n\n" for channel in channels[:10]: # Показываем первые 10 icon = "📢" if channel.chat_type == 'channel' else "👥" text += f"{icon} {channel.title}\n" text += f" 🆔 ID: {channel.chat_id}\n" if channel.username: text += f" @{channel.username}\n" text += "\n" if len(channels) > 10: text += f"... и еще {len(channels) - 10}\n" else: text += "Нет добавленных каналов или групп" buttons = [ [InlineKeyboardButton(text="✨ Добавить канал/группу", callback_data="admin_broadcast_add_channel")], [InlineKeyboardButton(text="📜 Список всех", callback_data="admin_broadcast_list_channels")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_broadcast_add_channel") async def admin_broadcast_add_channel_start(callback: CallbackQuery, state: FSMContext): """Начать добавление канала""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return text = ( "➕ Добавление канала или группы\n\n" "Отправьте ID канала или группы.\n\n" "Как узнать ID:\n" "1. Добавьте бота в канал/группу как администратора\n" "2. Перешлите любое сообщение из канала/группы боту @userinfobot\n" "3. Он покажет ID чата (обычно отрицательное число)\n\n" "Пример: -1001234567890\n\n" "Отправьте /cancel для отмены" ) buttons = [ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_broadcast_channels")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await state.set_state(AdminStates.broadcast_add_channel_id) @admin_router.message(StateFilter(AdminStates.broadcast_add_channel_id), F.text) async def admin_broadcast_add_channel_id(message: Message, state: FSMContext): """Обработка ID канала""" if not await check_admin_access(message.from_user.id): return try: chat_id = int(message.text.strip()) except ValueError: await message.answer( "❌ Неверный формат ID. Отправьте число, например: -1001234567890" ) return # Пытаемся получить информацию о чате try: chat = await message.bot.get_chat(chat_id) # Определяем тип чата if chat.type == 'channel': chat_type = 'channel' elif chat.type in ['group', 'supergroup']: chat_type = 'group' else: await message.answer( "❌ Неверный тип чата. Поддерживаются только каналы и группы." ) await state.clear() return # Сохраняем данные await state.update_data( chat_id=chat_id, chat_type=chat_type, title=chat.title, username=chat.username ) # Запрашиваем описание text = ( f"✅ Канал найден!\n\n" f"📱 Название: {chat.title}\n" f"🆔 ID: {chat_id}\n" f"📝 Тип: {'Канал' if chat_type == 'channel' else 'Группа'}\n" ) if chat.username: text += f"🔗 Username: @{chat.username}\n" text += "\n\nОтправьте описание для этого канала (необязательно) или /skip чтобы пропустить" await message.answer(text, parse_mode="HTML") await state.set_state(AdminStates.broadcast_add_channel_title) except Exception as e: await message.answer( f"❌ Ошибка получения информации о чате\n\n" f"Возможные причины:\n" f"• Бот не добавлен в канал/группу\n" f"• Неверный ID\n" f"• Бот не имеет прав администратора\n\n" f"Детали: {str(e)}", parse_mode="HTML" ) await state.clear() @admin_router.message(StateFilter(AdminStates.broadcast_add_channel_title), F.text) async def admin_broadcast_add_channel_description(message: Message, state: FSMContext): """Обработка описания канала""" if not await check_admin_access(message.from_user.id): return data = await state.get_data() description = None if message.text.strip() == '/skip' else message.text.strip() # Сохраняем в БД 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, first_name=message.from_user.first_name, last_name=message.from_user.last_name ) # Проверяем, не добавлен ли уже from sqlalchemy import select stmt = select(BroadcastChannel).where(BroadcastChannel.chat_id == data['chat_id']) result = await session.execute(stmt) existing = result.scalar_one_or_none() if existing: # Обновляем существующий existing.is_active = True existing.title = data['title'] existing.username = data.get('username') existing.description = description existing.chat_type = data['chat_type'] await session.commit() await message.answer( "✅ Канал обновлен!\n\n" f"📱 {data['title']}", parse_mode="HTML" ) else: # Создаем новый channel = BroadcastChannel( chat_id=data['chat_id'], chat_type=data['chat_type'], title=data['title'], username=data.get('username'), description=description, added_by=user.id ) session.add(channel) await session.commit() await message.answer( "✅ Канал добавлен!\n\n" f"📱 {data['title']}\n" f"Теперь вы можете использовать его для рассылок.", parse_mode="HTML" ) await state.clear() @admin_router.callback_query(F.data == "admin_broadcast_stats") async def admin_broadcast_stats(callback: CallbackQuery, state: FSMContext): """Статистика рассылок""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return async with async_session_maker() as session: from sqlalchemy import select, func, desc from ..core.models import BroadcastLog # Последние 5 рассылок stmt = select(BroadcastLog).order_by(desc(BroadcastLog.started_at)).limit(5) result = await session.execute(stmt) logs = result.scalars().all() # Общая статистика total_stmt = select(func.count(BroadcastLog.id)) total_result = await session.execute(total_stmt) total_broadcasts = total_result.scalar() # Статистика заблокированных blocked_stmt = select(func.count(BlockedUser.id)).where(BlockedUser.is_active == True) blocked_result = await session.execute(blocked_stmt) blocked_count = blocked_result.scalar() text = "📊 Статистика рассылок\n\n" text += f"📢 Всего рассылок: {total_broadcasts}\n" text += f"🚫 Заблокировали бота: {blocked_count}\n\n" if logs: text += "Последние 5 рассылок:\n\n" for log in logs: icon = "👤" if log.broadcast_type == 'direct' else ("📢" if log.broadcast_type == 'channel' else "👥") status_icon = "✅" if log.status == 'completed' else ("⏳" if log.status == 'in_progress' else "❌") text += f"{icon} {status_icon} {log.started_at.strftime('%d.%m %H:%M')}\n" if log.broadcast_type == 'direct': text += f" 👥 {log.success_count}/{log.total_recipients} доставлено\n" if log.blocked_count > 0: text += f" 🚫 {log.blocked_count} заблокировали\n" text += "\n" buttons = [ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_broadcast_inactive") async def admin_broadcast_inactive(callback: CallbackQuery, state: FSMContext): """Статистика по неактивным пользователям""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return from ..core.activity_service import ActivityService async with async_session_maker() as session: # Получаем неактивных пользователей inactive_users = await ActivityService.get_inactive_users(session, days=30) # Получаем уже заблокированных за неактивность from sqlalchemy import select blocked_stmt = select(BlockedUser).where( BlockedUser.error_type == 'inactive', BlockedUser.is_active == True ) blocked_result = await session.execute(blocked_stmt) blocked_inactive = list(blocked_result.scalars().all()) text = "⏰ Неактивные пользователи\n\n" text += f"📊 Неактивных более 30 дней: {len(inactive_users)}\n" text += f"🚫 Уже заблокировано за неактивность: {len(blocked_inactive)}\n\n" text += "Система автоматически проверяет активность пользователей каждый день в 03:00 " text += "и блокирует неактивных более 30 дней.\n\n" if inactive_users: text += "Неактивные пользователи (первые 10):\n\n" for i, user in enumerate(inactive_users[:10], 1): days_inactive = (datetime.now(timezone.utc) - user.last_activity).days text += f"{i}. @{user.username or 'без_username'} ({user.first_name})\n" text += f" Неактивен: {days_inactive} дней\n" buttons = [ [InlineKeyboardButton(text="🔃 Проверить сейчас", callback_data="admin_check_inactive_now")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_check_inactive_now") async def admin_check_inactive_now(callback: CallbackQuery, state: FSMContext): """Запустить проверку неактивных пользователей вручную""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return await callback.answer("⏳ Проверка запущена...", show_alert=False) from ..core.activity_service import ActivityService # Запускаем проверку marked = await ActivityService.check_and_mark_inactive_users() text = f"✅ Проверка завершена!\n\n" text += f"🚫 Помечено неактивных пользователей: {marked}\n\n" text += "Эти пользователи будут исключены из будущих рассылок." buttons = [ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast_inactive")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) # ============================================ # УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ # ============================================ @admin_router.callback_query(F.data == "admin_users") async def admin_users_menu(callback: CallbackQuery, state: FSMContext): """Меню управления пользователями""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return from ..core.user_management import UserManagementService async with async_session_maker() as session: stats = await UserManagementService.get_user_stats(session) text = ( "👥 Управление пользователями\n\n" f"📊 Статистика:\n" f"• Всего пользователей: {stats['total']}\n" f"• Зарегистрированных: {stats['registered']}\n" f"• Администраторов: {stats['admins']}\n" f"• Заблокированных в чате: {stats['chat_banned']}\n\n" "Выберите действие:" ) buttons = [ [InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_users_search")], [InlineKeyboardButton(text="📜 Все пользователи", callback_data="admin_users_list:1"), InlineKeyboardButton(text="🚫 Заблокированные", callback_data="admin_users_banned:1")], [InlineKeyboardButton(text="⌛ Неактивные", callback_data="admin_broadcast_inactive")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_users_search") async def admin_users_search_prompt(callback: CallbackQuery, state: FSMContext): """Запрос поискового запроса""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return text = ( "🔍 Поиск пользователей\n\n" "Введите поисковый запрос:\n" "• Username (@username или username)\n" "• Имя или фамилия\n" "• Telegram ID\n" "• Номер клубной карты\n" "• Никнейм\n\n" "Или отправьте /cancel для отмены" ) buttons = [ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_users")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await state.set_state(AdminStates.user_management_search) @admin_router.message(AdminStates.user_management_search) async def admin_users_search_process(message: Message, state: FSMContext): """Обработка поискового запроса""" if not await check_admin_access(message.from_user.id): return query = message.text.strip() if query == "/cancel": await message.answer("❌ Поиск отменен") await state.clear() return from ..core.user_management import UserManagementService async with async_session_maker() as session: users, total = await UserManagementService.search_users( session, query=query, page=1, per_page=15 ) if not users: text = f"❌ По запросу «{query}» ничего не найдено" buttons = [ [InlineKeyboardButton(text="◀️ В управление пользователями", callback_data="admin_users")] ] else: text = f"🔍 Результаты поиска: «{query}»\n" text += f"Найдено: {total} пользователей\n\n" buttons = [] for user in users: user_info = UserManagementService.format_user_info(user, detailed=False) # Убираем HTML теги для краткого отображения кнопки button_text = f"{user.first_name}" if user.username: button_text += f" (@{user.username})" if user.is_chat_banned: button_text += " 🚫" buttons.append([InlineKeyboardButton( text=button_text[:60], # Ограничение длины callback_data=f"admin_user_view:{user.id}" )]) # Добавляем пагинацию если есть еще пользователи if total > 15: nav_buttons = [] if total > 15: nav_buttons.append(InlineKeyboardButton( text="➡️ Далее", callback_data=f"admin_users_search_page:{query}:2" )) buttons.append(nav_buttons) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_users")]) await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await state.clear() @admin_router.callback_query(F.data.startswith("admin_users_list:")) async def admin_users_list(callback: CallbackQuery, state: FSMContext): """Список всех пользователей с пагинацией""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return page = int(callback.data.split(":")[1]) from ..core.user_management import UserManagementService async with async_session_maker() as session: users, total = await UserManagementService.search_users( session, page=page, per_page=15 ) text = f"📋 Все пользователи\n" text += f"Всего: {total} | Страница {page}\n\n" buttons = [] for user in users: button_text = f"{user.first_name}" if user.username: button_text += f" (@{user.username})" if user.is_chat_banned: button_text += " 🚫" buttons.append([InlineKeyboardButton( text=button_text[:60], callback_data=f"admin_user_view:{user.id}" )]) # Пагинация nav_buttons = [] if page > 1: nav_buttons.append(InlineKeyboardButton( text="⬅️ Назад", callback_data=f"admin_users_list:{page-1}" )) if page * 15 < total: nav_buttons.append(InlineKeyboardButton( text="➡️ Далее", callback_data=f"admin_users_list:{page+1}" )) if nav_buttons: buttons.append(nav_buttons) buttons.append([InlineKeyboardButton(text="◀️ В меню", callback_data="admin_users")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data.startswith("admin_users_banned:")) async def admin_users_banned_list(callback: CallbackQuery, state: FSMContext): """Список заблокированных пользователей""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return page = int(callback.data.split(":")[1]) from ..core.user_management import UserManagementService async with async_session_maker() as session: users, total = await UserManagementService.search_users( session, page=page, per_page=15, filters={'is_chat_banned': True} ) if not users: text = "✅ Нет заблокированных пользователей" buttons = [ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_users")] ] else: text = f"🚫 Заблокированные в чате\n" text += f"Всего: {total} | Страница {page}\n\n" buttons = [] for user in users: button_text = f"🚫 {user.first_name}" if user.username: button_text += f" (@{user.username})" buttons.append([InlineKeyboardButton( text=button_text[:60], callback_data=f"admin_user_view:{user.id}" )]) # Пагинация nav_buttons = [] if page > 1: nav_buttons.append(InlineKeyboardButton( text="⬅️ Назад", callback_data=f"admin_users_banned:{page-1}" )) if page * 15 < total: nav_buttons.append(InlineKeyboardButton( text="➡️ Далее", callback_data=f"admin_users_banned:{page+1}" )) if nav_buttons: buttons.append(nav_buttons) buttons.append([InlineKeyboardButton(text="◀️ В меню", callback_data="admin_users")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data.startswith("admin_user_view:")) async def admin_user_view(callback: CallbackQuery, state: FSMContext): """Просмотр информации о пользователе""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return user_id = int(callback.data.split(":")[1]) from ..core.user_management import UserManagementService async with async_session_maker() as session: user = await UserManagementService.get_user_by_id(session, user_id) if not user: await callback.answer("❌ Пользователь не найден", show_alert=True) return text = "👤 Информация о пользователе\n\n" text += UserManagementService.format_user_info(user, detailed=True) buttons = [] # Кнопка блокировки/разблокировки if user.is_chat_banned: buttons.append([InlineKeyboardButton( text="✅ Разблокировать в чате", callback_data=f"admin_user_unban:{user.id}" )]) else: buttons.append([InlineKeyboardButton( text="🚫 Заблокировать в чате", callback_data=f"admin_user_ban:{user.id}" )]) buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_users")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data.startswith("admin_user_ban:")) async def admin_user_ban(callback: CallbackQuery, state: FSMContext): """Заблокировать пользователя в чате""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return user_id = int(callback.data.split(":")[1]) from ..core.user_management import UserManagementService async with async_session_maker() as session: success = await UserManagementService.ban_user_in_chat(session, user_id) if success: await callback.answer("✅ Пользователь заблокирован в чате", show_alert=True) # Обновляем информацию await admin_user_view(callback, state) else: await callback.answer("❌ Ошибка блокировки", show_alert=True) @admin_router.callback_query(F.data.startswith("admin_user_unban:")) async def admin_user_unban(callback: CallbackQuery, state: FSMContext): """Разблокировать пользователя в чате""" if not await check_admin_access(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return user_id = int(callback.data.split(":")[1]) from ..core.user_management import UserManagementService async with async_session_maker() as session: success = await UserManagementService.unban_user_in_chat(session, user_id) if success: await callback.answer("✅ Пользователь разблокирован в чате", show_alert=True) # Обновляем информацию await admin_user_view(callback, state) else: await callback.answer("❌ Ошибка разблокировки", show_alert=True) # ========================= # УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ # ========================= @admin_router.callback_query(F.data == "admin_manage_admins") async def manage_admins_menu(callback: CallbackQuery): """Главное меню управления администраторами""" if not is_super_admin(callback.from_user.id): await callback.answer("❌ Только главные администраторы могут управлять правами", show_alert=True) return await callback.answer() text = "👑 Управление администраторами\n\n" text += f"Главные администраторы (.env): {len(ADMIN_IDS)}\n\n" text += "Выберите действие:" buttons = [ [InlineKeyboardButton(text="➕ Назначить админа", callback_data="admin_add_admin")], [InlineKeyboardButton(text="➖ Удалить админа", callback_data="admin_remove_admin")], [InlineKeyboardButton(text="📋 Список админов", callback_data="admin_list_admins_view")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_settings")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_list_admins_view") async def list_admins_view(callback: CallbackQuery): """Показать список всех администраторов""" if not is_super_admin(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return await callback.answer() async with async_session_maker() as session: from sqlalchemy import select # Получаем всех администраторов (назначенных через БД) result = await session.execute( select(User).where(User.is_admin == True).order_by(User.created_at.desc()) ) db_admins = result.scalars().all() text = "👑 Список администраторов\n\n" # Главные администраторы из .env text += "Главные администраторы (из .env):\n" for admin_id in ADMIN_IDS: text += f"🔴 ID: {admin_id}\n" text += "\n" # Назначенные администраторы if db_admins: text += "Назначенные администраторы:\n" for admin in db_admins: icon = "🟠" # Назначенный админ name = admin.first_name or admin.username or f"@ID_{admin.telegram_id}" text += f"{icon} {name} (ID: {admin.telegram_id})\n" else: text += "Назначенные администраторы: нет\n" buttons = [ [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_manage_admins")] ] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @admin_router.callback_query(F.data == "admin_add_admin") async def add_admin_start(callback: CallbackQuery, state: FSMContext): """Начать добавление нового администратора""" if not is_super_admin(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return await callback.answer() text = "👤 Назначение администратора\n\n" text += "Введите Telegram ID пользователя или его имя для поиска:" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="admin_manage_admins")] ]), parse_mode="HTML" ) await state.set_state(AdminStates.admin_add_search) @admin_router.message(StateFilter(AdminStates.admin_add_search)) async def search_user_for_admin(message: Message, state: FSMContext): """Поиск пользователя для назначения админом""" if not is_super_admin(message.from_user.id): await message.answer("❌ Доступ запрещен") return search_query = message.text.strip() async with async_session_maker() as session: user = None # Пробуем найти по ID try: telegram_id = int(search_query) user = await UserService.get_user_by_telegram_id(session, telegram_id) except ValueError: # Если не число, ищем по имени или username users = await UserService.search_users(session, search_query, limit=5) if users: user = users[0] if not user: await message.answer("❌ Пользователь не найден") await state.set_state(AdminStates.admin_add_search) return # Проверяем, не главный ли админ из .env if user.telegram_id in ADMIN_IDS: await message.answer("❌ Это главный администратор (.env). Уже имеет максимальные права") await state.set_state(AdminStates.admin_add_search) return # Проверяем, не админ ли уже if user.is_admin: await message.answer("❌ Этот пользователь уже администратор") await state.set_state(AdminStates.admin_add_search) return # Сохраняем в state и просим подтверждение await state.update_data(admin_user_id=user.id, admin_telegram_id=user.telegram_id) text = "👤 Подтверждение назначения администратора\n\n" text += f"Имя: {user.first_name or 'не указано'}\n" text += f"Username: {user.username or 'нет'}\n" text += f"Telegram ID: {user.telegram_id}\n" text += f"Зарегистрирован: {user.created_at.strftime('%d.%m.%Y %H:%M') if user.created_at else 'нет'}\n\n" text += "Вы уверены, что хотите дать этому пользователю права администратора?" await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="✅ Да, назначить", callback_data="admin_add_confirm_yes"), InlineKeyboardButton(text="❌ Отмена", callback_data="admin_manage_admins")], ]), parse_mode="HTML" ) await state.set_state(AdminStates.admin_add_confirm) @admin_router.callback_query(F.data == "admin_add_confirm_yes") async def confirm_add_admin(callback: CallbackQuery, state: FSMContext): """Подтвердить назначение админа""" if not is_super_admin(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return data = await state.get_data() admin_telegram_id = data.get('admin_telegram_id') async with async_session_maker() as session: success = await UserService.set_admin(session, admin_telegram_id, is_admin=True) if success: await callback.answer("✅ Администратор успешно назначен", show_alert=True) await state.clear() await manage_admins_menu(callback) else: await callback.answer("❌ Ошибка при назначении администратора", show_alert=True) @admin_router.callback_query(F.data == "admin_remove_admin") async def remove_admin_start(callback: CallbackQuery, state: FSMContext): """Начать удаление администратора""" if not is_super_admin(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return await callback.answer() async with async_session_maker() as session: from sqlalchemy import select # Получаем всех назначенных администраторов result = await session.execute( select(User).where(User.is_admin == True).order_by(User.created_at.desc()) ) admins = result.scalars().all() if not admins: await callback.answer("❌ Нет назначенных администраторов", show_alert=True) return text = "🗑️ Выберите администратора для удаления\n\n" buttons = [] for admin in admins[:20]: # Максимум 20 администраторов на странице name = admin.first_name or admin.username or f"@ID_{admin.telegram_id}" buttons.append([InlineKeyboardButton( text=f"🟠 {name}", callback_data=f"admin_remove_select:{admin.telegram_id}" )]) buttons.append([InlineKeyboardButton(text="❌ Отмена", callback_data="admin_manage_admins")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await state.set_state(AdminStates.admin_remove_select) @admin_router.callback_query(F.data.startswith("admin_remove_select:")) async def confirm_remove_admin(callback: CallbackQuery, state: FSMContext): """Подтвердить удаление администратора""" if not is_super_admin(callback.from_user.id): await callback.answer("❌ Доступ запрещен", show_alert=True) return admin_telegram_id = int(callback.data.split(":")[1]) async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, admin_telegram_id) if not user: await callback.answer("❌ Пользователь не найден", show_alert=True) return # Снять права администратора success = await UserService.set_admin(session, admin_telegram_id, is_admin=False) if success: await callback.answer("✅ Права администратора удалены", show_alert=True) await state.clear() await manage_admins_menu(callback) else: await callback.answer("❌ Ошибка при удалении прав", show_alert=True) # Экспорт роутера __all__ = ['admin_router']