First working version
This commit is contained in:
parent
1b46293ce2
commit
746e62d617
93
README.md
93
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).
|
||||||
|
|
@ -2,7 +2,8 @@ FROM python:3.13-slim
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
POETRY_VIRTUALENVS_CREATE=false
|
POETRY_VIRTUALENVS_CREATE=false \
|
||||||
|
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
|
from app.models.rank import Rank
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.schemas.rank import RankBase
|
||||||
from app.schemas.user import UserProfile
|
from app.schemas.user import UserProfile
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["profile"])
|
router = APIRouter(prefix="/api", tags=["profile"])
|
||||||
|
|
@ -23,3 +25,13 @@ def get_profile(
|
||||||
_ = current_user.competencies
|
_ = current_user.competencies
|
||||||
_ = current_user.artifacts
|
_ = current_user.artifacts
|
||||||
return UserProfile.model_validate(current_user)
|
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]
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@ from pathlib import Path
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
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"
|
project_name: str = "Alabuga Gamification API"
|
||||||
environment: str = "local"
|
environment: str = "local"
|
||||||
|
|
@ -33,6 +36,10 @@ def get_settings() -> Settings:
|
||||||
"""Кэшируем создание настроек, чтобы не читать файл каждый раз."""
|
"""Кэшируем создание настроек, чтобы не читать файл каждый раз."""
|
||||||
|
|
||||||
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)
|
settings.sqlite_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
uvicorn[standard]==0.30.1
|
uvicorn[standard]==0.30.1
|
||||||
SQLAlchemy==2.0.30
|
SQLAlchemy>=2.0.36,<3
|
||||||
alembic==1.13.1
|
alembic>=1.14.0,<2
|
||||||
pydantic==2.7.4
|
pydantic==2.9.2
|
||||||
pydantic-settings==2.3.2
|
pydantic-settings==2.10.1
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]==1.7.4
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]==3.3.0
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
bcrypt==4.1.3
|
bcrypt==4.1.3
|
||||||
email-validator==2.1.1
|
email-validator==2.1.1
|
||||||
pandas==2.2.2
|
|
||||||
openpyxl==3.1.3
|
|
||||||
fastapi-pagination==0.12.24
|
fastapi-pagination==0.12.24
|
||||||
Jinja2==3.1.4
|
Jinja2==3.1.4
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ services:
|
||||||
env_file:
|
env_file:
|
||||||
- backend/.env.example
|
- backend/.env.example
|
||||||
environment:
|
environment:
|
||||||
ALABUGA_ENVIRONMENT=docker
|
ALABUGA_ENVIRONMENT: docker
|
||||||
depends_on: []
|
depends_on: []
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
|
@ -23,9 +23,9 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
environment:
|
environment:
|
||||||
NEXT_PUBLIC_API_URL=http://backend:8000
|
NEXT_PUBLIC_API_URL: http://backend:8000
|
||||||
NEXT_PUBLIC_DEMO_EMAIL=candidate@alabuga.space
|
NEXT_PUBLIC_DEMO_EMAIL: candidate@alabuga.space
|
||||||
NEXT_PUBLIC_DEMO_PASSWORD=orbita123
|
NEXT_PUBLIC_DEMO_PASSWORD: orbita123
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ interface RankResponse {
|
||||||
async function fetchProfile() {
|
async function fetchProfile() {
|
||||||
const token = await getDemoToken();
|
const token = await getDemoToken();
|
||||||
const profile = await apiFetch<ProfileResponse>('/api/me', { authToken: token });
|
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);
|
const currentRank = ranks.find((rank) => rank.id === profile.current_rank_id);
|
||||||
return { token, profile, currentRank };
|
return { token, profile, currentRank };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.append(str(ROOT / 'backend'))
|
sys.path.append(str(ROOT / 'backend'))
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
from app.core.security import get_password_hash
|
from app.core.security import get_password_hash
|
||||||
from app.db.session import SessionLocal, engine
|
from app.db.session import SessionLocal, engine
|
||||||
from app.models.artifact import Artifact, ArtifactRarity
|
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.store import StoreItem
|
||||||
from app.models.user import Competency, CompetencyCategory, User, UserCompetency, UserRole
|
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:
|
def ensure_database() -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user