244 lines
10 KiB
Python
244 lines
10 KiB
Python
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"]
|
||
}
|