All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #7
6088 lines
266 KiB
Python
6088 lines
266 KiB
Python
"""
|
||
Расширенная админ-панель для управления розыгрышами
|
||
"""
|
||
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 = (
|
||
"📥 <b>Массовые операции</b>\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"📢 <b>Результаты розыгрыша</b>\n\n"
|
||
f"🎯 <b>{lottery.title}</b>\n\n"
|
||
f"🏆 <b>Победители:</b>\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"📥 <b>Экспорт пользователей</b>\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 = (
|
||
"📤 <b>Импорт пользователей</b>\n\n"
|
||
"Отправьте XLSX файл с данными пользователей.\n\n"
|
||
"📋 <b>Формат файла:</b>\n"
|
||
"Первая строка - заголовки (как в экспорте)\n"
|
||
"Обязательная колонка: Telegram ID\n\n"
|
||
"⚠️ <b>Внимание!</b>\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"✅ <b>Импорт завершен!</b>\n\n"
|
||
f"📊 <b>Статистика:</b>\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 = (
|
||
"📢 <b>Массовая рассылка</b>\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="<EFBFBD> Статистика рассылок", 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 = (
|
||
"📢 <b>Выберите тип рассылки</b>\n\n"
|
||
"Доступные варианты:\n"
|
||
"• <b>ЛС пользователям</b> - массовая рассылка всем зарегистрированным пользователям\n"
|
||
"• <b>В канал</b> - отправка сообщения в выбранный канал\n"
|
||
"• <b>В группу</b> - отправка сообщения в выбранную группу"
|
||
)
|
||
|
||
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 = (
|
||
"✉️ <b>Рассылка в личные сообщения</b>\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"❌ <b>Нет доступных {('каналов' if broadcast_type == 'channel' else 'групп')}</b>\n\n"
|
||
"Сначала добавьте канал или группу в разделе 'Управление каналами'"
|
||
)
|
||
buttons = [
|
||
[InlineKeyboardButton(text="📱 Управление каналами", callback_data="admin_broadcast_channels")],
|
||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast_start")]
|
||
]
|
||
else:
|
||
text = (
|
||
f"📢 <b>Выберите {'канал' if broadcast_type == 'channel' else 'группу'}</b>\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"📢 <b>Рассылка в {'канал' if channel.chat_type == 'channel' else 'группу'}</b>\n\n"
|
||
f"📱 <b>Название:</b> {channel.title}\n"
|
||
f"🆔 <b>ID:</b> <code>{channel.chat_id}</code>\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(
|
||
"📤 <b>Начинаю рассылку в ЛС...</b>\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(
|
||
"⚠️ <b>Нет зарегистрированных пользователей</b>\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"✅ <b>Рассылка завершена!</b>\n\n"
|
||
f"📊 <b>Статистика:</b>\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"📤 <b>Отправляю в {'канал' if channel.chat_type == 'channel' else 'группу'}...</b>\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"✅ <b>Сообщение отправлено!</b>\n\n"
|
||
f"📱 {'Канал' if channel.chat_type == 'channel' else 'Группа'}: {channel.title}",
|
||
parse_mode="HTML"
|
||
)
|
||
else:
|
||
await status_msg.edit_text(
|
||
f"❌ <b>Ошибка отправки</b>\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 = "📱 <b>Управление каналами и группами</b>\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} <b>{channel.title}</b>\n"
|
||
text += f" 🆔 ID: <code>{channel.chat_id}</code>\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 = (
|
||
"➕ <b>Добавление канала или группы</b>\n\n"
|
||
"Отправьте ID канала или группы.\n\n"
|
||
"<b>Как узнать ID:</b>\n"
|
||
"1. Добавьте бота в канал/группу как администратора\n"
|
||
"2. Перешлите любое сообщение из канала/группы боту @userinfobot\n"
|
||
"3. Он покажет ID чата (обычно отрицательное число)\n\n"
|
||
"Пример: <code>-1001234567890</code>\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"✅ <b>Канал найден!</b>\n\n"
|
||
f"📱 <b>Название:</b> {chat.title}\n"
|
||
f"🆔 <b>ID:</b> <code>{chat_id}</code>\n"
|
||
f"📝 <b>Тип:</b> {'Канал' if chat_type == 'channel' else 'Группа'}\n"
|
||
)
|
||
if chat.username:
|
||
text += f"🔗 <b>Username:</b> @{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"❌ <b>Ошибка получения информации о чате</b>\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(
|
||
"✅ <b>Канал обновлен!</b>\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(
|
||
"✅ <b>Канал добавлен!</b>\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 = "📊 <b>Статистика рассылок</b>\n\n"
|
||
text += f"📢 Всего рассылок: {total_broadcasts}\n"
|
||
text += f"🚫 Заблокировали бота: {blocked_count}\n\n"
|
||
|
||
if logs:
|
||
text += "<b>Последние 5 рассылок:</b>\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 = "⏰ <b>Неактивные пользователи</b>\n\n"
|
||
text += f"📊 Неактивных более 30 дней: {len(inactive_users)}\n"
|
||
text += f"🚫 Уже заблокировано за неактивность: {len(blocked_inactive)}\n\n"
|
||
|
||
text += "<i>Система автоматически проверяет активность пользователей каждый день в 03:00 "
|
||
text += "и блокирует неактивных более 30 дней.</i>\n\n"
|
||
|
||
if inactive_users:
|
||
text += "<b>Неактивные пользователи (первые 10):</b>\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"✅ <b>Проверка завершена!</b>\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 = (
|
||
"👥 <b>Управление пользователями</b>\n\n"
|
||
f"📊 <b>Статистика:</b>\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 = (
|
||
"🔍 <b>Поиск пользователей</b>\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"🔍 <b>Результаты поиска:</b> «{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"📋 <b>Все пользователи</b>\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"🚫 <b>Заблокированные в чате</b>\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 = "👤 <b>Информация о пользователе</b>\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 = "👑 <b>Управление администраторами</b>\n\n"
|
||
text += f"Главные администраторы (.env): <code>{len(ADMIN_IDS)}</code>\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 = "👑 <b>Список администраторов</b>\n\n"
|
||
|
||
# Главные администраторы из .env
|
||
text += "<b>Главные администраторы (из .env):</b>\n"
|
||
for admin_id in ADMIN_IDS:
|
||
text += f"🔴 ID: <code>{admin_id}</code>\n"
|
||
|
||
text += "\n"
|
||
|
||
# Назначенные администраторы
|
||
if db_admins:
|
||
text += "<b>Назначенные администраторы:</b>\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: <code>{admin.telegram_id}</code>)\n"
|
||
else:
|
||
text += "<b>Назначенные администраторы:</b> нет\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 = "👤 <b>Назначение администратора</b>\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 = "👤 <b>Подтверждение назначения администратора</b>\n\n"
|
||
text += f"Имя: {user.first_name or 'не указано'}\n"
|
||
text += f"Username: {user.username or 'нет'}\n"
|
||
text += f"Telegram ID: <code>{user.telegram_id}</code>\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 = "🗑️ <b>Выберите администратора для удаления</b>\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'] |