First working version

This commit is contained in:
danilgryaznev 2025-09-22 20:55:55 +02:00
parent 1b46293ce2
commit 746e62d617
8 changed files with 126 additions and 15 deletions

View File

@ -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).

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -27,7 +27,7 @@ interface RankResponse {
async function fetchProfile() {
const token = await getDemoToken();
const profile = await apiFetch<ProfileResponse>('/api/me', { authToken: token });
const ranks = await apiFetch<RankResponse[]>('/api/admin/ranks', { authToken: token });
const ranks = await apiFetch<RankResponse[]>('/api/ranks', { authToken: token });
const currentRank = ranks.find((rank) => rank.id === profile.current_rank_id);
return { token, profile, currentRank };
}

View File

@ -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: