Student Hub
Initial backend for Hugging Face Spaces
2f4298a
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import func, and_, desc
from typing import List
from datetime import datetime, timedelta
from database import get_db
from models import User, UserStats, GameSession, GameAction, SystemStats
from schemas import AdminStats, UserListItem, UserDetail, UserStatsResponse
from auth import get_current_admin_user
router = APIRouter(prefix="/api/admin", tags=["Admin"])
@router.get("/stats/overview", response_model=AdminStats)
async def get_admin_stats(
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Общая статистика для админ-панели"""
# Общее количество пользователей
total_users = db.query(func.count(User.id)).scalar()
# Активные пользователи сегодня (имели сессию за последние 24 часа)
today = datetime.utcnow() - timedelta(days=1)
active_users_today = db.query(func.count(func.distinct(GameSession.user_id))).filter(
GameSession.started_at >= today
).scalar()
# Новые пользователи сегодня
new_users_today = db.query(func.count(User.id)).filter(
User.created_at >= today
).scalar()
# Всего сессий
total_sessions = db.query(func.count(GameSession.id)).scalar()
# Средняя длительность сессии
avg_duration = db.query(func.avg(GameSession.duration)).filter(
GameSession.ended_at != None
).scalar() or 0
# Общее время игры
total_playtime = db.query(func.sum(UserStats.total_playtime)).scalar() or 0
# Общая статистика по комнатам
total_rooms = db.query(func.sum(UserStats.rooms_visited)).scalar() or 0
# Общая статистика по предметам
total_items = db.query(func.sum(UserStats.items_collected)).scalar() or 0
# Общая статистика по врагам
total_enemies = db.query(func.sum(UserStats.enemies_defeated)).scalar() or 0
return AdminStats(
total_users=total_users,
active_users_today=active_users_today or 0,
new_users_today=new_users_today or 0,
total_sessions=total_sessions or 0,
avg_session_duration=float(avg_duration),
total_playtime=total_playtime,
total_rooms_visited=total_rooms,
total_items_collected=total_items,
total_enemies_defeated=total_enemies
)
@router.get("/users", response_model=List[UserListItem])
async def get_all_users(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Список всех пользователей"""
users = db.query(User).offset(skip).limit(limit).all()
return users
@router.get("/users/{user_id}", response_model=UserDetail)
async def get_user_detail(
user_id: int,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Детальная информация о пользователе"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@router.patch("/users/{user_id}/toggle-active")
async def toggle_user_active(
user_id: int,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Активация/деактивация пользователя"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
user.is_active = not user.is_active
db.commit()
return {"message": f"User {'activated' if user.is_active else 'deactivated'}", "is_active": user.is_active}
@router.patch("/users/{user_id}/toggle-admin")
async def toggle_user_admin(
user_id: int,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Назначение/снятие прав администратора"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.id == current_user.id:
raise HTTPException(status_code=400, detail="Cannot modify your own admin status")
user.is_admin = not user.is_admin
db.commit()
return {"message": f"Admin rights {'granted' if user.is_admin else 'revoked'}", "is_admin": user.is_admin}
@router.delete("/users/{user_id}")
async def delete_user(
user_id: int,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Удаление пользователя"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.id == current_user.id:
raise HTTPException(status_code=400, detail="Cannot delete yourself")
db.delete(user)
db.commit()
return {"message": "User deleted successfully"}
@router.get("/stats/top-players")
async def get_top_players(
metric: str = "level",
limit: int = 10,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Топ игроков по различным метрикам"""
valid_metrics = ["level", "experience", "total_playtime", "enemies_defeated", "items_collected", "coins"]
if metric not in valid_metrics:
raise HTTPException(status_code=400, detail=f"Invalid metric. Choose from: {valid_metrics}")
# Получаем топ игроков
query = db.query(User, UserStats).join(UserStats).order_by(desc(getattr(UserStats, metric))).limit(limit)
results = []
for user, stats in query:
results.append({
"user_id": user.id,
"username": user.username,
"metric_value": getattr(stats, metric),
"level": stats.level,
"experience": stats.experience
})
return {"metric": metric, "top_players": results}
@router.get("/stats/sessions-timeline")
async def get_sessions_timeline(
days: int = 7,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Временная шкала сессий за последние N дней"""
start_date = datetime.utcnow() - timedelta(days=days)
# Группировка по дням
sessions_by_day = db.query(
func.date(GameSession.started_at).label('date'),
func.count(GameSession.id).label('count'),
func.avg(GameSession.duration).label('avg_duration')
).filter(
GameSession.started_at >= start_date
).group_by(
func.date(GameSession.started_at)
).all()
timeline = []
for day in sessions_by_day:
timeline.append({
"date": str(day.date),
"sessions_count": day.count,
"avg_duration": float(day.avg_duration) if day.avg_duration else 0
})
return {"days": days, "timeline": timeline}
@router.get("/stats/activity-heatmap")
async def get_activity_heatmap(
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Тепловая карта активности игроков по часам"""
# Группировка по часам дня
activity = db.query(
func.extract('hour', GameSession.started_at).label('hour'),
func.count(GameSession.id).label('count')
).group_by(
func.extract('hour', GameSession.started_at)
).all()
heatmap = {int(hour): count for hour, count in activity}
# Заполняем отсутствующие часы нулями
full_heatmap = {hour: heatmap.get(hour, 0) for hour in range(24)}
return {"heatmap": full_heatmap}
@router.get("/stats/user-retention")
async def get_user_retention(
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Статистика удержания пользователей"""
# Пользователи, зарегистрированные в последние 30 дней
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
new_users = db.query(User).filter(User.created_at >= thirty_days_ago).all()
# Из них активных в последние 7 дней
seven_days_ago = datetime.utcnow() - timedelta(days=7)
active_new_users = 0
for user in new_users:
has_recent_session = db.query(GameSession).filter(
GameSession.user_id == user.id,
GameSession.started_at >= seven_days_ago
).first()
if has_recent_session:
active_new_users += 1
retention_rate = (active_new_users / len(new_users) * 100) if new_users else 0
return {
"new_users_30d": len(new_users),
"active_users_7d": active_new_users,
"retention_rate": round(retention_rate, 2)
}
@router.get("/stats/game-metrics")
async def get_game_metrics(
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Метрики игрового процесса"""
# Средние показатели
avg_stats = db.query(
func.avg(UserStats.level).label('avg_level'),
func.avg(UserStats.energy).label('avg_energy'),
func.avg(UserStats.hunger).label('avg_hunger'),
func.avg(UserStats.coins).label('avg_coins'),
func.avg(UserStats.deaths).label('avg_deaths')
).first()
# Общие показатели
total_stats = db.query(
func.sum(UserStats.rooms_visited).label('total_rooms'),
func.sum(UserStats.items_collected).label('total_items'),
func.sum(UserStats.enemies_defeated).label('total_enemies'),
func.sum(UserStats.deaths).label('total_deaths')
).first()
return {
"averages": {
"level": float(avg_stats.avg_level) if avg_stats.avg_level else 0,
"energy": float(avg_stats.avg_energy) if avg_stats.avg_energy else 0,
"hunger": float(avg_stats.avg_hunger) if avg_stats.avg_hunger else 0,
"coins": float(avg_stats.avg_coins) if avg_stats.avg_coins else 0,
"deaths": float(avg_stats.avg_deaths) if avg_stats.avg_deaths else 0
},
"totals": {
"rooms_visited": total_stats.total_rooms or 0,
"items_collected": total_stats.total_items or 0,
"enemies_defeated": total_stats.total_enemies or 0,
"deaths": total_stats.total_deaths or 0
}
}
@router.get("/actions/recent")
async def get_recent_actions(
limit: int = 50,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Последние действия игроков"""
actions = db.query(GameAction, User).join(User).order_by(
desc(GameAction.timestamp)
).limit(limit).all()
results = []
for action, user in actions:
results.append({
"id": action.id,
"username": user.username,
"action_type": action.action_type,
"action_data": action.action_data,
"timestamp": action.timestamp
})
return {"recent_actions": results}