Some checks reported errors
continuous-integration/drone/push Build encountered an error
Основные изменения: ✨ Новые функции: - Система регистрации пользователей с множественными счетами - Автоматическое подтверждение выигрышей через inline-кнопки - Механизм переигровки для неподтвержденных выигрышей (24 часа) - Подтверждение на уровне счетов (каждый счет подтверждается отдельно) - Скрипт полной очистки базы данных 🔧 Технические улучшения: - Исправлена ошибка MissingGreenlet при lazy loading (добавлен joinedload/selectinload) - Добавлено поле claimed_at для отслеживания времени подтверждения - Пакетное добавление счетов с выбором розыгрыша - Проверка владения конкретным счетом при подтверждении 📚 Документация: - docs/AUTO_CONFIRM_SYSTEM.md - Полная документация системы подтверждения - docs/ACCOUNT_BASED_CONFIRMATION.md - Подтверждение на уровне счетов - docs/REGISTRATION_SYSTEM.md - Система регистрации - docs/ADMIN_COMMANDS.md - Команды администратора - docs/CLEAR_DATABASE.md - Очистка БД - docs/QUICK_GUIDE.md - Быстрое начало - docs/UPDATE_LOG.md - Журнал обновлений 🗄️ База данных: - Миграция 003: Таблицы accounts, winner_verifications - Миграция 004: Поле claimed_at в таблице winners - Скрипт scripts/clear_database.py для полной очистки 🎮 Новые команды: Админские: - /check_unclaimed <lottery_id> - Проверка неподтвержденных выигрышей - /redraw <lottery_id> - Повторный розыгрыш - /add_accounts - Пакетное добавление счетов - /list_accounts <telegram_id> - Список счетов пользователя Пользовательские: - /register - Регистрация с вводом данных - /my_account - Просмотр своих счетов - Callback confirm_win_{id} - Подтверждение выигрыша 🛠️ Makefile: - make clear-db - Очистка всех данных из БД (с подтверждением) 🔒 Безопасность: - Проверка владения счетом при подтверждении - Защита от подтверждения чужих счетов - Независимое подтверждение каждого выигрышного счета 📊 Логика работы: 1. Пользователь регистрируется и добавляет счета 2. Счета участвуют в розыгрыше 3. Победители получают уведомление с кнопкой подтверждения 4. Каждый счет подтверждается отдельно (24 часа на подтверждение) 5. Неподтвержденные выигрыши переигрываются через /redraw
3107 lines
136 KiB
Python
3107 lines
136 KiB
Python
"""
|
||
Расширенная админ-панель для управления розыгрышами
|
||
"""
|
||
from aiogram import Router, F
|
||
from aiogram.types import (
|
||
CallbackQuery, Message, 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 datetime import datetime, timedelta
|
||
import json
|
||
|
||
from ..core.database import async_session_maker
|
||
from ..core.services import UserService, LotteryService, ParticipationService
|
||
from ..core.config import ADMIN_IDS
|
||
from ..core.models import User
|
||
|
||
|
||
# Состояния для админки
|
||
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()
|
||
|
||
# Установка победителей
|
||
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()
|
||
|
||
|
||
admin_router = Router()
|
||
|
||
|
||
def is_admin(user_id: int) -> bool:
|
||
"""Проверка прав администратора"""
|
||
return user_id in ADMIN_IDS
|
||
|
||
|
||
def get_admin_main_keyboard() -> InlineKeyboardMarkup:
|
||
"""Главная админ-панель"""
|
||
buttons = [
|
||
[InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")],
|
||
[InlineKeyboardButton(text="👥 Управление участниками", callback_data="admin_participants")],
|
||
[InlineKeyboardButton(text="👑 Управление победителями", callback_data="admin_winners")],
|
||
[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_winner_display_settings")],
|
||
[InlineKeyboardButton(text="📋 Список всех розыгрышей", callback_data="admin_list_all_lotteries")],
|
||
[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="📥 Массовое добавление (ID)", callback_data="admin_bulk_add_participant"),
|
||
InlineKeyboardButton(text="🏦 Массовое добавление (счета)", callback_data="admin_bulk_add_accounts")
|
||
],
|
||
[InlineKeyboardButton(text="➖ Удалить участника", callback_data="admin_remove_participant")],
|
||
[
|
||
InlineKeyboardButton(text="📤 Массовое удаление (ID)", callback_data="admin_bulk_remove_participant"),
|
||
InlineKeyboardButton(text="🏦 Массовое удаление (счета)", callback_data="admin_bulk_remove_accounts")
|
||
],
|
||
[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_conduct_draw")],
|
||
[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 is_admin(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 is_admin(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):
|
||
"""Начать создание розыгрыша"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||
return
|
||
|
||
text = "📝 Создание нового розыгрыша\n\n"
|
||
text += "Шаг 1 из 4\n\n"
|
||
text += "Введите название розыгрыша:"
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")]
|
||
])
|
||
)
|
||
await state.set_state(AdminStates.lottery_title)
|
||
|
||
|
||
@admin_router.message(StateFilter(AdminStates.lottery_title))
|
||
async def process_lottery_title(message: Message, state: FSMContext):
|
||
"""Обработка названия розыгрыша"""
|
||
if not is_admin(message.from_user.id):
|
||
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 is_admin(message.from_user.id):
|
||
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 is_admin(message.from_user.id):
|
||
await message.answer("❌ Недостаточно прав")
|
||
return
|
||
|
||
prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()]
|
||
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 is_admin(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_user_by_telegram_id(session, callback.from_user.id)
|
||
|
||
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 is_admin(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 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) 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 ""
|
||
username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
|
||
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 is_admin(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 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
|
||
|
||
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_add_participant")
|
||
async def start_add_participant(callback: CallbackQuery, state: FSMContext):
|
||
"""Начать добавление участника"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
lotteries = await LotteryService.get_active_lotteries(session)
|
||
|
||
if not lotteries:
|
||
await callback.message.edit_text(
|
||
"❌ Нет активных розыгрышей",
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="🔙 Назад", callback_data="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 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_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 is_admin(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 is_admin(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 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
|
||
|
||
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 is_admin(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 is_admin(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 is_admin(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 is_admin(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 is_admin(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 is_admin(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 is_admin(callback.from_user.id):
|
||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
lotteries = await LotteryService.get_active_lotteries(session)
|
||
|
||
if not lotteries:
|
||
await callback.message.edit_text(
|
||
"❌ Нет активных розыгрышей",
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="🔙 Назад", callback_data="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 is_admin(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 is_admin(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 is_admin(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 is_admin(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 is_admin(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 == "admin_bulk_add_accounts")
|
||
async def start_bulk_add_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)
|
||
|
||
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 is_admin(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 is_admin(message.from_user.id):
|
||
await message.answer("❌ Недостаточно прав")
|
||
return
|
||
|
||
data = await state.get_data()
|
||
lottery_id = data['bulk_add_accounts_lottery_id']
|
||
|
||
# Парсим входные данные - поддерживаем и запятые, и переносы строк
|
||
account_inputs = []
|
||
for line in message.text.split('\n'):
|
||
for account in line.split(','):
|
||
account = account.strip()
|
||
if account:
|
||
account_inputs.append(account)
|
||
|
||
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 is_admin(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 is_admin(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 is_admin(message.from_user.id):
|
||
await message.answer("❌ Недостаточно прав")
|
||
return
|
||
|
||
data = await state.get_data()
|
||
lottery_id = data['bulk_remove_accounts_lottery_id']
|
||
|
||
# Парсим входные данные - поддерживаем и запятые, и переносы строк
|
||
account_inputs = []
|
||
for line in message.text.split('\n'):
|
||
for account in line.split(','):
|
||
account = account.strip()
|
||
if account:
|
||
account_inputs.append(account)
|
||
|
||
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 is_admin(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 is_admin(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 is_admin(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_"))
|
||
async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
||
"""Редирект на редактирование розыгрыша из детального просмотра"""
|
||
if not is_admin(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 is_admin(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):
|
||
"""Переключить активность розыгрыша"""
|
||
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)
|
||
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, None)
|
||
|
||
|
||
@admin_router.callback_query(F.data == "admin_finish_lottery")
|
||
async def start_finish_lottery(callback: CallbackQuery):
|
||
"""Завершить розыгрыш"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
lotteries = await LotteryService.get_active_lotteries(session)
|
||
|
||
if not lotteries:
|
||
await callback.message.edit_text(
|
||
"❌ Нет активных розыгрышей для завершения",
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="🔙 Назад", callback_data="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 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)
|
||
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 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:
|
||
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 is_admin(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 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)
|
||
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 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)
|
||
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 is_admin(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 is_admin(callback.from_user.id):
|
||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
lotteries = await LotteryService.get_active_lotteries(session)
|
||
|
||
if not lotteries:
|
||
await callback.message.edit_text(
|
||
"❌ Нет активных розыгрышей для установки победителей",
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="🔙 Назад", callback_data="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 is_admin(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 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
|
||
|
||
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 is_admin(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 += f"Введите Telegram ID или username пользователя:"
|
||
|
||
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):
|
||
"""Обработка пользователя-победителя"""
|
||
if not is_admin(message.from_user.id):
|
||
await message.answer("❌ Недостаточно прав")
|
||
return
|
||
|
||
user_input = message.text.strip()
|
||
|
||
# Пробуем определить, это 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
|
||
else:
|
||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||
|
||
if not user:
|
||
await message.answer("❌ Пользователь с таким ID не найден")
|
||
return
|
||
|
||
data = await state.get_data()
|
||
|
||
async with async_session_maker() as session:
|
||
success = await LotteryService.set_manual_winner(
|
||
session,
|
||
data['lottery_id'],
|
||
data['place'],
|
||
telegram_id
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
if success:
|
||
username = f"@{user.username}" if user.username else user.first_name
|
||
await message.answer(
|
||
f"✅ Предопределенный победитель установлен!\n\n"
|
||
f"🏆 Место: {data['place']}\n"
|
||
f"👤 Пользователь: {username}\n"
|
||
f"🆔 ID: {telegram_id}\n\n"
|
||
f"При проведении розыгрыша этот пользователь автоматически займет {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 is_admin(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 is_admin(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 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)
|
||
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 is_admin(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 is_admin(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 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)
|
||
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 is_admin(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 is_admin(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 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=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.startswith("admin_conduct_"))
|
||
async def conduct_lottery_draw(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
|
||
|
||
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
|
||
|
||
# Проводим розыгрыш через сервис
|
||
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
|
||
|
||
if winners_dict:
|
||
# Получаем победителей из базы
|
||
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"
|
||
|
||
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 is_admin(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 is_admin(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 = [
|
||
[InlineKeyboardButton(text="💾 Экспорт данных", callback_data="admin_export_data")],
|
||
[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 is_admin(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 callback.message.edit_text(
|
||
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 is_admin(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 is_admin(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 is_admin(callback.from_user.id):
|
||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||
return
|
||
|
||
await callback.answer(
|
||
"ℹ️ Функция в разработке\n\nУдаление пользователей требует дополнительной логики для сохранения целостности данных.",
|
||
show_alert=True
|
||
)
|
||
|
||
|
||
@admin_router.callback_query(F.data == "admin_cleanup_old_participations")
|
||
async def cleanup_old_participations(callback: CallbackQuery):
|
||
"""Очистка старых участий"""
|
||
if not is_admin(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 is_admin(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 is_admin(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 is_admin(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 is_admin(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()
|
||
|
||
|
||
# Экспорт роутера
|
||
__all__ = ['admin_router'] |