wokring marketplace without enchancts and durability on item
This commit is contained in:
45
app/api/marketplace.py
Normal file
45
app/api/marketplace.py
Normal 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
20
app/models/marketplace.py
Normal 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
|
@ -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
243
app/services/marketplace.py
Normal 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"]
|
||||||
|
}
|
@ -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
|
||||||
|
3
main.py
3
main.py
@ -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")
|
||||||
|
Reference in New Issue
Block a user