376 lines
14 KiB
Python
376 lines
14 KiB
Python
"""
|
||
Обработчики для работы со счетами в розыгрышах
|
||
"""
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||
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 config import ADMIN_IDS
|
||
from database import async_session_maker
|
||
from services import LotteryService
|
||
from account_services import AccountParticipationService
|
||
from account_utils import parse_accounts_from_message, validate_account_number
|
||
from typing import List
|
||
|
||
|
||
# Состояния FSM для работы со счетами
|
||
class AccountStates(StatesGroup):
|
||
waiting_for_lottery_choice = State() # Выбор розыгрыша для добавления счетов
|
||
waiting_for_winner_lottery = State() # Выбор розыгрыша для установки победителя
|
||
waiting_for_winner_place = State() # Выбор места победителя
|
||
searching_accounts = State() # Поиск счетов
|
||
|
||
|
||
# Создаем роутер
|
||
account_router = Router()
|
||
|
||
|
||
def is_admin(user_id: int) -> bool:
|
||
"""Проверка прав администратора"""
|
||
return user_id in ADMIN_IDS
|
||
|
||
|
||
@account_router.message(
|
||
F.text,
|
||
StateFilter(None),
|
||
~F.text.startswith('/') # Исключаем команды
|
||
)
|
||
async def detect_account_input(message: Message, state: FSMContext):
|
||
"""
|
||
Обнаружение ввода счетов в сообщении
|
||
Активируется только для администраторов
|
||
"""
|
||
if not is_admin(message.from_user.id):
|
||
return
|
||
|
||
# Парсим счета из сообщения
|
||
accounts = parse_accounts_from_message(message.text)
|
||
|
||
if not accounts:
|
||
return # Счета не обнаружены, пропускаем
|
||
|
||
# Сохраняем счета в состоянии
|
||
await state.update_data(detected_accounts=accounts)
|
||
|
||
# Формируем сообщение
|
||
accounts_text = "\n".join([f"• {acc}" for acc in accounts])
|
||
count = len(accounts)
|
||
|
||
text = (
|
||
f"🔍 <b>Обнаружен ввод счет{'а' if count == 1 else 'ов'}</b>\n\n"
|
||
f"Найдено: <b>{count}</b>\n\n"
|
||
f"{accounts_text}\n\n"
|
||
f"Выберите действие:"
|
||
)
|
||
|
||
# Кнопки выбора действия
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(
|
||
text="➕ Добавить в розыгрыш",
|
||
callback_data="account_action:add_to_lottery"
|
||
)],
|
||
[InlineKeyboardButton(
|
||
text="👑 Сделать победителем",
|
||
callback_data="account_action:set_as_winner"
|
||
)],
|
||
[InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data="account_action:cancel"
|
||
)]
|
||
])
|
||
|
||
await message.answer(text, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
|
||
@account_router.callback_query(F.data == "account_action:cancel")
|
||
async def cancel_account_action(callback: CallbackQuery, state: FSMContext):
|
||
"""Отмена действия со счетами"""
|
||
await state.clear()
|
||
await callback.message.edit_text("❌ Действие отменено")
|
||
await callback.answer()
|
||
|
||
|
||
@account_router.callback_query(F.data == "account_action:add_to_lottery")
|
||
async def choose_lottery_for_accounts(callback: CallbackQuery, state: FSMContext):
|
||
"""Выбор розыгрыша для добавления счетов"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
# Получаем активные розыгрыши
|
||
lotteries = await LotteryService.get_active_lotteries(session, limit=20)
|
||
|
||
if not lotteries:
|
||
await callback.message.edit_text(
|
||
"❌ Нет активных розыгрышей.\n\n"
|
||
"Сначала создайте розыгрыш через /admin"
|
||
)
|
||
await state.clear()
|
||
await callback.answer()
|
||
return
|
||
|
||
# Формируем кнопки с розыгрышами
|
||
buttons = []
|
||
for lottery in lotteries:
|
||
buttons.append([InlineKeyboardButton(
|
||
text=f"🎲 {lottery.title[:40]}",
|
||
callback_data=f"add_accounts_to:{lottery.id}"
|
||
)])
|
||
|
||
buttons.append([InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data="account_action:cancel"
|
||
)])
|
||
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
await callback.message.edit_text(
|
||
"📋 <b>Выберите розыгрыш:</b>",
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AccountStates.waiting_for_lottery_choice)
|
||
await callback.answer()
|
||
|
||
|
||
@account_router.callback_query(F.data.startswith("add_accounts_to:"))
|
||
async def add_accounts_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])
|
||
|
||
# Получаем сохраненные счета
|
||
data = await state.get_data()
|
||
accounts = data.get("detected_accounts", [])
|
||
|
||
if not accounts:
|
||
await callback.message.edit_text("❌ Счета не найдены")
|
||
await state.clear()
|
||
await callback.answer()
|
||
return
|
||
|
||
# Показываем процесс
|
||
await callback.message.edit_text("⏳ Добавляем счета...")
|
||
|
||
async with async_session_maker() as session:
|
||
# Получаем информацию о розыгрыше
|
||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||
|
||
if not lottery:
|
||
await callback.message.edit_text("❌ Розыгрыш не найден")
|
||
await state.clear()
|
||
await callback.answer()
|
||
return
|
||
|
||
# Добавляем счета
|
||
results = await AccountParticipationService.add_accounts_bulk(
|
||
session, lottery_id, accounts
|
||
)
|
||
|
||
# Формируем результат
|
||
text = f"<b>Результаты добавления в розыгрыш:</b>\n<i>{lottery.title}</i>\n\n"
|
||
text += f"✅ Добавлено: <b>{results['added']}</b>\n"
|
||
text += f"⚠️ Пропущено: <b>{results['skipped']}</b>\n\n"
|
||
|
||
if results['details']:
|
||
text += "<b>Детали:</b>\n"
|
||
text += "\n".join(results['details'][:20]) # Показываем первые 20
|
||
|
||
if len(results['details']) > 20:
|
||
text += f"\n... и ещё {len(results['details']) - 20}"
|
||
|
||
if results['errors']:
|
||
text += f"\n\n<b>Ошибки:</b>\n"
|
||
text += "\n".join(results['errors'][:10])
|
||
|
||
await callback.message.edit_text(text, parse_mode="HTML")
|
||
await state.clear()
|
||
await callback.answer("✅ Готово!")
|
||
|
||
|
||
@account_router.callback_query(F.data == "account_action:set_as_winner")
|
||
async def choose_lottery_for_winner(callback: CallbackQuery, state: FSMContext):
|
||
"""Выбор розыгрыша для установки победителя"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
|
||
return
|
||
|
||
# Проверяем, что у нас только один счет
|
||
data = await state.get_data()
|
||
accounts = data.get("detected_accounts", [])
|
||
|
||
if len(accounts) != 1:
|
||
await callback.message.edit_text(
|
||
"❌ Для установки победителя введите <b>один</b> счет",
|
||
parse_mode="HTML"
|
||
)
|
||
await state.clear()
|
||
await callback.answer()
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
# Получаем все розыгрыши (активные и завершенные)
|
||
lotteries = await LotteryService.get_all_lotteries(session, limit=30)
|
||
|
||
if not lotteries:
|
||
await callback.message.edit_text(
|
||
"❌ Нет розыгрышей.\n\n"
|
||
"Сначала создайте розыгрыш через /admin"
|
||
)
|
||
await state.clear()
|
||
await callback.answer()
|
||
return
|
||
|
||
# Формируем кнопки
|
||
buttons = []
|
||
for lottery in lotteries:
|
||
status = "✅" if lottery.is_completed else "🎲"
|
||
buttons.append([InlineKeyboardButton(
|
||
text=f"{status} {lottery.title[:35]}",
|
||
callback_data=f"winner_lottery:{lottery.id}"
|
||
)])
|
||
|
||
buttons.append([InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data="account_action:cancel"
|
||
)])
|
||
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
account = accounts[0]
|
||
await callback.message.edit_text(
|
||
f"👑 <b>Установка победителя</b>\n\n"
|
||
f"Счет: <code>{account}</code>\n\n"
|
||
f"Выберите розыгрыш:",
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AccountStates.waiting_for_winner_lottery)
|
||
await callback.answer()
|
||
|
||
|
||
@account_router.callback_query(F.data.startswith("winner_lottery:"))
|
||
async def choose_winner_place(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])
|
||
|
||
# Сохраняем ID розыгрыша
|
||
await state.update_data(winner_lottery_id=lottery_id)
|
||
|
||
async with async_session_maker() as session:
|
||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||
|
||
if not lottery:
|
||
await callback.message.edit_text("❌ Розыгрыш не найден")
|
||
await state.clear()
|
||
await callback.answer()
|
||
return
|
||
|
||
# Получаем призы
|
||
prizes = lottery.prizes or []
|
||
|
||
# Формируем кнопки с местами
|
||
buttons = []
|
||
for i, prize in enumerate(prizes[:10], 1): # Максимум 10 мест
|
||
prize_text = prize if isinstance(prize, str) else prize.get('description', f'Приз {i}')
|
||
buttons.append([InlineKeyboardButton(
|
||
text=f"🏆 Место {i}: {prize_text[:30]}",
|
||
callback_data=f"winner_place:{i}"
|
||
)])
|
||
|
||
# Если призов нет, предлагаем места 1-5
|
||
if not buttons:
|
||
for i in range(1, 6):
|
||
buttons.append([InlineKeyboardButton(
|
||
text=f"🏆 Место {i}",
|
||
callback_data=f"winner_place:{i}"
|
||
)])
|
||
|
||
buttons.append([InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data="account_action:cancel"
|
||
)])
|
||
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
data = await state.get_data()
|
||
account = data.get("detected_accounts", [])[0]
|
||
|
||
await callback.message.edit_text(
|
||
f"👑 <b>Установка победителя</b>\n\n"
|
||
f"Розыгрыш: <i>{lottery.title}</i>\n"
|
||
f"Счет: <code>{account}</code>\n\n"
|
||
f"Выберите место:",
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AccountStates.waiting_for_winner_place)
|
||
await callback.answer()
|
||
|
||
|
||
@account_router.callback_query(F.data.startswith("winner_place:"))
|
||
async def set_account_winner(callback: CallbackQuery, state: FSMContext):
|
||
"""Установка счета как победителя"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
|
||
return
|
||
|
||
place = int(callback.data.split(":")[1])
|
||
|
||
# Получаем данные
|
||
data = await state.get_data()
|
||
account = data.get("detected_accounts", [])[0]
|
||
lottery_id = data.get("winner_lottery_id")
|
||
|
||
if not account or not lottery_id:
|
||
await callback.message.edit_text("❌ Ошибка: данные не найдены")
|
||
await state.clear()
|
||
await callback.answer()
|
||
return
|
||
|
||
# Показываем процесс
|
||
await callback.message.edit_text("⏳ Устанавливаем победителя...")
|
||
|
||
async with async_session_maker() as session:
|
||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||
|
||
# Получаем приз для этого места
|
||
prize = None
|
||
if lottery.prizes and len(lottery.prizes) >= place:
|
||
prize_info = lottery.prizes[place - 1]
|
||
prize = prize_info if isinstance(prize_info, str) else prize_info.get('description')
|
||
|
||
# Устанавливаем победителя
|
||
result = await AccountParticipationService.set_account_as_winner(
|
||
session, lottery_id, account, place, prize
|
||
)
|
||
|
||
if result["success"]:
|
||
text = (
|
||
f"✅ <b>Победитель установлен!</b>\n\n"
|
||
f"Розыгрыш: <i>{lottery.title}</i>\n"
|
||
f"Счет: <code>{account}</code>\n"
|
||
f"Место: <b>{place}</b>\n"
|
||
)
|
||
if prize:
|
||
text += f"Приз: <i>{prize}</i>"
|
||
|
||
await callback.answer("✅ Победитель установлен!", show_alert=True)
|
||
else:
|
||
text = f"❌ {result['message']}"
|
||
await callback.answer("❌ Ошибка", show_alert=True)
|
||
|
||
await callback.message.edit_text(text, parse_mode="HTML")
|
||
await state.clear()
|