Files
bot-telegram/database/services.py
2025-09-14 22:09:39 +05:00

358 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервисы для работы с системой тестирования
Основная бизнес-логика для получения вопросов, сохранения ответов и работы с изображениями
"""
import json
import io
from typing import List, Optional, Dict, Any, Tuple
from sqlalchemy.orm import Session
from sqlalchemy import and_
from database.models import (
User, Test, TestCategory, Question, AnswerOption,
TestResult, UserAnswer, Image
)
import logging
logger = logging.getLogger(__name__)
class ImageService:
"""Сервис для работы с изображениями в базе данных"""
@staticmethod
def save_image_to_db(
db: Session,
filename: str,
content_type: str,
image_data: bytes,
alt_text: str = None
) -> Image:
"""
Сохранение изображения в базу данных
Args:
db: Сессия базы данных
filename: Имя файла
content_type: MIME тип (image/jpeg, image/png и т.д.)
image_data: Бинарные данные изображения
alt_text: Альтернативный текст
Returns:
Image: Созданная запись изображения
"""
image = Image(
filename=filename,
content_type=content_type,
file_size=len(image_data),
image_data=image_data,
alt_text=alt_text
)
db.add(image)
db.commit()
db.refresh(image)
return image
@staticmethod
def get_image_data(db: Session, image_id: int) -> Optional[Tuple[bytes, str, str]]:
"""
Получение данных изображения из БД
Args:
db: Сессия базы данных
image_id: ID изображения
Returns:
Tuple[bytes, str, str]: (image_data, content_type, filename) или None
"""
image = db.query(Image).filter(Image.id == image_id).first()
if image:
return image.image_data, image.content_type, image.filename
return None
@staticmethod
def get_image_by_filename(db: Session, filename: str) -> Optional[Image]:
"""Получение изображения по имени файла"""
return db.query(Image).filter(Image.filename == filename).first()
class TestService:
"""Сервис для работы с тестами"""
@staticmethod
def get_available_tests(db: Session) -> List[Test]:
"""Получение всех активных тестов"""
return db.query(Test).filter(Test.is_active == True).all()
@staticmethod
def get_test_by_title(db: Session, title: str) -> Optional[Test]:
"""Получение теста по названию (например, '📘 Тест Климова')"""
return db.query(Test).filter(
and_(Test.title == title, Test.is_active == True)
).first()
@staticmethod
def get_test_questions(db: Session, test_id: int) -> List[Question]:
"""
Получение всех вопросов теста в правильном порядке
Args:
db: Сессия базы данных
test_id: ID теста
Returns:
List[Question]: Список вопросов с загруженными связями
"""
return db.query(Question).filter(
and_(Question.test_id == test_id, Question.is_active == True)
).order_by(Question.order_index).all()
@staticmethod
def start_test(db: Session, user_id: int, test_id: int) -> TestResult:
"""
Начало прохождения теста пользователем
Args:
db: Сессия базы данных
user_id: ID пользователя
test_id: ID теста
Returns:
TestResult: Созданная запись результата теста
"""
# Проверяем, есть ли незавершенный тест
existing_result = db.query(TestResult).filter(
and_(
TestResult.user_id == user_id,
TestResult.test_id == test_id,
TestResult.is_completed == False
)
).first()
if existing_result:
return existing_result
# Создаем новый результат теста
test_result = TestResult(
user_id=user_id,
test_id=test_id,
is_completed=False
)
db.add(test_result)
db.commit()
db.refresh(test_result)
return test_result
@staticmethod
def save_answer(
db: Session,
test_result_id: int,
question_id: int,
selected_option_id: int
) -> UserAnswer:
"""
Сохранение ответа пользователя на вопрос
Args:
db: Сессия базы данных
test_result_id: ID результата теста
question_id: ID вопроса
selected_option_id: ID выбранного варианта ответа
Returns:
UserAnswer: Созданная запись ответа
"""
# Проверяем, есть ли уже ответ на этот вопрос
existing_answer = db.query(UserAnswer).filter(
and_(
UserAnswer.test_result_id == test_result_id,
UserAnswer.question_id == question_id
)
).first()
if existing_answer:
# Обновляем существующий ответ
existing_answer.selected_option_id = selected_option_id
db.commit()
return existing_answer
# Создаем новый ответ
user_answer = UserAnswer(
test_result_id=test_result_id,
question_id=question_id,
selected_option_id=selected_option_id
)
db.add(user_answer)
db.commit()
db.refresh(user_answer)
return user_answer
@staticmethod
def complete_test(db: Session, test_result_id: int) -> TestResult:
"""
Завершение теста и подсчет результатов
Args:
db: Сессия базы данных
test_result_id: ID результата теста
Returns:
TestResult: Обновленная запись результата с подсчитанными данными
"""
from datetime import datetime
test_result = db.query(TestResult).filter(
TestResult.id == test_result_id
).first()
if not test_result:
raise ValueError(f"TestResult с ID {test_result_id} не найден")
# Получаем все ответы пользователя для этого теста
answers = db.query(UserAnswer).filter(
UserAnswer.test_result_id == test_result_id
).all()
# Подсчитываем результаты по категориям
category_scores = {}
for answer in answers:
option = db.query(AnswerOption).filter(
AnswerOption.id == answer.selected_option_id
).first()
if option and option.category:
category = option.category
category_scores[category] = category_scores.get(category, 0) + 1
# Сохраняем результаты в JSON формате
test_result.result_data = json.dumps(category_scores, ensure_ascii=False)
test_result.completed_at = datetime.now()
test_result.is_completed = True
db.commit()
db.refresh(test_result)
return test_result
@staticmethod
def get_test_results(db: Session, test_result_id: int) -> Dict[str, Any]:
"""
Получение и форматирование результатов теста
Args:
db: Сессия базы данных
test_result_id: ID результата теста
Returns:
Dict: Словарь с результатами и процентами
"""
test_result = db.query(TestResult).filter(
TestResult.id == test_result_id
).first()
if not test_result or not test_result.result_data:
return {}
# Парсим результаты
category_scores = json.loads(test_result.result_data)
# Получаем максимальные возможные баллы для каждой категории
questions = TestService.get_test_questions(db, test_result.test_id)
max_scores = {}
for question in questions:
for option in question.answer_options:
if option.category:
max_scores[option.category] = max_scores.get(option.category, 0) + 1
# Вычисляем проценты
results = {}
for category, score in category_scores.items():
max_score = max_scores.get(category, 1)
percentage = (score / max_score) * 100
results[category] = {
'score': score,
'max_score': max_score,
'percentage': round(percentage, 1)
}
return results
@staticmethod
def get_user_test_history(db: Session, user_id: int) -> List[TestResult]:
"""Получение истории тестов пользователя"""
return db.query(TestResult).filter(
and_(TestResult.user_id == user_id, TestResult.is_completed == True)
).order_by(TestResult.completed_at.desc()).all()
class QuestionService:
"""Сервис для работы с вопросами"""
@staticmethod
def get_question_with_options(db: Session, question_id: int) -> Optional[Question]:
"""
Получение вопроса со всеми вариантами ответов и изображением
Args:
db: Сессия базы данных
question_id: ID вопроса
Returns:
Question: Вопрос с загруженными связями или None
"""
return db.query(Question).filter(Question.id == question_id).first()
@staticmethod
def format_question_for_bot(db: Session, question: Question) -> Dict[str, Any]:
"""
Форматирование вопроса для отправки в бот
Args:
db: Сессия базы данных
question: Объект вопроса
Returns:
Dict: Сформатированные данные вопроса
"""
# Получаем варианты ответов
options = db.query(AnswerOption).filter(
AnswerOption.question_id == question.id
).order_by(AnswerOption.order_index).all()
# Получаем данные изображения, если есть
image_data = None
if question.image_id:
image_info = ImageService.get_image_data(db, question.image_id)
if image_info:
image_data = {
'data': image_info[0],
'content_type': image_info[1],
'filename': image_info[2]
}
return {
'id': question.id,
'text': question.question_text,
'order_index': question.order_index,
'image': image_data,
'options': [
{
'id': option.id,
'text': option.option_text,
'category': option.category,
'order_index': option.order_index
}
for option in options
]
}