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
+201 -3
View File
@@ -1,4 +1,4 @@
from sqlalchemy import create_engine, Column, Integer, String, Float, Boolean, DateTime, ForeignKey, JSON, Text
from sqlalchemy import create_engine, Column, Integer, String, Float, Boolean, DateTime, ForeignKey, JSON, Text, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime
@@ -10,6 +10,14 @@ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Включаем WAL mode для параллельных чтений и уменьшения SQLITE_BUSY
with engine.connect() as conn:
conn.execute(text("PRAGMA journal_mode=WAL"))
conn.execute(text("PRAGMA synchronous=NORMAL"))
conn.execute(text("PRAGMA cache_size=-64000"))
conn.execute(text("PRAGMA busy_timeout=5000"))
conn.commit()
class User(Base):
__tablename__ = "users"
@@ -22,12 +30,19 @@ class User(Base):
created_at = Column(DateTime, default=datetime.utcnow)
last_login = Column(DateTime, nullable=True)
# Карта (кэш, источник правды — Bank API)
card_balance_cached = Column(Float, default=1000.0)
total_deposited = Column(Float, default=0.0) # всего закинуто card→site
total_withdrawn = Column(Float, default=0.0) # всего выведено site→card
last_deposit_date = Column(DateTime, nullable=True)
inventory_items = relationship("InventoryItem", back_populates="user", cascade="all, delete-orphan")
case_openings = relationship("CaseOpening", back_populates="user", cascade="all, delete-orphan")
contracts = relationship("Contract", back_populates="user", cascade="all, delete-orphan")
rpu_settings = relationship("UserRPU", back_populates="user", uselist=False, cascade="all, delete-orphan")
upgrades = relationship("Upgrade", back_populates="user", cascade="all, delete-orphan")
upgrade_rpu = relationship("UpgradeRPU", back_populates="user", uselist=False, cascade="all, delete-orphan")
transactions = relationship("TransactionLog", back_populates="user", cascade="all, delete-orphan")
class UpgradeRPU(Base):
__tablename__ = "upgrade_rpu"
@@ -46,6 +61,12 @@ class UpgradeRPU(Base):
total_success = Column(Integer, default=0)
total_spent_value = Column(Float, default=0.0) # Стоимость потерянных предметов
# Серии
current_win_streak = Column(Integer, default=0)
current_lose_streak = Column(Integer, default=0)
best_win_streak = Column(Integer, default=0)
worst_lose_streak = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -92,10 +113,41 @@ class UserRPU(Base):
auto_adjust = Column(Boolean, default=False)
# Статистика для авто-подкрутки
total_spent = Column(Float, default=0.0) # Всего потрачено
total_opened = Column(Integer, default=0) # Всего открыто кейсов
total_spent = Column(Float, default=0.0)
total_opened = Column(Integer, default=0)
total_value_received = Column(Float, default=0.0)
last_adjustment = Column(DateTime, nullable=True)
# Серии
current_streak = Column(Integer, default=0)
best_streak = Column(Integer, default=0)
worst_streak = Column(Integer, default=0)
# Последние результаты открытий
last_results = Column(String(500), default="")
# ── RPU v2: Session (сессии) ──
luck_budget = Column(Float, default=100.0) # 0-100, остаток удачи на сессию
session_spent = Column(Float, default=0.0) # потрачено в текущей сессии
session_won = Column(Float, default=0.0) # выиграно (ценность) в сессии
session_reset_date = Column(DateTime, nullable=True) # когда сброшена сессия
# ── RPU v2: Ceiling (потолок) ──
ceiling_multiplier = Column(Float, default=1.0) # временный множитель потолка (0.5-3.0)
ceiling_break_count = Column(Integer, default=0) # сколько раз пробивал потолок
ceiling_break_session = Column(Integer, default=0) # пробитий за сессию
# ── RPU v2: Comeback (комбек) ──
consecutive_loss_value = Column(Float, default=0.0) # стоимость последовательных проигрышей
consecutive_loss_count = Column(Integer, default=0)
comeback_active = Column(Boolean, default=False)
comeback_openings_left = Column(Integer, default=0)
comeback_multiplier = Column(Float, default=1.0) # множитель удачи в режиме комбека
# ── RPU v2: Hot/Cold анализ ──
hot_score = Column(Float, default=0.0) # >0 = горячий, <0 = холодный
last_activity_date = Column(DateTime, nullable=True)
# Мета-данные
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -103,6 +155,40 @@ class UserRPU(Base):
user = relationship("User", back_populates="rpu_settings")
class PromoCode(Base):
__tablename__ = "promo_codes"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(50), unique=True, nullable=False, index=True)
reward_type = Column(String(30), nullable=False) # card_to_site, luck_boost, ceiling_boost, free_case, reset_streak
reward_amount = Column(Float, default=0.0)
reward_data = Column(String(500), default="") # доп. параметры (JSON)
max_uses = Column(Integer, default=1)
used_count = Column(Integer, default=0)
start_date = Column(DateTime, default=datetime.utcnow)
end_date = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
created_by = Column(Integer, nullable=True)
class TransactionLog(Base):
__tablename__ = "transaction_log"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
tx_type = Column(String(30), nullable=False) # card_to_site, site_to_card, promo_bonus, admin
amount = Column(Float, default=0.0)
fee = Column(Float, default=0.0)
promo_code = Column(String(50), nullable=True)
item_name = Column(String(200), nullable=True) # при выводе скина
inventory_item_id = Column(Integer, nullable=True)
details = Column(String(500), default="")
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="transactions")
class InventoryItem(Base):
__tablename__ = "inventory_items"
@@ -117,6 +203,8 @@ class InventoryItem(Base):
obtained_from = Column(String(100)) # "case: название" или "contract"
obtained_at = Column(DateTime, default=datetime.utcnow)
is_equipped = Column(Boolean, default=False)
image_url = Column(String(500), default="")
price_rub = Column(Float, default=0.0)
user = relationship("User", back_populates="inventory_items")
@@ -196,6 +284,30 @@ class ActivityFeed(Base):
Base.metadata.create_all(bind=engine)
# Миграция: добавляем недостающие индексы для производительности
try:
from sqlalchemy import inspect
inspector = inspect(engine)
indices_to_add = {
"inventory_items": ["user_id"],
"case_openings": ["user_id", "opened_at"],
"upgrades": ["user_id"],
"contracts": ["user_id"],
"activity_feed": ["user_id"],
"user_achievements": ["user_id", "achievement_id"],
}
with engine.connect() as conn:
for tbl, cols in indices_to_add.items():
existing = [ix["name"] for ix in inspector.get_indexes(tbl)]
for col in cols:
ix_name = f"ix_{tbl}_{col}"
if ix_name not in existing:
conn.execute(text(f"CREATE INDEX IF NOT EXISTS {ix_name} ON {tbl} ({col})"))
conn.commit()
print("[DB] Индексы: OK")
except Exception as e:
print(f"[DB] Миграция индексов: {e}")
# Миграция: добавляем колонку hidden если её нет
try:
from sqlalchemy import inspect
@@ -209,6 +321,92 @@ try:
except Exception as e:
print(f"[DB] Миграция hidden: {e}")
# Миграция: добавляем image_url и price_rub в inventory_items
try:
from sqlalchemy import text
inspector = inspect(engine)
cols = [c["name"] for c in inspector.get_columns("inventory_items")]
if "image_url" not in cols:
with engine.connect() as conn:
conn.execute(text("ALTER TABLE inventory_items ADD COLUMN image_url VARCHAR(500) DEFAULT ''"))
conn.execute(text("ALTER TABLE inventory_items ADD COLUMN price_rub FLOAT DEFAULT 0.0"))
conn.commit()
# Backfill existing rows from ALL_ITEMS
try:
from backend import ALL_ITEMS
with engine.connect() as conn:
rows = conn.execute(text("SELECT id, item_id FROM inventory_items WHERE image_url = '' OR price_rub = 0.0")).fetchall()
for row_id, item_id in rows:
if 0 <= item_id < len(ALL_ITEMS):
item = ALL_ITEMS[item_id]
img = item.get("image_url", "")
pr = item.get("price_rub", 0.0)
conn.execute(
text("UPDATE inventory_items SET image_url = :img, price_rub = :pr WHERE id = :rid"),
{"img": img, "pr": pr, "rid": row_id}
)
conn.commit()
print(f"[DB] Backfilled {len(rows)} inventory_items from ALL_ITEMS")
except Exception as e2:
print(f"[DB] Backfill inventory_items: {e2}")
except Exception as e:
print(f"[DB] Миграция inventory_items: {e}")
# Миграция: новые поля в users (карта)
try:
inspector = inspect(engine)
cols = [c["name"] for c in inspector.get_columns("users")]
if "card_balance_cached" not in cols:
with engine.connect() as conn:
conn.execute(text("ALTER TABLE users ADD COLUMN card_balance_cached FLOAT DEFAULT 1000.0"))
conn.execute(text("ALTER TABLE users ADD COLUMN total_deposited FLOAT DEFAULT 0.0"))
conn.execute(text("ALTER TABLE users ADD COLUMN total_withdrawn FLOAT DEFAULT 0.0"))
conn.execute(text("ALTER TABLE users ADD COLUMN last_deposit_date TIMESTAMP"))
conn.commit()
print("[DB] Миграция users (card): OK")
except Exception as e:
print(f"[DB] Миграция users card: {e}")
# Миграция: новые поля в user_rpu (v2)
try:
inspector = inspect(engine)
cols = [c["name"] for c in inspector.get_columns("user_rpu")]
for col, coltype in [
("luck_budget", "FLOAT DEFAULT 100.0"),
("session_spent", "FLOAT DEFAULT 0.0"),
("session_won", "FLOAT DEFAULT 0.0"),
("session_reset_date", "TIMESTAMP"),
("ceiling_multiplier", "FLOAT DEFAULT 1.0"),
("ceiling_break_count", "INTEGER DEFAULT 0"),
("ceiling_break_session", "INTEGER DEFAULT 0"),
("consecutive_loss_value", "FLOAT DEFAULT 0.0"),
("consecutive_loss_count", "INTEGER DEFAULT 0"),
("comeback_active", "BOOLEAN DEFAULT 0"),
("comeback_openings_left", "INTEGER DEFAULT 0"),
("comeback_multiplier", "FLOAT DEFAULT 1.0"),
("hot_score", "FLOAT DEFAULT 0.0"),
("last_activity_date", "TIMESTAMP"),
]:
if col not in cols:
with engine.connect() as conn:
conn.execute(text(f"ALTER TABLE user_rpu ADD COLUMN {col} {coltype}"))
conn.commit()
print(f"[DB] Миграция user_rpu: {col}")
except Exception as e:
print(f"[DB] Миграция user_rpu: {e}")
# Миграция: создаём PromoCode если нет
try:
Base.metadata.tables["promo_codes"].create(bind=engine, checkfirst=True)
except:
pass
# Миграция: создаём TransactionLog если нет
try:
Base.metadata.tables["transaction_log"].create(bind=engine, checkfirst=True)
except:
pass
def get_db():
db = SessionLocal()
try: