Feature: серверная часть, ФИО и результаты теста в БД; правка ответа эскалация 80%

Made-with: Cursor
This commit is contained in:
2026-03-16 07:46:37 +00:00
parent 2de0d2cfbd
commit 05388b2ba6
14 changed files with 1971 additions and 48 deletions

109
backend/app/main.py Normal file
View File

@@ -0,0 +1,109 @@
"""FastAPI-приложение LMS «Управление ИТ (ОМС)»."""
import logging
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from pathlib import Path
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from app.config import settings
from app.database import get_db, init_db
from app.models import Participant
from app.schemas import (
StartRequest,
StartResponse,
CompleteRequest,
CompleteResponse,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Инициализация при старте, создание таблиц."""
init_db()
yield
# shutdown при необходимости
app = FastAPI(
title="LMS Управление ИТ (ОМС)",
description="API для фиксации прохождения обучения и результатов тестирования",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Статика: курс (HTML) — из CONTENT_PATH в Docker или content/ рядом с backend
static_path = Path(settings.content_path) if settings.content_path else (Path(__file__).resolve().parent.parent.parent / "content")
if static_path.exists():
app.mount("/content", StaticFiles(directory=str(static_path), html=True), name="content")
@app.get("/")
async def root():
"""Редирект на страницу курса."""
from fastapi.responses import RedirectResponse
return RedirectResponse(url="/content/it_course_v2.html", status_code=302)
@app.get("/api/health")
async def health():
"""Проверка доступности API."""
return {"status": "ok"}
@app.post("/api/start", response_model=StartResponse)
def api_start(body: StartRequest, db: Session = Depends(get_db)):
"""
Регистрация начала обучения.
Принимает ФИО, создаёт запись в БД, возвращает participant_id для последующей отправки результата.
"""
fio = body.fio.strip()
if not fio:
raise HTTPException(status_code=400, detail="ФИО не может быть пустым")
participant = Participant(fio=fio)
db.add(participant)
db.commit()
db.refresh(participant)
logger.info("Обучение начато: participant_id=%s, fio=%s", participant.id, participant.fio)
return StartResponse(participant_id=participant.id, fio=participant.fio)
@app.post("/api/complete", response_model=CompleteResponse)
def api_complete(body: CompleteRequest, db: Session = Depends(get_db)):
"""
Фиксация результата тестирования.
Обновляет запись участника по participant_id: score, percent, passed, completed_at.
"""
participant = db.query(Participant).filter(Participant.id == body.participant_id).first()
if not participant:
raise HTTPException(status_code=404, detail="Участник не найден")
participant.completed_at = datetime.now(timezone.utc)
participant.score = body.score
participant.total_questions = body.total_questions
participant.percent = body.percent
participant.passed = body.passed
db.commit()
logger.info(
"Тест завершён: participant_id=%s, score=%s/%s, percent=%s, passed=%s",
participant.id,
body.score,
body.total_questions,
body.percent,
body.passed,
)
return CompleteResponse(participant_id=participant.id, saved=True)