From 746e62d617c069b30a8e584ed844f73c541e3cca Mon Sep 17 00:00:00 2001 From: danilgryaznev Date: Mon, 22 Sep 2025 20:55:55 +0200 Subject: [PATCH] First working version --- README.md | 93 +++++++++++++++++++++++++++++++++ backend/Dockerfile | 3 +- backend/app/api/routes/users.py | 12 +++++ backend/app/core/config.py | 9 +++- backend/requirements.txt | 11 ++-- docker-compose.yaml | 8 +-- frontend/src/app/page.tsx | 2 +- scripts/seed_data.py | 3 +- 8 files changed, 126 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e69de29..8718b25 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,93 @@ +# Alabuga Gamification Platform + +Проект реализует прототип геймифицированного модуля для кадровой системы «Алабуги». Мы создаём космический лор, ранги, миссии, журнал событий и магазин артефактов. Репозиторий содержит backend на FastAPI (Python 3.13) и фронтенд на Next.js (TypeScript). + +## Содержимое репозитория + +- `backend/` — FastAPI, SQLAlchemy, Alembic, бизнес-логика геймификации. +- `frontend/` — Next.js с SSR, дизайн в космической стилистике. +- `scripts/` — служебные скрипты (сидирование демо-данных). +- `docs/` — документация, дополнительный лор. +- `docker-compose.yaml` — инфраструктура проекта. + +## Быстрый старт в Docker + +1. Установите Docker и Docker Compose. +2. Скопируйте пример конфигурации и при необходимости измените значения: + ```bash + cp backend/.env.example backend/.env + cp frontend/.env.example frontend/.env + ``` +3. Запустите окружение: + ```bash + docker compose up --build + ``` +4. После запуска будут доступны сервисы: + - API: http://localhost:8000 (документация Swagger — `/docs`). + - Фронтенд: http://localhost:3000. + +Docker Compose автоматически переопределяет `ALABUGA_SQLITE_PATH=/data/app.db`, чтобы база сохранялась во внешнем volume. Для локального запуска вне Docker оставьте путь `./data/app.db` из примера. + +## Локальная разработка backend + +```bash +cd backend +python -m venv .venv +source .venv/bin/activate +export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 +pip install -r requirements-dev.txt + +# подготовьте переменные окружения (однократно) +cp .env.example .env + +# применяем миграции +alembic upgrade head + +# создаём демо-данные (команда выполняется из корня репозитория) +cd .. +python -m scripts.seed_data +cd backend + +# Запуск API +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +## Локальная разработка фронтенда + +```bash +cd frontend +npm install +cp .env.example .env +npm run dev +``` + +## Пользовательские учётные записи (сидированные) + +| Роль | Email | Пароль | +| --- | --- | --- | +| Пилот | `candidate@alabuga.space` | `orbita123` | +| HR | `hr@alabuga.space` | `orbita123` | + +## Тестирование + +```bash +cd backend +pytest +``` + +## Основные сценарии, реализованные в бэкенде + +- Авторизация по email/паролю с JWT. +- Получение профиля пилота со списком компетенций и артефактов. +- Получение миссий, отправка отчётов, модерация HR. +- Начисление опыта, маны и повышение ранга по трём условиям из ТЗ. +- Журнал событий, экспортируемый через API. +- Магазин артефактов с оформлением заказа. +- Админ-панель для HR: создание миссий, очередь модерации, список рангов. + +## План развития + +- Реализовать фронтенд-интерфейс всех экранов. +- Добавить экспорт CSV и агрегаты аналитики в API. +- Настроить CI/CD и автотесты во фреймворках фронтенда. +- Расширить документацию в `docs/` (описание лора, сценарии e2e). diff --git a/backend/Dockerfile b/backend/Dockerfile index 261d010..698bbc2 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,8 @@ FROM python:3.13-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - POETRY_VIRTUALENVS_CREATE=false + POETRY_VIRTUALENVS_CREATE=false \ + PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 WORKDIR /app diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index f0e3943..06dab78 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -7,7 +7,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_user from app.db.session import get_db +from app.models.rank import Rank from app.models.user import User +from app.schemas.rank import RankBase from app.schemas.user import UserProfile router = APIRouter(prefix="/api", tags=["profile"]) @@ -23,3 +25,13 @@ def get_profile( _ = current_user.competencies _ = current_user.artifacts return UserProfile.model_validate(current_user) + + +@router.get("/ranks", response_model=list[RankBase], summary="Перечень рангов") +def list_ranks( + *, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) +) -> list[RankBase]: + """Возвращаем ранги по возрастанию требований.""" + + ranks = db.query(Rank).order_by(Rank.required_xp).all() + return [RankBase.model_validate(rank) for rank in ranks] diff --git a/backend/app/core/config.py b/backend/app/core/config.py index df08098..495559b 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -6,10 +6,13 @@ from pathlib import Path from pydantic_settings import BaseSettings, SettingsConfigDict +BASE_DIR = Path(__file__).resolve().parents[2] + + class Settings(BaseSettings): """Глобальные настройки сервиса.""" - model_config = SettingsConfigDict(env_file=".env", env_prefix="ALABUGA_", extra="ignore") + model_config = SettingsConfigDict(env_file=BASE_DIR / ".env", env_prefix="ALABUGA_", extra="ignore") project_name: str = "Alabuga Gamification API" environment: str = "local" @@ -33,6 +36,10 @@ def get_settings() -> Settings: """Кэшируем создание настроек, чтобы не читать файл каждый раз.""" settings = Settings() + + if not settings.sqlite_path.is_absolute(): + settings.sqlite_path = (BASE_DIR / settings.sqlite_path).resolve() + settings.sqlite_path.parent.mkdir(parents=True, exist_ok=True) return settings diff --git a/backend/requirements.txt b/backend/requirements.txt index 257c771..a7fd47f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,16 +1,13 @@ fastapi==0.111.0 uvicorn[standard]==0.30.1 -SQLAlchemy==2.0.30 -alembic==1.13.1 -pydantic==2.7.4 -pydantic-settings==2.3.2 +SQLAlchemy>=2.0.36,<3 +alembic>=1.14.0,<2 +pydantic==2.9.2 +pydantic-settings==2.10.1 passlib[bcrypt]==1.7.4 python-jose[cryptography]==3.3.0 python-multipart==0.0.9 bcrypt==4.1.3 email-validator==2.1.1 -pandas==2.2.2 -openpyxl==3.1.3 fastapi-pagination==0.12.24 Jinja2==3.1.4 - diff --git a/docker-compose.yaml b/docker-compose.yaml index 3c29645..9e97bbc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,7 +13,7 @@ services: env_file: - backend/.env.example environment: - ALABUGA_ENVIRONMENT=docker + ALABUGA_ENVIRONMENT: docker depends_on: [] frontend: @@ -23,9 +23,9 @@ services: ports: - '3000:3000' environment: - NEXT_PUBLIC_API_URL=http://backend:8000 - NEXT_PUBLIC_DEMO_EMAIL=candidate@alabuga.space - NEXT_PUBLIC_DEMO_PASSWORD=orbita123 + NEXT_PUBLIC_API_URL: http://backend:8000 + NEXT_PUBLIC_DEMO_EMAIL: candidate@alabuga.space + NEXT_PUBLIC_DEMO_PASSWORD: orbita123 volumes: - ./frontend:/app - /app/node_modules diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index afc31df..3622591 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -27,7 +27,7 @@ interface RankResponse { async function fetchProfile() { const token = await getDemoToken(); const profile = await apiFetch('/api/me', { authToken: token }); - const ranks = await apiFetch('/api/admin/ranks', { authToken: token }); + const ranks = await apiFetch('/api/ranks', { authToken: token }); const currentRank = ranks.find((rank) => rank.id === profile.current_rank_id); return { token, profile, currentRank }; } diff --git a/scripts/seed_data.py b/scripts/seed_data.py index 4d693d4..2fff2a8 100644 --- a/scripts/seed_data.py +++ b/scripts/seed_data.py @@ -11,6 +11,7 @@ from sqlalchemy.orm import Session ROOT = Path(__file__).resolve().parents[1] sys.path.append(str(ROOT / 'backend')) +from app.core.config import settings from app.core.security import get_password_hash from app.db.session import SessionLocal, engine from app.models.artifact import Artifact, ArtifactRarity @@ -20,7 +21,7 @@ from app.models.rank import Rank, RankCompetencyRequirement, RankMissionRequirem from app.models.store import StoreItem from app.models.user import Competency, CompetencyCategory, User, UserCompetency, UserRole -DATA_SENTINEL = Path("/data/.seeded") +DATA_SENTINEL = settings.sqlite_path.parent / ".seeded" def ensure_database() -> None: