diff --git a/app/api/store.py b/app/api/store.py new file mode 100644 index 0000000..b3b73d0 --- /dev/null +++ b/app/api/store.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException +from app.services.store_cape import StoreCapeService +from app.models.cape import CapeStoreUpdate, CapePurchase +from typing import Optional + +router = APIRouter( + prefix="/store", + tags=["Store"] +) + +store_cape_service = StoreCapeService() + +@router.get("/capes") +async def get_all_capes(): + """Получение списка всех плащей в магазине""" + return await store_cape_service.get_all_capes() + +@router.get("/capes/{cape_id}") +async def get_cape_by_id(cape_id: str): + """Получение плаща по ID""" + return await store_cape_service.get_cape_by_id(cape_id) + +@router.post("/capes") +async def add_cape( + name: str = Form(...), + description: str = Form(...), + price: int = Form(...), + cape_file: UploadFile = File(...) +): + """Добавление нового плаща в магазин""" + return await store_cape_service.add_cape(name, description, price, cape_file) + +@router.put("/capes/{cape_id}") +async def update_cape(cape_id: str, update_data: CapeStoreUpdate): + """Обновление информации о плаще""" + return await store_cape_service.update_cape(cape_id, update_data) + +@router.delete("/capes/{cape_id}") +async def delete_cape(cape_id: str): + """Удаление плаща из магазина""" + return await store_cape_service.delete_cape(cape_id) + +@router.post("/purchase/cape") +async def purchase_cape(username: str, cape_id: str): + """Покупка плаща пользователем""" + return await store_cape_service.purchase_cape(username, cape_id) + +@router.get("/user/{username}/capes") +async def get_user_purchased_capes(username: str): + """Получение всех приобретенных плащей пользователя""" + return await store_cape_service.get_user_purchased_capes(username) + +@router.post("/user/{username}/capes/activate/{cape_id}") +async def activate_purchased_cape(username: str, cape_id: str): + """Активация приобретенного плаща""" + return await store_cape_service.activate_purchased_cape(username, cape_id) diff --git a/app/models/cape.py b/app/models/cape.py index 8980cad..1a14799 100644 --- a/app/models/cape.py +++ b/app/models/cape.py @@ -1,4 +1,25 @@ from pydantic import BaseModel +from typing import Optional class CapeUpdate(BaseModel): cape_url: str + +class CapeStore(BaseModel): + id: str + name: str + description: str + price: int + file_name: str + +class CapeStoreCreate(BaseModel): + name: str + description: str + price: int + +class CapeStoreUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[int] = None + +class CapePurchase(BaseModel): + cape_id: str diff --git a/app/services/store_cape.py b/app/services/store_cape.py new file mode 100644 index 0000000..e028503 --- /dev/null +++ b/app/services/store_cape.py @@ -0,0 +1,270 @@ +from fastapi import HTTPException, UploadFile +from app.db.database import users_collection +from app.core.config import FILES_URL +from datetime import datetime +import uuid +from pathlib import Path +import os +import shutil +from app.models.cape import CapeStore, CapeStoreUpdate + +# Создаем коллекцию для плащей в БД +from app.db.database import db +store_capes_collection = db.store_capes + +class StoreCapeService: + async def add_cape(self, name: str, description: str, price: int, cape_file: UploadFile): + """Добавление нового плаща в магазин""" + # Проверка типа файла + if not cape_file.content_type.startswith('image/'): + raise HTTPException(status_code=400, detail="Файл должен быть изображением") + + # Определяем расширение + ext = None + if cape_file.content_type == "image/png": + ext = "png" + elif cape_file.content_type == "image/gif": + ext = "gif" + else: + raise HTTPException(status_code=400, detail="Поддерживаются только PNG и GIF плащи") + + # Проверка размера файла (максимум 2MB) + max_size = 2 * 1024 * 1024 # 2MB + contents = await cape_file.read() + if len(contents) > max_size: + raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)") + + # Создаем папку для плащей магазина, если ее нет + cape_dir = Path("app/static/capes_store") + cape_dir.mkdir(parents=True, exist_ok=True) + + # Генерируем ID и имя файла + cape_id = str(uuid.uuid4()) + cape_filename = f"store_cape_{cape_id}.{ext}" + cape_path = cape_dir / cape_filename + + # Сохраняем файл + with open(cape_path, "wb") as f: + f.write(contents) + + # Создаем запись в БД + cape_data = { + "id": cape_id, + "name": name, + "description": description, + "price": price, + "file_name": cape_filename, + "created_at": datetime.utcnow() + } + + await store_capes_collection.insert_one(cape_data) + + return {"id": cape_id, "status": "success"} + + async def get_all_capes(self): + """Получение всех плащей из магазина""" + capes = await store_capes_collection.find().to_list(1000) + + result = [] + for cape in capes: + result.append({ + "id": cape["id"], + "name": cape["name"], + "description": cape["description"], + "price": cape["price"], + "image_url": f"{FILES_URL}/capes_store/{cape['file_name']}" + }) + + return result + + async def get_cape_by_id(self, cape_id: str): + """Получение плаща по ID""" + cape = await store_capes_collection.find_one({"id": cape_id}) + if not cape: + raise HTTPException(status_code=404, detail="Плащ не найден") + + return { + "id": cape["id"], + "name": cape["name"], + "description": cape["description"], + "price": cape["price"], + "image_url": f"{FILES_URL}/capes_store/{cape['file_name']}" + } + + async def update_cape(self, cape_id: str, update_data: CapeStoreUpdate): + """Обновление информации о плаще""" + cape = await store_capes_collection.find_one({"id": cape_id}) + if not cape: + raise HTTPException(status_code=404, detail="Плащ не найден") + + # Готовим данные для обновления + update = {} + if update_data.name: + update["name"] = update_data.name + if update_data.description: + update["description"] = update_data.description + if update_data.price is not None: + update["price"] = update_data.price + + if update: + result = await store_capes_collection.update_one( + {"id": cape_id}, + {"$set": update} + ) + + if result.modified_count == 0: + raise HTTPException(status_code=500, detail="Ошибка при обновлении") + + return {"status": "success"} + + async def delete_cape(self, cape_id: str): + """Удаление плаща из магазина""" + cape = await store_capes_collection.find_one({"id": cape_id}) + if not cape: + raise HTTPException(status_code=404, detail="Плащ не найден") + + # Удаляем файл + cape_path = Path(f"app/static/capes_store/{cape['file_name']}") + if cape_path.exists(): + try: + cape_path.unlink() + except Exception as e: + print(f"Ошибка при удалении файла: {e}") + + # Удаляем из БД + result = await store_capes_collection.delete_one({"id": cape_id}) + if result.deleted_count == 0: + raise HTTPException(status_code=500, detail="Ошибка при удалении из БД") + + return {"status": "success"} + + async def purchase_cape(self, username: str, cape_id: str): + """Покупка плаща пользователем""" + # Находим пользователя + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Находим плащ + cape = await store_capes_collection.find_one({"id": cape_id}) + if not cape: + raise HTTPException(status_code=404, detail="Плащ не найден") + + # Проверяем достаточно ли монет + user_coins = user.get("coins", 0) + if user_coins < cape["price"]: + raise HTTPException(status_code=400, + detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}") + + # Копируем плащ из хранилища магазина в персональную папку пользователя + cape_store_path = Path(f"app/static/capes_store/{cape['file_name']}") + + # Создаем папку для плащей пользователя + cape_dir = Path("app/static/capes") + cape_dir.mkdir(parents=True, exist_ok=True) + + # Генерируем имя файла для персонального плаща + filename_parts = cape['file_name'].split('.') + ext = filename_parts[-1] + cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}" + cape_path = cape_dir / cape_filename + + # Копируем файл + try: + shutil.copy(cape_store_path, cape_path) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Ошибка при копировании файла: {e}") + + # Обновляем данные пользователя + # 1. Списываем монеты + # 2. Устанавливаем новый плащ + # 3. Добавляем плащ в список приобретенных + result = await users_collection.update_one( + {"username": username}, + {"$set": { + "coins": user_coins - cape["price"], + "cloak_url": f"{FILES_URL}/capes/{cape_filename}" + }, + "$push": { + "purchased_capes": { + "cape_id": cape_id, + "cape_name": cape["name"], + "file_name": cape_filename, + "purchased_at": datetime.utcnow() + } + }} + ) + + if result.modified_count == 0: + # Если обновление не удалось, удаляем файл плаща + if os.path.exists(cape_path): + os.remove(cape_path) + raise HTTPException(status_code=500, detail="Ошибка при обновлении данных пользователя") + + # Логируем покупку в БД + purchase_data = { + "username": username, + "user_id": user["_id"], + "cape_id": cape_id, + "cape_name": cape["name"], + "price": cape["price"], + "purchase_date": datetime.utcnow() + } + + from app.db.database import db + await db.purchases.insert_one(purchase_data) + + return { + "status": "success", + "message": f"Плащ '{cape['name']}' успешно приобретен", + "remaining_coins": user_coins - cape["price"] + } + + async def get_user_purchased_capes(self, username: str): + """Получение всех плащей, приобретенных пользователем""" + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + purchased_capes = user.get("purchased_capes", []) + result = [] + + for cape in purchased_capes: + result.append({ + "cape_id": cape.get("cape_id"), + "cape_name": cape.get("cape_name"), + "image_url": f"{FILES_URL}/capes/{cape.get('file_name')}", + "purchased_at": cape.get("purchased_at"), + "is_active": user.get("cloak_url") == f"{FILES_URL}/capes/{cape.get('file_name')}" + }) + + return result + + async def activate_purchased_cape(self, username: str, cape_id: str): + """Активация приобретенного плаща""" + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Проверяем, что плащ был приобретен + purchased_capes = user.get("purchased_capes", []) + selected_cape = None + + for cape in purchased_capes: + if cape.get("cape_id") == cape_id: + selected_cape = cape + break + + if not selected_cape: + raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных") + + # Устанавливаем выбранный плащ + await users_collection.update_one( + {"username": username}, + {"$set": {"cloak_url": f"{FILES_URL}/capes/{selected_cape.get('file_name')}"}} + ) + + return { + "status": "success", + "message": f"Плащ '{selected_cape.get('cape_name')}' активирован" + } diff --git a/app/static/capes_store/store_cape_402f1edf-be9f-44b1-a9c1-5f94c02dd4fc.png b/app/static/capes_store/store_cape_402f1edf-be9f-44b1-a9c1-5f94c02dd4fc.png new file mode 100644 index 0000000..452bff4 Binary files /dev/null and b/app/static/capes_store/store_cape_402f1edf-be9f-44b1-a9c1-5f94c02dd4fc.png differ diff --git a/main.py b/main.py index 3f5a7fd..3988aae 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from app.api import users, skins, capes, meta, server +from app.api import users, skins, capes, meta, server, store from fastapi.middleware.cors import CORSMiddleware app = FastAPI() @@ -10,13 +10,14 @@ app.include_router(users.router) app.include_router(skins.router) app.include_router(capes.router) app.include_router(server.router) +app.include_router(store.router) # Монтируем статику app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins") app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes") +app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store") # CORS, middleware и т.д. - app.add_middleware( CORSMiddleware, allow_origins=["*"],