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(
|
async def open_case(
|
||||||
case_id: str,
|
case_id: str,
|
||||||
username: str,
|
username: str,
|
||||||
server_id: str,
|
server_ip: str,
|
||||||
case_service: CaseService = Depends(get_case_service)
|
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
|
name: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
price: int = Field(gt=0)
|
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 # 🔹 Картинка кейса
|
image_url: Optional[str] = None # 🔹 Картинка кейса
|
||||||
items: List[CaseItem]
|
items: List[CaseItem]
|
||||||
|
|
||||||
@ -27,6 +27,6 @@ class CaseUpdate(BaseModel):
|
|||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
price: Optional[int] = Field(default=None, gt=0)
|
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 # 🔹 Можно менять картинку
|
image_url: Optional[str] = None # 🔹 Можно менять картинку
|
||||||
items: Optional[List[CaseItem]] = 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
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from app.db.database import db, users_collection
|
from app.db.database import db, users_collection
|
||||||
from app.services.coins import CoinsService
|
from app.services.coins import CoinsService
|
||||||
from app.models.case import CaseCreate, CaseUpdate
|
from app.models.case import CaseCreate, CaseUpdate
|
||||||
@ -10,7 +9,8 @@ from app.models.case import CaseCreate, CaseUpdate
|
|||||||
cases_collection = db.cases
|
cases_collection = db.cases
|
||||||
case_openings_collection = db.case_openings
|
case_openings_collection = db.case_openings
|
||||||
game_servers_collection = db.game_servers
|
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:
|
class CaseService:
|
||||||
@ -33,7 +33,7 @@ class CaseService:
|
|||||||
"name": case_data.name,
|
"name": case_data.name,
|
||||||
"description": case_data.description,
|
"description": case_data.description,
|
||||||
"price": case_data.price,
|
"price": case_data.price,
|
||||||
"server_ids": case_data.server_ids or ["*"],
|
"server_ips": case_data.server_ips or ["*"],
|
||||||
"image_url": case_data.image_url, # 🔹 сохраняем картинку
|
"image_url": case_data.image_url, # 🔹 сохраняем картинку
|
||||||
"items": items,
|
"items": items,
|
||||||
"created_at": datetime.utcnow()
|
"created_at": datetime.utcnow()
|
||||||
@ -50,7 +50,7 @@ class CaseService:
|
|||||||
"name": c["name"],
|
"name": c["name"],
|
||||||
"description": c.get("description"),
|
"description": c.get("description"),
|
||||||
"price": c["price"],
|
"price": c["price"],
|
||||||
"server_ids": c.get("server_ids", ["*"]),
|
"server_ips": c.get("server_ips", ["*"]),
|
||||||
"image_url": c.get("image_url"), # 🔹 отдаем картинку
|
"image_url": c.get("image_url"), # 🔹 отдаем картинку
|
||||||
"items_count": len(c.get("items", []))
|
"items_count": len(c.get("items", []))
|
||||||
}
|
}
|
||||||
@ -76,8 +76,8 @@ class CaseService:
|
|||||||
update["description"] = data.description
|
update["description"] = data.description
|
||||||
if data.price is not None:
|
if data.price is not None:
|
||||||
update["price"] = data.price
|
update["price"] = data.price
|
||||||
if data.server_ids is not None:
|
if data.server_ips is not None:
|
||||||
update["server_ids"] = data.server_ids
|
update["server_ips"] = data.server_ips
|
||||||
if data.image_url is not None:
|
if data.image_url is not None:
|
||||||
update["image_url"] = data.image_url # 🔹 обновляем картинку
|
update["image_url"] = data.image_url # 🔹 обновляем картинку
|
||||||
if data.items is not None:
|
if data.items is not None:
|
||||||
@ -100,7 +100,7 @@ class CaseService:
|
|||||||
raise HTTPException(status_code=404, detail="Кейс не найден")
|
raise HTTPException(status_code=404, detail="Кейс не найден")
|
||||||
return {"status": "success"}
|
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. Пользователь
|
# 1. Пользователь
|
||||||
user = await users_collection.find_one({"username": username})
|
user = await users_collection.find_one({"username": username})
|
||||||
if not user:
|
if not user:
|
||||||
@ -115,16 +115,15 @@ class CaseService:
|
|||||||
if not items:
|
if not items:
|
||||||
raise HTTPException(status_code=400, detail="В кейсе нет предметов")
|
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. Сервер
|
# 3. Сервер
|
||||||
server = await game_servers_collection.find_one({"id": server_id})
|
server = await game_servers_collection.find_one({"ip": server_ip})
|
||||||
if not server:
|
if not server:
|
||||||
raise HTTPException(status_code=404, detail="Сервер не найден")
|
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. Проверяем баланс
|
# 4. Проверяем баланс
|
||||||
user_balance = await self.coins_service.get_balance(username)
|
user_balance = await self.coins_service.get_balance(username)
|
||||||
price = case["price"]
|
price = case["price"]
|
||||||
@ -166,20 +165,23 @@ class CaseService:
|
|||||||
"meta": (chosen_item.get("meta") or {})
|
"meta": (chosen_item.get("meta") or {})
|
||||||
}
|
}
|
||||||
|
|
||||||
operation = {
|
inventory_item = {
|
||||||
"id": operation_id,
|
"id": str(uuid.uuid4()),
|
||||||
"type": "case_reward",
|
"username": username,
|
||||||
"player_name": username,
|
"server_ip": server_ip,
|
||||||
"item_data": item_data,
|
"item_data": item_data,
|
||||||
"price": 0, # тут можно не использовать, т.к. уже оплачен сам кейс
|
"source": {
|
||||||
"server_ip": server.get("ip"),
|
"type": "case",
|
||||||
"status": "pending",
|
"case_id": case_id,
|
||||||
|
"case_name": case.get("name"),
|
||||||
|
},
|
||||||
|
"status": "stored",
|
||||||
"created_at": datetime.utcnow(),
|
"created_at": datetime.utcnow(),
|
||||||
"case_id": case_id,
|
"delivered_at": None,
|
||||||
"case_name": case["name"],
|
"withdraw_operation_id": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
await marketplace_operations.insert_one(operation)
|
await player_inventory_collection.insert_one(inventory_item)
|
||||||
|
|
||||||
# 8. Лог открытия кейса
|
# 8. Лог открытия кейса
|
||||||
opening_log = {
|
opening_log = {
|
||||||
@ -188,8 +190,7 @@ class CaseService:
|
|||||||
"user_id": user.get("_id"),
|
"user_id": user.get("_id"),
|
||||||
"case_id": case_id,
|
"case_id": case_id,
|
||||||
"case_name": case["name"],
|
"case_name": case["name"],
|
||||||
"server_id": server_id,
|
"server_ip": server_ip,
|
||||||
"server_ip": server.get("ip"),
|
|
||||||
"reward_item": chosen_item,
|
"reward_item": chosen_item,
|
||||||
"price": price,
|
"price": price,
|
||||||
"opened_at": datetime.utcnow()
|
"opened_at": datetime.utcnow()
|
||||||
@ -203,6 +204,6 @@ class CaseService:
|
|||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Кейс '{case['name']}' открыт",
|
"message": f"Кейс '{case['name']}' открыт",
|
||||||
"reward": chosen_item,
|
"reward": chosen_item,
|
||||||
"operation_id": operation_id,
|
"inventory_item_id": inventory_item["id"],
|
||||||
"balance": new_balance
|
"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,18 +112,27 @@ class MarketplaceService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def confirm_operation(self, operation_id: str, status: str = "success", error: str = None):
|
async def confirm_operation(self, operation_id: str, status: str = "success", error: str = None):
|
||||||
"""Подтвердить выполнение операции"""
|
update = {"status": status}
|
||||||
update = {
|
|
||||||
"status": status
|
|
||||||
}
|
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
update["error"] = error
|
update["error"] = error
|
||||||
|
|
||||||
result = await marketplace_operations.update_one(
|
await marketplace_operations.update_one({"id": operation_id}, {"$set": update})
|
||||||
{"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"}
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|||||||
5
main.py
5
main.py
@ -3,7 +3,7 @@ import os
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
import httpx
|
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 fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.core.config import CAPES_DIR, CAPES_STORE_DIR, SKINS_DIR
|
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(store.router)
|
||||||
app.include_router(pranks.router)
|
app.include_router(pranks.router)
|
||||||
app.include_router(marketplace.router)
|
app.include_router(marketplace.router)
|
||||||
|
app.include_router(case.router)
|
||||||
|
app.include_router(inventory.router)
|
||||||
app.include_router(bonuses.router)
|
app.include_router(bonuses.router)
|
||||||
app.include_router(news.router)
|
app.include_router(news.router)
|
||||||
app.include_router(case.router)
|
|
||||||
app.include_router(telegram.router)
|
app.include_router(telegram.router)
|
||||||
app.include_router(admin_daily_quests.router)
|
app.include_router(admin_daily_quests.router)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user