Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
fa9611cc99 |
41
app/api/bonuses.py
Normal file
41
app/api/bonuses.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from fastapi import APIRouter, Query, Body
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from app.models.bonus import PurchaseBonus
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/api/bonuses",
|
||||||
|
tags=["Bonuses"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/effects")
|
||||||
|
async def get_user_effects(username: str):
|
||||||
|
"""Получить активные эффекты пользователя для плагина"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().get_user_active_effects(username)
|
||||||
|
|
||||||
|
@router.get("/types")
|
||||||
|
async def get_bonus_types():
|
||||||
|
"""Получить доступные типы бонусов"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().list_available_bonuses()
|
||||||
|
|
||||||
|
@router.get("/user/{username}")
|
||||||
|
async def get_user_bonuses(username: str):
|
||||||
|
"""Получить активные бонусы пользователя"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().get_user_bonuses(username)
|
||||||
|
|
||||||
|
@router.post("/purchase")
|
||||||
|
async def purchase_bonus(purchase_bonus: PurchaseBonus):
|
||||||
|
"""Купить бонус"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().purchase_bonus(purchase_bonus.username, purchase_bonus.bonus_type_id)
|
||||||
|
|
||||||
|
@router.post("/upgrade")
|
||||||
|
async def upgrade_user_bonus(username: str = Body(...), bonus_id: str = Body(...)):
|
||||||
|
"""Улучшить существующий бонус"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().upgrade_bonus(username, bonus_id)
|
||||||
|
|
48
app/models/bonus.py
Normal file
48
app/models/bonus.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class PurchaseBonus(BaseModel):
|
||||||
|
username: str
|
||||||
|
bonus_type_id: str
|
||||||
|
|
||||||
|
class BonusEffect(BaseModel):
|
||||||
|
effect_type: str
|
||||||
|
effect_value: float
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class BonusType(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
effect_type: str # "experience", "strength", "speed", etc.
|
||||||
|
base_effect_value: float # Базовое значение эффекта (например, 1.0 для +100%)
|
||||||
|
effect_increment: float # Прирост эффекта за уровень (например, 0.1 для +10%)
|
||||||
|
price: int # Базовая цена
|
||||||
|
upgrade_price: int # Цена улучшения за уровень
|
||||||
|
duration: int # Длительность в секундах (0 для бесконечных)
|
||||||
|
max_level: int = 0 # 0 = без ограничения уровней
|
||||||
|
|
||||||
|
class UserTypeBonus(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
effect_type: str
|
||||||
|
effect_value: float
|
||||||
|
level: int
|
||||||
|
purchased_at: datetime
|
||||||
|
can_upgrade: bool
|
||||||
|
upgrade_price: int
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
is_active: bool = True
|
||||||
|
is_permanent: bool
|
||||||
|
|
||||||
|
class UserBonus(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
username: str
|
||||||
|
bonus_type_id: str
|
||||||
|
level: int
|
||||||
|
purchased_at: datetime
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
is_active: bool
|
226
app/services/bonus.py
Normal file
226
app/services/bonus.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from app.db.database import db
|
||||||
|
from app.services.coins import CoinsService
|
||||||
|
from app.models.bonus import BonusType
|
||||||
|
|
||||||
|
# Коллекции для бонусов
|
||||||
|
bonus_types_collection = db.bonus_types
|
||||||
|
user_bonuses_collection = db.user_bonuses
|
||||||
|
|
||||||
|
class BonusService:
|
||||||
|
async def get_user_active_effects(self, username: str):
|
||||||
|
"""Получить активные эффекты пользователя для плагина"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
return {"effects": []}
|
||||||
|
|
||||||
|
# Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих
|
||||||
|
active_bonuses = await user_bonuses_collection.find({
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"is_active": True,
|
||||||
|
}).to_list(50)
|
||||||
|
|
||||||
|
effects = []
|
||||||
|
for bonus in active_bonuses:
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]})
|
||||||
|
if bonus_type:
|
||||||
|
# Рассчитываем итоговое значение эффекта с учетом уровня
|
||||||
|
level = bonus.get("level", 1)
|
||||||
|
effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"]
|
||||||
|
|
||||||
|
effect = {
|
||||||
|
"effect_type": bonus_type["effect_type"],
|
||||||
|
"effect_value": effect_value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Для временных бонусов добавляем срок
|
||||||
|
if bonus.get("expires_at"):
|
||||||
|
effect["expires_at"] = bonus["expires_at"].isoformat()
|
||||||
|
|
||||||
|
effects.append(effect)
|
||||||
|
|
||||||
|
return {"effects": effects}
|
||||||
|
|
||||||
|
async def list_available_bonuses(self):
|
||||||
|
"""Получить список доступных типов бонусов"""
|
||||||
|
bonuses = await bonus_types_collection.find().to_list(50)
|
||||||
|
return {"bonuses": [BonusType(**bonus) for bonus in bonuses]}
|
||||||
|
|
||||||
|
async def get_user_bonuses(self, username: str):
|
||||||
|
"""Получить активные бонусы пользователя"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
# Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих
|
||||||
|
active_bonuses = await user_bonuses_collection.find({
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"is_active": True,
|
||||||
|
}).to_list(50)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for bonus in active_bonuses:
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]})
|
||||||
|
if bonus_type:
|
||||||
|
# Рассчитываем итоговое значение эффекта с учетом уровня
|
||||||
|
level = bonus.get("level", 1)
|
||||||
|
effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"]
|
||||||
|
|
||||||
|
bonus_data = {
|
||||||
|
"id": bonus["id"],
|
||||||
|
"name": bonus_type["name"],
|
||||||
|
"description": bonus_type["description"],
|
||||||
|
"effect_type": bonus_type["effect_type"],
|
||||||
|
"effect_value": effect_value,
|
||||||
|
"level": level,
|
||||||
|
"purchased_at": bonus["purchased_at"].isoformat(),
|
||||||
|
"can_upgrade": bonus_type["max_level"] == 0 or level < bonus_type["max_level"],
|
||||||
|
"upgrade_price": bonus_type["upgrade_price"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Для временных бонусов добавляем срок
|
||||||
|
if bonus.get("expires_at"):
|
||||||
|
bonus_data["expires_at"] = bonus["expires_at"].isoformat()
|
||||||
|
bonus_data["time_left"] = (bonus["expires_at"] - datetime.utcnow()).total_seconds()
|
||||||
|
else:
|
||||||
|
bonus_data["is_permanent"] = True
|
||||||
|
|
||||||
|
result.append(bonus_data)
|
||||||
|
|
||||||
|
return {"bonuses": result}
|
||||||
|
|
||||||
|
async def purchase_bonus(self, username: str, bonus_type_id: str):
|
||||||
|
"""Покупка базового бонуса пользователем"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
# Находим пользователя
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
# Находим тип бонуса
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": bonus_type_id})
|
||||||
|
if not bonus_type:
|
||||||
|
raise HTTPException(status_code=404, detail="Бонус не найден")
|
||||||
|
|
||||||
|
# Проверяем, есть ли уже такой бонус у пользователя
|
||||||
|
existing_bonus = await user_bonuses_collection.find_one({
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"bonus_type_id": bonus_type_id,
|
||||||
|
"is_active": True
|
||||||
|
})
|
||||||
|
|
||||||
|
if existing_bonus:
|
||||||
|
raise HTTPException(status_code=400, detail="Этот бонус уже приобретен. Вы можете улучшить его.")
|
||||||
|
|
||||||
|
# Проверяем достаточно ли монет
|
||||||
|
coins_service = CoinsService()
|
||||||
|
user_coins = await coins_service.get_balance(username)
|
||||||
|
|
||||||
|
if user_coins < bonus_type["price"]:
|
||||||
|
raise HTTPException(status_code=400,
|
||||||
|
detail=f"Недостаточно монет. Требуется: {bonus_type['price']}, имеется: {user_coins}")
|
||||||
|
|
||||||
|
# Создаем запись о бонусе для пользователя
|
||||||
|
bonus_id = str(uuid.uuid4())
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
# Если бонус имеет длительность
|
||||||
|
expires_at = None
|
||||||
|
if bonus_type["duration"] > 0:
|
||||||
|
expires_at = now + timedelta(seconds=bonus_type["duration"])
|
||||||
|
|
||||||
|
user_bonus = {
|
||||||
|
"id": bonus_id,
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"username": username,
|
||||||
|
"bonus_type_id": bonus_type_id,
|
||||||
|
"level": 1, # Начальный уровень
|
||||||
|
"purchased_at": now,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
"is_active": True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сохраняем бонус в БД
|
||||||
|
await user_bonuses_collection.insert_one(user_bonus)
|
||||||
|
|
||||||
|
# Списываем монеты
|
||||||
|
await coins_service.decrease_balance(username, bonus_type["price"])
|
||||||
|
|
||||||
|
# Формируем текст сообщения
|
||||||
|
duration_text = "навсегда" if bonus_type["duration"] == 0 else f"на {bonus_type['duration'] // 60} мин."
|
||||||
|
message = f"Бонус '{bonus_type['name']}' успешно приобретен {duration_text}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": message,
|
||||||
|
"remaining_coins": user_coins - bonus_type["price"]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def upgrade_bonus(self, username: str, bonus_id: str):
|
||||||
|
"""Улучшение уже купленного бонуса"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
# Находим пользователя
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
# Находим бонус пользователя
|
||||||
|
user_bonus = await user_bonuses_collection.find_one({
|
||||||
|
"id": bonus_id,
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"is_active": True
|
||||||
|
})
|
||||||
|
|
||||||
|
if not user_bonus:
|
||||||
|
raise HTTPException(status_code=404, detail="Бонус не найден или не принадлежит вам")
|
||||||
|
|
||||||
|
# Находим тип бонуса
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": user_bonus["bonus_type_id"]})
|
||||||
|
if not bonus_type:
|
||||||
|
raise HTTPException(status_code=404, detail="Тип бонуса не найден")
|
||||||
|
|
||||||
|
# Проверяем ограничение на максимальный уровень
|
||||||
|
current_level = user_bonus["level"]
|
||||||
|
if bonus_type["max_level"] > 0 and current_level >= bonus_type["max_level"]:
|
||||||
|
raise HTTPException(status_code=400, detail="Достигнут максимальный уровень бонуса")
|
||||||
|
|
||||||
|
# Рассчитываем стоимость улучшения
|
||||||
|
upgrade_price = bonus_type["upgrade_price"]
|
||||||
|
|
||||||
|
# Проверяем достаточно ли монет
|
||||||
|
coins_service = CoinsService()
|
||||||
|
user_coins = await coins_service.get_balance(username)
|
||||||
|
|
||||||
|
if user_coins < upgrade_price:
|
||||||
|
raise HTTPException(status_code=400,
|
||||||
|
detail=f"Недостаточно монет. Требуется: {upgrade_price}, имеется: {user_coins}")
|
||||||
|
|
||||||
|
# Обновляем уровень бонуса
|
||||||
|
new_level = current_level + 1
|
||||||
|
|
||||||
|
await user_bonuses_collection.update_one(
|
||||||
|
{"id": bonus_id},
|
||||||
|
{"$set": {"level": new_level}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Списываем монеты
|
||||||
|
await coins_service.decrease_balance(username, upgrade_price)
|
||||||
|
|
||||||
|
# Рассчитываем новое значение эффекта
|
||||||
|
new_effect_value = bonus_type["base_effect_value"] + (new_level - 1) * bonus_type["effect_increment"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Бонус '{bonus_type['name']}' улучшен до уровня {new_level}",
|
||||||
|
"new_level": new_level,
|
||||||
|
"effect_value": new_effect_value,
|
||||||
|
"remaining_coins": user_coins - upgrade_price
|
||||||
|
}
|
9
main.py
9
main.py
@ -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, store, pranks, marketplace
|
from app.api import users, skins, capes, meta, server, store, pranks, marketplace, bonuses
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@ -13,11 +13,12 @@ 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(bonuses.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")
|
app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store")
|
||||||
|
|
||||||
# CORS, middleware и т.д.
|
# CORS, middleware и т.д.
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
54
scripts/add_test_bonuses.py
Normal file
54
scripts/add_test_bonuses.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# scripts/add_test_bonuses.py
|
||||||
|
from app.db.database import db
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Коллекция для бонусов
|
||||||
|
bonus_types_collection = db.bonus_types
|
||||||
|
|
||||||
|
# Очищаем существующие записи
|
||||||
|
bonus_types_collection.delete_many({})
|
||||||
|
|
||||||
|
# Добавляем типы бонусов
|
||||||
|
bonus_types = [
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": "Бонус опыта",
|
||||||
|
"description": "Увеличивает получаемый опыт на 100% (+10% за уровень)",
|
||||||
|
"effect_type": "experience",
|
||||||
|
"base_effect_value": 1.0, # +100%
|
||||||
|
"effect_increment": 0.1, # +10% за уровень
|
||||||
|
"price": 100,
|
||||||
|
"upgrade_price": 50,
|
||||||
|
"duration": 0, # Бесконечный
|
||||||
|
"max_level": 0 # Без ограничения уровня
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": "Бонус силы",
|
||||||
|
"description": "Увеличивает силу атаки на 10% (+5% за уровень)",
|
||||||
|
"effect_type": "strength",
|
||||||
|
"base_effect_value": 0.1,
|
||||||
|
"effect_increment": 0.05,
|
||||||
|
"price": 75,
|
||||||
|
"upgrade_price": 40,
|
||||||
|
"duration": 0, # Бесконечный
|
||||||
|
"max_level": 10 # Максимум 10 уровней
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": "Бонус скорости",
|
||||||
|
"description": "Временно увеличивает скорость передвижения на 20%",
|
||||||
|
"effect_type": "speed",
|
||||||
|
"base_effect_value": 0.2,
|
||||||
|
"effect_increment": 0.05,
|
||||||
|
"price": 40,
|
||||||
|
"upgrade_price": 30,
|
||||||
|
"duration": 1800, # 30 минут
|
||||||
|
"max_level": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Вставляем бонусы в БД
|
||||||
|
bonus_types_collection.insert_many(bonus_types)
|
||||||
|
print(f"Добавлено {len(bonus_types)} типов бонусов")
|
Reference in New Issue
Block a user