test cases
This commit is contained in:
38
app/api/case.py
Normal file
38
app/api/case.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from typing import List
|
||||||
|
from app.services.case import CaseService
|
||||||
|
from app.models.case import CaseCreate, CaseUpdate
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/cases", tags=["Cases"])
|
||||||
|
|
||||||
|
def get_case_service():
|
||||||
|
return CaseService()
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def list_cases(case_service: CaseService = Depends(get_case_service)):
|
||||||
|
return await case_service.list_cases()
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def create_case(case_data: CaseCreate, case_service: CaseService = Depends(get_case_service)):
|
||||||
|
return await case_service.create_case(case_data)
|
||||||
|
|
||||||
|
@router.get("/{case_id}")
|
||||||
|
async def get_case(case_id: str, case_service: CaseService = Depends(get_case_service)):
|
||||||
|
return await case_service.get_case(case_id)
|
||||||
|
|
||||||
|
@router.put("/{case_id}")
|
||||||
|
async def update_case(case_id: str, data: CaseUpdate, case_service: CaseService = Depends(get_case_service)):
|
||||||
|
return await case_service.update_case(case_id, data)
|
||||||
|
|
||||||
|
@router.delete("/{case_id}")
|
||||||
|
async def delete_case(case_id: str, case_service: CaseService = Depends(get_case_service)):
|
||||||
|
return await case_service.delete_case(case_id)
|
||||||
|
|
||||||
|
@router.post("/{case_id}/open")
|
||||||
|
async def open_case(
|
||||||
|
case_id: str,
|
||||||
|
username: str,
|
||||||
|
server_id: str,
|
||||||
|
case_service: CaseService = Depends(get_case_service)
|
||||||
|
):
|
||||||
|
return await case_service.open_case(username=username, case_id=case_id, server_id=server_id)
|
||||||
32
app/models/case.py
Normal file
32
app/models/case.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
|
class CaseItemMeta(BaseModel):
|
||||||
|
display_name: Optional[str] = None
|
||||||
|
lore: Optional[List[str]] = None
|
||||||
|
enchants: Optional[Dict[str, int]] = None
|
||||||
|
durability: Optional[int] = None
|
||||||
|
|
||||||
|
class CaseItem(BaseModel):
|
||||||
|
id: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
material: str
|
||||||
|
amount: int = 1
|
||||||
|
weight: int = 1
|
||||||
|
meta: Optional[CaseItemMeta] = None
|
||||||
|
|
||||||
|
class CaseCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
price: int = Field(gt=0)
|
||||||
|
server_ids: List[str] = Field(default_factory=lambda: ["*"])
|
||||||
|
image_url: Optional[str] = None # 🔹 Картинка кейса
|
||||||
|
items: List[CaseItem]
|
||||||
|
|
||||||
|
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
|
||||||
|
image_url: Optional[str] = None # 🔹 Можно менять картинку
|
||||||
|
items: Optional[List[CaseItem]] = None
|
||||||
207
app/services/case.py
Normal file
207
app/services/case.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
|
||||||
|
class CaseService:
|
||||||
|
def __init__(self):
|
||||||
|
self.coins_service = CoinsService()
|
||||||
|
|
||||||
|
async def create_case(self, case_data: CaseCreate):
|
||||||
|
case_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Генерим id для предметов, если не заданы
|
||||||
|
items = []
|
||||||
|
for item in case_data.items:
|
||||||
|
item_dict = item.dict()
|
||||||
|
if not item_dict.get("id"):
|
||||||
|
item_dict["id"] = str(uuid.uuid4())
|
||||||
|
items.append(item_dict)
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
"id": case_id,
|
||||||
|
"name": case_data.name,
|
||||||
|
"description": case_data.description,
|
||||||
|
"price": case_data.price,
|
||||||
|
"server_ids": case_data.server_ids or ["*"],
|
||||||
|
"image_url": case_data.image_url, # 🔹 сохраняем картинку
|
||||||
|
"items": items,
|
||||||
|
"created_at": datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
await cases_collection.insert_one(doc)
|
||||||
|
return {"status": "success", "id": case_id}
|
||||||
|
|
||||||
|
async def list_cases(self):
|
||||||
|
cases = await cases_collection.find().to_list(1000)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": c["id"],
|
||||||
|
"name": c["name"],
|
||||||
|
"description": c.get("description"),
|
||||||
|
"price": c["price"],
|
||||||
|
"server_ids": c.get("server_ids", ["*"]),
|
||||||
|
"image_url": c.get("image_url"), # 🔹 отдаем картинку
|
||||||
|
"items_count": len(c.get("items", []))
|
||||||
|
}
|
||||||
|
for c in cases
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_case(self, case_id: str):
|
||||||
|
case = await cases_collection.find_one({"id": case_id})
|
||||||
|
if not case:
|
||||||
|
raise HTTPException(status_code=404, detail="Кейс не найден")
|
||||||
|
return case
|
||||||
|
|
||||||
|
async def update_case(self, case_id: str, data: CaseUpdate):
|
||||||
|
case = await cases_collection.find_one({"id": case_id})
|
||||||
|
if not case:
|
||||||
|
raise HTTPException(status_code=404, detail="Кейс не найден")
|
||||||
|
|
||||||
|
update = {}
|
||||||
|
if data.name is not None:
|
||||||
|
update["name"] = data.name
|
||||||
|
if data.description is not None:
|
||||||
|
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.image_url is not None:
|
||||||
|
update["image_url"] = data.image_url # 🔹 обновляем картинку
|
||||||
|
if data.items is not None:
|
||||||
|
items = []
|
||||||
|
for item in data.items:
|
||||||
|
item_dict = item.dict()
|
||||||
|
if not item_dict.get("id"):
|
||||||
|
item_dict["id"] = str(uuid.uuid4())
|
||||||
|
items.append(item_dict)
|
||||||
|
update["items"] = items
|
||||||
|
|
||||||
|
if update:
|
||||||
|
await cases_collection.update_one({"id": case_id}, {"$set": update})
|
||||||
|
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
async def delete_case(self, case_id: str):
|
||||||
|
result = await cases_collection.delete_one({"id": case_id})
|
||||||
|
if result.deleted_count == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Кейс не найден")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
async def open_case(self, username: str, case_id: str, server_id: str):
|
||||||
|
# 1. Пользователь
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
# 2. Кейс
|
||||||
|
case = await cases_collection.find_one({"id": case_id})
|
||||||
|
if not case:
|
||||||
|
raise HTTPException(status_code=404, detail="Кейс не найден")
|
||||||
|
|
||||||
|
items = case.get("items", [])
|
||||||
|
if not items:
|
||||||
|
raise HTTPException(status_code=400, detail="В кейсе нет предметов")
|
||||||
|
|
||||||
|
# 3. Сервер
|
||||||
|
server = await game_servers_collection.find_one({"id": server_id})
|
||||||
|
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"]
|
||||||
|
|
||||||
|
if user_balance < price:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Недостаточно монет. Требуется: {price}, имеется: {user_balance}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Выбираем предмет по весам (шансам)
|
||||||
|
weights = [max(0, i.get("weight", 1)) for i in items]
|
||||||
|
total_weight = sum(weights)
|
||||||
|
if total_weight <= 0:
|
||||||
|
raise HTTPException(status_code=500, detail="Неверно настроены шансы предметов (вес ≤ 0)")
|
||||||
|
|
||||||
|
rnd = random.uniform(0, total_weight)
|
||||||
|
current = 0
|
||||||
|
chosen_item = None
|
||||||
|
for item, w in zip(items, weights):
|
||||||
|
current += w
|
||||||
|
if rnd <= current:
|
||||||
|
chosen_item = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if not chosen_item:
|
||||||
|
# на всякий случай, но по логике не должно случиться
|
||||||
|
chosen_item = items[-1]
|
||||||
|
|
||||||
|
# 6. Списываем монеты
|
||||||
|
await self.coins_service.decrease_balance(username, price)
|
||||||
|
|
||||||
|
# 7. Создаём операцию для сервера — как в Marketplace, только другой type :contentReference[oaicite:3]{index=3}
|
||||||
|
operation_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
item_data = {
|
||||||
|
"material": chosen_item["material"],
|
||||||
|
"amount": chosen_item.get("amount", 1),
|
||||||
|
"meta": (chosen_item.get("meta") or {})
|
||||||
|
}
|
||||||
|
|
||||||
|
operation = {
|
||||||
|
"id": operation_id,
|
||||||
|
"type": "case_reward",
|
||||||
|
"player_name": username,
|
||||||
|
"item_data": item_data,
|
||||||
|
"price": 0, # тут можно не использовать, т.к. уже оплачен сам кейс
|
||||||
|
"server_ip": server.get("ip"),
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": datetime.utcnow(),
|
||||||
|
"case_id": case_id,
|
||||||
|
"case_name": case["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
await marketplace_operations.insert_one(operation)
|
||||||
|
|
||||||
|
# 8. Лог открытия кейса
|
||||||
|
opening_log = {
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"username": username,
|
||||||
|
"user_id": user.get("_id"),
|
||||||
|
"case_id": case_id,
|
||||||
|
"case_name": case["name"],
|
||||||
|
"server_id": server_id,
|
||||||
|
"server_ip": server.get("ip"),
|
||||||
|
"reward_item": chosen_item,
|
||||||
|
"price": price,
|
||||||
|
"opened_at": datetime.utcnow()
|
||||||
|
}
|
||||||
|
await case_openings_collection.insert_one(opening_log)
|
||||||
|
|
||||||
|
# 9. Можно вернуть новый баланс
|
||||||
|
new_balance = await self.coins_service.get_balance(username)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Кейс '{case['name']}' открыт",
|
||||||
|
"reward": chosen_item,
|
||||||
|
"operation_id": operation_id,
|
||||||
|
"balance": new_balance
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user