Files
sasheg 47c7f17c90 RPU v2, promo/bank система, админка, оптимизации
— RPU v2: бюджет удачи, потолок (ceiling), комбек после
  проигрышей, hot/cold анализ, интеграция с promo-фазами
— Bank API: mock-банк для карты (депозит/вывод с балансом 1000₽)
— Promo-сервис: автогенерация промокодов каждый час, типы
  (card_to_site, luck_boost, ceiling_boost, free_case, reset_streak)
— Promo-фазы: LUCKY/NEUTRAL/UNLUCKY, переключение каждые 1-6ч
— Admin: история транзакций (user_history), управление промокодами,
  управление promo-фазами, API фильтров/коллекций, RPU info
— Database: WAL mode, новые поля RPU v2, TransactionLog,
  карточный баланс, статистики (streaks, ceiling, budget)
— Frontend: /deposit, /promo/activate, /withdraw, /card-balance,
  /online-count, /rpu-info, lifespan вместо startup event
— UI: user_history.html, улучшения профиля, кейсов, навигации
— Cases: обновление cases.json (1892 строки новых данных)
— Achievements: кэш коллекций, оптимизация is_custom проверок
— Sounds: дополнительные звуковые эффекты
— Upgrade: интеграция с RPU (множители, streaks)
— .gitignore: исключены .db, .bak, __pycache__
2026-07-25 13:11:08 +00:00

1161 lines
40 KiB
Python

from fastapi import APIRouter, Request, Depends, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from sqlalchemy import desc, func
from typing import Optional
import json
from datetime import datetime
from pathlib import Path
from database import get_db, User, InventoryItem, CaseOpening, Contract, Upgrade, TransactionLog
from auth import get_current_admin
from rpu import get_user_rpu, calculate_rpu_level, get_rpu_description
admin_router = APIRouter(prefix="/admin", tags=["admin"])
templates = Jinja2Templates(directory="templates")
def money_filter(value):
try:
return "{:,.0f}".format(float(value)).replace(",", " ")
except (ValueError, TypeError):
return "0"
templates.env.filters["money"] = money_filter
# Загрузка конфигов
CASES_FILE = Path("cases.json")
MAINPAGE_FILE = Path("mainpage.json")
def load_cases_config():
if CASES_FILE.exists():
with open(CASES_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return []
def save_cases_config(cases):
with open(CASES_FILE, 'w', encoding='utf-8') as f:
json.dump(cases, f, ensure_ascii=False, indent=2)
def load_mainpage_config():
if MAINPAGE_FILE.exists():
with open(MAINPAGE_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return []
def save_mainpage_config(config):
with open(MAINPAGE_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
def get_rpu_level_for_user(user_id: int) -> float:
"""Хелпер для шаблона"""
db = next(get_db())
rpu = get_user_rpu(db, user_id)
return calculate_rpu_level(rpu)
# ========== СТРАНИЦЫ ==========
@admin_router.get("/", response_class=HTMLResponse)
async def admin_dashboard(
request: Request,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
"""Главная страница админки"""
# Основная статистика
total_users = db.query(User).count()
total_items = db.query(InventoryItem).count()
total_openings = db.query(CaseOpening).count()
total_contracts = db.query(Contract).count()
total_balance = db.query(func.sum(User.balance)).scalar() or 0
# Статистика РПУ
from rpu import UserRPU
all_rpu = db.query(UserRPU).all()
rpu_count = len(all_rpu)
avg_rpu = 50.0
lucky_count = 0
normal_count = 0
unlucky_count = 0
auto_adjust_count = 0
total_spent = 0.0
if rpu_count > 0:
for r in all_rpu:
level = calculate_rpu_level(r)
if level > 60:
lucky_count += 1
elif level < 40:
unlucky_count += 1
else:
normal_count += 1
if r.auto_adjust:
auto_adjust_count += 1
total_spent += r.total_spent
avg_rpu = sum(calculate_rpu_level(r) for r in all_rpu) / rpu_count
recent_users = db.query(User).order_by(desc(User.created_at)).limit(10).all()
return templates.TemplateResponse(request, "admin/dashboard.html", {
"user": admin,
"total_users": total_users,
"total_items": total_items,
"total_openings": total_openings,
"total_contracts": total_contracts,
"total_balance": total_balance,
"recent_users": recent_users,
"rpu_count": rpu_count,
"avg_rpu": round(avg_rpu, 1),
"lucky_count": lucky_count,
"normal_count": normal_count,
"unlucky_count": unlucky_count,
"auto_adjust_count": auto_adjust_count,
"total_spent": total_spent,
"get_rpu_level": get_rpu_level_for_user
})
@admin_router.get("/users", response_class=HTMLResponse)
async def admin_users(
request: Request,
search: Optional[str] = None,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
"""Список пользователей"""
query = db.query(User)
if search:
query = query.filter(User.username.contains(search))
users = query.order_by(desc(User.created_at)).limit(100).all()
# Добавляем уровень РПУ для каждого пользователя
users_with_rpu = []
for u in users:
rpu = get_user_rpu(db, u.id)
users_with_rpu.append({
"id": u.id,
"username": u.username,
"balance": u.balance,
"is_admin": u.is_admin,
"created_at": u.created_at,
"case_openings": db.query(CaseOpening).filter(CaseOpening.user_id == u.id).count(),
"contracts": db.query(Contract).filter(Contract.user_id == u.id).count(),
"inventory_count": db.query(InventoryItem).filter(InventoryItem.user_id == u.id).count(),
"rpu_level": round(calculate_rpu_level(rpu))
})
return templates.TemplateResponse(request, "admin/users.html", {
"user": admin,
"users": users_with_rpu,
"search": search
})
@admin_router.get("/user/{user_id}", response_class=HTMLResponse)
async def admin_user_detail(
user_id: int,
request: Request,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
raise HTTPException(404, "Пользователь не найден")
total_items = db.query(InventoryItem).filter(InventoryItem.user_id == user_id).count()
total_openings = db.query(CaseOpening).filter(CaseOpening.user_id == user_id).count()
total_contracts = db.query(Contract).filter(Contract.user_id == user_id).count()
inventory = db.query(InventoryItem).filter(
InventoryItem.user_id == user_id
).order_by(desc(InventoryItem.obtained_at)).limit(50).all()
return templates.TemplateResponse(request, "admin/user_detail.html", {
"user": admin,
"target_user": target_user,
"total_items": total_items,
"total_openings": total_openings,
"total_contracts": total_contracts,
"inventory": inventory
})
@admin_router.get("/user/{user_id}/history", response_class=HTMLResponse)
async def admin_user_history(
user_id: int,
request: Request,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
raise HTTPException(404, "Пользователь не найден")
# Все открытия кейсов
openings = db.query(CaseOpening).filter(
CaseOpening.user_id == user_id
).order_by(desc(CaseOpening.opened_at)).limit(200).all()
# Все апгрейды
upgrades = db.query(Upgrade).filter(
Upgrade.user_id == user_id
).order_by(desc(Upgrade.created_at)).limit(200).all()
# Все транзакции
transactions = db.query(TransactionLog).filter(
TransactionLog.user_id == user_id
).order_by(desc(TransactionLog.created_at)).limit(200).all()
from rpu import calculate_ceiling, get_user_total_value, get_user_rpu
ceiling = calculate_ceiling(db, user_id)
total_value = get_user_total_value(db, user_id)
rpu = get_user_rpu(db, user_id)
return templates.TemplateResponse(request, "admin/user_history.html", {
"user": admin,
"target_user": target_user,
"openings": openings,
"upgrades": upgrades,
"transactions": transactions,
"ceiling": ceiling,
"total_value": total_value,
"rpu": rpu,
})
@admin_router.get("/cases", response_class=HTMLResponse)
async def admin_cases(
request: Request,
admin: User = Depends(get_current_admin)
):
from frontend import ALL_ITEMS
cases = load_cases_config()
mainpage = load_mainpage_config()
for case in cases:
if not isinstance(case.get('items'), list):
case['items'] = []
case['items_count'] = len(case['items'])
all_items = []
for i, item in enumerate(ALL_ITEMS[:500]):
all_items.append({
"id": item.get("_id", i),
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", "")
})
return templates.TemplateResponse(request, "admin/cases.html", {
"user": admin,
"cases": cases,
"mainpage": mainpage,
"all_items": all_items,
"cases_config": {c['name']: c for c in cases}
})
# ========== API ПОЛЬЗОВАТЕЛИ ==========
@admin_router.post("/api/user/{user_id}/balance")
async def admin_set_balance(
user_id: int,
amount: float = Form(...),
operation: str = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
old_balance = target_user.balance
if operation == "add":
target_user.balance += amount
message = f"Добавлено {amount:.2f}"
elif operation == "remove":
target_user.balance = max(0, target_user.balance - amount)
message = f"Снято {amount:.2f}"
else:
target_user.balance = amount
message = f"Установлен баланс {amount:.2f}"
db.commit()
return JSONResponse({
"success": True,
"old_balance": old_balance,
"new_balance": target_user.balance,
"message": message
})
@admin_router.post("/api/user/{user_id}/give-item")
async def admin_give_item(
user_id: int,
item_id: int = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
from frontend import ALL_ITEMS, generate_item_float
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
item_data = None
for idx, item in enumerate(ALL_ITEMS):
if item.get("_id", idx) == item_id:
item_data = item
break
if not item_data:
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
item_float = item_data.get("mid_float_used") or generate_item_float(item_data.get("rarity", "Unknown"))
inventory_item = InventoryItem(
user_id=user_id,
item_id=item_id,
market_hash_name=item_data.get("market_hash_name", "Unknown"),
rarity=item_data.get("rarity", "Unknown"),
wear=item_data.get("wear", "Unknown"),
float_value=item_float,
type=item_data.get("type", "Normal"),
image_url=item_data.get("image_url", ""),
price_rub=item_data.get("price_rub", 0),
obtained_from="admin"
)
db.add(inventory_item)
db.commit()
from achievements import check_collection_achievements
check_collection_achievements(db, user_id)
return JSONResponse({
"success": True,
"message": f"Предмет '{item_data.get('market_hash_name')}' выдан пользователю {target_user.username}"
})
@admin_router.post("/api/user/{user_id}/toggle-ban")
async def admin_toggle_ban(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
target_user.is_banned = not target_user.is_banned
db.commit()
return JSONResponse({
"success": True,
"is_banned": target_user.is_banned,
"message": f"Пользователь {'забанен' if target_user.is_banned else 'разбанен'}"
})
@admin_router.post("/api/user/{user_id}/toggle-admin")
async def admin_toggle_admin(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
if target_user.id == admin.id:
return JSONResponse(status_code=400, content={"error": "Нельзя изменить свой статус"})
target_user.is_admin = not target_user.is_admin
db.commit()
return JSONResponse({
"success": True,
"is_admin": target_user.is_admin,
"message": f"Статус администратора {'включен' if target_user.is_admin else 'выключен'}"
})
@admin_router.post("/api/user/{user_id}/delete-item/{inventory_id}")
async def admin_delete_item(
user_id: int,
inventory_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
item = db.query(InventoryItem).filter(
InventoryItem.id == inventory_id,
InventoryItem.user_id == user_id
).first()
if not item:
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
db.delete(item)
db.commit()
return JSONResponse({"success": True, "message": "Предмет удален"})
# ========== API РПУ ==========
@admin_router.get("/api/user/{user_id}/rpu")
async def admin_get_user_rpu(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
rpu = get_user_rpu(db, user_id)
from promo_phase import get_promo_phase_info
promo = get_promo_phase_info()
# Потолок
from rpu import calculate_ceiling
ceiling = calculate_ceiling(db, user_id)
return {
"user_id": user_id,
"username": target_user.username,
"multipliers": {
"consumer": rpu.consumer_multiplier,
"industrial": rpu.industrial_multiplier,
"mil_spec": rpu.mil_spec_multiplier,
"restricted": rpu.restricted_multiplier,
"classified": rpu.classified_multiplier,
"covert": rpu.covert_multiplier,
"rare_special": rpu.rare_special_multiplier,
},
"luck_multiplier": rpu.luck_multiplier,
"auto_adjust": rpu.auto_adjust,
"rpu_level": calculate_rpu_level(rpu),
"stats": {
"total_spent": rpu.total_spent,
"total_opened": rpu.total_opened,
"total_value_received": rpu.total_value_received,
"current_streak": rpu.current_streak,
"best_streak": rpu.best_streak,
"worst_streak": rpu.worst_streak
},
# ── RPU v2 ──
"v2": {
"ceiling": {
"total_deposited": target_user.total_deposited or 0,
"ceiling_multiplier": rpu.ceiling_multiplier,
"current_ceiling": ceiling,
"break_count": rpu.ceiling_break_count,
"break_session": rpu.ceiling_break_session,
},
"session": {
"luck_budget": round(rpu.luck_budget, 1),
"session_spent": round(rpu.session_spent, 0),
"session_won": round(rpu.session_won, 0),
"reset_date": rpu.session_reset_date.isoformat() if rpu.session_reset_date else None,
},
"comeback": {
"active": rpu.comeback_active,
"openings_left": rpu.comeback_openings_left,
"multiplier": rpu.comeback_multiplier,
"consecutive_loss_value": round(rpu.consecutive_loss_value or 0, 0),
"consecutive_loss_count": rpu.consecutive_loss_count or 0,
},
"hot_cold": {
"hot_score": round(rpu.hot_score or 0, 2),
"last_activity": rpu.last_activity_date.isoformat() if rpu.last_activity_date else None,
},
"promo_phase": {
"phase": promo["phase"],
"effect": promo["effect"],
"next_switch": promo["next_switch"],
"remaining_minutes": promo["remaining_minutes"],
}
}
}
@admin_router.post("/api/user/{user_id}/rpu/update")
async def admin_update_user_rpu(
user_id: int,
consumer: float = Form(1.0),
industrial: float = Form(1.0),
mil_spec: float = Form(1.0),
restricted: float = Form(1.0),
classified: float = Form(1.0),
covert: float = Form(1.0),
rare_special: float = Form(1.0),
luck: float = Form(1.0),
auto_adjust: bool = Form(False),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
rpu = get_user_rpu(db, user_id)
rpu.consumer_multiplier = max(0.1, min(3.0, consumer))
rpu.industrial_multiplier = max(0.1, min(3.0, industrial))
rpu.mil_spec_multiplier = max(0.1, min(3.0, mil_spec))
rpu.restricted_multiplier = max(0.1, min(3.0, restricted))
rpu.classified_multiplier = max(0.1, min(3.0, classified))
rpu.covert_multiplier = max(0.1, min(3.0, covert))
rpu.rare_special_multiplier = max(0.1, min(3.0, rare_special))
rpu.luck_multiplier = max(0.1, min(3.0, luck))
rpu.auto_adjust = auto_adjust
rpu.updated_at = datetime.utcnow()
db.commit()
return JSONResponse({
"success": True,
"message": f"Настройки РПУ для {target_user.username} обновлены",
"rpu_level": calculate_rpu_level(rpu)
})
@admin_router.post("/api/user/{user_id}/rpu/preset")
async def admin_set_rpu_preset(
user_id: int,
preset: str = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
# Теперь: низкий множитель = высокий РПУ (не везёт)
# высокий множитель = низкий РПУ (везёт)
presets = {
"lucky": {"all": 2.0, "luck": 1.8}, # ~10% РПУ - очень везёт
"normal": {"all": 1.0, "luck": 1.0}, # ~50% РПУ - обычно
"unlucky": {"all": 0.6, "luck": 0.7}, # ~75% РПУ - не везёт
"very_unlucky": {"all": 0.3, "luck": 0.4} # ~95% РПУ - проклят
}
if preset not in presets:
return JSONResponse(status_code=400, content={"error": "Неизвестный пресет"})
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
rpu = get_user_rpu(db, user_id)
p = presets[preset]
rpu.consumer_multiplier = p["all"]
rpu.industrial_multiplier = p["all"]
rpu.mil_spec_multiplier = p["all"]
rpu.restricted_multiplier = p["all"]
rpu.classified_multiplier = p["all"]
rpu.covert_multiplier = p["all"]
rpu.rare_special_multiplier = p["all"]
rpu.luck_multiplier = p["luck"]
rpu.updated_at = datetime.utcnow()
db.commit()
level = calculate_rpu_level(rpu)
desc = get_rpu_description(level)
return JSONResponse({
"success": True,
"message": f"Пресет '{preset}' применен. Уровень РПУ: {level:.0f}% ({desc})"
})
# ========== API КЕЙСЫ ==========
@admin_router.post("/api/cases/create")
async def admin_create_case(
case_name: str = Form(...),
display_name: str = Form(...),
price_open: float = Form(...),
description: str = Form(""),
image_url: str = Form(""),
items_json: str = Form(...),
admin: User = Depends(get_current_admin)
):
cases = load_cases_config()
for case in cases:
if case['name'] == case_name:
return JSONResponse(status_code=400, content={"error": "Кейс с таким именем уже существует"})
try:
parsed_items = json.loads(items_json)
except:
return JSONResponse(status_code=400, content={"error": "Неверный формат списка предметов"})
new_case = {
"name": case_name,
"display_name": display_name,
"price_open": price_open,
"items": parsed_items,
"image_url": image_url,
"description": description
}
cases.append(new_case)
save_cases_config(cases)
return JSONResponse({
"success": True,
"case": new_case,
"message": f"Кейс '{display_name}' создан"
})
@admin_router.post("/api/cases/update/{case_name}")
async def admin_update_case(
case_name: str,
display_name: str = Form(...),
price_open: float = Form(...),
description: str = Form(""),
image_url: str = Form(""),
items_json: str = Form(...),
new_name: str = Form(""),
admin: User = Depends(get_current_admin)
):
cases = load_cases_config()
for case in cases:
if case['name'] == case_name:
try:
parsed_items = json.loads(items_json)
except:
return JSONResponse(status_code=400, content={"error": "Неверный формат списка предметов"})
target_name = new_name or case_name
# Проверяем конфликт имени при переименовании
if new_name and new_name != case_name:
for c in cases:
if c['name'] == new_name:
return JSONResponse(status_code=400, content={"error": "Кейс с таким именем уже существует"})
case['name'] = new_name
case['display_name'] = display_name
case['price_open'] = price_open
case['description'] = description
case['image_url'] = image_url
case['items'] = parsed_items
save_cases_config(cases)
# Обновляем mainpage если переименован
if new_name and new_name != case_name:
mainpage = load_mainpage_config()
for section in mainpage:
section['cases'] = [new_name if c == case_name else c for c in section['cases']]
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"case": case,
"message": f"Кейс '{display_name}' обновлен"
})
return JSONResponse(status_code=404, content={"error": "Кейс не найден"})
@admin_router.post("/api/cases/delete/{case_name}")
async def admin_delete_case(
case_name: str,
admin: User = Depends(get_current_admin)
):
cases = load_cases_config()
cases = [c for c in cases if c['name'] != case_name]
save_cases_config(cases)
mainpage = load_mainpage_config()
for section in mainpage:
section['cases'] = [c for c in section['cases'] if c != case_name]
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Кейс '{case_name}' удален"
})
@admin_router.post("/api/mainpage/update")
async def admin_update_mainpage(
config: str = Form(...),
admin: User = Depends(get_current_admin)
):
try:
new_config = json.loads(config)
except:
return JSONResponse(status_code=400, content={"error": "Неверный формат JSON"})
save_mainpage_config(new_config)
return JSONResponse({
"success": True,
"message": "Конфигурация главной страницы обновлена"
})
@admin_router.get("/api/items/search")
async def admin_search_items(
q: str = "",
min_price: float = 0,
max_price: float = 0,
limit: int = 200,
rarity_filter: str = "",
collection_filter: str = "",
type_filter: str = "",
weapon_filter: str = "",
sort_by: str = "",
sort_order: str = "asc",
include_subvariants: bool = False
):
"""Поиск предметов для добавления в кейс / апгрейда."""
from frontend import ALL_ITEMS
from backend import COLLECTIONS_INDEX
query = q.lower().strip()
results = []
seen_bases = set()
def accept_item(item, idx):
if not include_subvariants and item.get("is_subvariant"):
return False
if min_price > 0 and item.get("price_rub", 0) < min_price:
return False
if max_price > 0 and item.get("price_rub", 0) > max_price:
return False
if rarity_filter:
item_rarity = item.get("rarity", "").lower()
if rarity_filter.lower() not in item_rarity:
return False
if collection_filter:
item_coll = item.get("collection", "").lower()
if collection_filter.lower() not in item_coll:
return False
if type_filter:
item_type = item.get("type", "").lower()
if type_filter.lower() != item_type:
return False
if weapon_filter:
item_weapon = item.get("weapon", "").lower()
if weapon_filter.lower() != item_weapon:
return False
return True
def make_result(item, idx):
name = item.get("market_hash_name", "Unknown")
dedup_key = f"{name}|{item.get('weapon','')}|{item.get('pattern','')}|{item.get('rarity','')}"
if dedup_key in seen_bases:
return None
seen_bases.add(dedup_key)
return {
"id": item.get("_id", idx),
"name": name,
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", ""),
"price_rub": item.get("price_rub", 0),
"weapon": item.get("weapon", ""),
"pattern": item.get("pattern", ""),
"collection": item.get("collection", ""),
"type": item.get("type", "Normal"),
}
if query and len(query) < 2:
return []
if not query or len(query) < 2:
if any([min_price > 0, max_price > 0, rarity_filter, collection_filter,
type_filter, weapon_filter]):
for idx, item in enumerate(ALL_ITEMS):
if not accept_item(item, idx):
continue
r = make_result(item, idx)
if r:
results.append(r)
if len(results) >= limit:
break
else:
step = max(1, len(ALL_ITEMS) // limit)
for idx in range(0, len(ALL_ITEMS), step):
item = ALL_ITEMS[idx]
if not accept_item(item, idx):
continue
r = make_result(item, idx)
if r:
results.append(r)
if len(results) >= limit:
break
else:
for idx, item in enumerate(ALL_ITEMS):
if not accept_item(item, idx):
continue
name = item.get("market_hash_name", "").lower()
weapon = item.get("weapon", "").lower()
pattern = item.get("pattern", "").lower()
coll = item.get("collection", "").lower()
if query in name or query in weapon or query in pattern or query in coll:
r = make_result(item, idx)
if r:
results.append(r)
if len(results) >= limit:
break
# Сортировка
if sort_by in ("price_rub", "name", "rarity"):
reverse = sort_order == "desc"
if sort_by == "rarity":
rarity_rank = {
"Consumer Grade": 0, "Industrial Grade": 1,
"Mil-Spec": 2, "Mil-Spec Grade": 2,
"Restricted": 3, "Classified": 4,
"Covert": 5, "Rare Special Item": 6,
"Extraordinary": 6, "Contraband": 7
}
results.sort(key=lambda x: rarity_rank.get(x.get("rarity", ""), 0),
reverse=reverse)
else:
results.sort(key=lambda x: x.get(sort_by, ""), reverse=reverse)
return results[:200]
@admin_router.get("/api/items/filters")
async def admin_items_filters():
"""Возвращает списки уникальных значений для фильтров"""
from frontend import ALL_ITEMS
weapons = set()
types = set()
for item in ALL_ITEMS:
if item.get("is_subvariant"):
continue
w = item.get("weapon", "")
if w:
weapons.add(w)
t = item.get("type", "")
if t:
types.add(t)
return {
"weapons": sorted(weapons),
"types": sorted(types),
}
@admin_router.get("/api/collections")
async def admin_list_collections():
"""Возвращает список всех коллекций для фильтра"""
from backend import COLLECTIONS_INDEX
return {"collections": sorted(COLLECTIONS_INDEX.keys())}
@admin_router.get("/api/items/by-collection")
async def admin_items_by_collection(
collection: str = "",
rarity_filter: str = "",
type_filter: str = "",
weapon_filter: str = "",
include_subvariants: bool = False
):
"""Возвращает предметы из указанной коллекции"""
from backend import COLLECTIONS_INDEX
from frontend import get_item
results = []
seen_bases = set()
if collection not in COLLECTIONS_INDEX:
return results
for idx in COLLECTIONS_INDEX[collection]:
item = get_item(idx)
if not item:
continue
if not include_subvariants and item.get("is_subvariant"):
continue
if rarity_filter:
if rarity_filter.lower() not in item.get("rarity", "").lower():
continue
if type_filter:
if type_filter.lower() != item.get("type", "").lower():
continue
if weapon_filter:
if weapon_filter.lower() != item.get("weapon", "").lower():
continue
name = item.get("market_hash_name", "Unknown")
dedup_key = f"{name}|{item.get('weapon','')}|{item.get('pattern','')}|{item.get('rarity','')}"
if dedup_key in seen_bases:
continue
seen_bases.add(dedup_key)
results.append({
"id": item.get("_id", idx),
"name": name,
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", ""),
"price_rub": item.get("price_rub", 0),
"weapon": item.get("weapon", ""),
"pattern": item.get("pattern", ""),
"collection": item.get("collection", ""),
"type": item.get("type", "Normal"),
})
return results
@admin_router.post("/api/mainpage/section/add")
async def admin_add_mainpage_section(
tab: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Добавить новую секцию на главную"""
mainpage = load_mainpage_config()
# Проверяем, нет ли уже такой секции
for section in mainpage:
if section['tab'] == tab:
return JSONResponse(status_code=400, content={"error": "Секция уже существует"})
mainpage.append({
"tab": tab,
"cases": []
})
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Секция '{tab}' добавлена"
})
@admin_router.post("/api/mainpage/section/delete")
async def admin_delete_mainpage_section(
tab: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Удалить секцию с главной"""
mainpage = load_mainpage_config()
mainpage = [s for s in mainpage if s['tab'] != tab]
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Секция '{tab}' удалена"
})
@admin_router.post("/api/mainpage/section/add-case")
async def admin_add_case_to_section(
tab: str = Form(...),
case_name: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Добавить кейс в секцию"""
mainpage = load_mainpage_config()
for section in mainpage:
if section['tab'] == tab:
if case_name not in section['cases']:
section['cases'].append(case_name)
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Кейс '{case_name}' добавлен в секцию '{tab}'"
})
else:
return JSONResponse(status_code=400, content={"error": "Кейс уже в секции"})
return JSONResponse(status_code=404, content={"error": "Секция не найдена"})
@admin_router.post("/api/mainpage/section/remove-case")
async def admin_remove_case_from_section(
tab: str = Form(...),
case_name: str = Form(...),
admin: User = Depends(get_current_admin)
):
"""Удалить кейс из секции"""
mainpage = load_mainpage_config()
for section in mainpage:
if section['tab'] == tab:
if case_name in section['cases']:
section['cases'].remove(case_name)
save_mainpage_config(mainpage)
return JSONResponse({
"success": True,
"message": f"Кейс '{case_name}' удален из секции '{tab}'"
})
return JSONResponse(status_code=404, content={"error": "Секция или кейс не найдены"})
@admin_router.get("/api/cases/list")
async def admin_get_cases_list(
admin: User = Depends(get_current_admin)
):
"""Получить список всех кейсов для выбора"""
cases = load_cases_config()
return [{"name": c['name'], "display_name": c.get('display_name', c['name'])} for c in cases]
@admin_router.get("/api/items/{item_id}")
async def admin_get_item(
item_id: int,
admin: User = Depends(get_current_admin)
):
"""Получить информацию о предмете по ID"""
from frontend import ALL_ITEMS
for idx, item in enumerate(ALL_ITEMS):
if item.get("_id", idx) == item_id:
return {
"id": item_id,
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"image_url": item.get("image_url", ""),
"price_rub": item.get("price_rub", 0),
"weapon": item.get("weapon", ""),
"pattern": item.get("pattern", ""),
"type": item.get("type", "Normal"),
"wear": item.get("wear", "")
}
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
# ========== API ПРОМОКОДЫ ==========
@admin_router.get("/api/promocodes")
async def admin_list_promocodes(
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
codes = db.query(PromoCode).order_by(desc(PromoCode.created_at)).limit(100).all()
return [{
"id": c.id,
"code": c.code,
"reward_type": c.reward_type,
"reward_amount": c.reward_amount,
"max_uses": c.max_uses,
"used_count": c.used_count,
"start_date": c.start_date.isoformat() if c.start_date else None,
"end_date": c.end_date.isoformat() if c.end_date else None,
"is_active": c.is_active,
} for c in codes]
@admin_router.post("/api/promocodes/create")
async def admin_create_promocode(
code: str = Form(...),
reward_type: str = Form(...),
reward_amount: float = Form(0),
reward_data: str = Form(""),
max_uses: int = Form(1),
end_date_str: str = Form(""),
is_active: bool = Form(True),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
existing = db.query(PromoCode).filter(PromoCode.code == code).first()
if existing:
return JSONResponse(status_code=400, content={"error": "Промокод с таким кодом уже существует"})
end_date = None
if end_date_str:
try:
end_date = datetime.fromisoformat(end_date_str)
except:
pass
promo = PromoCode(
code=code,
reward_type=reward_type,
reward_amount=reward_amount,
reward_data=reward_data,
max_uses=max_uses,
end_date=end_date,
is_active=is_active,
created_by=admin.id,
)
db.add(promo)
db.commit()
return {"success": True, "message": f"Промокод '{code}' создан"}
@admin_router.post("/api/promocodes/toggle/{promo_id}")
async def admin_toggle_promocode(
promo_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
promo = db.query(PromoCode).filter(PromoCode.id == promo_id).first()
if not promo:
return JSONResponse(status_code=404, content={"error": "Промокод не найден"})
promo.is_active = not promo.is_active
db.commit()
return {"success": True, "is_active": promo.is_active}
@admin_router.post("/api/promocodes/delete/{promo_id}")
async def admin_delete_promocode(
promo_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
promo = db.query(PromoCode).filter(PromoCode.id == promo_id).first()
if not promo:
return JSONResponse(status_code=404, content={"error": "Промокод не найден"})
db.delete(promo)
db.commit()
return {"success": True, "message": "Промокод удален"}
@admin_router.get("/api/promo-phase")
async def admin_get_promo_phase(
admin: User = Depends(get_current_admin)
):
"""Информация о текущей промо-фазе (для админки)"""
from promo_phase import get_phase_debug_info
return get_phase_debug_info()
# ========== API ТРАНЗАКЦИИ ==========
@admin_router.get("/api/transactions")
async def admin_list_transactions(
user_id: Optional[int] = None,
limit: int = 100,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
query = db.query(TransactionLog)
if user_id:
query = query.filter(TransactionLog.user_id == user_id)
txns = query.order_by(desc(TransactionLog.created_at)).limit(limit).all()
return [{
"id": t.id,
"user_id": t.user_id,
"tx_type": t.tx_type,
"amount": t.amount,
"fee": t.fee,
"promo_code": t.promo_code,
"item_name": t.item_name,
"details": t.details,
"created_at": t.created_at.isoformat(),
} for t in txns]
@admin_router.post("/api/reset-password/{user_id}")
async def admin_reset_password(
user_id: int,
new_password: str = Form(...),
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin)
):
"""Сброс пароля пользователя (только для админов)"""
from auth import get_password_hash
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
target_user.hashed_password = get_password_hash(new_password)
db.commit()
return JSONResponse({
"success": True,
"message": f"Пароль для {target_user.username} изменен"
})