This commit is contained in:
2025-09-14 22:09:39 +05:00
parent 8ad2bfbe59
commit 3c418a97b4
30 changed files with 3404 additions and 0 deletions

358
database/services.py Normal file
View File

@@ -0,0 +1,358 @@
"""
Сервисы для работы с системой тестирования
Основная бизнес-логика для получения вопросов, сохранения ответов и работы с изображениями
"""
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
]
}