init commit

This commit is contained in:
2025-11-12 20:57:36 +09:00
commit e0075d91b6
40 changed files with 8544 additions and 0 deletions

732
main.py Normal file
View File

@@ -0,0 +1,732 @@
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import (
Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
BotCommand
)
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from sqlalchemy.ext.asyncio import AsyncSession
import asyncio
import logging
import signal
import sys
from config import BOT_TOKEN, ADMIN_IDS
from database import async_session_maker, init_db
from services import UserService, LotteryService, ParticipationService
from admin_panel import admin_router
from async_decorators import (
async_user_action, admin_async_action, db_operation,
TaskManagerMiddleware, shutdown_task_manager,
format_task_stats, TaskPriority
)
from account_utils import validate_account_number, format_account_number
from winner_display import format_winner_display
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Состояния для FSM
class CreateLotteryStates(StatesGroup):
waiting_for_title = State()
waiting_for_description = State()
waiting_for_prizes = State()
class SetWinnerStates(StatesGroup):
waiting_for_lottery_id = State()
waiting_for_place = State()
waiting_for_user_id = State()
class AccountStates(StatesGroup):
waiting_for_account_number = State()
# Инициализация бота
bot = Bot(token=BOT_TOKEN)
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
router = Router()
# Подключаем middleware для управления задачами
dp.message.middleware(TaskManagerMiddleware())
dp.callback_query.middleware(TaskManagerMiddleware())
def is_admin(user_id: int) -> bool:
"""Проверка, является ли пользователь администратором"""
return user_id in ADMIN_IDS
def get_main_keyboard(is_admin_user: bool = False) -> InlineKeyboardMarkup:
"""Главная клавиатура"""
buttons = [
[InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="list_lotteries")],
[InlineKeyboardButton(text="📝 Мои участия", callback_data="my_participations")],
[InlineKeyboardButton(text="💳 Мой счёт", callback_data="my_account")]
]
if is_admin_user:
buttons.extend([
[InlineKeyboardButton(text="🔧 Админ-панель", callback_data="admin_panel")],
[InlineKeyboardButton(text=" Создать розыгрыш", callback_data="create_lottery")],
[InlineKeyboardButton(text="👑 Установить победителя", callback_data="set_winner")],
[InlineKeyboardButton(text="📊 Статистика задач", callback_data="task_stats")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@router.message(Command("start"))
async def cmd_start(message: Message):
"""Обработчик команды /start"""
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
)
# Устанавливаем права администратора, если пользователь в списке
if message.from_user.id in ADMIN_IDS:
await UserService.set_admin(session, message.from_user.id, True)
is_admin_user = is_admin(message.from_user.id)
welcome_text = f"Добро пожаловать, {message.from_user.first_name}! 🎉\n\n"
welcome_text += "Это бот для проведения розыгрышей.\n\n"
welcome_text += "Выберите действие из меню ниже:"
if is_admin_user:
welcome_text += "\n\n👑 У вас есть права администратора!"
await message.answer(
welcome_text,
reply_markup=get_main_keyboard(is_admin_user)
)
@router.callback_query(F.data == "list_lotteries")
async def show_active_lotteries(callback: CallbackQuery):
"""Показать активные розыгрыши"""
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="back_to_main")]
])
)
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"
text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
buttons.append([
InlineKeyboardButton(
text=f"🎲 {lottery.title}",
callback_data=f"lottery_{lottery.id}"
)
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
@router.callback_query(F.data.startswith("lottery_"))
async def show_lottery_details(callback: CallbackQuery):
"""Показать детали розыгрыша"""
lottery_id = int(callback.data.split("_")[1])
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not lottery:
await callback.answer("Розыгрыш не найден", show_alert=True)
return
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
# Проверяем, участвует ли пользователь
is_participating = any(
p.user_id == user.id for p in lottery.participations
) if user else False
text = f"🎯 {lottery.title}\n\n"
text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n"
if lottery.prizes:
text += "🏆 Призы:\n"
for i, prize in enumerate(lottery.prizes, 1):
text += f"{i}. {prize}\n"
text += "\n"
text += f"👥 Участников: {participants_count}\n"
text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n"
if lottery.is_completed:
text += "\n✅ Розыгрыш завершен"
# Показываем победителей
async with async_session_maker() as session:
winners = await LotteryService.get_winners(session, lottery_id)
if winners:
text += "\n\n🏆 Победители:\n"
for winner in winners:
# Используем новую систему отображения
winner_display = format_winner_display(winner.user, lottery, show_sensitive_data=False)
text += f"{winner.place}. {winner_display}\n"
else:
text += f"\n🟢 Статус: {'Активен' if lottery.is_active else 'Неактивен'}"
if is_participating:
text += "\n✅ Вы участвуете в розыгрыше"
buttons = []
if not lottery.is_completed and lottery.is_active and not is_participating:
buttons.append([
InlineKeyboardButton(
text="🎫 Участвовать",
callback_data=f"join_{lottery_id}"
)
])
if is_admin(callback.from_user.id) and not lottery.is_completed:
buttons.append([
InlineKeyboardButton(
text="🎲 Провести розыгрыш",
callback_data=f"conduct_{lottery_id}"
)
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="list_lotteries")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
@router.callback_query(F.data.startswith("join_"))
async def join_lottery(callback: CallbackQuery):
"""Присоединиться к розыгрышу"""
lottery_id = int(callback.data.split("_")[1])
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer("Ошибка получения данных пользователя", show_alert=True)
return
success = await LotteryService.add_participant(session, lottery_id, user.id)
if success:
await callback.answer("✅ Вы успешно присоединились к розыгрышу!", show_alert=True)
else:
await callback.answer("❌ Вы уже участвуете в этом розыгрыше", show_alert=True)
# Обновляем информацию о розыгрыше
await show_lottery_details(callback)
@router.callback_query(F.data.startswith("conduct_"))
async def conduct_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:
results = await LotteryService.conduct_draw(session, lottery_id)
if not results:
await callback.answer("Не удалось провести розыгрыш", show_alert=True)
return
text = "🎉 Розыгрыш завершен!\n\n🏆 Победители:\n\n"
for place, winner_info in results.items():
user = winner_info['user']
prize = winner_info['prize']
# Используем новую систему отображения
winner_display = format_winner_display(user, lottery, show_sensitive_data=False)
text += f"{place}. {winner_display}\n"
text += f" 🎁 {prize}\n\n"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К розыгрышам", callback_data="list_lotteries")]
])
)
# Создание розыгрыша
@router.callback_query(F.data == "create_lottery")
async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
"""Начать создание розыгрыша"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
await callback.message.edit_text(
"📝 Создание нового розыгрыша\n\n"
"Введите название розыгрыша:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
])
)
await state.set_state(CreateLotteryStates.waiting_for_title)
@router.message(StateFilter(CreateLotteryStates.waiting_for_title))
async def process_lottery_title(message: Message, state: FSMContext):
"""Обработка названия розыгрыша"""
await state.update_data(title=message.text)
await message.answer(
"📋 Введите описание розыгрыша (или отправьте '-' для пропуска):"
)
await state.set_state(CreateLotteryStates.waiting_for_description)
@router.message(StateFilter(CreateLotteryStates.waiting_for_description))
async def process_lottery_description(message: Message, state: FSMContext):
"""Обработка описания розыгрыша"""
description = None if message.text == "-" else message.text
await state.update_data(description=description)
await message.answer(
"🏆 Введите призы через новую строку:\n\n"
"Пример:\n"
"1000 рублей\n"
"iPhone 15\n"
"Подарочный сертификат"
)
await state.set_state(CreateLotteryStates.waiting_for_prizes)
@router.message(StateFilter(CreateLotteryStates.waiting_for_prizes))
async def process_lottery_prizes(message: Message, state: FSMContext):
"""Обработка призов розыгрыша"""
prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()]
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
data = await state.get_data()
lottery = await LotteryService.create_lottery(
session,
title=data['title'],
description=data['description'],
prizes=prizes,
creator_id=user.id
)
await state.clear()
text = f"✅ Розыгрыш успешно создан!\n\n"
text += f"🎯 Название: {lottery.title}\n"
text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n"
text += f"🏆 Призы:\n"
for i, prize in enumerate(prizes, 1):
text += f"{i}. {prize}\n"
await message.answer(
text,
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
)
# Установка ручного победителя
@router.callback_query(F.data == "set_winner")
async def start_set_winner(callback: CallbackQuery, state: FSMContext):
"""Начать установку ручного победителя"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
async with async_session_maker() as session:
lotteries = await LotteryService.get_active_lotteries(session)
if not lotteries:
await callback.message.edit_text(
"❌ Нет активных розыгрышей",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
])
)
return
text = "👑 Установка ручного победителя\n\n"
text += "Выберите розыгрыш:\n\n"
buttons = []
for lottery in lotteries:
text += f"🎯 {lottery.title} (ID: {lottery.id})\n"
buttons.append([
InlineKeyboardButton(
text=f"{lottery.title}",
callback_data=f"setwinner_{lottery.id}"
)
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
@router.callback_query(F.data.startswith("setwinner_"))
async def select_winner_place(callback: CallbackQuery, state: FSMContext):
"""Выбор места для ручного победителя"""
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 3
text = f"👑 Установка ручного победителя для розыгрыша:\n"
text += f"🎯 {lottery.title}\n\n"
text += f"Введите номер места (1-{num_prizes}):"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="set_winner")]
])
)
await state.set_state(SetWinnerStates.waiting_for_place)
@router.message(StateFilter(SetWinnerStates.waiting_for_place))
async def process_winner_place(message: Message, state: FSMContext):
"""Обработка места победителя"""
try:
place = int(message.text)
if place < 1:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректный номер места (положительное число)")
return
await state.update_data(place=place)
await message.answer(
f"👑 Установка ручного победителя на {place} место\n\n"
"Введите Telegram ID пользователя:"
)
await state.set_state(SetWinnerStates.waiting_for_user_id)
@router.message(StateFilter(SetWinnerStates.waiting_for_user_id))
async def process_winner_user_id(message: Message, state: FSMContext):
"""Обработка ID пользователя-победителя"""
try:
telegram_id = int(message.text)
except ValueError:
await message.answer("❌ Введите корректный Telegram ID (число)")
return
data = await state.get_data()
async with async_session_maker() as session:
success = await LotteryService.set_manual_winner(
session,
data['lottery_id'],
data['place'],
telegram_id
)
await state.clear()
if success:
await message.answer(
f"✅ Ручной победитель установлен!\n\n"
f"🏆 Место: {data['place']}\n"
f"👤 Telegram ID: {telegram_id}",
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
)
else:
await message.answer(
"Не удалось установить ручного победителя.\n"
"Проверьте, что пользователь существует в системе.",
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
)
@router.callback_query(F.data == "my_participations")
async def show_my_participations(callback: CallbackQuery):
"""Показать участие пользователя в розыгрышах"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer("Ошибка получения данных пользователя", show_alert=True)
return
participations = await ParticipationService.get_user_participations(session, user.id)
if not participations:
await callback.message.edit_text(
"📝 Вы пока не участвуете в розыгрышах",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
])
)
return
text = "📝 Ваши участия в розыгрышах:\n\n"
for participation in participations:
lottery = participation.lottery
status = "✅ Завершен" if lottery.is_completed else "🟢 Активен"
text += f"🎯 {lottery.title}\n"
text += f"📊 Статус: {status}\n"
text += f"📅 Участие с: {participation.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
])
)
# Хэндлеры для работы с номерами счетов
@router.callback_query(F.data == "my_account")
@db_operation()
async def show_my_account(callback: CallbackQuery):
"""Показать информацию о счёте пользователя"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer("Пользователь не найден", show_alert=True)
return
text = "💳 **Ваш клиентский счёт**\n\n"
if user.account_number:
# Показываем маскированный номер для безопасности
from account_utils import mask_account_number
masked = mask_account_number(user.account_number, show_last_digits=6)
text += f"📋 Номер счёта: `{masked}`\n"
text += f"✅ Статус: Активен\n\n"
text += " Счёт используется для идентификации в розыгрышах"
else:
text += "❌ Счёт не привязан\n\n"
text += "Привяжите счёт для участия в розыгрышах"
buttons = []
if user.account_number:
buttons.append([InlineKeyboardButton(text="🔄 Изменить счёт", callback_data="change_account")])
else:
buttons.append([InlineKeyboardButton(text=" Привязать счёт", callback_data="add_account")])
buttons.append([InlineKeyboardButton(text="🔙 Главное меню", callback_data="back_to_main")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
parse_mode="Markdown"
)
@router.callback_query(F.data.in_(["add_account", "change_account"]))
@db_operation()
async def start_account_setup(callback: CallbackQuery, state: FSMContext):
"""Начало процесса привязки/изменения счёта"""
await state.set_state(AccountStates.waiting_for_account_number)
action = "привязки" if callback.data == "add_account" else "изменения"
text = f"💳 **Процедура {action} счёта**\n\n"
text += "Введите номер вашего клиентского счёта в формате:\n"
text += "`12-34-56-78-90-12-34-56`\n\n"
text += "📝 **Требования:**\n"
text += "• Ровно 16 цифр\n"
text += "• Разделены дефисами через каждые 2 цифры\n"
text += "• Номер должен быть уникальным\n\n"
text += "✉️ Отправьте номер счёта в ответном сообщении"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="my_account")]
]),
parse_mode="Markdown"
)
@router.message(StateFilter(AccountStates.waiting_for_account_number))
@db_operation()
async def process_account_number(message: Message, state: FSMContext):
"""Обработка введённого номера счёта"""
account_input = message.text.strip()
# Форматируем и валидируем номер
formatted_number = format_account_number(account_input)
if not formatted_number:
await message.answer(
"❌ **Некорректный формат номера счёта**\n\n"
"Номер должен содержать ровно 16 цифр.\n"
"Пример правильного формата: `12-34-56-78-90-12-34-56`\n\n"
"Попробуйте ещё раз:",
parse_mode="Markdown"
)
return
async with async_session_maker() as session:
# Проверяем уникальность
existing_user = await UserService.get_user_by_account(session, formatted_number)
if existing_user and existing_user.telegram_id != message.from_user.id:
await message.answer(
"❌ **Номер счёта уже используется**\n\n"
"Данный номер счёта уже привязан к другому пользователю.\n"
"Убедитесь, что вы вводите правильный номер.\n\n"
"Попробуйте ещё раз:"
)
return
# Обновляем номер счёта
success = await UserService.set_account_number(
session, message.from_user.id, formatted_number
)
if success:
await state.clear()
await message.answer(
f"✅ **Счёт успешно привязан!**\n\n"
f"💳 Номер счёта: `{formatted_number}`\n\n"
f"Теперь вы можете участвовать в розыгрышах.\n"
f"Ваш номер счёта будет использоваться для идентификации.",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_main")]
])
)
else:
await message.answer(
"❌ **Ошибка привязки счёта**\n\n"
"Произошла ошибка при сохранении номера счёта.\n"
"Попробуйте ещё раз или обратитесь к администратору.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="my_account")]
])
)
@router.callback_query(F.data == "task_stats")
@admin_async_action()
async def show_task_stats(callback: CallbackQuery):
"""Показать статистику задач (только для админов)"""
if not is_admin(callback.from_user.id):
await callback.answer("Доступ запрещён", show_alert=True)
return
stats_text = await format_task_stats()
await callback.message.edit_text(
stats_text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="task_stats")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
]),
parse_mode="Markdown"
)
@router.callback_query(F.data == "back_to_main")
async def back_to_main(callback: CallbackQuery, state: FSMContext):
"""Вернуться в главное меню"""
await state.clear()
is_admin_user = is_admin(callback.from_user.id)
await callback.message.edit_text(
"🏠 Главное меню\n\nВыберите действие:",
reply_markup=get_main_keyboard(is_admin_user)
)
async def set_commands():
"""Установка команд бота"""
commands = [
BotCommand(command="start", description="🚀 Запустить бота"),
]
await bot.set_my_commands(commands)
async def main():
"""Главная функция"""
# Инициализация базы данных
await init_db()
# Установка команд
await set_commands()
# Подключение роутеров
dp.include_router(router)
dp.include_router(admin_router)
# Обработка сигналов для graceful shutdown
def signal_handler():
logger.info("Получен сигнал завершения, остановка бота...")
asyncio.create_task(shutdown_task_manager())
# Настройка обработчиков сигналов
if sys.platform != "win32":
for sig in (signal.SIGTERM, signal.SIGINT):
asyncio.get_event_loop().add_signal_handler(sig, signal_handler)
# Запуск бота
logger.info("Бот запущен")
try:
await dp.start_polling(bot)
finally:
# Остановка менеджера задач при завершении
await shutdown_task_manager()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Бот остановлен пользователем")
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
finally:
logger.info("Завершение работы")