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