From 6b8f116608796e2a930689c1e828e773c519fa2e Mon Sep 17 00:00:00 2001 From: DIKER0K Date: Sat, 19 Jul 2025 04:13:04 +0500 Subject: [PATCH] wokring marketplace without enchancts and durability on item --- app/api/marketplace.py | 45 ++++++ app/models/marketplace.py | 20 +++ app/services/coins.py | 40 ++++++ app/services/marketplace.py | 243 +++++++++++++++++++++++++++++++++ app/services/server/command.py | 1 + main.py | 3 +- 6 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 app/api/marketplace.py create mode 100644 app/models/marketplace.py create mode 100644 app/services/marketplace.py diff --git a/app/api/marketplace.py b/app/api/marketplace.py new file mode 100644 index 0000000..2476d63 --- /dev/null +++ b/app/api/marketplace.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Query, Body +from typing import Optional +from app.models.marketplace import BuyItemRequest + +router = APIRouter( + prefix="/api/marketplace", + tags=["Marketplace"] +) + +@router.get("/items") +async def get_marketplace_items( + server_ip: Optional[str] = None, + page: int = Query(1, ge=1), + limit: int = Query(20, ge=1, le=100) +): + """Получить список предметов на торговой площадке""" + from app.services.marketplace import MarketplaceService + return await MarketplaceService().list_items(server_ip, page, limit) + +@router.get("/items/{item_id}") +async def get_marketplace_item(item_id: str): + """Получить информацию о конкретном предмете""" + from app.services.marketplace import MarketplaceService + return await MarketplaceService().get_item(item_id) + +@router.post("/items/sell") +async def sell_item( + username: str = Body(...), + slot_index: int = Body(...), + amount: int = Body(...), + price: int = Body(...), + server_ip: str = Body(...) +): + """Выставить предмет на продажу""" + from app.services.marketplace import MarketplaceService + return await MarketplaceService().add_item(username, slot_index, amount, price, server_ip) + +@router.post("/items/buy/{item_id}") +async def buy_item( + item_id: str, + request: BuyItemRequest +): + """Купить предмет""" + from app.services.marketplace import MarketplaceService + return await MarketplaceService().buy_item(request.username, item_id) diff --git a/app/models/marketplace.py b/app/models/marketplace.py new file mode 100644 index 0000000..734dd28 --- /dev/null +++ b/app/models/marketplace.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel +from typing import Optional, List, Dict, Any + +class MarketplaceItemBase(BaseModel): + material: str + amount: int + price: int + seller_name: str + server_ip: str + display_name: Optional[str] = None + lore: Optional[List[str]] = None + enchants: Optional[Dict[str, int]] = None + item_data: Optional[Dict[str, Any]] = None # Дополнительные данные предмета + +class MarketplaceItem(MarketplaceItemBase): + id: str + created_at: str + +class BuyItemRequest(BaseModel): + username: str diff --git a/app/services/coins.py b/app/services/coins.py index 54e6fb8..46cb99e 100644 --- a/app/services/coins.py +++ b/app/services/coins.py @@ -1,5 +1,6 @@ from datetime import datetime from app.db.database import users_collection, sessions_collection +from fastapi import HTTPException class CoinsService: async def update_player_coins(self, player_id: str, player_name: str, online_time: int, server_ip: str): @@ -100,3 +101,42 @@ class CoinsService: "formatted": f"{hours}ч {minutes}м {seconds}с" } } + + async def get_balance(self, username: str) -> int: + """Получить текущий баланс пользователя""" + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден") + return user.get("coins", 0) + + async def increase_balance(self, username: str, amount: int) -> int: + """Увеличить баланс пользователя""" + if amount <= 0: + raise ValueError("Сумма должна быть положительной") + + result = await users_collection.update_one( + {"username": username}, + {"$inc": {"coins": amount}} + ) + + if result.modified_count == 0: + raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден") + + user = await users_collection.find_one({"username": username}) + return user.get("coins", 0) + + async def decrease_balance(self, username: str, amount: int) -> int: + """Уменьшить баланс пользователя""" + if amount <= 0: + raise ValueError("Сумма должна быть положительной") + + result = await users_collection.update_one( + {"username": username}, + {"$inc": {"coins": -amount}} # Уменьшаем на отрицательное значение + ) + + if result.modified_count == 0: + raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден") + + user = await users_collection.find_one({"username": username}) + return user.get("coins", 0) diff --git a/app/services/marketplace.py b/app/services/marketplace.py new file mode 100644 index 0000000..05433cb --- /dev/null +++ b/app/services/marketplace.py @@ -0,0 +1,243 @@ +import uuid +from datetime import datetime +from fastapi import HTTPException +from app.db.database import db +from app.services.coins import CoinsService +from app.services.server.command import CommandService + +# Коллекция для хранения товаров на торговой площадке +marketplace_collection = db.marketplace_items + +# Добавьте эту функцию +def _serialize_mongodb_doc(doc): + """Преобразует MongoDB документ для JSON сериализации""" + if doc is None: + return None + + result = {} + for key, value in doc.items(): + # Обработка ObjectId + if key == "_id": + result["_id"] = str(value) + continue + + # Обработка ISODate + if isinstance(value, datetime): + result[key] = value.isoformat() + # Обработка вложенных словарей + elif isinstance(value, dict): + if "$date" in value: + # Это ISODate + result[key] = datetime.fromisoformat(value["$date"].replace("Z", "+00:00")).isoformat() + else: + result[key] = _serialize_mongodb_doc(value) + # Обработка списков + elif isinstance(value, list): + result[key] = [_serialize_mongodb_doc(item) if isinstance(item, dict) else item for item in value] + else: + result[key] = value + + return result + +class MarketplaceService: + async def list_items(self, server_ip: str = None, page: int = 1, limit: int = 20): + """Получить список предметов на торговой площадке""" + query = {} + if server_ip: + query["server_ip"] = server_ip + + total = await marketplace_collection.count_documents(query) + + items_cursor = marketplace_collection.find(query) \ + .sort("created_at", -1) \ + .skip((page - 1) * limit) \ + .limit(limit) + + items = await items_cursor.to_list(limit) + + # Преобразуем каждый документ + serialized_items = [_serialize_mongodb_doc(item) for item in items] + + return { + "items": serialized_items, + "total": total, + "page": page, + "pages": (total + limit - 1) // limit + } + + async def get_item(self, item_id: str): + """Получить информацию о конкретном предмете""" + item = await marketplace_collection.find_one({"id": item_id}) + if not item: + raise HTTPException(status_code=404, detail="Предмет не найден") + return _serialize_mongodb_doc(item) + + async def add_item(self, username: str, slot_index: int, amount: int, price: int, server_ip: str): + """Выставить предмет на продажу""" + # 1. Получаем инвентарь игрока + cmd_service = CommandService() + inventory_result = await cmd_service.get_player_inventory(username, server_ip, timeout=15) + + if inventory_result["status"] != "success": + raise HTTPException(status_code=400, + detail=f"Не удалось получить инвентарь игрока. Игрок должен быть онлайн.") + + # 2. Находим предмет в указанном слоте + item_data = None + for item in inventory_result.get("inventory", []): + if item.get("slot") == slot_index: + item_data = item + break + + if not item_data or item_data.get("material") == "AIR" or item_data.get("amount") < amount: + raise HTTPException(status_code=400, + detail=f"В указанном слоте нет предмета или недостаточное количество") + + # 3. Создаем запись о предмете на торговой площадке + item_id = str(uuid.uuid4()) + marketplace_item = { + "id": item_id, + "material": item_data.get("material"), + "amount": amount, + "price": price, + "seller_name": username, + "server_ip": server_ip, + "display_name": item_data.get("display_name"), + "lore": item_data.get("lore"), + "enchants": item_data.get("enchants"), + "item_data": item_data, + "created_at": datetime.utcnow() + } + + await marketplace_collection.insert_one(marketplace_item) + + # 4. Удаляем предмет из инвентаря игрока + # Определяем тип слота и его номер для команды replaceitem + slot_type = "inventory" + slot_num = slot_index + + # Преобразуем слот инвентаря в соответствующий формат для команды + if 0 <= slot_index <= 8: + # Панель быстрого доступа (хотбар) + slot_type = "hotbar" + slot_num = slot_index + elif 9 <= slot_index <= 35: + # Основной инвентарь + slot_type = "inventory" + slot_num = slot_index - 9 + elif slot_index == 36: + # Ботинки + slot_type = "armor.feet" + slot_num = 0 + elif slot_index == 37: + # Поножи + slot_type = "armor.legs" + slot_num = 0 + elif slot_index == 38: + # Нагрудник + slot_type = "armor.chest" + slot_num = 0 + elif slot_index == 39: + # Шлем + slot_type = "armor.head" + slot_num = 0 + elif slot_index == 40: + # Вторая рука + slot_type = "weapon.offhand" + slot_num = 0 + + # Выполняем команду + # Для Minecraft 1.16.5+ + command = f"item replace entity {username} {slot_type}.{slot_num} with air" + # Для более старых версий (1.13-1.16) + # command = f"replaceitem entity {username} {slot_type}.{slot_num} air" + + from app.models.server.command import ServerCommand + cmd = ServerCommand( + command=command, + server_ip=server_ip, + require_online_player=True, + target_message=f"Вы выставили на продажу {amount} шт. предмета {item_data.get('display_name', item_data.get('material'))} за {price} монет" + ) + + await cmd_service.add_command(cmd) + + return {"status": "success", "item_id": item_id} + + async def buy_item(self, buyer_username: str, item_id: str): + """Купить предмет с торговой площадки""" + # 1. Находим предмет + item = await marketplace_collection.find_one({"id": item_id}) + if not item: + raise HTTPException(status_code=404, detail="Предмет не найден") + + # 2. Проверяем, что покупатель не является продавцом + if item["seller_name"] == buyer_username: + raise HTTPException(status_code=400, detail="Вы не можете купить свой же предмет") + + # 3. Проверяем баланс покупателя + coins_service = CoinsService() + buyer_balance = await coins_service.get_balance(buyer_username) + + if buyer_balance < item["price"]: + raise HTTPException(status_code=400, + detail=f"Недостаточно монет. Требуется: {item['price']}, имеется: {buyer_balance}") + + # 4. Проверяем, что покупатель онлайн на сервере + cmd_service = CommandService() + try: + await cmd_service.get_player_inventory(buyer_username, item["server_ip"], timeout=5) + except: + raise HTTPException(status_code=400, + detail=f"Вы должны быть онлайн на сервере для совершения покупки") + + # 5. Списываем деньги с покупателя + await coins_service.decrease_balance(buyer_username, item["price"]) + + # 6. Начисляем деньги продавцу + await coins_service.increase_balance(item["seller_name"], item["price"]) + + # 7. Добавляем предмет в инвентарь покупателя + material = item["material"] + amount = item["amount"] + + # Создаем команду с учетом всех свойств предмета + command_base = f"give {buyer_username} {material} {amount}" + + # Если у предмета есть мета-данные, добавляем их через NBT + nbt_tags = [] + + if item.get("display_name"): + nbt_tags.append(f'display:{{Name:\'[{{"text":"{item["display_name"]}","italic":false}}]\'}}') + + if item.get("lore"): + lore_json = ','.join([f'[{{"text":"{line}","italic":false}}]' for line in item["lore"]]) + nbt_tags.append(f'display:{{Lore:[{lore_json}]}}') + + if item.get("enchants"): + enchant_tags = [] + for ench_id, level in item["enchants"].items(): + enchant_tags.append(f'{{id:"{ench_id}",lvl:{level}s}}') + nbt_tags.append(f'Enchantments:[{",".join(enchant_tags)}]') + + if nbt_tags: + command_base += " " + "{" + ",".join(nbt_tags) + "}" + + from app.models.server.command import ServerCommand + cmd = ServerCommand( + command=command_base, + server_ip=item["server_ip"], + require_online_player=True, + target_message=f"Вы купили {amount} шт. предмета {item.get('display_name', material)} за {item['price']} монет" + ) + + await cmd_service.add_command(cmd) + + # 8. Удаляем предмет с торговой площадки + await marketplace_collection.delete_one({"id": item_id}) + + return { + "status": "success", + "message": f"Вы купили {amount} шт. предмета {item.get('display_name', material)}", + "remaining_balance": buyer_balance - item["price"] + } diff --git a/app/services/server/command.py b/app/services/server/command.py index a4aca99..6e80efb 100644 --- a/app/services/server/command.py +++ b/app/services/server/command.py @@ -3,6 +3,7 @@ from datetime import datetime from fastapi import HTTPException from typing import Dict from app.db.database import db +import asyncio # Создаем коллекции для хранения команд и инвентаря pending_commands_collection = db.pending_commands diff --git a/main.py b/main.py index 7fdadd0..8a1572b 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from app.api import users, skins, capes, meta, server, store, pranks +from app.api import users, skins, capes, meta, server, store, pranks, marketplace from fastapi.middleware.cors import CORSMiddleware app = FastAPI() @@ -12,6 +12,7 @@ app.include_router(capes.router) app.include_router(server.router) app.include_router(store.router) app.include_router(pranks.router) +app.include_router(marketplace.router) # Монтируем статику app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")