wokring marketplace without enchancts and durability on item

This commit is contained in:
2025-07-19 04:13:04 +05:00
parent 44e12723ad
commit 6b8f116608
6 changed files with 351 additions and 1 deletions

45
app/api/marketplace.py Normal file
View File

@ -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)

20
app/models/marketplace.py Normal file
View File

@ -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

View File

@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from app.db.database import users_collection, sessions_collection from app.db.database import users_collection, sessions_collection
from fastapi import HTTPException
class CoinsService: class CoinsService:
async def update_player_coins(self, player_id: str, player_name: str, online_time: int, server_ip: str): 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}с" "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)

243
app/services/marketplace.py Normal file
View File

@ -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"]
}

View File

@ -3,6 +3,7 @@ from datetime import datetime
from fastapi import HTTPException from fastapi import HTTPException
from typing import Dict from typing import Dict
from app.db.database import db from app.db.database import db
import asyncio
# Создаем коллекции для хранения команд и инвентаря # Создаем коллекции для хранения команд и инвентаря
pending_commands_collection = db.pending_commands pending_commands_collection = db.pending_commands

View File

@ -1,6 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles 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 from fastapi.middleware.cors import CORSMiddleware
app = FastAPI() app = FastAPI()
@ -12,6 +12,7 @@ app.include_router(capes.router)
app.include_router(server.router) app.include_router(server.router)
app.include_router(store.router) app.include_router(store.router)
app.include_router(pranks.router) app.include_router(pranks.router)
app.include_router(marketplace.router)
# Монтируем статику # Монтируем статику
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins") app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")