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__
This commit is contained in:
@@ -8,7 +8,7 @@ import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from database import get_db, User, InventoryItem, CaseOpening, Contract
|
||||
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
|
||||
|
||||
@@ -184,6 +184,48 @@ async def admin_user_detail(
|
||||
"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,
|
||||
@@ -197,6 +239,7 @@ async def admin_cases(
|
||||
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]):
|
||||
@@ -272,7 +315,7 @@ async def admin_give_item(
|
||||
if not item_data:
|
||||
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
|
||||
|
||||
item_float = generate_item_float(item_data.get("rarity", "Unknown"))
|
||||
item_float = item_data.get("mid_float_used") or generate_item_float(item_data.get("rarity", "Unknown"))
|
||||
|
||||
inventory_item = InventoryItem(
|
||||
user_id=user_id,
|
||||
@@ -282,6 +325,8 @@ async def admin_give_item(
|
||||
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)
|
||||
@@ -370,6 +415,13 @@ async def admin_get_user_rpu(
|
||||
|
||||
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,
|
||||
@@ -387,7 +439,44 @@ async def admin_get_user_rpu(
|
||||
"rpu_level": calculate_rpu_level(rpu),
|
||||
"stats": {
|
||||
"total_spent": rpu.total_spent,
|
||||
"total_opened": rpu.total_opened
|
||||
"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"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,30 +570,30 @@ async def admin_set_rpu_preset(
|
||||
|
||||
@admin_router.post("/api/cases/create")
|
||||
async def admin_create_case(
|
||||
name: str = Form(...),
|
||||
case_name: str = Form(...),
|
||||
display_name: str = Form(...),
|
||||
price: float = Form(...),
|
||||
price_open: float = Form(...),
|
||||
description: str = Form(""),
|
||||
image_url: str = Form(""),
|
||||
items: str = Form(...),
|
||||
items_json: str = Form(...),
|
||||
admin: User = Depends(get_current_admin)
|
||||
):
|
||||
cases = load_cases_config()
|
||||
|
||||
for case in cases:
|
||||
if case['name'] == name:
|
||||
if case['name'] == case_name:
|
||||
return JSONResponse(status_code=400, content={"error": "Кейс с таким именем уже существует"})
|
||||
|
||||
try:
|
||||
item_ids = json.loads(items)
|
||||
parsed_items = json.loads(items_json)
|
||||
except:
|
||||
return JSONResponse(status_code=400, content={"error": "Неверный формат списка предметов"})
|
||||
|
||||
new_case = {
|
||||
"name": name,
|
||||
"name": case_name,
|
||||
"display_name": display_name,
|
||||
"price_open": price,
|
||||
"items": item_ids,
|
||||
"price_open": price_open,
|
||||
"items": parsed_items,
|
||||
"image_url": image_url,
|
||||
"description": description
|
||||
}
|
||||
@@ -522,10 +611,11 @@ async def admin_create_case(
|
||||
async def admin_update_case(
|
||||
case_name: str,
|
||||
display_name: str = Form(...),
|
||||
price: float = Form(...),
|
||||
price_open: float = Form(...),
|
||||
description: str = Form(""),
|
||||
image_url: str = Form(""),
|
||||
items: str = Form(...),
|
||||
items_json: str = Form(...),
|
||||
new_name: str = Form(""),
|
||||
admin: User = Depends(get_current_admin)
|
||||
):
|
||||
cases = load_cases_config()
|
||||
@@ -533,18 +623,34 @@ async def admin_update_case(
|
||||
for case in cases:
|
||||
if case['name'] == case_name:
|
||||
try:
|
||||
item_ids = json.loads(items)
|
||||
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
|
||||
case['price_open'] = price_open
|
||||
case['description'] = description
|
||||
case['image_url'] = image_url
|
||||
case['items'] = item_ids
|
||||
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,
|
||||
@@ -594,58 +700,91 @@ async def admin_search_items(
|
||||
q: str = "",
|
||||
min_price: float = 0,
|
||||
max_price: float = 0,
|
||||
limit: int = 200
|
||||
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 min_price > 0 or max_price > 0:
|
||||
if any([min_price > 0, max_price > 0, rarity_filter, collection_filter,
|
||||
type_filter, weapon_filter]):
|
||||
for idx, item in enumerate(ALL_ITEMS):
|
||||
if accept_item(item, idx):
|
||||
item_id = item.get("_id", idx)
|
||||
results.append({
|
||||
"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", "")
|
||||
})
|
||||
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):
|
||||
if not accept_item(ALL_ITEMS[idx], idx):
|
||||
continue
|
||||
item = ALL_ITEMS[idx]
|
||||
item_id = item.get("_id", idx)
|
||||
results.append({
|
||||
"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", "")
|
||||
})
|
||||
if len(results) >= 200:
|
||||
break
|
||||
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):
|
||||
@@ -653,24 +792,113 @@ async def admin_search_items(
|
||||
name = item.get("market_hash_name", "").lower()
|
||||
weapon = item.get("weapon", "").lower()
|
||||
pattern = item.get("pattern", "").lower()
|
||||
|
||||
if query in name or query in weapon or query in pattern:
|
||||
item_id = item.get("_id", idx)
|
||||
results.append({
|
||||
"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", "")
|
||||
})
|
||||
if len(results) >= 200:
|
||||
break
|
||||
|
||||
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(...),
|
||||
@@ -787,6 +1015,129 @@ async def admin_get_item(
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user