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:
2026-07-25 13:11:08 +00:00
parent 6638ab99f3
commit 47c7f17c90
30 changed files with 5862 additions and 1295 deletions
+352
View File
@@ -0,0 +1,352 @@
"""
Promo Service — логика промокодов и депозитов.
"""
from datetime import datetime, timedelta
import random
from typing import Optional
from sqlalchemy.orm import Session
from database import User, PromoCode, TransactionLog
from bank_api import bank_client
from promo_phase import get_promo_effect_for_user, get_current_phase
DAILY_DEPOSIT_LIMIT = 999999999.0 # без лимита
# ── Авто-генерация промокодов ──
PROMO_WORDS = ["storm", "glitch", "frost", "blaze", "cipher", "nexus", "phantom", "prism"]
PROMO_PERCENTS = [10, 15, 20, 21, 24, 25]
# Хранилище текущего авто-промокода (in-memory кэш)
_current_auto_promo: Optional[dict] = None
def generate_auto_promo_code() -> str:
"""Генерирует промокод формата слово-процент, например 'storm-21'"""
word = random.choice(PROMO_WORDS)
pct = random.choice(PROMO_PERCENTS)
return f"{word}-{pct}"
def get_or_create_auto_promo(db: Session) -> PromoCode:
"""
Возвращает активный авто-сгенерированный промокод.
Если его нет или он истёк (>1ч) — создаёт новый.
"""
global _current_auto_promo
now = datetime.utcnow()
# Ищем существующий авто-промокод, созданный <1 часа назад
candidates = db.query(PromoCode).filter(
PromoCode.reward_type == "card_to_site",
PromoCode.is_active == True,
PromoCode.created_by == -1 # маркер авто-генерации
).order_by(PromoCode.created_at.desc()).limit(1).all()
if candidates:
promo = candidates[0]
age = (now - promo.created_at).total_seconds()
if age < 3600: # меньше часа — живой
_current_auto_promo = {
"code": promo.code,
"reward_amount": promo.reward_amount,
"created_at": promo.created_at.isoformat(),
}
return promo
# Создаём новый
code = generate_auto_promo_code()
pct = int(code.split("-")[1])
reward = pct # процент от daily лимита как сумма бонуса
new_promo = PromoCode(
code=code,
reward_type="card_to_site",
reward_amount=reward,
reward_data="auto",
max_uses=999,
used_count=0,
start_date=now,
end_date=now + timedelta(hours=1),
is_active=True,
created_by=-1,
)
db.add(new_promo)
db.commit()
_current_auto_promo = {
"code": new_promo.code,
"reward_amount": new_promo.reward_amount,
"created_at": new_promo.created_at.isoformat(),
}
return new_promo
def get_current_auto_promo(db: Session) -> Optional[dict]:
"""Возвращает информацию о текущем авто-промокоде."""
promo = get_or_create_auto_promo(db)
now = datetime.utcnow()
age = (now - promo.created_at).total_seconds()
remaining = max(0, 3600 - age)
return {
"code": promo.code,
"reward_amount": promo.reward_amount,
"created_at": promo.created_at.isoformat(),
"expires_in_seconds": int(remaining),
"used_count": promo.used_count,
}
def validate_promo_code(db: Session, code: str) -> Optional[dict]:
"""Проверяет промокод, возвращает данные или None"""
promo = db.query(PromoCode).filter(
PromoCode.code == code,
PromoCode.is_active == True
).first()
if not promo:
return None
now = datetime.utcnow()
if promo.end_date and now > promo.end_date:
return {"error": "Срок действия промокода истёк"}
if promo.start_date and now < promo.start_date:
return {"error": "Промокод ещё не активен"}
if promo.max_uses > 0 and promo.used_count >= promo.max_uses:
return {"error": "Промокод уже использован максимальное количество раз"}
return {
"id": promo.id,
"code": promo.code,
"reward_type": promo.reward_type,
"reward_amount": promo.reward_amount,
"reward_data": promo.reward_data,
}
async def activate_promo_code(db: Session, user_id: int, code: str, amount: float = 0) -> dict:
"""
Активирует промокод.
Типы: card_to_site, luck_boost, ceiling_boost, free_case, reset_streak
"""
promo_data = validate_promo_code(db, code)
if not promo_data:
return {"success": False, "error": "Промокод не найден"}
if "error" in promo_data:
return {"success": False, "error": promo_data["error"]}
promo = db.query(PromoCode).filter(PromoCode.id == promo_data["id"]).first()
if not promo:
return {"success": False, "error": "Ошибка загрузки промокода"}
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {"success": False, "error": "Пользователь не найден"}
rpu = user.rpu_settings
result = {"success": True, "code": code, "type": promo.reward_type}
promo.used_count += 1
# Записываем транзакцию
txn = TransactionLog(
user_id=user_id,
tx_type="promo_bonus",
amount=0,
promo_code=code,
details=f"Промокод: {code} ({promo.reward_type})"
)
db.add(txn)
if promo.reward_type == "card_to_site":
if amount <= 0:
return {"success": False, "error": "Укажите сумму депозита"}
pct = promo.reward_amount # например 24 → 24%
bonus = round(amount * pct / 100, 2)
total = round(amount + bonus, 2)
# Проверяем баланс карты (требуем только сумму депозита)
bank_balance = await bank_client.get_balance(user_id)
if bank_balance < amount:
return {"success": False, "error": f"Недостаточно средств на карте. Нужно: {amount}"}
# Списываем с карты только депозит
await bank_client.transfer_to_site(user_id, amount, promo_code=code)
# Зачисляем на сайт (депозит + бонус от платформы)
user.balance += total
user.total_deposited += amount
txn.amount = total
txn.details = f"Депозит {amount}₽ + бонус {pct}% ({bonus}₽) по промокоду {code}"
result["amount"] = total
result["deposit"] = amount
result["bonus"] = bonus
result["new_balance"] = user.balance
elif promo.reward_type == "luck_boost":
boost = promo.reward_amount / 100.0 # например 20 → 0.2
if rpu:
rpu.luck_multiplier = min(3.0, rpu.luck_multiplier + boost)
result["boost"] = boost
elif promo.reward_type == "ceiling_boost":
boost_mult = promo.reward_amount / 100.0 + 1.0 # 50 → 1.5
if rpu:
rpu.ceiling_multiplier = min(3.0, rpu.ceiling_multiplier * boost_mult)
result["ceiling_boost"] = boost_mult
elif promo.reward_type == "free_case":
# Бесплатное открытие — просто отмечаем в результате
result["free_case"] = promo.reward_data or ""
elif promo.reward_type == "reset_streak":
if rpu:
rpu.current_streak = 0
rpu.consecutive_loss_count = 0
rpu.consecutive_loss_value = 0.0
result["reset_streak"] = True
db.commit()
return result
async def direct_deposit(db: Session, user_id: int, amount: float, promo_code: str = "") -> dict:
"""
Прямой депозит с карты на сайт.
Если указан промокод — применяет бонус поверх депозита.
"""
if amount <= 0:
return {"success": False, "error": "Сумма должна быть положительной"}
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {"success": False, "error": "Пользователь не найден"}
# Проверка промокода (если указан)
bonus = 0
promo_pct = 0
if promo_code:
promo_data = validate_promo_code(db, promo_code)
if promo_data and "error" not in promo_data:
promo = db.query(PromoCode).filter(PromoCode.id == promo_data["id"]).first()
if promo and promo.reward_type == "card_to_site":
promo_pct = promo.reward_amount
bonus = round(amount * promo_pct / 100, 2)
promo.used_count += 1
total = round(amount + bonus, 2)
# Проверка дневного лимита
today = datetime.utcnow().date()
daily_total = db.query(TransactionLog).filter(
TransactionLog.user_id == user_id,
TransactionLog.tx_type == "card_to_site",
).all()
today_deposits = sum(
t.amount for t in daily_total
if t.created_at.date() == today
)
if today_deposits + amount > DAILY_DEPOSIT_LIMIT:
remaining = max(0, DAILY_DEPOSIT_LIMIT - today_deposits)
return {
"success": False,
"error": f"Дневной лимит депозита: {remaining:.2f}"
}
# Проверяем баланс карты
bank_balance = await bank_client.get_balance(user_id)
if bank_balance < amount:
return {"success": False, "error": "Недостаточно средств на карте"}
# Списываем с карты (только депозит, бонус от платформы)
await bank_client.transfer_to_site(user_id, amount)
# Зачисляем на сайт
user.balance += total
user.total_deposited += amount
user.last_deposit_date = datetime.utcnow()
details = f"Прямой депозит: +{amount}"
if bonus > 0:
details += f" + бонус {promo_pct}% ({bonus}₽)"
txn = TransactionLog(
user_id=user_id,
tx_type="card_to_site",
amount=total,
promo_code=promo_code if promo_code else None,
details=details
)
db.add(txn)
db.commit()
return {
"success": True,
"amount": total,
"deposit": amount,
"bonus": bonus,
"new_balance": user.balance,
"daily_remaining": max(0, DAILY_DEPOSIT_LIMIT - today_deposits - amount)
}
async def withdraw_item(db: Session, user_id: int, inventory_item_id: int) -> dict:
"""
Вывод скина: продажа предмета из инвентаря → зачисление на карту (с комиссией).
"""
from database import InventoryItem
item = db.query(InventoryItem).filter(
InventoryItem.id == inventory_item_id,
InventoryItem.user_id == user_id
).first()
if not item:
return {"success": False, "error": "Предмет не найден в инвентаре"}
price = item.price_rub or 0
if price <= 0:
return {"success": False, "error": "Предмет не имеет цены"}
user = db.query(User).filter(User.id == user_id).first()
result = await bank_client.withdraw_to_card(user_id, price)
if not result["success"]:
return {"success": False, "error": "Ошибка вывода"}
# Обновляем статистику пользователя
user.total_withdrawn += result["net"] # net = price - fee
db.delete(item)
txn = TransactionLog(
user_id=user_id,
tx_type="site_to_card",
amount=price,
fee=result["fee"],
item_name=item.market_hash_name,
inventory_item_id=inventory_item_id,
details=f"Вывод: {item.market_hash_name} за {price}₽ (комиссия {result['fee']}₽)"
)
db.add(txn)
db.commit()
from rpu import check_withdrawal_penalty
check_withdrawal_penalty(db, user_id)
return {
"success": True,
"item_name": item.market_hash_name,
"gross": price,
"fee": result["fee"],
"net": result["net"],
"new_card_balance": result["new_card_balance"]
}