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}