diff --git a/app/api/case.py b/app/api/case.py new file mode 100644 index 0000000..9196035 --- /dev/null +++ b/app/api/case.py @@ -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) diff --git a/app/models/case.py b/app/models/case.py new file mode 100644 index 0000000..d7a4b27 --- /dev/null +++ b/app/models/case.py @@ -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 \ No newline at end of file diff --git a/app/services/case.py b/app/services/case.py new file mode 100644 index 0000000..c041936 --- /dev/null +++ b/app/services/case.py @@ -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 + }