"""Админские обработчики для управления счетами и верификации""" from aiogram import Router, F, Bot from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from sqlalchemy import select, and_ from src.core.database import async_session_maker from src.core.registration_services import AccountService, WinnerNotificationService, RegistrationService from src.core.services import UserService, LotteryService, ParticipationService from src.core.models import User, Winner, Account, Participation from src.core.config import ADMIN_IDS from src.core.permissions import admin_only router = Router() class AddAccountStates(StatesGroup): waiting_for_data = State() choosing_lottery = State() @router.message(Command("cancel")) @admin_only async def cancel_command(message: Message, state: FSMContext): """Отменить текущую операцию и сбросить состояние""" await state.clear() await message.answer("✅ Состояние сброшено. Все операции отменены.") @router.message(Command("add_account")) @admin_only async def add_account_command(message: Message, state: FSMContext): """ Добавить счет пользователю по клубной карте Формат: /add_account Или: /add_account (затем вводить данные построчно) """ parts = message.text.split(maxsplit=2) # Если данные указаны в команде if len(parts) == 3: club_card = parts[1] account_number = parts[2] await process_single_account(message, club_card, account_number, state) else: # Запрашиваем данные await state.set_state(AddAccountStates.waiting_for_data) await message.answer( "💳 **Добавление счетов**\n\n" "📋 **Формат 1 (однострочный):**\n" "`карта счет`\n" "Пример: `2223 11-22-33-44-55-66-77`\n\n" "📋 **Формат 2 (многострочный из таблицы):**\n" "Скопируйте столбцы со счетами и картами - система сама распознает\n\n" "**Для нескольких счетов:**\n" "`2223 11-22-33-44-55-66-77`\n" "`2223 88-99-00-11-22-33-44`\n" "`3334 12-34-56-78-90-12-34`\n\n" "❌ Отправьте /cancel для отмены", parse_mode="Markdown" ) async def process_single_account(message: Message, club_card: str, account_number: str, state: FSMContext): """Обработка одного счета""" try: async with async_session_maker() as session: # Создаем счет account = await AccountService.create_account( session, club_card_number=club_card, account_number=account_number ) # Получаем владельца owner = await AccountService.get_account_owner(session, account_number) # Сохраняем данные счета в state для добавления в розыгрыш await state.update_data( accounts=[{ 'club_card': club_card, 'account_number': account_number, 'account_id': account.id }] ) text = f"✅ Счет успешно добавлен!\n\n" text += f"🎫 Клубная карта: {club_card}\n" text += f"💳 Счет: {account_number}\n" if owner: text += f"👤 Владелец: {owner.first_name}\n\n" # Отправляем уведомление владельцу с форматированием try: await message.bot.send_message( owner.telegram_id, f"✅ К вашему профилю добавлен счет:\n\n" f"💳 `{account_number}`\n\n" f"Теперь вы можете участвовать в розыгрышах!", parse_mode="Markdown" ) text += "📨 Владельцу отправлено уведомление\n\n" except Exception as e: text += f"⚠️ Не удалось отправить уведомление: {str(e)}\n\n" # Предлагаем добавить в розыгрыш await show_lottery_selection(message, text, state) except ValueError as e: await message.answer(f"❌ Ошибка: {str(e)}") await state.clear() except Exception as e: await message.answer(f"❌ Произошла ошибка: {str(e)}") await state.clear() @router.message(AddAccountStates.waiting_for_data) async def process_accounts_data(message: Message, state: FSMContext): """Обработка данных счетов (один или несколько)""" if message.text.strip().lower() == '/cancel': await state.clear() await message.answer("❌ Операция отменена") return lines = message.text.strip().split('\n') # Ограничение: максимум 1000 счетов за раз MAX_ACCOUNTS = 1000 if len(lines) > MAX_ACCOUNTS: await message.answer( f"⚠️ Слишком много счетов!\n\n" f"Максимум за раз: {MAX_ACCOUNTS}\n" f"Вы отправили: {len(lines)} строк\n\n" f"Разделите данные на несколько частей." ) await state.clear() return # Отправляем начальное уведомление progress_msg = await message.answer( f"⏳ Обработка {len(lines)} строк...\n" f"Пожалуйста, подождите..." ) accounts_data = [] errors = [] BATCH_SIZE = 100 # Обрабатываем по 100 счетов за раз # Универсальный парсер: поддержка однострочного и многострочного формата i = 0 while i < len(lines): line = lines[i].strip() # Пропускаем пустые строки и строки с названиями/датами if not line or any(x in line.lower() for x in ['viposnova', '0.00', ':']): i += 1 continue # Проверяем, есть ли в строке пробел (однострочный формат: "карта счет") if ' ' in line: # Однострочный формат: разделяем по первому пробелу parts = line.split(maxsplit=1) if len(parts) == 2: club_card, account_number = parts else: errors.append(f"Строка {i+1}: неверный формат") i += 1 continue else: # Многострочный формат: текущая строка - счет, следующая - карта account_number = line i += 1 if i >= len(lines): errors.append(f"Строка {i}: отсутствует номер карты после счета {account_number}") break club_card = lines[i].strip() # Пропускаем, если следующая строка содержит мусор if not club_card or any(x in club_card.lower() for x in ['viposnova', '0.00', ':']): errors.append(f"Строка {i}: некорректный номер карты после счета {account_number}") i += 1 continue # Создаем счет try: async with async_session_maker() as session: account = await AccountService.create_account( session, club_card_number=club_card, account_number=account_number ) owner = await AccountService.get_account_owner(session, account_number) accounts_data.append({ 'club_card': club_card, 'account_number': account_number, 'account_id': account.id, 'owner': owner, 'owner_id': owner.telegram_id if owner else None }) # Обновляем progress каждые 50 счетов if len(accounts_data) % 50 == 0: try: await progress_msg.edit_text( f"⏳ Обработано: {len(accounts_data)} / ~{len(lines)}\n" f"❌ Ошибок: {len(errors)}" ) except: pass # Игнорируем ошибки редактирования except ValueError as e: errors.append(f"Счет {account_number} (карта {club_card}): {str(e)}") except Exception as e: errors.append(f"Счет {account_number}: {str(e)}") i += 1 # Удаляем progress сообщение try: await progress_msg.delete() except: pass # Группируем счета по владельцам и отправляем групповые уведомления if accounts_data: from collections import defaultdict accounts_by_owner = defaultdict(list) for acc in accounts_data: if acc['owner_id']: accounts_by_owner[acc['owner_id']].append(acc['account_number']) # Отправляем групповые уведомления for owner_id, account_numbers in accounts_by_owner.items(): try: if len(account_numbers) == 1: # Одиночное уведомление notification_text = ( "✅ К вашему профилю добавлен счет:\n\n" f"💳 `{account_numbers[0]}`\n\n" "Теперь вы можете участвовать в розыгрышах!" ) await message.bot.send_message( owner_id, notification_text, parse_mode="Markdown" ) elif len(account_numbers) <= 50: # Групповое уведомление (до 50 счетов) notification_text = ( f"✅ К вашему профилю добавлено счетов: *{len(account_numbers)}*\n\n" "💳 *Ваши счета:*\n" ) for acc_num in account_numbers: notification_text += f"• `{acc_num}`\n" notification_text += "\nТеперь вы можете участвовать в розыгрышах!" await message.bot.send_message( owner_id, notification_text, parse_mode="Markdown" ) else: # Много счетов - показываем первые 10 и кнопку notification_text = ( f"✅ К вашему профилю добавлено счетов: *{len(account_numbers)}*\n\n" "💳 *Первые 10 счетов:*\n" ) for acc_num in account_numbers[:10]: notification_text += f"• `{acc_num}`\n" notification_text += f"\n_...и ещё {len(account_numbers) - 10} счетов_\n\n" notification_text += "Теперь вы можете участвовать в розыгрышах!" # Кнопка для просмотра всех счетов keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton( text="📋 Просмотреть все счета", callback_data="view_my_accounts" )] ]) await message.bot.send_message( owner_id, notification_text, parse_mode="Markdown", reply_markup=keyboard ) except Exception as e: pass # Игнорируем ошибки отправки уведомлений # Формируем отчет text = f"📊 **Результаты добавления счетов**\n\n" if accounts_data: text += f"✅ **Успешно добавлено: {len(accounts_data)}**\n\n" for acc in accounts_data: text += f"• {acc['club_card']} → {acc['account_number']}\n" if acc['owner']: text += f" 👤 {acc['owner'].first_name}\n" text += "\n" if errors: text += f"❌ **Ошибки: {len(errors)}**\n\n" for error in errors[:5]: # Показываем максимум 5 ошибок text += f"• {error}\n" if len(errors) > 5: text += f"\n... и еще {len(errors) - 5} ошибок\n" if not accounts_data: await message.answer(text) await state.clear() return # Сохраняем данные и предлагаем добавить в розыгрыш await state.update_data(accounts=accounts_data) await show_lottery_selection(message, text, state) async def show_lottery_selection(message: Message, prev_text: str, state: FSMContext): """Показать выбор розыгрыша для добавления счетов""" async with async_session_maker() as session: lotteries = await LotteryService.get_active_lotteries(session) if not lotteries: await message.answer( prev_text + "ℹ️ Нет активных розыгрышей для добавления счетов" ) await state.clear() return await state.set_state(AddAccountStates.choosing_lottery) buttons = [] for lottery in lotteries[:10]: # Максимум 10 розыгрышей buttons.append([ InlineKeyboardButton( text=f"🎯 {lottery.title}", callback_data=f"add_to_lottery_{lottery.id}" ) ]) buttons.append([ InlineKeyboardButton(text="❌ Пропустить", callback_data="skip_lottery_add") ]) await message.answer( prev_text + "➕ **Добавить счета в розыгрыш?**\n\n" "Выберите розыгрыш из списка:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons) ) @router.callback_query(F.data.startswith("add_to_lottery_")) async def add_accounts_to_lottery(callback: CallbackQuery, state: FSMContext): """Добавить счета в выбранный розыгрыш""" lottery_id = int(callback.data.split("_")[-1]) data = await state.get_data() accounts = data.get('accounts', []) if not accounts: await callback.answer("❌ Нет данных о счетах", show_alert=True) await state.clear() return success_count = 0 errors = [] async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await callback.answer("❌ Розыгрыш не найден", show_alert=True) await state.clear() return for acc in accounts: try: # Добавляем участие через account_id # Проверяем, не участвует ли уже existing = await session.execute( select(Participation).where( and_( Participation.lottery_id == lottery_id, Participation.account_id == acc['account_id'] ) ) ) if existing.scalar_one_or_none(): errors.append(f"{acc['account_number']}: уже участвует") continue # Создаем участие participation = Participation( lottery_id=lottery_id, account_id=acc['account_id'], account_number=acc['account_number'] ) session.add(participation) success_count += 1 except Exception as e: errors.append(f"{acc['account_number']}: {str(e)}") await session.commit() text = f"📊 **Добавление в розыгрыш '{lottery.title}'**\n\n" if success_count: text += f"✅ Добавлено счетов: {success_count}\n\n" if errors: text += f"⚠️ Ошибки: {len(errors)}\n" for error in errors[:3]: text += f"• {error}\n" await callback.message.edit_text(text) await state.clear() @router.callback_query(F.data == "skip_lottery_add") async def skip_lottery_add(callback: CallbackQuery, state: FSMContext): """Пропустить добавление в розыгрыш""" await callback.message.edit_text("✅ Счета добавлены без участия в розыгрышах") await state.clear() @router.message(Command("remove_account")) @admin_only async def remove_account_command(message: Message): """ Деактивировать счет(а) Формат: /remove_account [account_number2] [account_number3] ... Можно указать несколько счетов через пробел для массового удаления """ parts = message.text.split() if len(parts) < 2: await message.answer( "❌ Неверный формат команды\n\n" "Используйте: /remove_account [account_number2] ...\n\n" "Примеры:\n" "• /remove_account 12-34-56-78-90-12-34\n" "• /remove_account 12-34-56-78-90-12-34 98-76-54-32-10-98-76" ) return account_numbers = parts[1:] # Все аргументы после команды try: results = { 'success': [], 'not_found': [], 'errors': [] } async with async_session_maker() as session: for account_number in account_numbers: try: success = await AccountService.deactivate_account(session, account_number) if success: results['success'].append(account_number) else: results['not_found'].append(account_number) except Exception as e: results['errors'].append((account_number, str(e))) # Формируем отчёт response_parts = [] if results['success']: response_parts.append( f"✅ *Деактивировано счетов: {len(results['success'])}*\n" + "\n".join(f"• `{acc}`" for acc in results['success']) ) if results['not_found']: response_parts.append( f"❌ *Не найдено счетов: {len(results['not_found'])}*\n" + "\n".join(f"• `{acc}`" for acc in results['not_found']) ) if results['errors']: response_parts.append( f"⚠️ *Ошибки при обработке: {len(results['errors'])}*\n" + "\n".join(f"• `{acc}`: {err}" for acc, err in results['errors']) ) if not response_parts: await message.answer("❌ Не удалось обработать ни один счет") else: await message.answer("\n\n".join(response_parts), parse_mode="Markdown") except Exception as e: await message.answer(f"❌ Критическая ошибка: {str(e)}") @router.message(Command("verify_winner")) @admin_only async def verify_winner_command(message: Message): """ Подтвердить выигрыш по коду верификации Формат: /verify_winner Пример: /verify_winner AB12CD34 1 """ parts = message.text.split() if len(parts) != 3: await message.answer( "❌ Неверный формат команды\n\n" "Используйте:\n" "/verify_winner \n\n" "Пример:\n" "/verify_winner AB12CD34 1" ) return verification_code = parts[1].upper() try: lottery_id = int(parts[2]) except ValueError: await message.answer("❌ lottery_id должен быть числом") return try: async with async_session_maker() as session: # Проверяем существование розыгрыша lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await message.answer(f"❌ Розыгрыш #{lottery_id} не найден") return # Подтверждаем выигрыш winner = await WinnerNotificationService.verify_winner( session, verification_code=verification_code, lottery_id=lottery_id ) if not winner: await message.answer( f"❌ Выигрыш не найден\n\n" f"Возможные причины:\n" f"• Неверный код верификации\n" f"• Пользователь не является победителем в розыгрыше #{lottery_id}\n" f"• Выигрыш уже был подтвержден" ) return # Получаем пользователя user = await RegistrationService.get_user_by_verification_code(session, verification_code) text = "✅ Выигрыш подтвержден!\n\n" text += f"🎯 Розыгрыш: {lottery.title}\n" text += f"🏆 Место: {winner.place}\n" text += f"🎁 Приз: {winner.prize}\n\n" if user: text += f"👤 Победитель: {user.first_name}\n" text += f"🎫 Клубная карта: {user.club_card_number}\n" if user.phone: text += f"📱 Телефон: {user.phone}\n" # Отправляем уведомление победителю try: bot = message.bot await bot.send_message( user.telegram_id, f"✅ Ваш выигрыш подтвержден!\n\n" f"🎯 Розыгрыш: {lottery.title}\n" f"🏆 Место: {winner.place}\n" f"🎁 Приз: {winner.prize}\n\n" f"Администратор свяжется с вами для получения приза." ) text += "\n📨 Победителю отправлено уведомление" except Exception as e: text += f"\n⚠️ Не удалось отправить уведомление: {str(e)}" if winner.account_number: text += f"💳 Счет: {winner.account_number}\n" await message.answer(text) except Exception as e: await message.answer(f"❌ Ошибка: {str(e)}") @router.message(Command("winner_status")) @admin_only async def winner_status_command(message: Message): """ Показать статус всех победителей розыгрыша Формат: /winner_status """ parts = message.text.split() if len(parts) != 2: await message.answer( "❌ Неверный формат команды\n\n" "Используйте: /winner_status " ) return try: lottery_id = int(parts[1]) except ValueError: await message.answer("❌ lottery_id должен быть числом") return try: async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await message.answer(f"❌ Розыгрыш #{lottery_id} не найден") return winners = await LotteryService.get_winners(session, lottery_id) if not winners: await message.answer(f"В розыгрыше '{lottery.title}' пока нет победителей") return text = f"🏆 Победители розыгрыша '{lottery.title}':\n\n" for winner in winners: status_icon = "✅" if winner.is_claimed else "⏳" notified_icon = "📨" if winner.is_notified else "📭" text += f"{status_icon} {winner.place} место - {winner.prize}\n" # Получаем информацию о победителе async with async_session_maker() as session: if winner.user_id: user_result = await session.execute( select(User).where(User.id == winner.user_id) ) user = user_result.scalar_one_or_none() if user: text += f" 👤 {user.first_name}" if user.club_card_number: text += f" (КК: {user.club_card_number})" text += "\n" if winner.account_number: text += f" 💳 {winner.account_number}\n" # Статус подтверждения if winner.is_claimed: text += f" ✅ Подтвержден\n" else: text += f" ⏳ Ожидает подтверждения\n" text += "\n" await message.answer(text) except Exception as e: await message.answer(f"❌ Ошибка: {str(e)}") @router.message(Command("user_info")) @admin_only async def user_info_command(message: Message): """ Показать информацию о пользователе Формат: /user_info """ parts = message.text.split() if len(parts) != 2: await message.answer( "❌ Неверный формат команды\n\n" "Используйте: /user_info " ) return club_card = parts[1] try: async with async_session_maker() as session: user = await RegistrationService.get_user_by_club_card(session, club_card) if not user: await message.answer(f"❌ Пользователь с клубной картой {club_card} не найден") return # Получаем счета accounts = await AccountService.get_user_accounts(session, user.id) # Получаем выигрыши winners_result = await session.execute( select(Winner).where(Winner.user_id == user.id) ) winners = winners_result.scalars().all() text = f"👤 Информация о пользователе\n\n" text += f"🎫 Клубная карта: {user.club_card_number}\n" text += f"👤 Имя: {user.first_name}" if user.last_name: text += f" {user.last_name}" text += "\n" if user.username: text += f"📱 Telegram: @{user.username}\n" if user.phone: text += f"📞 Телефон: {user.phone}\n" text += f"🔑 Код верификации: {user.verification_code}\n" text += f"📅 Зарегистрирован: {user.created_at.strftime('%d.%m.%Y')}\n\n" # Счета text += f"💳 Счета ({len(accounts)}):\n" if accounts: for acc in accounts: status = "✅" if acc.is_active else "❌" text += f" {status} {acc.account_number}\n" else: text += " Нет счетов\n" # Выигрыши text += f"\n🏆 Выигрыши ({len(winners)}):\n" if winners: for w in winners: status = "✅" if w.is_claimed else "⏳" text += f" {status} {w.place} место - {w.prize}\n" else: text += " Нет выигрышей\n" await message.answer(text) except Exception as e: await message.answer(f"❌ Ошибка: {str(e)}") @router.callback_query(F.data == "view_my_accounts") async def view_my_accounts_callback(callback: CallbackQuery): """Показать все счета пользователя""" import asyncio try: async with async_session_maker() as session: # Получаем пользователя user_result = await session.execute( select(User).where(User.telegram_id == callback.from_user.id) ) user = user_result.scalar_one_or_none() if not user: await callback.answer("❌ Пользователь не найден", show_alert=True) return # Получаем все счета accounts = await AccountService.get_user_accounts(session, user.id) if not accounts: await callback.answer("У вас нет счетов", show_alert=True) return # Отвечаем на callback сразу, чтобы не было timeout await callback.answer("⏳ Загружаю ваши счета...") # Если счетов много - предупреждаем о задержке batches_count = (len(accounts) + 49) // 50 # Округление вверх if batches_count > 5: await callback.message.answer( f"📊 Найдено счетов: *{len(accounts)}*\n" f"📤 Отправка {batches_count} сообщений с задержкой (~{batches_count//2} сек)\n\n" f"⏳ _Пожалуйста, подождите. Бот не завис._", parse_mode="Markdown" ) # Формируем сообщение с пагинацией (по 50 счетов на сообщение) BATCH_SIZE = 50 for i in range(0, len(accounts), BATCH_SIZE): batch = accounts[i:i+BATCH_SIZE] text = f"💳 *Ваши счета ({i+1}-{min(i+BATCH_SIZE, len(accounts))} из {len(accounts)}):*\n\n" for acc in batch: status = "✅" if acc.is_active else "❌" text += f"{status} `{acc.account_number}`\n" try: await callback.message.answer(text, parse_mode="Markdown") # Задержка между сообщениями для избежания flood control if i + BATCH_SIZE < len(accounts): await asyncio.sleep(0.5) # 500ms между сообщениями except Exception as send_error: # Если flood control - ждём дольше if "Flood control" in str(send_error) or "Too Many Requests" in str(send_error): await asyncio.sleep(2) await callback.message.answer(text, parse_mode="Markdown") else: raise except Exception as e: # Не используем callback.answer в except - может быть timeout try: await callback.message.answer(f"❌ Ошибка: {str(e)}") except: pass # Игнорируем если не получилось отправить