Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Добавлено уведомление о задержке при отправке >250 счетов - Реализовано массовое удаление счетов через /remove_account - Исправлен flood control с задержкой 500ms между сообщениями - Callback.answer() перенесён в начало для предотвращения timeout - Добавлена обработка TelegramRetryAfter с повторной попыткой
812 lines
34 KiB
Python
812 lines
34 KiB
Python
"""Админские обработчики для управления счетами и верификации"""
|
||
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 <club_card> <account_number>
|
||
Или: /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_number1> [account_number2] [account_number3] ...
|
||
Можно указать несколько счетов через пробел для массового удаления
|
||
"""
|
||
|
||
parts = message.text.split()
|
||
if len(parts) < 2:
|
||
await message.answer(
|
||
"❌ Неверный формат команды\n\n"
|
||
"Используйте: /remove_account <account_number1> [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 <verification_code> <lottery_id>
|
||
Пример: /verify_winner AB12CD34 1
|
||
"""
|
||
|
||
parts = message.text.split()
|
||
if len(parts) != 3:
|
||
await message.answer(
|
||
"❌ Неверный формат команды\n\n"
|
||
"Используйте:\n"
|
||
"/verify_winner <verification_code> <lottery_id>\n\n"
|
||
"Пример:\n"
|
||
"/verify_winner AB12CD34 1"
|
||
)
|
||
return
|
||
|
||
verification_code = parts[1].upper()
|
||
|
||
try:
|
||
lottery_id = int(parts[2])
|
||
except ValueError:
|
||
await message.answer("❌ lottery_id должен быть числом")
|
||
return
|
||
|
||
try:
|
||
async with async_session_maker() as session:
|
||
# Проверяем существование розыгрыша
|
||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||
if not lottery:
|
||
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
|
||
return
|
||
|
||
# Подтверждаем выигрыш
|
||
winner = await WinnerNotificationService.verify_winner(
|
||
session,
|
||
verification_code=verification_code,
|
||
lottery_id=lottery_id
|
||
)
|
||
|
||
if not winner:
|
||
await message.answer(
|
||
f"❌ Выигрыш не найден\n\n"
|
||
f"Возможные причины:\n"
|
||
f"• Неверный код верификации\n"
|
||
f"• Пользователь не является победителем в розыгрыше #{lottery_id}\n"
|
||
f"• Выигрыш уже был подтвержден"
|
||
)
|
||
return
|
||
|
||
# Получаем пользователя
|
||
user = await RegistrationService.get_user_by_verification_code(session, verification_code)
|
||
|
||
text = "✅ Выигрыш подтвержден!\n\n"
|
||
text += f"🎯 Розыгрыш: {lottery.title}\n"
|
||
text += f"🏆 Место: {winner.place}\n"
|
||
text += f"🎁 Приз: {winner.prize}\n\n"
|
||
|
||
if user:
|
||
text += f"👤 Победитель: {user.first_name}\n"
|
||
text += f"🎫 Клубная карта: {user.club_card_number}\n"
|
||
if user.phone:
|
||
text += f"📱 Телефон: {user.phone}\n"
|
||
|
||
# Отправляем уведомление победителю
|
||
try:
|
||
bot = message.bot
|
||
await bot.send_message(
|
||
user.telegram_id,
|
||
f"✅ Ваш выигрыш подтвержден!\n\n"
|
||
f"🎯 Розыгрыш: {lottery.title}\n"
|
||
f"🏆 Место: {winner.place}\n"
|
||
f"🎁 Приз: {winner.prize}\n\n"
|
||
f"Администратор свяжется с вами для получения приза."
|
||
)
|
||
text += "\n📨 Победителю отправлено уведомление"
|
||
except Exception as e:
|
||
text += f"\n⚠️ Не удалось отправить уведомление: {str(e)}"
|
||
|
||
if winner.account_number:
|
||
text += f"💳 Счет: {winner.account_number}\n"
|
||
|
||
await message.answer(text)
|
||
|
||
except Exception as e:
|
||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||
|
||
|
||
@router.message(Command("winner_status"))
|
||
@admin_only
|
||
async def winner_status_command(message: Message):
|
||
"""
|
||
Показать статус всех победителей розыгрыша
|
||
Формат: /winner_status <lottery_id>
|
||
"""
|
||
|
||
parts = message.text.split()
|
||
if len(parts) != 2:
|
||
await message.answer(
|
||
"❌ Неверный формат команды\n\n"
|
||
"Используйте: /winner_status <lottery_id>"
|
||
)
|
||
return
|
||
|
||
try:
|
||
lottery_id = int(parts[1])
|
||
except ValueError:
|
||
await message.answer("❌ lottery_id должен быть числом")
|
||
return
|
||
|
||
try:
|
||
async with async_session_maker() as session:
|
||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||
if not lottery:
|
||
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
|
||
return
|
||
|
||
winners = await LotteryService.get_winners(session, lottery_id)
|
||
|
||
if not winners:
|
||
await message.answer(f"В розыгрыше '{lottery.title}' пока нет победителей")
|
||
return
|
||
|
||
text = f"🏆 Победители розыгрыша '{lottery.title}':\n\n"
|
||
|
||
for winner in winners:
|
||
status_icon = "✅" if winner.is_claimed else "⏳"
|
||
notified_icon = "📨" if winner.is_notified else "📭"
|
||
|
||
text += f"{status_icon} {winner.place} место - {winner.prize}\n"
|
||
|
||
# Получаем информацию о победителе
|
||
async with async_session_maker() as session:
|
||
if winner.user_id:
|
||
user_result = await session.execute(
|
||
select(User).where(User.id == winner.user_id)
|
||
)
|
||
user = user_result.scalar_one_or_none()
|
||
if user:
|
||
text += f" 👤 {user.first_name}"
|
||
if user.club_card_number:
|
||
text += f" (КК: {user.club_card_number})"
|
||
text += "\n"
|
||
|
||
if winner.account_number:
|
||
text += f" 💳 {winner.account_number}\n"
|
||
|
||
# Статус подтверждения
|
||
if winner.is_claimed:
|
||
text += f" ✅ Подтвержден\n"
|
||
else:
|
||
text += f" ⏳ Ожидает подтверждения\n"
|
||
|
||
text += "\n"
|
||
|
||
await message.answer(text)
|
||
|
||
except Exception as e:
|
||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||
|
||
|
||
@router.message(Command("user_info"))
|
||
@admin_only
|
||
async def user_info_command(message: Message):
|
||
"""
|
||
Показать информацию о пользователе
|
||
Формат: /user_info <club_card>
|
||
"""
|
||
|
||
parts = message.text.split()
|
||
if len(parts) != 2:
|
||
await message.answer(
|
||
"❌ Неверный формат команды\n\n"
|
||
"Используйте: /user_info <club_card>"
|
||
)
|
||
return
|
||
|
||
club_card = parts[1]
|
||
|
||
try:
|
||
async with async_session_maker() as session:
|
||
user = await RegistrationService.get_user_by_club_card(session, club_card)
|
||
|
||
if not user:
|
||
await message.answer(f"❌ Пользователь с клубной картой {club_card} не найден")
|
||
return
|
||
|
||
# Получаем счета
|
||
accounts = await AccountService.get_user_accounts(session, user.id)
|
||
|
||
# Получаем выигрыши
|
||
winners_result = await session.execute(
|
||
select(Winner).where(Winner.user_id == user.id)
|
||
)
|
||
winners = winners_result.scalars().all()
|
||
|
||
text = f"👤 Информация о пользователе\n\n"
|
||
text += f"🎫 Клубная карта: {user.club_card_number}\n"
|
||
text += f"👤 Имя: {user.first_name}"
|
||
if user.last_name:
|
||
text += f" {user.last_name}"
|
||
text += "\n"
|
||
|
||
if user.username:
|
||
text += f"📱 Telegram: @{user.username}\n"
|
||
|
||
if user.phone:
|
||
text += f"📞 Телефон: {user.phone}\n"
|
||
|
||
text += f"🔑 Код верификации: {user.verification_code}\n"
|
||
text += f"📅 Зарегистрирован: {user.created_at.strftime('%d.%m.%Y')}\n\n"
|
||
|
||
# Счета
|
||
text += f"💳 Счета ({len(accounts)}):\n"
|
||
if accounts:
|
||
for acc in accounts:
|
||
status = "✅" if acc.is_active else "❌"
|
||
text += f" {status} {acc.account_number}\n"
|
||
else:
|
||
text += " Нет счетов\n"
|
||
|
||
# Выигрыши
|
||
text += f"\n🏆 Выигрыши ({len(winners)}):\n"
|
||
if winners:
|
||
for w in winners:
|
||
status = "✅" if w.is_claimed else "⏳"
|
||
text += f" {status} {w.place} место - {w.prize}\n"
|
||
else:
|
||
text += " Нет выигрышей\n"
|
||
|
||
await message.answer(text)
|
||
|
||
except Exception as e:
|
||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||
|
||
|
||
@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 # Игнорируем если не получилось отправить
|