390 lines
20 KiB
Python
390 lines
20 KiB
Python
"""
|
||
Современный Telegram бот с использованием SQLAlchemy и сервисов
|
||
"""
|
||
|
||
import asyncio
|
||
import io
|
||
from datetime import datetime
|
||
from aiogram import Bot, Dispatcher, types
|
||
from aiogram.filters import Command
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
from aiogram.fsm.storage.memory import MemoryStorage
|
||
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, BufferedInputFile
|
||
from aiogram.filters.state import StateFilter
|
||
import logging
|
||
|
||
from bot_config import config
|
||
from database.database import get_db_session
|
||
from database.repositories import UserRepository, SettingsRepository
|
||
from database.services import TestService, QuestionService, ImageService
|
||
|
||
# Настройка логирования
|
||
logging.basicConfig(
|
||
level=getattr(logging, config.LOG_LEVEL),
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Инициализация бота
|
||
bot = Bot(token=config.TELEGRAM_BOT_TOKEN)
|
||
dp = Dispatcher(storage=MemoryStorage())
|
||
|
||
# ===== FSM States =====
|
||
class Form(StatesGroup):
|
||
consent = State()
|
||
fio = State()
|
||
dob = State()
|
||
phone = State()
|
||
city = State()
|
||
test_in_progress = State()
|
||
|
||
# ===== /start command =====
|
||
@dp.message(Command("start"))
|
||
async def start(message: types.Message, state: FSMContext):
|
||
"""Обработка команды /start"""
|
||
try:
|
||
# Создаем или получаем пользователя
|
||
with get_db_session() as session:
|
||
user, created = UserRepository.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
|
||
)
|
||
session.commit()
|
||
|
||
if created:
|
||
logger.info(f"Создан новый пользователь: {message.from_user.id}")
|
||
|
||
kb = [
|
||
[KeyboardButton(text="✅ Да, согласен"), KeyboardButton(text="❌ Нет")]
|
||
]
|
||
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
|
||
|
||
await message.answer(
|
||
"Привет! Я помогу пройти тест Климова.\\n\\n"
|
||
"Сначала — согласие на обработку персональных данных.\\n"
|
||
"Ты согласен?",
|
||
reply_markup=keyboard
|
||
)
|
||
await state.set_state(Form.consent)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка в команде start: {e}")
|
||
await message.answer("Произошла ошибка. Попробуйте позже.")
|
||
|
||
# ===== Согласие на обработку данных =====
|
||
@dp.message(Form.consent)
|
||
async def consent(message: types.Message, state: FSMContext):
|
||
"""Обработка согласия на обработку персональных данных"""
|
||
if message.text.startswith("✅"):
|
||
await state.set_state(Form.fio)
|
||
await message.answer("Введите своё ФИО:", reply_markup=ReplyKeyboardRemove())
|
||
else:
|
||
await message.answer("Без согласия мы не можем продолжить 🙏", reply_markup=ReplyKeyboardRemove())
|
||
await state.clear()
|
||
|
||
# ===== Сбор ФИО =====
|
||
@dp.message(Form.fio)
|
||
async def collect_fio(message: types.Message, state: FSMContext):
|
||
"""Сбор ФИО пользователя"""
|
||
await state.update_data(fio=message.text)
|
||
await state.set_state(Form.dob)
|
||
await message.answer("Введите дату рождения (ДД.MM.ГГГГ):")
|
||
|
||
# ===== Сбор даты рождения =====
|
||
@dp.message(Form.dob)
|
||
async def collect_dob(message: types.Message, state: FSMContext):
|
||
"""Сбор даты рождения"""
|
||
try:
|
||
# Проверяем формат даты
|
||
datetime.strptime(message.text, "%d.%m.%Y")
|
||
await state.update_data(dob=message.text)
|
||
await state.set_state(Form.phone)
|
||
await message.answer("Введите номер телефона:")
|
||
except ValueError:
|
||
await message.answer("Неверный формат даты. Введите дату в формате ДД.MM.ГГГГ")
|
||
|
||
# ===== Сбор телефона =====
|
||
@dp.message(Form.phone)
|
||
async def collect_phone(message: types.Message, state: FSMContext):
|
||
"""Сбор номера телефона"""
|
||
await state.update_data(phone=message.text)
|
||
await state.set_state(Form.city)
|
||
await message.answer("Из какого ты населённого пункта?")
|
||
|
||
# ===== Сбор города и сохранение анкеты =====
|
||
@dp.message(Form.city)
|
||
async def collect_city_and_save(message: types.Message, state: FSMContext):
|
||
"""Сбор города и сохранение анкеты пользователя"""
|
||
try:
|
||
await state.update_data(city=message.text)
|
||
data = await state.get_data()
|
||
|
||
# Преобразуем дату
|
||
dob_parsed = datetime.strptime(data["dob"], "%d.%m.%Y").date()
|
||
|
||
# Сохраняем данные пользователя
|
||
with get_db_session() as session:
|
||
user = UserRepository.get_user_by_telegram_id(session, message.from_user.id)
|
||
if user:
|
||
# Обновляем дополнительные данные пользователя
|
||
# Здесь нужно будет расширить модель User для хранения ФИО, даты рождения и т.д.
|
||
# Или создать отдельную таблицу для профилей пользователей
|
||
logger.info(f"Пользователь {message.from_user.id} заполнил анкету: "
|
||
f"ФИО: {data['fio']}, ДР: {dob_parsed}, Телефон: {data['phone']}, Город: {data['city']}")
|
||
session.commit()
|
||
|
||
await message.answer("Анкета сохранена ✅")
|
||
|
||
# Показываем доступные тесты
|
||
await show_available_tests(message, state)
|
||
|
||
except ValueError:
|
||
await message.answer("Неверный формат даты. Введите дату в формате ДД.MM.ГГГГ")
|
||
await state.set_state(Form.dob)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при сохранении анкеты пользователя {message.from_user.id}: {e}")
|
||
await message.answer("Произошла ошибка при сохранении анкеты. Попробуйте позже.")
|
||
|
||
async def show_available_tests(message: types.Message, state: FSMContext):
|
||
"""Показать доступные тесты"""
|
||
try:
|
||
with get_db_session() as session:
|
||
tests = TestService.get_available_tests(session)
|
||
|
||
if not tests:
|
||
await message.answer("К сожалению, тесты пока недоступны.")
|
||
return
|
||
|
||
# Создаем клавиатуру с тестами
|
||
kb = []
|
||
for test in tests:
|
||
kb.append([KeyboardButton(text=test.title)])
|
||
|
||
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
|
||
await state.clear()
|
||
await message.answer("Выберите тест:", reply_markup=keyboard)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при получении списка тестов: {e}")
|
||
await message.answer("Произошла ошибка при загрузке тестов.")
|
||
|
||
# ===== Выбор теста =====
|
||
@dp.message(StateFilter(None))
|
||
async def select_test(message: types.Message, state: FSMContext):
|
||
"""Обработка выбора теста"""
|
||
try:
|
||
with get_db_session() as session:
|
||
# Получаем пользователя
|
||
user = UserRepository.get_user_by_telegram_id(session, message.from_user.id)
|
||
if not user:
|
||
await message.answer("Пользователь не найден. Используйте /start")
|
||
return
|
||
|
||
# Ищем тест по названию
|
||
test = TestService.get_test_by_title(session, message.text)
|
||
if not test:
|
||
await message.answer("Тест не найден или недоступен.")
|
||
return
|
||
|
||
# Начинаем тест
|
||
test_result = TestService.start_test(session, user.id, test.id)
|
||
session.commit()
|
||
|
||
# Получаем вопросы теста
|
||
questions = TestService.get_test_questions(session, test.id)
|
||
if not questions:
|
||
await message.answer("В тесте нет вопросов. Обратитесь к администратору.")
|
||
return
|
||
|
||
# Сохраняем данные теста в состоянии
|
||
await state.update_data(
|
||
test_id=test.id,
|
||
test_result_id=test_result.id,
|
||
questions=[(q.id, q.order_index) for q in questions],
|
||
current_question_index=0
|
||
)
|
||
await state.set_state(Form.test_in_progress)
|
||
|
||
# Отправляем первый вопрос
|
||
await send_question(message.from_user.id, state, session)
|
||
|
||
logger.info(f"Пользователь {message.from_user.id} начал тест: {test.title}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при выборе теста: {e}")
|
||
await message.answer("Произошла ошибка при запуске теста. Попробуйте позже.")
|
||
|
||
async def send_question(user_id: int, state: FSMContext, session):
|
||
"""Отправка вопроса пользователю"""
|
||
try:
|
||
data = await state.get_data()
|
||
questions = data.get("questions", [])
|
||
current_index = data.get("current_question_index", 0)
|
||
|
||
if current_index >= len(questions):
|
||
# Тест завершен
|
||
await complete_test(user_id, state, session)
|
||
return
|
||
|
||
question_id, _ = questions[current_index]
|
||
|
||
# Получаем вопрос с вариантами ответов
|
||
question = QuestionService.get_question_with_options(session, question_id)
|
||
if not question:
|
||
await bot.send_message(user_id, "Ошибка при загрузке вопроса.")
|
||
return
|
||
|
||
# Форматируем вопрос для бота
|
||
question_data = QuestionService.format_question_for_bot(session, question)
|
||
|
||
# Создаем клавиатуру с вариантами ответов
|
||
kb = []
|
||
for option in question_data['options']:
|
||
kb.append([KeyboardButton(text=option['text'])])
|
||
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
|
||
|
||
# Отправляем изображение, если есть
|
||
if question_data.get('image'):
|
||
image_data = question_data['image']
|
||
try:
|
||
image_file = BufferedInputFile(
|
||
image_data['data'],
|
||
filename=image_data['filename']
|
||
)
|
||
await bot.send_photo(
|
||
chat_id=user_id,
|
||
photo=image_file,
|
||
caption=f"Вопрос {current_index + 1} из {len(questions)}:\\n{question_data['text'] or 'Выберите вариант'}",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отправки изображения: {e}")
|
||
|
||
# Отправляем текстовый вопрос
|
||
question_text = question_data['text'] or "Выберите вариант"
|
||
await bot.send_message(
|
||
chat_id=user_id,
|
||
text=f"Вопрос {current_index + 1} из {len(questions)}:\\n{question_text}",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при отправке вопроса: {e}")
|
||
await bot.send_message(user_id, "Произошла ошибка. Попробуйте позже.")
|
||
|
||
# ===== Обработка ответов на вопросы =====
|
||
@dp.message(Form.test_in_progress)
|
||
async def handle_answer(message: types.Message, state: FSMContext):
|
||
"""Обработка ответа на вопрос теста"""
|
||
try:
|
||
data = await state.get_data()
|
||
questions = data.get("questions", [])
|
||
current_index = data.get("current_question_index", 0)
|
||
test_result_id = data.get("test_result_id")
|
||
|
||
if current_index >= len(questions):
|
||
return
|
||
|
||
question_id, _ = questions[current_index]
|
||
|
||
with get_db_session() as session:
|
||
# Получаем вопрос с вариантами ответов
|
||
question = QuestionService.get_question_with_options(session, question_id)
|
||
if not question:
|
||
await message.answer("Ошибка при обработке ответа.")
|
||
return
|
||
|
||
# Ищем выбранный вариант ответа
|
||
selected_option = None
|
||
for option in question.answer_options:
|
||
if option.option_text == message.text:
|
||
selected_option = option
|
||
break
|
||
|
||
if not selected_option:
|
||
await message.answer("Пожалуйста, выберите один из предложенных вариантов.")
|
||
return
|
||
|
||
# Сохраняем ответ
|
||
TestService.save_answer(session, test_result_id, question_id, selected_option.id)
|
||
session.commit()
|
||
|
||
# Переходим к следующему вопросу
|
||
next_index = current_index + 1
|
||
await state.update_data(current_question_index=next_index)
|
||
|
||
# Отправляем следующий вопрос или завершаем тест
|
||
await send_question(message.from_user.id, state, session)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при обработке ответа: {e}")
|
||
await message.answer("Произошла ошибка при обработке ответа.")
|
||
|
||
async def complete_test(user_id: int, state: FSMContext, session):
|
||
"""Завершение теста и показ результатов"""
|
||
try:
|
||
data = await state.get_data()
|
||
test_result_id = data.get("test_result_id")
|
||
|
||
# Завершаем тест и подсчитываем результаты
|
||
test_result = TestService.complete_test(session, test_result_id)
|
||
session.commit()
|
||
|
||
# Получаем форматированные результаты
|
||
results = TestService.get_test_results(session, test_result_id)
|
||
|
||
# Формируем сообщение с результатами
|
||
result_text = "🎉 Тест завершен!\\n\\nВаши результаты:\\n"
|
||
|
||
for category, data in results.items():
|
||
result_text += f"• {category}: {data['percentage']}%\\n"
|
||
|
||
# Определяем ведущий тип
|
||
if results:
|
||
top_category = max(results.keys(), key=lambda x: results[x]['percentage'])
|
||
result_text += f"\\n🏆 Ваш ведущий тип: {top_category}"
|
||
|
||
await bot.send_message(user_id, result_text, reply_markup=ReplyKeyboardRemove())
|
||
|
||
# Отправляем описания профессий
|
||
profession_descriptions = {
|
||
"Человек-природа": "К типу 'человек-природа' можно отнести профессии, связанные с изучением живой и неживой природы (микробиолог, агрохимик, геолог), с уходом за растениями и животными (лесовод, овощевод, зоотехник).",
|
||
"Человек-техника": "Тип 'человек-техника' включает в себя профессии, связанные с созданием, монтажом, сборкой и наладкой технических устройств (каменщик, монтажник, сварщик, инженер-конструктор), эксплуатацией технических средств (водитель, крановщик, токарь, швея), ремонтом техники.",
|
||
"Человек-человек": "К профессиям типа 'человек-человек' относятся профессии, связанные с медицинским обслуживанием (врач, медсестра, санитарка), обучением и воспитанием (воспитатель, няня, учитель, преподаватель, тренер), бытовым обслуживанием (продавец, проводник, официант).",
|
||
"Человек-знаковая система": "Тип 'человек - знаковая система' объединяет профессии, связанные с текстами (корректор, оператор ПК, переводчик, библиотекарь), с цифрами, формулами и таблицами (программист, экономист, бухгалтер, кассир), с чертежами, картами, схемами (штурман, чертежник).",
|
||
"Человек-художественный образ": "К типу 'человек - художественный образ' можно отнести профессии, связанные с созданием, проектированием, моделированием художественных произведений (художник, журналист, модельер, композитор), с воспроизведением, изготовлением различных изделий по эскизу, образцу."
|
||
}
|
||
|
||
for category, description in profession_descriptions.items():
|
||
await bot.send_message(user_id, f"🟢 {category}\\n\\n{description}")
|
||
|
||
# Финальное сообщение
|
||
await bot.send_message(user_id,
|
||
"Задать вопрос или записаться на индивидуальную консультацию по построению образовательного маршрута можно здесь: @chrtakova11\\n\\n"
|
||
"Узнать подробнее о профессиях: https://postupi.online/professii/so/")
|
||
|
||
await state.clear()
|
||
logger.info(f"Пользователь {user_id} завершил тест")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при завершении теста: {e}")
|
||
await bot.send_message(user_id, "Произошла ошибка при подсчете результатов.")
|
||
|
||
# ===== Запуск бота =====
|
||
async def main():
|
||
"""Основная функция запуска бота"""
|
||
logger.info("Запуск Telegram бота...")
|
||
try:
|
||
await dp.start_polling(bot)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при запуске бота: {e}")
|
||
finally:
|
||
logger.info("Бот остановлен")
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main()) |