add algam
This commit is contained in:
390
telegram_bot.py
Normal file
390
telegram_bot.py
Normal file
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
Современный 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())
|
||||
Reference in New Issue
Block a user