Student Hub commited on
Commit
2f4298a
·
0 Parent(s):

Initial backend for Hugging Face Spaces

Browse files
DEPLOYMENT.md ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎮 2D Game Backend - Инструкция по деплою на Hugging Face Spaces
2
+
3
+ ## Шаг 1: Подготовка
4
+
5
+ 1. Создайте аккаунт на [Hugging Face](https://huggingface.co)
6
+ 2. Перейдите в раздел [Spaces](https://huggingface.co/spaces)
7
+ 3. Нажмите **"Create new Space"**
8
+
9
+ ## Шаг 2: Настройка Space
10
+
11
+ 1. **Space name**: Выберите имя (например, `2d-game-backend`)
12
+ 2. **License**: Выберите лицензию (MIT рекомендуется)
13
+ 3. **SDK**: Выберите **Docker**
14
+ 4. **Space hardware**: Free CPU (достаточно для начала)
15
+ 5. Нажмите **"Create Space"**
16
+
17
+ ## Шаг 3: Загрузка файлов
18
+
19
+ ### Вариант A: Через веб-интерфейс
20
+
21
+ 1. Загрузите все файлы из папки `backend/`:
22
+ - `main.py`
23
+ - `models.py`
24
+ - `schemas.py`
25
+ - `database.py`
26
+ - `auth.py`
27
+ - `config.py`
28
+ - `requirements.txt`
29
+ - `Dockerfile`
30
+ - `README.md`
31
+ - Папку `routes/` со всеми файлами
32
+
33
+ 2. Убедитесь, что файл `README.md` начинается с frontmatter:
34
+ ```yaml
35
+ ---
36
+ title: 2D Game Backend
37
+ emoji: 🎮
38
+ colorFrom: blue
39
+ colorTo: purple
40
+ sdk: docker
41
+ pinned: false
42
+ app_port: 7860
43
+ ---
44
+ ```
45
+
46
+ ### Вариант B: Через Git
47
+
48
+ ```bash
49
+ # Клонируйте ваш Space
50
+ git clone https://huggingface.co/spaces/<your-username>/<space-name>
51
+ cd <space-name>
52
+
53
+ # Скопируйте все файлы из backend/
54
+ cp -r ../backend/* .
55
+
56
+ # Добавьте и закоммитьте
57
+ git add .
58
+ git commit -m "Initial backend deployment"
59
+ git push
60
+ ```
61
+
62
+ ## Шаг 4: Настройка переменных окружения (опционально)
63
+
64
+ В настройках Space добавьте секреты:
65
+
66
+ 1. Перейдите в **Settings** вашего Space
67
+ 2. В разделе **Repository secrets** добавьте:
68
+ - `SECRET_KEY` - случайная строка для JWT
69
+ - `ADMIN_PASSWORD` - пароль администратора (по умолчанию: admin123)
70
+
71
+ ## Шаг 5: Запуск
72
+
73
+ Space автоматически запустится после загрузки файлов. Процесс займет 2-5 минут.
74
+
75
+ ## Шаг 6: Проверка
76
+
77
+ 1. Откройте URL вашего Space: `https://<your-username>-<space-name>.hf.space`
78
+ 2. Должна появиться JSON-ответ от API
79
+ 3. Перейдите на `/docs` для Swagger документации: `https://<your-username>-<space-name>.hf.space/docs`
80
+
81
+ ## Шаг 7: Тестирование API
82
+
83
+ ### Регистрация пользователя
84
+ ```bash
85
+ curl -X POST "https://<your-space-url>/api/auth/register" \
86
+ -H "Content-Type: application/json" \
87
+ -d '{
88
+ "username": "testuser",
89
+ "email": "test@example.com",
90
+ "password": "password123"
91
+ }'
92
+ ```
93
+
94
+ ### Вход администратора
95
+ ```bash
96
+ curl -X POST "https://<your-space-url>/api/auth/login" \
97
+ -H "Content-Type: application/x-www-form-urlencoded" \
98
+ -d "username=admin&password=admin123"
99
+ ```
100
+
101
+ ### Получение статистики (нужен токен)
102
+ ```bash
103
+ curl -X GET "https://<your-space-url>/api/game/stats" \
104
+ -H "Authorization: Bearer <your-token>"
105
+ ```
106
+
107
+ ## Структура проекта
108
+
109
+ ```
110
+ backend/
111
+ ├── main.py # Главный файл приложения
112
+ ├── models.py # Модели базы данных
113
+ ├── schemas.py # Pydantic схемы
114
+ ├── database.py # Настройка БД
115
+ ├── auth.py # Авторизация и JWT
116
+ ├── config.py # Конфигурация
117
+ ├── requirements.txt # Зависимости Python
118
+ ├── Dockerfile # Docker конфигурация
119
+ ├── README.md # Документация
120
+ └── routes/
121
+ ├── __init__.py
122
+ ├── auth_routes.py # Роуты авторизации
123
+ ├── game.py # Игровые роуты
124
+ └── admin.py # Админ-панель
125
+ ```
126
+
127
+ ## Важные замечания
128
+
129
+ 1. **База данных**: По умолчанию используется SQLite. Данные сохраняются в `/app/data/game.db`
130
+ 2. **Безопасность**: Обязательно измените `SECRET_KEY` и пароль администратора в продакшене
131
+ 3. **CORS**: Настроен для всех источников (`*`). Для продакшена укажите конкретные домены
132
+ 4. **Логи**: Доступны в интерфейсе Space во вкладке "Logs"
133
+
134
+ ## Обновление Space
135
+
136
+ При изменении кода просто загрузите новые файлы или сделайте git push. Space автоматически пересоберется.
137
+
138
+ ## Подключение к ��ронтенду
139
+
140
+ В вашем фронтенд-коде используйте URL API:
141
+
142
+ ```typescript
143
+ const API_URL = 'https://<your-username>-<space-name>.hf.space';
144
+
145
+ // Пример регистрации
146
+ async function register(username: string, email: string, password: string) {
147
+ const response = await fetch(`${API_URL}/api/auth/register`, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/json' },
150
+ body: JSON.stringify({ username, email, password })
151
+ });
152
+ return response.json();
153
+ }
154
+
155
+ // Пример получения статистики
156
+ async function getStats(token: string) {
157
+ const response = await fetch(`${API_URL}/api/game/stats`, {
158
+ headers: { 'Authorization': `Bearer ${token}` }
159
+ });
160
+ return response.json();
161
+ }
162
+ ```
163
+
164
+ ## Мониторинг
165
+
166
+ - **Health check**: `GET /health`
167
+ - **API docs**: `/docs`
168
+ - **ReDoc**: `/redoc`
169
+
170
+ ## Troubleshooting
171
+
172
+ ### Space не запускается
173
+ 1. Проверьте логи в интерфейсе HF
174
+ 2. Убедитесь что все файлы загружены
175
+ 3. Проверьте Dockerfile на ошибки
176
+
177
+ ### База данных не инициализируется
178
+ - Проверьте права на запись в `/app/data`
179
+ - Убедитесь что SQLite установлен (включен в образ)
180
+
181
+ ### Ошибки импорта
182
+ - Проверьте `requirements.txt`
183
+ - Убедитесь что все файлы в нужных директориях
184
+
185
+ ## Полезные ссылки
186
+
187
+ - [Hugging Face Spaces Documentation](https://huggingface.co/docs/hub/spaces)
188
+ - [FastAPI Documentation](https://fastapi.tiangolo.com/)
189
+ - [Docker Documentation](https://docs.docker.com/)
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Установка зависимостей
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Копирование кода приложения
10
+ COPY . .
11
+
12
+ # Создание директории для базы данных
13
+ RUN mkdir -p /app/data
14
+
15
+ # Переменные окружения
16
+ ENV DATABASE_URL=sqlite:///./data/game.db
17
+ ENV PYTHONUNBUFFERED=1
18
+
19
+ # Экспонирование порта
20
+ EXPOSE 7860
21
+
22
+ # Команда запуска
23
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 2D Game Backend
3
+ emoji: 🎮
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 7860
9
+ ---
10
+
11
+ # 2D Game Backend API
12
+
13
+ Backend сервер для 2D игры с полной системой авторизации, управлением игроками и админ-панелью.
14
+
15
+ ## Функции
16
+
17
+ ### 🔐 Авторизация
18
+ - Регистрация пользователей
19
+ - JWT аутентификация
20
+ - Управление профилями
21
+
22
+ ### 🎮 Игровая механика
23
+ - Управление энергией и сытостью
24
+ - Статистика игроков (уровень, опыт, монеты)
25
+ - Трекинг игровых сессий
26
+ - Логирование действий игроков
27
+
28
+ ### 👨‍💼 Админ-панель
29
+ - Общая статистика игры
30
+ - Управление пользователями
31
+ - Топ игроков по различным метрикам
32
+ - Аналитика активности
33
+ - Временные графики
34
+ - Метрики удержания игроков
35
+ - История действий
36
+
37
+ ## API Документация
38
+
39
+ После запуска доступна по адресу: `/docs`
40
+
41
+ ## Эндпоинты
42
+
43
+ ### Авторизация
44
+ - `POST /api/auth/register` - Регистрация
45
+ - `POST /api/auth/login` - Вход
46
+ - `GET /api/auth/me` - Текущий пользователь
47
+
48
+ ### Игра
49
+ - `GET /api/game/stats` - Статистика игрока
50
+ - `PATCH /api/game/stats` - Обновление статистики
51
+ - `POST /api/game/stats/consume-energy` - Расход энергии
52
+ - `POST /api/game/stats/restore-energy` - Восстановление энергии
53
+ - `POST /api/game/stats/consume-hunger` - Расход сытости
54
+ - `POST /api/game/stats/restore-hunger` - Восстановление сытости
55
+ - `POST /api/game/session/start` - Начало сессии
56
+ - `POST /api/game/session/end` - Завершение сессии
57
+
58
+ ### Админка (требуется admin права)
59
+ - `GET /api/admin/stats/overview` - Общая статистика
60
+ - `GET /api/admin/users` - Список пользователей
61
+ - `GET /api/admin/stats/top-players` - Топ игроков
62
+ - `GET /api/admin/stats/sessions-timeline` - График сессий
63
+ - `GET /api/admin/stats/activity-heatmap` - Тепловая карта активности
64
+ - `GET /api/admin/stats/user-retention` - Удержание пользователей
65
+ - `GET /api/admin/stats/game-metrics` - Игровые метрики
66
+ - `GET /api/admin/actions/recent` - Последние действия
67
+
68
+ ## Настройка
69
+
70
+ По умолчанию создается администратор:
71
+ - Username: `admin`
72
+ - Password: `admin123`
73
+
74
+ ⚠️ **Важно:** Измените пароль администратора после первого входа!
75
+
76
+ ## База данных
77
+
78
+ Использует SQLite для простоты деплоя. Для продакшена можно переключиться на PostgreSQL через переменную окружения `DATABASE_URL`.
__init__.py ADDED
File without changes
__pycache__/auth.cpython-311.pyc ADDED
Binary file (7.8 kB). View file
 
__pycache__/config.cpython-311.pyc ADDED
Binary file (1.47 kB). View file
 
__pycache__/database.cpython-311.pyc ADDED
Binary file (1.14 kB). View file
 
__pycache__/main.cpython-311.pyc ADDED
Binary file (3.08 kB). View file
 
__pycache__/models.cpython-311.pyc ADDED
Binary file (6.49 kB). View file
 
__pycache__/schemas.cpython-311.pyc ADDED
Binary file (7.12 kB). View file
 
auth.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from typing import Optional
3
+ from jose import JWTError, jwt
4
+ from passlib.context import CryptContext
5
+ from fastapi import Depends, HTTPException, status
6
+ from fastapi.security import OAuth2PasswordBearer
7
+ from sqlalchemy.orm import Session
8
+ from database import get_db
9
+ from models import User, UserStats
10
+ from schemas import TokenData
11
+ from config import get_settings
12
+
13
+ settings = get_settings()
14
+
15
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
16
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
17
+
18
+
19
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
20
+ """Проверка пароля"""
21
+ return pwd_context.verify(plain_password, hashed_password)
22
+
23
+
24
+ def get_password_hash(password: str) -> str:
25
+ """Хеширование пароля"""
26
+ return pwd_context.hash(password)
27
+
28
+
29
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
30
+ """Создание JWT токена"""
31
+ to_encode = data.copy()
32
+ if expires_delta:
33
+ expire = datetime.utcnow() + expires_delta
34
+ else:
35
+ expire = datetime.utcnow() + timedelta(minutes=15)
36
+ to_encode.update({"exp": expire})
37
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
38
+ return encoded_jwt
39
+
40
+
41
+ def authenticate_user(db: Session, username: str, password: str):
42
+ """Аутентификация пользователя"""
43
+ user = db.query(User).filter(User.username == username).first()
44
+ if not user:
45
+ return False
46
+ if not verify_password(password, user.hashed_password):
47
+ return False
48
+ return user
49
+
50
+
51
+ async def get_current_user(
52
+ token: str = Depends(oauth2_scheme),
53
+ db: Session = Depends(get_db)
54
+ ) -> User:
55
+ """Получение текущего пользователя из токена"""
56
+ credentials_exception = HTTPException(
57
+ status_code=status.HTTP_401_UNAUTHORIZED,
58
+ detail="Could not validate credentials",
59
+ headers={"WWW-Authenticate": "Bearer"},
60
+ )
61
+ try:
62
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
63
+ username: str = payload.get("sub")
64
+ if username is None:
65
+ raise credentials_exception
66
+ token_data = TokenData(username=username)
67
+ except JWTError:
68
+ raise credentials_exception
69
+
70
+ user = db.query(User).filter(User.username == token_data.username).first()
71
+ if user is None:
72
+ raise credentials_exception
73
+ return user
74
+
75
+
76
+ async def get_current_active_user(
77
+ current_user: User = Depends(get_current_user)
78
+ ) -> User:
79
+ """Проверка активности пользователя"""
80
+ if not current_user.is_active:
81
+ raise HTTPException(status_code=400, detail="Inactive user")
82
+ return current_user
83
+
84
+
85
+ async def get_current_admin_user(
86
+ current_user: User = Depends(get_current_active_user)
87
+ ) -> User:
88
+ """Проверка прав администратора"""
89
+ if not current_user.is_admin:
90
+ raise HTTPException(
91
+ status_code=status.HTTP_403_FORBIDDEN,
92
+ detail="Not enough permissions"
93
+ )
94
+ return current_user
95
+
96
+
97
+ def create_user(db: Session, username: str, email: str, password: str, is_admin: bool = False) -> User:
98
+ """Создание нового пользователя"""
99
+ # Проверка существования пользователя
100
+ if db.query(User).filter(User.username == username).first():
101
+ raise HTTPException(
102
+ status_code=status.HTTP_400_BAD_REQUEST,
103
+ detail="Username already registered"
104
+ )
105
+ if db.query(User).filter(User.email == email).first():
106
+ raise HTTPException(
107
+ status_code=status.HTTP_400_BAD_REQUEST,
108
+ detail="Email already registered"
109
+ )
110
+
111
+ # Создание пользователя
112
+ hashed_password = get_password_hash(password)
113
+ user = User(
114
+ username=username,
115
+ email=email,
116
+ hashed_password=hashed_password,
117
+ is_admin=is_admin
118
+ )
119
+ db.add(user)
120
+ db.commit()
121
+ db.refresh(user)
122
+
123
+ # Создание статистики пользователя
124
+ user_stats = UserStats(user_id=user.id)
125
+ db.add(user_stats)
126
+ db.commit()
127
+
128
+ return user
129
+
130
+
131
+ def init_admin_user(db: Session):
132
+ """Инициализация администратора при первом запуске"""
133
+ admin = db.query(User).filter(User.username == settings.ADMIN_USERNAME).first()
134
+ if not admin:
135
+ create_user(
136
+ db=db,
137
+ username=settings.ADMIN_USERNAME,
138
+ email=settings.ADMIN_EMAIL,
139
+ password=settings.ADMIN_PASSWORD,
140
+ is_admin=True
141
+ )
142
+ print(f"Admin user created: {settings.ADMIN_USERNAME}")
config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ from functools import lru_cache
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ DATABASE_URL: str = "sqlite:///./game.db"
7
+ SECRET_KEY: str = "your-secret-key-change-in-production"
8
+ ALGORITHM: str = "HS256"
9
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
10
+ ADMIN_USERNAME: str = "admin"
11
+ ADMIN_PASSWORD: str = "admin123"
12
+ ADMIN_EMAIL: str = "admin@example.com"
13
+
14
+ class Config:
15
+ env_file = ".env"
16
+
17
+
18
+ @lru_cache()
19
+ def get_settings():
20
+ return Settings()
database.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+ from config import get_settings
5
+
6
+ settings = get_settings()
7
+
8
+ engine = create_engine(
9
+ settings.DATABASE_URL,
10
+ connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
11
+ )
12
+
13
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
14
+
15
+ Base = declarative_base()
16
+
17
+
18
+ def get_db():
19
+ db = SessionLocal()
20
+ try:
21
+ yield db
22
+ finally:
23
+ db.close()
game.db ADDED
Binary file (57.3 kB). View file
 
main.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from contextlib import asynccontextmanager
4
+ from database import engine, get_db
5
+ from models import Base
6
+ from routes import auth_routes, game, admin
7
+ from auth import init_admin_user
8
+
9
+
10
+ @asynccontextmanager
11
+ async def lifespan(app: FastAPI):
12
+ """Lifecycle events"""
13
+ # Startup
14
+ Base.metadata.create_all(bind=engine)
15
+
16
+ # Инициализация администратора
17
+ db = next(get_db())
18
+ try:
19
+ init_admin_user(db)
20
+ finally:
21
+ db.close()
22
+
23
+ print("✅ Database initialized")
24
+ print("✅ Admin user initialized")
25
+
26
+ yield
27
+
28
+ # Shutdown
29
+ print("👋 Shutting down...")
30
+
31
+
32
+ app = FastAPI(
33
+ title="2D Game API",
34
+ description="Backend API для 2D игры с авторизацией и статистикой",
35
+ version="1.0.0",
36
+ lifespan=lifespan
37
+ )
38
+
39
+ # CORS настройки
40
+ app.add_middleware(
41
+ CORSMiddleware,
42
+ allow_origins=["*"], # В продакшене укажите конкретные домены
43
+ allow_credentials=True,
44
+ allow_methods=["*"],
45
+ allow_headers=["*"],
46
+ )
47
+
48
+ # Подключение роутеров
49
+ app.include_router(auth_routes.router)
50
+ app.include_router(game.router)
51
+ app.include_router(admin.router)
52
+
53
+
54
+ @app.get("/")
55
+ async def root():
56
+ """Корневой эндпоинт"""
57
+ return {
58
+ "message": "2D Game API",
59
+ "version": "1.0.0",
60
+ "docs": "/docs",
61
+ "endpoints": {
62
+ "auth": "/api/auth",
63
+ "game": "/api/game",
64
+ "admin": "/api/admin"
65
+ }
66
+ }
67
+
68
+
69
+ @app.get("/health")
70
+ async def health_check():
71
+ """Проверка работоспособности"""
72
+ return {"status": "healthy"}
73
+
74
+
75
+ if __name__ == "__main__":
76
+ import uvicorn
77
+ uvicorn.run(app, host="0.0.0.0", port=8000)
models.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey, Text
2
+ from sqlalchemy.orm import relationship
3
+ from sqlalchemy.sql import func
4
+ from database import Base
5
+
6
+
7
+ class User(Base):
8
+ __tablename__ = "users"
9
+
10
+ id = Column(Integer, primary_key=True, index=True)
11
+ username = Column(String(50), unique=True, index=True, nullable=False)
12
+ email = Column(String(100), unique=True, index=True, nullable=False)
13
+ hashed_password = Column(String(255), nullable=False)
14
+ is_active = Column(Boolean, default=True)
15
+ is_admin = Column(Boolean, default=False)
16
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
17
+ last_login = Column(DateTime(timezone=True), onupdate=func.now())
18
+
19
+ # Relationships
20
+ stats = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
21
+ sessions = relationship("GameSession", back_populates="user", cascade="all, delete-orphan")
22
+
23
+
24
+ class UserStats(Base):
25
+ __tablename__ = "user_stats"
26
+
27
+ id = Column(Integer, primary_key=True, index=True)
28
+ user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
29
+
30
+ # Игровые параметры
31
+ energy = Column(Float, default=100.0)
32
+ max_energy = Column(Float, default=100.0)
33
+ hunger = Column(Float, default=100.0) # сытость
34
+ max_hunger = Column(Float, default=100.0)
35
+
36
+ # Статистика
37
+ total_playtime = Column(Integer, default=0) # в секундах
38
+ level = Column(Integer, default=1)
39
+ experience = Column(Integer, default=0)
40
+ coins = Column(Integer, default=0)
41
+
42
+ # Игровые достижения
43
+ rooms_visited = Column(Integer, default=0)
44
+ items_collected = Column(Integer, default=0)
45
+ enemies_defeated = Column(Integer, default=0)
46
+ deaths = Column(Integer, default=0)
47
+
48
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
49
+
50
+ # Relationship
51
+ user = relationship("User", back_populates="stats")
52
+
53
+
54
+ class GameSession(Base):
55
+ __tablename__ = "game_sessions"
56
+
57
+ id = Column(Integer, primary_key=True, index=True)
58
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
59
+
60
+ started_at = Column(DateTime(timezone=True), server_default=func.now())
61
+ ended_at = Column(DateTime(timezone=True), nullable=True)
62
+ duration = Column(Integer, default=0) # в секундах
63
+
64
+ # Статистика сессии
65
+ energy_consumed = Column(Float, default=0.0)
66
+ hunger_consumed = Column(Float, default=0.0)
67
+ rooms_visited_session = Column(Integer, default=0)
68
+ items_collected_session = Column(Integer, default=0)
69
+ enemies_defeated_session = Column(Integer, default=0)
70
+ deaths_session = Column(Integer, default=0)
71
+
72
+ # Relationship
73
+ user = relationship("User", back_populates="sessions")
74
+
75
+
76
+ class GameAction(Base):
77
+ """Лог действий пользователя для детальной аналитики"""
78
+ __tablename__ = "game_actions"
79
+
80
+ id = Column(Integer, primary_key=True, index=True)
81
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
82
+ action_type = Column(String(50), nullable=False) # move, attack, collect, eat, rest
83
+ action_data = Column(Text, nullable=True) # JSON данные
84
+ timestamp = Column(DateTime(timezone=True), server_default=func.now())
85
+
86
+
87
+ class SystemStats(Base):
88
+ """Системная статистика для админки"""
89
+ __tablename__ = "system_stats"
90
+
91
+ id = Column(Integer, primary_key=True, index=True)
92
+ date = Column(DateTime(timezone=True), server_default=func.now())
93
+
94
+ # Общие показатели
95
+ total_users = Column(Integer, default=0)
96
+ active_users_today = Column(Integer, default=0)
97
+ new_users_today = Column(Integer, default=0)
98
+ total_sessions = Column(Integer, default=0)
99
+ avg_session_duration = Column(Float, default=0.0)
100
+
101
+ # Игровая статистика
102
+ total_playtime = Column(Integer, default=0)
103
+ total_rooms_visited = Column(Integer, default=0)
104
+ total_items_collected = Column(Integer, default=0)
105
+ total_enemies_defeated = Column(Integer, default=0)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ sqlalchemy==2.0.23
4
+ psycopg2-binary==2.9.9
5
+ python-jose[cryptography]==3.3.0
6
+ passlib[bcrypt]==1.7.4
7
+ python-multipart==0.0.6
8
+ pydantic==2.5.0
9
+ pydantic-settings==2.1.0
10
+ python-dotenv==1.0.0
11
+ alembic==1.12.1
12
+ email-validator==2.1.0
routes/__init__.py ADDED
File without changes
routes/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (184 Bytes). View file
 
routes/__pycache__/admin.cpython-311.pyc ADDED
Binary file (19.3 kB). View file
 
routes/__pycache__/auth_routes.cpython-311.pyc ADDED
Binary file (3.72 kB). View file
 
routes/__pycache__/game.cpython-311.pyc ADDED
Binary file (11.3 kB). View file
 
routes/admin.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+ from sqlalchemy import func, and_, desc
4
+ from typing import List
5
+ from datetime import datetime, timedelta
6
+ from database import get_db
7
+ from models import User, UserStats, GameSession, GameAction, SystemStats
8
+ from schemas import AdminStats, UserListItem, UserDetail, UserStatsResponse
9
+ from auth import get_current_admin_user
10
+
11
+ router = APIRouter(prefix="/api/admin", tags=["Admin"])
12
+
13
+
14
+ @router.get("/stats/overview", response_model=AdminStats)
15
+ async def get_admin_stats(
16
+ current_user: User = Depends(get_current_admin_user),
17
+ db: Session = Depends(get_db)
18
+ ):
19
+ """Общая статистика для админ-панели"""
20
+
21
+ # Общее количество пользователей
22
+ total_users = db.query(func.count(User.id)).scalar()
23
+
24
+ # Активные пользователи сегодня (имели сессию за последние 24 часа)
25
+ today = datetime.utcnow() - timedelta(days=1)
26
+ active_users_today = db.query(func.count(func.distinct(GameSession.user_id))).filter(
27
+ GameSession.started_at >= today
28
+ ).scalar()
29
+
30
+ # Новые пользователи сегодня
31
+ new_users_today = db.query(func.count(User.id)).filter(
32
+ User.created_at >= today
33
+ ).scalar()
34
+
35
+ # Всего сессий
36
+ total_sessions = db.query(func.count(GameSession.id)).scalar()
37
+
38
+ # Средняя длительность сессии
39
+ avg_duration = db.query(func.avg(GameSession.duration)).filter(
40
+ GameSession.ended_at != None
41
+ ).scalar() or 0
42
+
43
+ # Общее время игры
44
+ total_playtime = db.query(func.sum(UserStats.total_playtime)).scalar() or 0
45
+
46
+ # Общая статистика по комнатам
47
+ total_rooms = db.query(func.sum(UserStats.rooms_visited)).scalar() or 0
48
+
49
+ # Общая статистика по предметам
50
+ total_items = db.query(func.sum(UserStats.items_collected)).scalar() or 0
51
+
52
+ # Общая статистика по врагам
53
+ total_enemies = db.query(func.sum(UserStats.enemies_defeated)).scalar() or 0
54
+
55
+ return AdminStats(
56
+ total_users=total_users,
57
+ active_users_today=active_users_today or 0,
58
+ new_users_today=new_users_today or 0,
59
+ total_sessions=total_sessions or 0,
60
+ avg_session_duration=float(avg_duration),
61
+ total_playtime=total_playtime,
62
+ total_rooms_visited=total_rooms,
63
+ total_items_collected=total_items,
64
+ total_enemies_defeated=total_enemies
65
+ )
66
+
67
+
68
+ @router.get("/users", response_model=List[UserListItem])
69
+ async def get_all_users(
70
+ skip: int = 0,
71
+ limit: int = 100,
72
+ current_user: User = Depends(get_current_admin_user),
73
+ db: Session = Depends(get_db)
74
+ ):
75
+ """Список всех пользователей"""
76
+ users = db.query(User).offset(skip).limit(limit).all()
77
+ return users
78
+
79
+
80
+ @router.get("/users/{user_id}", response_model=UserDetail)
81
+ async def get_user_detail(
82
+ user_id: int,
83
+ current_user: User = Depends(get_current_admin_user),
84
+ db: Session = Depends(get_db)
85
+ ):
86
+ """Детальная информация о пользователе"""
87
+ user = db.query(User).filter(User.id == user_id).first()
88
+ if not user:
89
+ raise HTTPException(status_code=404, detail="User not found")
90
+
91
+ return user
92
+
93
+
94
+ @router.patch("/users/{user_id}/toggle-active")
95
+ async def toggle_user_active(
96
+ user_id: int,
97
+ current_user: User = Depends(get_current_admin_user),
98
+ db: Session = Depends(get_db)
99
+ ):
100
+ """Активация/деактивация пользователя"""
101
+ user = db.query(User).filter(User.id == user_id).first()
102
+ if not user:
103
+ raise HTTPException(status_code=404, detail="User not found")
104
+
105
+ user.is_active = not user.is_active
106
+ db.commit()
107
+
108
+ return {"message": f"User {'activated' if user.is_active else 'deactivated'}", "is_active": user.is_active}
109
+
110
+
111
+ @router.patch("/users/{user_id}/toggle-admin")
112
+ async def toggle_user_admin(
113
+ user_id: int,
114
+ current_user: User = Depends(get_current_admin_user),
115
+ db: Session = Depends(get_db)
116
+ ):
117
+ """Назначение/снятие прав администратора"""
118
+ user = db.query(User).filter(User.id == user_id).first()
119
+ if not user:
120
+ raise HTTPException(status_code=404, detail="User not found")
121
+
122
+ if user.id == current_user.id:
123
+ raise HTTPException(status_code=400, detail="Cannot modify your own admin status")
124
+
125
+ user.is_admin = not user.is_admin
126
+ db.commit()
127
+
128
+ return {"message": f"Admin rights {'granted' if user.is_admin else 'revoked'}", "is_admin": user.is_admin}
129
+
130
+
131
+ @router.delete("/users/{user_id}")
132
+ async def delete_user(
133
+ user_id: int,
134
+ current_user: User = Depends(get_current_admin_user),
135
+ db: Session = Depends(get_db)
136
+ ):
137
+ """Удаление пользовате��я"""
138
+ user = db.query(User).filter(User.id == user_id).first()
139
+ if not user:
140
+ raise HTTPException(status_code=404, detail="User not found")
141
+
142
+ if user.id == current_user.id:
143
+ raise HTTPException(status_code=400, detail="Cannot delete yourself")
144
+
145
+ db.delete(user)
146
+ db.commit()
147
+
148
+ return {"message": "User deleted successfully"}
149
+
150
+
151
+ @router.get("/stats/top-players")
152
+ async def get_top_players(
153
+ metric: str = "level",
154
+ limit: int = 10,
155
+ current_user: User = Depends(get_current_admin_user),
156
+ db: Session = Depends(get_db)
157
+ ):
158
+ """Топ игроков по различным метрикам"""
159
+
160
+ valid_metrics = ["level", "experience", "total_playtime", "enemies_defeated", "items_collected", "coins"]
161
+ if metric not in valid_metrics:
162
+ raise HTTPException(status_code=400, detail=f"Invalid metric. Choose from: {valid_metrics}")
163
+
164
+ # Получаем топ игроков
165
+ query = db.query(User, UserStats).join(UserStats).order_by(desc(getattr(UserStats, metric))).limit(limit)
166
+
167
+ results = []
168
+ for user, stats in query:
169
+ results.append({
170
+ "user_id": user.id,
171
+ "username": user.username,
172
+ "metric_value": getattr(stats, metric),
173
+ "level": stats.level,
174
+ "experience": stats.experience
175
+ })
176
+
177
+ return {"metric": metric, "top_players": results}
178
+
179
+
180
+ @router.get("/stats/sessions-timeline")
181
+ async def get_sessions_timeline(
182
+ days: int = 7,
183
+ current_user: User = Depends(get_current_admin_user),
184
+ db: Session = Depends(get_db)
185
+ ):
186
+ """Временная шкала сессий за последние N дней"""
187
+
188
+ start_date = datetime.utcnow() - timedelta(days=days)
189
+
190
+ # Группировка по дням
191
+ sessions_by_day = db.query(
192
+ func.date(GameSession.started_at).label('date'),
193
+ func.count(GameSession.id).label('count'),
194
+ func.avg(GameSession.duration).label('avg_duration')
195
+ ).filter(
196
+ GameSession.started_at >= start_date
197
+ ).group_by(
198
+ func.date(GameSession.started_at)
199
+ ).all()
200
+
201
+ timeline = []
202
+ for day in sessions_by_day:
203
+ timeline.append({
204
+ "date": str(day.date),
205
+ "sessions_count": day.count,
206
+ "avg_duration": float(day.avg_duration) if day.avg_duration else 0
207
+ })
208
+
209
+ return {"days": days, "timeline": timeline}
210
+
211
+
212
+ @router.get("/stats/activity-heatmap")
213
+ async def get_activity_heatmap(
214
+ current_user: User = Depends(get_current_admin_user),
215
+ db: Session = Depends(get_db)
216
+ ):
217
+ """Тепловая карта активности игроков по часам"""
218
+
219
+ # Группировка по часам дня
220
+ activity = db.query(
221
+ func.extract('hour', GameSession.started_at).label('hour'),
222
+ func.count(GameSession.id).label('count')
223
+ ).group_by(
224
+ func.extract('hour', GameSession.started_at)
225
+ ).all()
226
+
227
+ heatmap = {int(hour): count for hour, count in activity}
228
+
229
+ # Заполняем отсутствующие часы нулями
230
+ full_heatmap = {hour: heatmap.get(hour, 0) for hour in range(24)}
231
+
232
+ return {"heatmap": full_heatmap}
233
+
234
+
235
+ @router.get("/stats/user-retention")
236
+ async def get_user_retention(
237
+ current_user: User = Depends(get_current_admin_user),
238
+ db: Session = Depends(get_db)
239
+ ):
240
+ """Статистика удержания пользователей"""
241
+
242
+ # Пользователи, зарегистрированные в последние 30 дней
243
+ thirty_days_ago = datetime.utcnow() - timedelta(days=30)
244
+ new_users = db.query(User).filter(User.created_at >= thirty_days_ago).all()
245
+
246
+ # Из них активных в последние 7 дней
247
+ seven_days_ago = datetime.utcnow() - timedelta(days=7)
248
+ active_new_users = 0
249
+
250
+ for user in new_users:
251
+ has_recent_session = db.query(GameSession).filter(
252
+ GameSession.user_id == user.id,
253
+ GameSession.started_at >= seven_days_ago
254
+ ).first()
255
+ if has_recent_session:
256
+ active_new_users += 1
257
+
258
+ retention_rate = (active_new_users / len(new_users) * 100) if new_users else 0
259
+
260
+ return {
261
+ "new_users_30d": len(new_users),
262
+ "active_users_7d": active_new_users,
263
+ "retention_rate": round(retention_rate, 2)
264
+ }
265
+
266
+
267
+ @router.get("/stats/game-metrics")
268
+ async def get_game_metrics(
269
+ current_user: User = Depends(get_current_admin_user),
270
+ db: Session = Depends(get_db)
271
+ ):
272
+ """Метрики игрового процесса"""
273
+
274
+ # Средние показатели
275
+ avg_stats = db.query(
276
+ func.avg(UserStats.level).label('avg_level'),
277
+ func.avg(UserStats.energy).label('avg_energy'),
278
+ func.avg(UserStats.hunger).label('avg_hunger'),
279
+ func.avg(UserStats.coins).label('avg_coins'),
280
+ func.avg(UserStats.deaths).label('avg_deaths')
281
+ ).first()
282
+
283
+ # Общие показатели
284
+ total_stats = db.query(
285
+ func.sum(UserStats.rooms_visited).label('total_rooms'),
286
+ func.sum(UserStats.items_collected).label('total_items'),
287
+ func.sum(UserStats.enemies_defeated).label('total_enemies'),
288
+ func.sum(UserStats.deaths).label('total_deaths')
289
+ ).first()
290
+
291
+ return {
292
+ "averages": {
293
+ "level": float(avg_stats.avg_level) if avg_stats.avg_level else 0,
294
+ "energy": float(avg_stats.avg_energy) if avg_stats.avg_energy else 0,
295
+ "hunger": float(avg_stats.avg_hunger) if avg_stats.avg_hunger else 0,
296
+ "coins": float(avg_stats.avg_coins) if avg_stats.avg_coins else 0,
297
+ "deaths": float(avg_stats.avg_deaths) if avg_stats.avg_deaths else 0
298
+ },
299
+ "totals": {
300
+ "rooms_visited": total_stats.total_rooms or 0,
301
+ "items_collected": total_stats.total_items or 0,
302
+ "enemies_defeated": total_stats.total_enemies or 0,
303
+ "deaths": total_stats.total_deaths or 0
304
+ }
305
+ }
306
+
307
+
308
+ @router.get("/actions/recent")
309
+ async def get_recent_actions(
310
+ limit: int = 50,
311
+ current_user: User = Depends(get_current_admin_user),
312
+ db: Session = Depends(get_db)
313
+ ):
314
+ """Последние действия игроков"""
315
+
316
+ actions = db.query(GameAction, User).join(User).order_by(
317
+ desc(GameAction.timestamp)
318
+ ).limit(limit).all()
319
+
320
+ results = []
321
+ for action, user in actions:
322
+ results.append({
323
+ "id": action.id,
324
+ "username": user.username,
325
+ "action_type": action.action_type,
326
+ "action_data": action.action_data,
327
+ "timestamp": action.timestamp
328
+ })
329
+
330
+ return {"recent_actions": results}
routes/auth_routes.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import timedelta
2
+ from fastapi import APIRouter, Depends, HTTPException, status
3
+ from fastapi.security import OAuth2PasswordRequestForm
4
+ from sqlalchemy.orm import Session
5
+ from database import get_db
6
+ from schemas import UserCreate, UserResponse, Token
7
+ from auth import (
8
+ authenticate_user,
9
+ create_access_token,
10
+ create_user,
11
+ get_current_active_user
12
+ )
13
+ from config import get_settings
14
+ from models import User
15
+
16
+ settings = get_settings()
17
+ router = APIRouter(prefix="/api/auth", tags=["Authentication"])
18
+
19
+
20
+ @router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
21
+ async def register(user_data: UserCreate, db: Session = Depends(get_db)):
22
+ """Регистрация нового пользователя"""
23
+ user = create_user(
24
+ db=db,
25
+ username=user_data.username,
26
+ email=user_data.email,
27
+ password=user_data.password
28
+ )
29
+ return user
30
+
31
+
32
+ @router.post("/login", response_model=Token)
33
+ async def login(
34
+ form_data: OAuth2PasswordRequestForm = Depends(),
35
+ db: Session = Depends(get_db)
36
+ ):
37
+ """Вход в систему"""
38
+ user = authenticate_user(db, form_data.username, form_data.password)
39
+ if not user:
40
+ raise HTTPException(
41
+ status_code=status.HTTP_401_UNAUTHORIZED,
42
+ detail="Incorrect username or password",
43
+ headers={"WWW-Authenticate": "Bearer"},
44
+ )
45
+
46
+ # Обновление времени последнего входа
47
+ from datetime import datetime
48
+ user.last_login = datetime.utcnow()
49
+ db.commit()
50
+
51
+ access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
52
+ access_token = create_access_token(
53
+ data={"sub": user.username}, expires_delta=access_token_expires
54
+ )
55
+ return {"access_token": access_token, "token_type": "bearer"}
56
+
57
+
58
+ @router.get("/me", response_model=UserResponse)
59
+ async def read_users_me(current_user: User = Depends(get_current_active_user)):
60
+ """Получение информации о текущем пользователе"""
61
+ return current_user
62
+
63
+
64
+ @router.post("/logout")
65
+ async def logout(current_user: User = Depends(get_current_active_user)):
66
+ """Выход из системы"""
67
+ return {"message": "Successfully logged out"}
routes/game.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+ from sqlalchemy import func
4
+ from typing import List
5
+ from datetime import datetime, timedelta
6
+ from database import get_db
7
+ from models import User, UserStats, GameSession, GameAction
8
+ from schemas import (
9
+ UserStatsResponse,
10
+ UserStatsUpdate,
11
+ GameSessionCreate,
12
+ GameSessionResponse
13
+ )
14
+ from auth import get_current_active_user
15
+
16
+ router = APIRouter(prefix="/api/game", tags=["Game"])
17
+
18
+
19
+ @router.get("/stats", response_model=UserStatsResponse)
20
+ async def get_user_stats(
21
+ current_user: User = Depends(get_current_active_user),
22
+ db: Session = Depends(get_db)
23
+ ):
24
+ """Получение статистики пользователя"""
25
+ stats = db.query(UserStats).filter(UserStats.user_id == current_user.id).first()
26
+ if not stats:
27
+ # Создаем статистику если не существует
28
+ stats = UserStats(user_id=current_user.id)
29
+ db.add(stats)
30
+ db.commit()
31
+ db.refresh(stats)
32
+ return stats
33
+
34
+
35
+ @router.patch("/stats", response_model=UserStatsResponse)
36
+ async def update_user_stats(
37
+ stats_update: UserStatsUpdate,
38
+ current_user: User = Depends(get_current_active_user),
39
+ db: Session = Depends(get_db)
40
+ ):
41
+ """Обновление статистики пользователя"""
42
+ stats = db.query(UserStats).filter(UserStats.user_id == current_user.id).first()
43
+ if not stats:
44
+ raise HTTPException(status_code=404, detail="Stats not found")
45
+
46
+ # Обновление полей
47
+ update_data = stats_update.dict(exclude_unset=True)
48
+ for field, value in update_data.items():
49
+ setattr(stats, field, value)
50
+
51
+ # Автоматический расчет уровня на основе опыта
52
+ if stats_update.experience is not None:
53
+ stats.level = 1 + (stats.experience // 100)
54
+
55
+ db.commit()
56
+ db.refresh(stats)
57
+ return stats
58
+
59
+
60
+ @router.post("/stats/consume-energy")
61
+ async def consume_energy(
62
+ amount: float,
63
+ current_user: User = Depends(get_current_active_user),
64
+ db: Session = Depends(get_db)
65
+ ):
66
+ """Расход энергии"""
67
+ stats = db.query(UserStats).filter(UserStats.user_id == current_user.id).first()
68
+ if not stats:
69
+ raise HTTPException(status_code=404, detail="Stats not found")
70
+
71
+ stats.energy = max(0, stats.energy - amount)
72
+ db.commit()
73
+
74
+ return {"energy": stats.energy, "message": f"Consumed {amount} energy"}
75
+
76
+
77
+ @router.post("/stats/restore-energy")
78
+ async def restore_energy(
79
+ amount: float,
80
+ current_user: User = Depends(get_current_active_user),
81
+ db: Session = Depends(get_db)
82
+ ):
83
+ """Восстановление энергии"""
84
+ stats = db.query(UserStats).filter(UserStats.user_id == current_user.id).first()
85
+ if not stats:
86
+ raise HTTPException(status_code=404, detail="Stats not found")
87
+
88
+ stats.energy = min(stats.max_energy, stats.energy + amount)
89
+ db.commit()
90
+
91
+ return {"energy": stats.energy, "message": f"Restored {amount} energy"}
92
+
93
+
94
+ @router.post("/stats/consume-hunger")
95
+ async def consume_hunger(
96
+ amount: float,
97
+ current_user: User = Depends(get_current_active_user),
98
+ db: Session = Depends(get_db)
99
+ ):
100
+ """Расход сытости"""
101
+ stats = db.query(UserStats).filter(UserStats.user_id == current_user.id).first()
102
+ if not stats:
103
+ raise HTTPException(status_code=404, detail="Stats not found")
104
+
105
+ stats.hunger = max(0, stats.hunger - amount)
106
+ db.commit()
107
+
108
+ return {"hunger": stats.hunger, "message": f"Consumed {amount} hunger"}
109
+
110
+
111
+ @router.post("/stats/restore-hunger")
112
+ async def restore_hunger(
113
+ amount: float,
114
+ current_user: User = Depends(get_current_active_user),
115
+ db: Session = Depends(get_db)
116
+ ):
117
+ """Восстановление сытости (еда)"""
118
+ stats = db.query(UserStats).filter(UserStats.user_id == current_user.id).first()
119
+ if not stats:
120
+ raise HTTPException(status_code=404, detail="Stats not found")
121
+
122
+ stats.hunger = min(stats.max_hunger, stats.hunger + amount)
123
+ db.commit()
124
+
125
+ return {"hunger": stats.hunger, "message": f"Restored {amount} hunger"}
126
+
127
+
128
+ @router.post("/session/start", response_model=GameSessionResponse)
129
+ async def start_game_session(
130
+ current_user: User = Depends(get_current_active_user),
131
+ db: Session = Depends(get_db)
132
+ ):
133
+ """Начало игровой сессии"""
134
+ # Проверка активной сессии
135
+ active_session = db.query(GameSession).filter(
136
+ GameSession.user_id == current_user.id,
137
+ GameSession.ended_at == None
138
+ ).first()
139
+
140
+ if active_session:
141
+ return active_session
142
+
143
+ # Создание новой сессии
144
+ session = GameSession(user_id=current_user.id)
145
+ db.add(session)
146
+ db.commit()
147
+ db.refresh(session)
148
+
149
+ return session
150
+
151
+
152
+ @router.post("/session/end", response_model=GameSessionResponse)
153
+ async def end_game_session(
154
+ current_user: User = Depends(get_current_active_user),
155
+ db: Session = Depends(get_db)
156
+ ):
157
+ """Завершение игровой сессии"""
158
+ session = db.query(GameSession).filter(
159
+ GameSession.user_id == current_user.id,
160
+ GameSession.ended_at == None
161
+ ).first()
162
+
163
+ if not session:
164
+ raise HTTPException(status_code=404, detail="No active session found")
165
+
166
+ session.ended_at = datetime.utcnow()
167
+ session.duration = int((session.ended_at - session.started_at).total_seconds())
168
+
169
+ # Обновление общей статистики
170
+ stats = db.query(UserStats).filter(UserStats.user_id == current_user.id).first()
171
+ if stats:
172
+ stats.total_playtime += session.duration
173
+
174
+ db.commit()
175
+ db.refresh(session)
176
+
177
+ return session
178
+
179
+
180
+ @router.get("/sessions", response_model=List[GameSessionResponse])
181
+ async def get_user_sessions(
182
+ limit: int = 10,
183
+ current_user: User = Depends(get_current_active_user),
184
+ db: Session = Depends(get_db)
185
+ ):
186
+ """Получение истории игровых сессий"""
187
+ sessions = db.query(GameSession).filter(
188
+ GameSession.user_id == current_user.id
189
+ ).order_by(GameSession.started_at.desc()).limit(limit).all()
190
+
191
+ return sessions
192
+
193
+
194
+ @router.post("/action/{action_type}")
195
+ async def log_game_action(
196
+ action_type: str,
197
+ action_data: str = None,
198
+ current_user: User = Depends(get_current_active_user),
199
+ db: Session = Depends(get_db)
200
+ ):
201
+ """Логирование игрового действия"""
202
+ action = GameAction(
203
+ user_id=current_user.id,
204
+ action_type=action_type,
205
+ action_data=action_data
206
+ )
207
+ db.add(action)
208
+ db.commit()
209
+
210
+ return {"message": "Action logged", "action_type": action_type}
schemas.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr, Field
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+
6
+ # User schemas
7
+ class UserBase(BaseModel):
8
+ username: str = Field(..., min_length=3, max_length=50)
9
+ email: EmailStr
10
+
11
+
12
+ class UserCreate(UserBase):
13
+ password: str = Field(..., min_length=6)
14
+
15
+
16
+ class UserResponse(UserBase):
17
+ id: int
18
+ is_active: bool
19
+ is_admin: bool
20
+ created_at: datetime
21
+ last_login: Optional[datetime] = None
22
+
23
+ class Config:
24
+ from_attributes = True
25
+
26
+
27
+ # Auth schemas
28
+ class Token(BaseModel):
29
+ access_token: str
30
+ token_type: str
31
+
32
+
33
+ class TokenData(BaseModel):
34
+ username: Optional[str] = None
35
+
36
+
37
+ class LoginRequest(BaseModel):
38
+ username: str
39
+ password: str
40
+
41
+
42
+ # UserStats schemas
43
+ class UserStatsBase(BaseModel):
44
+ energy: float = 100.0
45
+ max_energy: float = 100.0
46
+ hunger: float = 100.0
47
+ max_hunger: float = 100.0
48
+ level: int = 1
49
+ experience: int = 0
50
+ coins: int = 0
51
+
52
+
53
+ class UserStatsResponse(UserStatsBase):
54
+ id: int
55
+ user_id: int
56
+ total_playtime: int
57
+ rooms_visited: int
58
+ items_collected: int
59
+ enemies_defeated: int
60
+ deaths: int
61
+ updated_at: datetime
62
+
63
+ class Config:
64
+ from_attributes = True
65
+
66
+
67
+ class UserStatsUpdate(BaseModel):
68
+ energy: Optional[float] = None
69
+ hunger: Optional[float] = None
70
+ experience: Optional[int] = None
71
+ coins: Optional[int] = None
72
+ rooms_visited: Optional[int] = None
73
+ items_collected: Optional[int] = None
74
+ enemies_defeated: Optional[int] = None
75
+ deaths: Optional[int] = None
76
+
77
+
78
+ # GameSession schemas
79
+ class GameSessionCreate(BaseModel):
80
+ pass
81
+
82
+
83
+ class GameSessionResponse(BaseModel):
84
+ id: int
85
+ user_id: int
86
+ started_at: datetime
87
+ ended_at: Optional[datetime] = None
88
+ duration: int
89
+ energy_consumed: float
90
+ hunger_consumed: float
91
+ rooms_visited_session: int
92
+ items_collected_session: int
93
+ enemies_defeated_session: int
94
+ deaths_session: int
95
+
96
+ class Config:
97
+ from_attributes = True
98
+
99
+
100
+ # Admin schemas
101
+ class AdminStats(BaseModel):
102
+ total_users: int
103
+ active_users_today: int
104
+ new_users_today: int
105
+ total_sessions: int
106
+ avg_session_duration: float
107
+ total_playtime: int
108
+ total_rooms_visited: int
109
+ total_items_collected: int
110
+ total_enemies_defeated: int
111
+
112
+
113
+ class UserListItem(BaseModel):
114
+ id: int
115
+ username: str
116
+ email: str
117
+ is_active: bool
118
+ is_admin: bool
119
+ created_at: datetime
120
+ last_login: Optional[datetime] = None
121
+
122
+ class Config:
123
+ from_attributes = True
124
+
125
+
126
+ class UserDetail(UserListItem):
127
+ stats: Optional[UserStatsResponse] = None
test_api.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ from time import sleep
4
+
5
+ BASE_URL = "http://localhost:8000"
6
+
7
+ def print_response(title, response):
8
+ """Красивый вывод ответа"""
9
+ print(f"\n{'='*60}")
10
+ print(f"🔹 {title}")
11
+ print(f"{'='*60}")
12
+ print(f"Status: {response.status_code}")
13
+ try:
14
+ print(json.dumps(response.json(), indent=2, ensure_ascii=False))
15
+ except:
16
+ print(response.text)
17
+
18
+ def test_api():
19
+ """Тестирование всех эндпоинтов API"""
20
+
21
+ # 1. Проверка корневого эндпоинта
22
+ print("\n🚀 НАЧАЛО ТЕСТИРОВАНИЯ API")
23
+ response = requests.get(f"{BASE_URL}/")
24
+ print_response("GET / - Корневой эндпоинт", response)
25
+
26
+ # 2. Health check
27
+ response = requests.get(f"{BASE_URL}/health")
28
+ print_response("GET /health - Проверка здоровья", response)
29
+
30
+ # 3. Регистрация нового пользователя
31
+ user_data = {
32
+ "username": "testuser",
33
+ "email": "test@example.com",
34
+ "password": "password123"
35
+ }
36
+ response = requests.post(f"{BASE_URL}/api/auth/register", json=user_data)
37
+ print_response("POST /api/auth/register - Регистрация", response)
38
+
39
+ # 4. Вход пользователя
40
+ login_data = {
41
+ "username": "testuser",
42
+ "password": "password123"
43
+ }
44
+ response = requests.post(
45
+ f"{BASE_URL}/api/auth/login",
46
+ data=login_data,
47
+ headers={"Content-Type": "application/x-www-form-urlencoded"}
48
+ )
49
+ print_response("POST /api/auth/login - Вход пользователя", response)
50
+
51
+ if response.status_code == 200:
52
+ user_token = response.json()["access_token"]
53
+ print(f"✅ Получен токен пользователя")
54
+ else:
55
+ print("❌ Не удалось получить токен")
56
+ return
57
+
58
+ # 5. Информация о текущем пользователе
59
+ headers = {"Authorization": f"Bearer {user_token}"}
60
+ response = requests.get(f"{BASE_URL}/api/auth/me", headers=headers)
61
+ print_response("GET /api/auth/me - Текущий пользователь", response)
62
+
63
+ # 6. Получение статистики игрока
64
+ response = requests.get(f"{BASE_URL}/api/game/stats", headers=headers)
65
+ print_response("GET /api/game/stats - Статистика игрока", response)
66
+
67
+ # 7. Начало игровой сессии
68
+ response = requests.post(f"{BASE_URL}/api/game/session/start", headers=headers)
69
+ print_response("POST /api/game/session/start - Начало сессии", response)
70
+
71
+ # 8. Расход энергии
72
+ response = requests.post(f"{BASE_URL}/api/game/stats/consume-energy?amount=10", headers=headers)
73
+ print_response("POST /api/game/stats/consume-energy - Расход энергии", response)
74
+
75
+ # 9. Восстановление энергии
76
+ response = requests.post(f"{BASE_URL}/api/game/stats/restore-energy?amount=5", headers=headers)
77
+ print_response("POST /api/game/stats/restore-energy - Восстановление энергии", response)
78
+
79
+ # 10. Расход сытости
80
+ response = requests.post(f"{BASE_URL}/api/game/stats/consume-hunger?amount=15", headers=headers)
81
+ print_response("POST /api/game/stats/consume-hunger - Расход сытости", response)
82
+
83
+ # 11. Восстановление сытости (еда)
84
+ response = requests.post(f"{BASE_URL}/api/game/stats/restore-hunger?amount=20", headers=headers)
85
+ print_response("POST /api/game/stats/restore-hunger - Еда", response)
86
+
87
+ # 12. Обновление статистики
88
+ stats_update = {
89
+ "experience": 50,
90
+ "coins": 100,
91
+ "rooms_visited": 5,
92
+ "items_collected": 10,
93
+ "enemies_defeated": 3
94
+ }
95
+ response = requests.patch(f"{BASE_URL}/api/game/stats", json=stats_update, headers=headers)
96
+ print_response("PATCH /api/game/stats - Обновление статистики", response)
97
+
98
+ # 13. Логирование действия
99
+ response = requests.post(
100
+ f"{BASE_URL}/api/game/action/move?action_data=room_1_to_room_2",
101
+ headers=headers
102
+ )
103
+ print_response("POST /api/game/action/move - Логирование действия", response)
104
+
105
+ # Подождем 2 секунды для сессии
106
+ print("\n⏳ Ждем 2 секунды для статистики сессии...")
107
+ sleep(2)
108
+
109
+ # 14. Завершение сессии
110
+ response = requests.post(f"{BASE_URL}/api/game/session/end", headers=headers)
111
+ print_response("POST /api/game/session/end - Завершение сессии", response)
112
+
113
+ # 15. История сессий
114
+ response = requests.get(f"{BASE_URL}/api/game/sessions?limit=5", headers=headers)
115
+ print_response("GET /api/game/sessions - История сессий", response)
116
+
117
+ # ТЕСТИРОВАНИЕ АДМИНКИ
118
+ print("\n" + "="*60)
119
+ print("👨‍💼 ТЕСТИРОВАНИЕ АДМИН-ПАНЕЛИ")
120
+ print("="*60)
121
+
122
+ # 16. Вход администратора
123
+ admin_login = {
124
+ "username": "admin",
125
+ "password": "admin123"
126
+ }
127
+ response = requests.post(
128
+ f"{BASE_URL}/api/auth/login",
129
+ data=admin_login,
130
+ headers={"Content-Type": "application/x-www-form-urlencoded"}
131
+ )
132
+ print_response("POST /api/auth/login - Вход администратора", response)
133
+
134
+ if response.status_code == 200:
135
+ admin_token = response.json()["access_token"]
136
+ print(f"✅ Получен токен администратора")
137
+ else:
138
+ print("❌ Не удалось получить токен администратора")
139
+ return
140
+
141
+ admin_headers = {"Authorization": f"Bearer {admin_token}"}
142
+
143
+ # 17. Общая статистика
144
+ response = requests.get(f"{BASE_URL}/api/admin/stats/overview", headers=admin_headers)
145
+ print_response("GET /api/admin/stats/overview - Общая статистика", response)
146
+
147
+ # 18. Список пользователей
148
+ response = requests.get(f"{BASE_URL}/api/admin/users", headers=admin_headers)
149
+ print_response("GET /api/admin/users - Список пользователей", response)
150
+
151
+ # 19. Топ игроков
152
+ response = requests.get(f"{BASE_URL}/api/admin/stats/top-players?metric=level&limit=5", headers=admin_headers)
153
+ print_response("GET /api/admin/stats/top-players - Топ по уровню", response)
154
+
155
+ # 20. График сессий
156
+ response = requests.get(f"{BASE_URL}/api/admin/stats/sessions-timeline?days=7", headers=admin_headers)
157
+ print_response("GET /api/admin/stats/sessions-timeline - График сессий", response)
158
+
159
+ # 21. Тепловая карта активности
160
+ response = requests.get(f"{BASE_URL}/api/admin/stats/activity-heatmap", headers=admin_headers)
161
+ print_response("GET /api/admin/stats/activity-heatmap - Тепловая карта", response)
162
+
163
+ # 22. Удержание пользователей
164
+ response = requests.get(f"{BASE_URL}/api/admin/stats/user-retention", headers=admin_headers)
165
+ print_response("GET /api/admin/stats/user-retention - Удержание", response)
166
+
167
+ # 23. Игровые метрики
168
+ response = requests.get(f"{BASE_URL}/api/admin/stats/game-metrics", headers=admin_headers)
169
+ print_response("GET /api/admin/stats/game-metrics - Игровые метрики", response)
170
+
171
+ # 24. Последние действия
172
+ response = requests.get(f"{BASE_URL}/api/admin/actions/recent?limit=10", headers=admin_headers)
173
+ print_response("GET /api/admin/actions/recent - Последние действия", response)
174
+
175
+ # Итоги
176
+ print("\n" + "="*60)
177
+ print("✅ ТЕСТИРОВАНИЕ ЗАВЕРШЕНО!")
178
+ print("="*60)
179
+ print("\n📊 Проверьте документацию API: http://localhost:8000/docs")
180
+ print("📊 Альтернативная документация: http://localhost:8000/redoc")
181
+
182
+ if __name__ == "__main__":
183
+ try:
184
+ test_api()
185
+ except requests.exceptions.ConnectionError:
186
+ print("\n❌ Ошибка: Не удалось подключиться к серверу")
187
+ print("Убедитесь, что сервер запущен на http://localhost:8000")
188
+ except Exception as e:
189
+ print(f"\n❌ Ошибка: {e}")