From bb74dbbba768d7ea67129a3a3956e109d5beb223 Mon Sep 17 00:00:00 2001 From: DIKER0K Date: Tue, 16 Dec 2025 00:15:29 +0500 Subject: [PATCH] test player_inventory --- app/api/case.py | 4 +- app/api/inventory.py | 29 +++++++++++ app/models/case.py | 4 +- app/models/inventory.py | 26 ++++++++++ app/services/case.py | 53 +++++++++++---------- app/services/inventory.py | 95 +++++++++++++++++++++++++++++++++++++ app/services/marketplace.py | 31 +++++++----- main.py | 5 +- 8 files changed, 204 insertions(+), 43 deletions(-) create mode 100644 app/api/inventory.py create mode 100644 app/models/inventory.py create mode 100644 app/services/inventory.py diff --git a/app/api/case.py b/app/api/case.py index 9196035..4cbef63 100644 --- a/app/api/case.py +++ b/app/api/case.py @@ -32,7 +32,7 @@ async def delete_case(case_id: str, case_service: CaseService = Depends(get_case async def open_case( case_id: str, username: str, - server_id: str, + server_ip: str, case_service: CaseService = Depends(get_case_service) ): - return await case_service.open_case(username=username, case_id=case_id, server_id=server_id) + return await case_service.open_case(username=username, case_id=case_id, server_ip=server_ip) diff --git a/app/api/inventory.py b/app/api/inventory.py new file mode 100644 index 0000000..470876e --- /dev/null +++ b/app/api/inventory.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, Depends +from app.services.inventory import InventoryService +from app.models.inventory import InventoryWithdrawRequest + +router = APIRouter(prefix="/inventory", tags=["Inventory"]) + +def get_inventory_service(): + return InventoryService() + +@router.get("/items") +async def list_inventory_items( + username: str, + server_ip: str, + page: int = 1, + limit: int = 20, + inventory: InventoryService = Depends(get_inventory_service), +): + return await inventory.list_items(username=username, server_ip=server_ip, page=page, limit=limit) + +@router.post("/withdraw") +async def withdraw_inventory_item( + data: InventoryWithdrawRequest, + inventory: InventoryService = Depends(get_inventory_service), +): + return await inventory.withdraw_item( + username=data.username, + item_id=data.item_id, + server_ip=data.server_ip, + ) diff --git a/app/models/case.py b/app/models/case.py index d7a4b27..43a467f 100644 --- a/app/models/case.py +++ b/app/models/case.py @@ -19,7 +19,7 @@ class CaseCreate(BaseModel): name: str description: Optional[str] = None price: int = Field(gt=0) - server_ids: List[str] = Field(default_factory=lambda: ["*"]) + server_ips: List[str] = Field(default_factory=lambda: ["*"]) image_url: Optional[str] = None # 🔹 Картинка кейса items: List[CaseItem] @@ -27,6 +27,6 @@ class CaseUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None price: Optional[int] = Field(default=None, gt=0) - server_ids: Optional[List[str]] = None + server_ips: Optional[List[str]] = None image_url: Optional[str] = None # 🔹 Можно менять картинку items: Optional[List[CaseItem]] = None \ No newline at end of file diff --git a/app/models/inventory.py b/app/models/inventory.py new file mode 100644 index 0000000..9bfe9d1 --- /dev/null +++ b/app/models/inventory.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel, Field +from typing import Dict, Any, Optional, List +from datetime import datetime + +class InventoryItemCreate(BaseModel): + username: str + server_ip: str + item_data: Dict[str, Any] + source: Dict[str, Any] # например {"type":"case","case_id": "...", "case_name": "...", "rarity": "..."} + created_at: datetime = Field(default_factory=datetime.utcnow) + +class InventoryItem(BaseModel): + id: str + username: str + server_ip: str + item_data: Dict[str, Any] + source: Dict[str, Any] + status: str = "stored" # stored | withdrawing | delivered | failed + created_at: datetime + delivered_at: Optional[datetime] = None + withdraw_operation_id: Optional[str] = None + +class InventoryWithdrawRequest(BaseModel): + username: str + item_id: str + server_ip: str diff --git a/app/services/case.py b/app/services/case.py index dc65d76..9e7660b 100644 --- a/app/services/case.py +++ b/app/services/case.py @@ -2,7 +2,6 @@ import uuid import random from datetime import datetime from fastapi import HTTPException - from app.db.database import db, users_collection from app.services.coins import CoinsService from app.models.case import CaseCreate, CaseUpdate @@ -10,7 +9,8 @@ from app.models.case import CaseCreate, CaseUpdate cases_collection = db.cases case_openings_collection = db.case_openings game_servers_collection = db.game_servers -marketplace_operations = db.marketplace_operations # уже есть в Marketplace.py :contentReference[oaicite:1]{index=1} +marketplace_operations = db.marketplace_operations +player_inventory_collection = db.player_inventory class CaseService: @@ -33,7 +33,7 @@ class CaseService: "name": case_data.name, "description": case_data.description, "price": case_data.price, - "server_ids": case_data.server_ids or ["*"], + "server_ips": case_data.server_ips or ["*"], "image_url": case_data.image_url, # 🔹 сохраняем картинку "items": items, "created_at": datetime.utcnow() @@ -50,7 +50,7 @@ class CaseService: "name": c["name"], "description": c.get("description"), "price": c["price"], - "server_ids": c.get("server_ids", ["*"]), + "server_ips": c.get("server_ips", ["*"]), "image_url": c.get("image_url"), # 🔹 отдаем картинку "items_count": len(c.get("items", [])) } @@ -76,8 +76,8 @@ class CaseService: update["description"] = data.description if data.price is not None: update["price"] = data.price - if data.server_ids is not None: - update["server_ids"] = data.server_ids + if data.server_ips is not None: + update["server_ips"] = data.server_ips if data.image_url is not None: update["image_url"] = data.image_url # 🔹 обновляем картинку if data.items is not None: @@ -100,7 +100,7 @@ class CaseService: raise HTTPException(status_code=404, detail="Кейс не найден") return {"status": "success"} - async def open_case(self, username: str, case_id: str, server_id: str): + async def open_case(self, username: str, case_id: str, server_ip: str): # 1. Пользователь user = await users_collection.find_one({"username": username}) if not user: @@ -114,17 +114,16 @@ class CaseService: items = case.get("items", []) if not items: raise HTTPException(status_code=400, detail="В кейсе нет предметов") + + allowed = case.get("server_ips") or ["*"] + if "*" not in allowed and server_ip not in allowed: + raise HTTPException(status_code=403, detail="Этот кейс не может быть открыт на этом сервере") # 3. Сервер - server = await game_servers_collection.find_one({"id": server_id}) + server = await game_servers_collection.find_one({"ip": server_ip}) if not server: raise HTTPException(status_code=404, detail="Сервер не найден") - # Проверяем, доступен ли кейс на этом сервере (логика как у пакостей) :contentReference[oaicite:2]{index=2} - server_ids = case.get("server_ids", ["*"]) - if server_ids and "*" not in server_ids and server_id not in server_ids: - raise HTTPException(status_code=400, detail="Кейс недоступен на выбранном сервере") - # 4. Проверяем баланс user_balance = await self.coins_service.get_balance(username) price = case["price"] @@ -166,20 +165,23 @@ class CaseService: "meta": (chosen_item.get("meta") or {}) } - operation = { - "id": operation_id, - "type": "case_reward", - "player_name": username, + inventory_item = { + "id": str(uuid.uuid4()), + "username": username, + "server_ip": server_ip, "item_data": item_data, - "price": 0, # тут можно не использовать, т.к. уже оплачен сам кейс - "server_ip": server.get("ip"), - "status": "pending", + "source": { + "type": "case", + "case_id": case_id, + "case_name": case.get("name"), + }, + "status": "stored", "created_at": datetime.utcnow(), - "case_id": case_id, - "case_name": case["name"], + "delivered_at": None, + "withdraw_operation_id": None, } - await marketplace_operations.insert_one(operation) + await player_inventory_collection.insert_one(inventory_item) # 8. Лог открытия кейса opening_log = { @@ -188,8 +190,7 @@ class CaseService: "user_id": user.get("_id"), "case_id": case_id, "case_name": case["name"], - "server_id": server_id, - "server_ip": server.get("ip"), + "server_ip": server_ip, "reward_item": chosen_item, "price": price, "opened_at": datetime.utcnow() @@ -203,6 +204,6 @@ class CaseService: "status": "success", "message": f"Кейс '{case['name']}' открыт", "reward": chosen_item, - "operation_id": operation_id, + "inventory_item_id": inventory_item["id"], "balance": new_balance } diff --git a/app/services/inventory.py b/app/services/inventory.py new file mode 100644 index 0000000..89e2b93 --- /dev/null +++ b/app/services/inventory.py @@ -0,0 +1,95 @@ +from datetime import datetime +from uuid import uuid4 +from fastapi import HTTPException +from app.db.database import db + +player_inventory_collection = db.player_inventory +marketplace_operations_collection = db.marketplace_operations + +def _serialize_mongodb_doc(doc): + """Преобразует MongoDB документ для JSON сериализации""" + if doc is None: + return None + + # Добавить проверку на список + if isinstance(doc, list): + return [_serialize_mongodb_doc(item) for item in doc] + + 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 InventoryService: + async def list_items(self, username: str, server_ip: str, page: int = 1, limit: int = 20): + q = {"username": username, "server_ip": server_ip, "status": {"$in": ["stored", "withdrawing"]}} + skip = max(page - 1, 0) * limit + + items = await player_inventory_collection.find(q) \ + .sort("created_at", -1) \ + .skip(skip) \ + .limit(limit) \ + .to_list(length=limit) + + serialized_items = _serialize_mongodb_doc(items) + total = await player_inventory_collection.count_documents(q) + return {"items": serialized_items, "page": page, "limit": limit, "total": total} + + async def withdraw_item(self, username: str, item_id: str, server_ip: str): + item = await player_inventory_collection.find_one({"id": item_id}) + if not item: + raise HTTPException(status_code=404, detail="Item not found") + + if item["username"] != username: + raise HTTPException(status_code=403, detail="Not your item") + + if item["server_ip"] != server_ip: + raise HTTPException(status_code=400, detail="Wrong server_ip for this item") + + if item.get("status") != "stored": + raise HTTPException(status_code=400, detail="Item is not available for withdraw") + + # создаём операцию выдачи НА СЕРВЕР (тип оставляем case_reward) + op_id = str(uuid4()) + operation = { + "id": op_id, + "type": "case_reward", + "player_name": username, + "item_data": item["item_data"], + "server_ip": server_ip, + "status": "pending", + "created_at": datetime.utcnow(), + + # важно: связка с инвентарём, чтобы confirm мог отметить delivered + "inventory_item_id": item_id, + "source": item.get("source"), + } + await marketplace_operations_collection.insert_one(operation) + + # помечаем предмет как withdrawing + await player_inventory_collection.update_one( + {"id": item_id}, + {"$set": {"status": "withdrawing", "withdraw_operation_id": op_id}} + ) + + return {"ok": True, "operation_id": op_id, "item_id": item_id} diff --git a/app/services/marketplace.py b/app/services/marketplace.py index 86fc5bc..4507811 100644 --- a/app/services/marketplace.py +++ b/app/services/marketplace.py @@ -112,19 +112,28 @@ class MarketplaceService: } async def confirm_operation(self, operation_id: str, status: str = "success", error: str = None): - """Подтвердить выполнение операции""" - update = { - "status": status - } - + update = {"status": status} if error: update["error"] = error - - result = await marketplace_operations.update_one( - {"id": operation_id}, - {"$set": update} - ) - + + await marketplace_operations.update_one({"id": operation_id}, {"$set": update}) + + # ✅ ДОБАВИТЬ ЭТО: + operation = await marketplace_operations.find_one({"id": operation_id}) + if operation and operation.get("type") == "case_reward" and operation.get("inventory_item_id"): + inv_id = operation["inventory_item_id"] + + if status in ("success", "completed", "done"): + await db.player_inventory.update_one( + {"id": inv_id}, + {"$set": {"status": "delivered", "delivered_at": datetime.utcnow()}} + ) + elif status in ("failed", "error", "cancelled"): + await db.player_inventory.update_one( + {"id": inv_id}, + {"$set": {"status": "stored", "withdraw_operation_id": None}} + ) + return {"status": "success"} async def update_item_details(self, operation_id: str, item_data: dict): diff --git a/main.py b/main.py index d690692..a6a06cc 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ import os from fastapi import FastAPI from fastapi.staticfiles import StaticFiles import httpx -from app.api import admin_daily_quests, news, users, skins, capes, meta, server, store, pranks, marketplace, bonuses, case +from app.api import admin_daily_quests, inventory, news, users, skins, capes, meta, server, store, pranks, marketplace, bonuses, case from fastapi.middleware.cors import CORSMiddleware from app.core.config import CAPES_DIR, CAPES_STORE_DIR, SKINS_DIR @@ -58,9 +58,10 @@ app.include_router(server.router) app.include_router(store.router) app.include_router(pranks.router) app.include_router(marketplace.router) +app.include_router(case.router) +app.include_router(inventory.router) app.include_router(bonuses.router) app.include_router(news.router) -app.include_router(case.router) app.include_router(telegram.router) app.include_router(admin_daily_quests.router)