add: store cape

This commit is contained in:
2025-07-18 03:39:21 +05:00
parent ff65e4a333
commit d52d4dbf75
5 changed files with 350 additions and 2 deletions

56
app/api/store.py Normal file
View File

@ -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)

View File

@ -1,4 +1,25 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class CapeUpdate(BaseModel): class CapeUpdate(BaseModel):
cape_url: str 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

270
app/services/store_cape.py Normal file
View File

@ -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')}' активирован"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,6 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles 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 from fastapi.middleware.cors import CORSMiddleware
app = FastAPI() app = FastAPI()
@ -10,13 +10,14 @@ app.include_router(users.router)
app.include_router(skins.router) app.include_router(skins.router)
app.include_router(capes.router) app.include_router(capes.router)
app.include_router(server.router) app.include_router(server.router)
app.include_router(store.router)
# Монтируем статику # Монтируем статику
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins") app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes") 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 и т.д. # CORS, middleware и т.д.
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],