feat: new endpoints for users and updated models

This commit is contained in:
2025-07-18 02:58:22 +05:00
parent 733977f56e
commit 2e59d03784
10 changed files with 351 additions and 1 deletions

21
app/api/server.py Normal file
View File

@ -0,0 +1,21 @@
from fastapi import APIRouter
from app.services.server.command import CommandService
from app.services.server.event import EventService
from app.models.server.command import ServerCommand
router = APIRouter(
prefix="/api/server",
tags=["Server Management"]
)
@router.post("/events")
async def receive_server_event(event_data: dict):
return await EventService().process_event(event_data)
@router.post("/commands")
async def add_server_command(command_data: ServerCommand):
return await CommandService().add_command(command_data)
@router.get("/commands")
async def get_server_commands(server_ip: str):
return await CommandService().get_commands(server_ip)

View File

@ -2,6 +2,16 @@ from fastapi import APIRouter, HTTPException, Body, Response
from app.models.user import UserCreate, UserLogin
from app.models.request import ValidateRequest
from app.services.auth import AuthService
from app.db.database import users_collection, sessions_collection
from datetime import datetime
import json
from fastapi import HTTPException
from datetime import datetime, timedelta
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
from app.models.server.playtime import PlayerSession, PlayerPlaytime
from app.services.coins import CoinsService
coins_service = CoinsService()
router = APIRouter(
tags=["Users"]
@ -45,3 +55,65 @@ async def join_server(request_data: dict = Body(...)):
@router.get("/sessionserver/session/minecraft/hasJoined")
async def has_joined(username: str, serverId: str):
return await AuthService().has_joined(username, serverId)
@router.get("/users/{username}/coins")
async def get_user_coins(username: str):
coins_data = await coins_service.get_player_coins(username)
if not coins_data:
raise HTTPException(status_code=404, detail="User not found")
return coins_data
@router.get("/users")
async def get_users():
"""Получение списка всех пользователей"""
users = await users_collection.find().to_list(1000)
# Исключаем чувствительные данные перед отправкой
safe_users = []
for user in users:
safe_users.append({
"username": user["username"],
"uuid": user["uuid"],
"skin_url": user.get("skin_url"),
"cloak_url": user.get("cloak_url"),
"coins": user.get("coins", 0),
"total_time_played": user.get("total_time_played", 0),
"is_active": user.get("is_active", True)
})
return {"users": safe_users, "count": len(safe_users)}
@router.get("/users/{uuid}")
async def get_user_by_uuid(uuid: str):
"""Получение пользователя по UUID"""
user = await users_collection.find_one({"uuid": uuid})
if not user:
# Пробуем разные форматы UUID
if '-' in uuid:
user = await users_collection.find_one({"uuid": uuid.replace('-', '')})
else:
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
user = await users_collection.find_one({"uuid": formatted_uuid})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Исключаем чувствительные данные
safe_user = {
"username": user["username"],
"uuid": user["uuid"],
"skin_url": user.get("skin_url"),
"cloak_url": user.get("cloak_url"),
"coins": user.get("coins", 0),
"total_time_played": user.get("total_time_played", 0),
"is_active": user.get("is_active", True),
"created_at": user.get("created_at")
}
if "total_time_played" in safe_user:
total_time = safe_user["total_time_played"]
hours, remainder = divmod(total_time, 3600)
minutes, seconds = divmod(remainder, 60)
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
return safe_user

View File

@ -0,0 +1,7 @@
from pydantic import BaseModel
from typing import Optional
class ServerCommand(BaseModel):
command: str
server_ip: str
require_online_player: Optional[bool] = False

View File

@ -0,0 +1,17 @@
from pydantic import BaseModel
from typing import Optional, List, Dict
from datetime import datetime
class PlayerEvent(BaseModel):
event_type: str
player_id: Optional[str] = None
player_name: str
duration: Optional[int] = None # в секундах
timestamp: Optional[int] = None # UNIX timestamp в миллисекундах
server_ip: str
class OnlinePlayersUpdate(BaseModel):
event_type: str = "online_players_update"
players: List[Dict]
timestamp: int
server_ip: str

View File

@ -0,0 +1,17 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class PlayerSession(BaseModel):
player_id: str
player_name: str
server_ip: str
start_time: datetime
end_time: Optional[datetime] = None
duration: Optional[int] = None # в секундах
class PlayerPlaytime(BaseModel):
player_id: str
player_name: str
total_time: int # общее время в секундах
last_coins_update: datetime # последнее время начисления монет

View File

@ -19,6 +19,8 @@ class UserInDB(BaseModel):
skin_url: Optional[str] = None
skin_model: Optional[str] = "classic"
cloak_url: Optional[str] = None
coins: int = 0 # Новое поле для монет
total_time_played: int = 0 # Общее время игры в секундах
is_active: bool = True
created_at: datetime = datetime.utcnow()

102
app/services/coins.py Normal file
View File

@ -0,0 +1,102 @@
from datetime import datetime
from app.db.database import users_collection, sessions_collection
class CoinsService:
async def update_player_coins(self, player_id: str, player_name: str, online_time: int, server_ip: str):
"""Обновляет монеты игрока на основе времени онлайн"""
# Находим пользователя
user = await self._find_user_by_uuid(player_id)
if not user:
return # Пользователь не найден
# Находим последнее обновление монет
last_update = await sessions_collection.find_one({
"player_id": player_id,
"server_ip": server_ip,
"update_type": "coins_update"
}, sort=[("timestamp", -1)])
now = datetime.now()
current_coins = user.get("coins", 0)
current_total_time = user.get("total_time_played", 0)
if last_update:
# Время с последнего начисления
last_timestamp = last_update["timestamp"]
seconds_since_update = int((now - last_timestamp).total_seconds())
# Начисляем монеты только за полные минуты
minutes_to_reward = seconds_since_update // 60
# Если прошло меньше минуты, пропускаем
if minutes_to_reward < 1:
return
else:
# Первое обновление (ограничиваем для безопасности)
minutes_to_reward = min(online_time // 60, 5)
if minutes_to_reward > 0:
# Обновляем монеты и время
new_coins = current_coins + minutes_to_reward
new_total_time = current_total_time + (minutes_to_reward * 60)
# Сохраняем в БД
await users_collection.update_one(
{"_id": user["_id"]},
{"$set": {
"coins": new_coins,
"total_time_played": new_total_time
}}
)
# Сохраняем запись о начислении
await sessions_collection.insert_one({
"player_id": player_id,
"player_name": player_name,
"server_ip": server_ip,
"update_type": "coins_update",
"timestamp": now,
"minutes_added": minutes_to_reward,
"coins_added": minutes_to_reward
})
print(f"[{now}] Игроку {user.get('username')} начислено {minutes_to_reward} монет. "
f"Всего монет: {new_coins}")
async def _find_user_by_uuid(self, player_id: str):
"""Находит пользователя по UUID с поддержкой разных форматов"""
# Пробуем найти как есть
user = await users_collection.find_one({"uuid": player_id})
if user:
return user
# Пробуем разные форматы UUID
if '-' in player_id:
user = await users_collection.find_one({"uuid": player_id.replace('-', '')})
else:
formatted_uuid = f"{player_id[:8]}-{player_id[8:12]}-{player_id[12:16]}-{player_id[16:20]}-{player_id[20:]}"
user = await users_collection.find_one({"uuid": formatted_uuid})
return user
async def get_player_coins(self, username: str):
"""Возвращает информацию о монетах и времени игрока"""
user = await users_collection.find_one({"username": username})
if not user:
return None
total_time = user.get("total_time_played", 0)
hours, remainder = divmod(total_time, 3600)
minutes, seconds = divmod(remainder, 60)
return {
"username": username,
"coins": user.get("coins", 0),
"total_time_played": {
"seconds": total_time,
"formatted": f"{hours}ч {minutes}м {seconds}с"
}
}

View File

@ -0,0 +1,41 @@
import uuid
from datetime import datetime
from fastapi import HTTPException
from typing import Dict
# Глобальное хранилище команд (в реальном проекте используйте БД)
pending_commands: Dict[str, Dict] = {}
class CommandService:
async def add_command(self, command_data):
try:
command_id = str(uuid.uuid4())
pending_commands[command_id] = {
"command": command_data.command,
"server_ip": command_data.server_ip,
"require_online_player": command_data.require_online_player,
"created_at": datetime.now().isoformat()
}
print(f"[{datetime.now()}] Добавлена команда: {command_data.command} "
f"для сервера {command_data.server_ip}")
return {"status": "success", "command_id": command_id}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def get_commands(self, server_ip: str):
try:
# Получаем команды для указанного сервера
commands = [
{"id": cmd_id, "command": cmd["command"], "require_online_player": cmd["require_online_player"]}
for cmd_id, cmd in pending_commands.items()
if cmd["server_ip"] == server_ip
]
# Удаляем полученные команды (чтобы не выполнять их повторно)
for cmd_id in list(pending_commands.keys()):
if pending_commands[cmd_id]["server_ip"] == server_ip:
del pending_commands[cmd_id]
return {"status": "success", "commands": commands}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View File

@ -0,0 +1,70 @@
from fastapi import HTTPException
from datetime import datetime
import json
from app.services.coins import CoinsService
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
class EventService:
def __init__(self):
self.coins_service = CoinsService()
async def process_event(self, event_data: dict):
try:
event_type = event_data.get("event_type")
server_ip = event_data.get("server_ip", "unknown")
if event_type == "player_join":
player_name = event_data["player_name"]
player_id = event_data["player_id"]
print(f"[{datetime.now()}] Игрок вошел: {player_name} (ID: {player_id}) "
f"IP сервера: {server_ip}")
elif event_type == "player_quit":
player_name = event_data["player_name"]
player_id = event_data["player_id"]
print(f"[{datetime.now()}] Игрок вышел: {player_name} (ID: {player_id}) "
f"IP сервера: {server_ip}")
elif event_type == "player_session":
player_name = event_data["player_name"]
player_id = event_data["player_id"]
duration = event_data["duration"]
# Обновляем монеты через выделенный сервис
await self.coins_service.update_player_coins(player_id, player_name, duration, server_ip)
print(f"[{datetime.now()}] Игрок {player_name} провел на сервере: {duration} секунд "
f"IP сервера: {server_ip}")
elif event_type == "online_players_update":
players = event_data["players"]
print(f"\n[{datetime.now()}] Текущие онлайн-игроки ({len(players)}): "
f"IP сервера: {server_ip}")
# Обрабатываем каждого игрока
for player in players:
player_id = player["player_id"]
player_name = player["player_name"]
online_time = player["online_time"]
# Обновляем монеты через выделенный сервис
await self.coins_service.update_player_coins(
player_id, player_name, online_time, server_ip
)
hours, remainder = divmod(online_time, 3600)
minutes, seconds = divmod(remainder, 60)
print(f" - {player_name} (ID: {player_id}) "
f"Онлайн: {hours}ч {minutes}м {seconds}с")
print()
else:
print(f"[{datetime.now()}] Неизвестное событие: {json.dumps(event_data, indent=2)}")
raise HTTPException(status_code=400, detail="Invalid event type")
return {"status": "success", "message": "Event processed"}
except Exception as e:
print(f"[{datetime.now()}] Ошибка обработки события: {str(e)}")
print(f"Полученные данные: {json.dumps(event_data, indent=2)}")
raise HTTPException(status_code=400, detail=str(e))