test player_inventory

This commit is contained in:
2025-12-16 00:15:29 +05:00
parent 80a9fbe148
commit bb74dbbba7
8 changed files with 204 additions and 43 deletions

View File

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

29
app/api/inventory.py Normal file
View File

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

View File

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

26
app/models/inventory.py Normal file
View File

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

View File

@ -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:
@ -115,16 +115,15 @@ class CaseService:
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",
"created_at": datetime.utcnow(),
"source": {
"type": "case",
"case_id": case_id,
"case_name": case["name"],
"case_name": case.get("name"),
},
"status": "stored",
"created_at": datetime.utcnow(),
"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
}

95
app/services/inventory.py Normal file
View File

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

View File

@ -112,17 +112,26 @@ 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"}

View File

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