test player_inventory
This commit is contained in:
@ -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
29
app/api/inventory.py
Normal 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,
|
||||
)
|
||||
@ -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
26
app/models/inventory.py
Normal 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
|
||||
@ -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
95
app/services/inventory.py
Normal 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}
|
||||
@ -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"}
|
||||
|
||||
5
main.py
5
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user