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 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=false
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user