feat: initial microservices project structure

Services:
- api: FastAPI gateway with JWT auth, async endpoints, WebSocket
- worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM
- worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction
- worker-notifier: SMTP email notifications
- worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting

Infrastructure:
- docker-compose.yml (production) + docker-compose.dev.yml (hot reload)
- Nginx reverse proxy + WebSocket support
- PostgreSQL 16 with Alembic migrations
- Elasticsearch 8 with Russian/English analyzers
- MinIO, RabbitMQ, Redis, Ollama

Frontend:
- React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5
- 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register

Scripts:
- Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-05-24 19:42:39 +05:00
commit 7758315632
120 changed files with 9500 additions and 0 deletions

113
services/api/app/main.py Normal file
View File

@@ -0,0 +1,113 @@
"""Точка входа FastAPI приложения — Академический помощник (anti-plagiarism)."""
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.api import auth, documents, reports, search, tasks
from app.config import settings
from app.core.websocket_manager import ws_manager
from app.database import engine
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""
Lifecycle менеджер приложения.
Выполняет инициализацию при старте и очистку при остановке.
"""
logger.info("Запуск Академического помощника...")
# Проверка соединений при старте
try:
async with engine.connect() as conn:
await conn.execute(__import__("sqlalchemy").text("SELECT 1"))
logger.info("PostgreSQL: соединение установлено")
except Exception as e:
logger.error(f"PostgreSQL: ошибка соединения: {e}")
yield
# Очистка при остановке
logger.info("Остановка приложения...")
await engine.dispose()
app = FastAPI(
title="Академический помощник API",
description=(
"Сервис антиплагиата и поиска академических источников. "
"Семантический поиск через FAISS GPU + Elasticsearch, "
"проверка плагиата в 4 уровня, ГОСТ библиография."
),
version="0.1.0",
lifespan=lifespan,
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
)
# ─── CORS ─────────────────────────────────────────────────────────────────────
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ─── Роутеры ──────────────────────────────────────────────────────────────────
app.include_router(auth.router, prefix="/api")
app.include_router(tasks.router, prefix="/api")
app.include_router(search.router, prefix="/api")
app.include_router(documents.router, prefix="/api")
app.include_router(reports.router, prefix="/api")
# ─── WebSocket ────────────────────────────────────────────────────────────────
@app.websocket("/ws/tasks/{task_id}")
async def websocket_task_updates(websocket: WebSocket, task_id: str) -> None:
"""
WebSocket endpoint для real-time обновлений статуса задачи.
Клиент подключается и получает обновления при изменении статуса задачи.
Соединение закрывается при получении статуса done/failed.
"""
await ws_manager.connect(task_id, websocket)
try:
while True:
# Ждём входящих сообщений (ping/pong для поддержания соединения)
data = await websocket.receive_text()
if data == "ping":
await websocket.send_text("pong")
except WebSocketDisconnect:
ws_manager.disconnect(task_id, websocket)
logger.info(f"WebSocket клиент отключился от задачи {task_id!r}")
# ─── Health check ──────────────────────────────────────────────────────────────
@app.get("/health", tags=["monitoring"])
async def health_check() -> JSONResponse:
"""Проверка работоспособности сервиса."""
return JSONResponse(
content={
"status": "ok",
"service": "api",
"version": "0.1.0",
}
)
@app.get("/", include_in_schema=False)
async def root() -> JSONResponse:
"""Корневой endpoint — редирект к документации."""
return JSONResponse(
content={"message": "Академический помощник API", "docs": "/api/docs"}
)