47c7f17c90
— 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__
415 lines
18 KiB
Python
415 lines
18 KiB
Python
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
|
||
import json
|
||
from pathlib import Path
|
||
|
||
DATABASE_URL = "sqlite:///./cs2_simulator.db"
|
||
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"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
username = Column(String(50), unique=True, nullable=False)
|
||
hashed_password = Column(String(200), nullable=False)
|
||
balance = Column(Float, default=0.0)
|
||
is_admin = Column(Boolean, default=False)
|
||
is_banned = Column(Boolean, default=False)
|
||
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"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
|
||
|
||
# Множитель шанса апгрейда (1.0 = стандартный)
|
||
upgrade_multiplier = Column(Float, default=1.0)
|
||
|
||
# Автоматическая подкрутка
|
||
auto_adjust = Column(Boolean, default=False)
|
||
|
||
# Статистика
|
||
total_attempts = Column(Integer, default=0)
|
||
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)
|
||
|
||
user = relationship("User", back_populates="upgrade_rpu")
|
||
|
||
|
||
class Upgrade(Base):
|
||
__tablename__ = "upgrades"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
input_item_id = Column(Integer, nullable=False)
|
||
input_item_name = Column(String(200))
|
||
input_item_image_url = Column(String(500), default="")
|
||
target_item_id = Column(Integer, nullable=False)
|
||
target_item_name = Column(String(200))
|
||
target_item_image_url = Column(String(500), default="")
|
||
success = Column(Boolean, default=False)
|
||
probability = Column(Float) # Базовый шанс
|
||
rpu_adjusted_probability = Column(Float) # Шанс с учетом РПУ
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
user = relationship("User", back_populates="upgrades")
|
||
|
||
class UserRPU(Base):
|
||
__tablename__ = "user_rpu"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
|
||
|
||
# Множители шансов для каждой редкости (1.0 = стандартный шанс)
|
||
consumer_multiplier = Column(Float, default=1.0)
|
||
industrial_multiplier = Column(Float, default=1.0)
|
||
mil_spec_multiplier = Column(Float, default=1.0)
|
||
restricted_multiplier = Column(Float, default=1.0)
|
||
classified_multiplier = Column(Float, default=1.0)
|
||
covert_multiplier = Column(Float, default=1.0)
|
||
rare_special_multiplier = Column(Float, default=1.0)
|
||
|
||
# Общий множитель удачи
|
||
luck_multiplier = Column(Float, default=1.0)
|
||
|
||
# Автоматическая подкрутка
|
||
auto_adjust = Column(Boolean, default=False)
|
||
|
||
# Статистика для авто-подкрутки
|
||
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)
|
||
|
||
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"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
item_id = Column(Integer, nullable=False) # ID из ALL_ITEMS
|
||
market_hash_name = Column(String(200), nullable=False)
|
||
rarity = Column(String(50))
|
||
wear = Column(String(50))
|
||
float_value = Column(Float, default=0.0)
|
||
type = Column(String(20), default="Normal")
|
||
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")
|
||
|
||
class CaseOpening(Base):
|
||
__tablename__ = "case_openings"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
case_name = Column(String(100), nullable=False)
|
||
item_id = Column(Integer, nullable=False)
|
||
item_name = Column(String(200), nullable=False)
|
||
rarity = Column(String(50))
|
||
float_value = Column(Float)
|
||
opened_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
user = relationship("User", back_populates="case_openings")
|
||
|
||
class Contract(Base):
|
||
__tablename__ = "contracts"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
input_item_ids = Column(Text, nullable=False) # JSON список ID из инвентаря
|
||
output_item_id = Column(Integer, nullable=False)
|
||
output_item_name = Column(String(200))
|
||
output_float = Column(Float)
|
||
probability = Column(Float)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
user = relationship("User", back_populates="contracts")
|
||
|
||
|
||
class Achievement(Base):
|
||
__tablename__ = "achievements"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
name = Column(String(100), unique=True, nullable=False)
|
||
title = Column(String(200), nullable=False)
|
||
description = Column(String(500))
|
||
icon = Column(String(50), default="🏆")
|
||
category = Column(String(50), default="general") # cases, contracts, upgrade, social, general
|
||
requirement_type = Column(String(50), nullable=False) # open_cases, contracts, upgrade_wins, total_spent, valuable_item, etc.
|
||
requirement_value = Column(Integer, nullable=False) # how many
|
||
reward_amount = Column(Float, default=0.0) # bonus rubles
|
||
sort_order = Column(Integer, default=0)
|
||
hidden = Column(Boolean, default=False) # секретные достижения
|
||
|
||
user_achievements = relationship("UserAchievement", back_populates="achievement")
|
||
|
||
|
||
class UserAchievement(Base):
|
||
__tablename__ = "user_achievements"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
achievement_id = Column(Integer, ForeignKey("achievements.id"), nullable=False)
|
||
progress = Column(Integer, default=0) # текущий прогресс
|
||
unlocked_at = Column(DateTime, nullable=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
user = relationship("User", backref="user_achievements")
|
||
achievement = relationship("Achievement", back_populates="user_achievements")
|
||
|
||
|
||
class ActivityFeed(Base):
|
||
__tablename__ = "activity_feed"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
username = Column(String(50), nullable=False)
|
||
activity_type = Column(String(50), nullable=False) # case_open, contract, upgrade, achievement, trade, crash
|
||
message = Column(String(500), nullable=False)
|
||
data_json = Column(Text, default="{}") # дополнительные данные (item_name, price, etc.)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
user = relationship("User", backref="activities")
|
||
|
||
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
|
||
inspector = inspect(engine)
|
||
cols = [c["name"] for c in inspector.get_columns("achievements")]
|
||
if "hidden" not in cols:
|
||
from sqlalchemy import text
|
||
with engine.connect() as conn:
|
||
conn.execute(text("ALTER TABLE achievements ADD COLUMN hidden BOOLEAN DEFAULT 0"))
|
||
conn.commit()
|
||
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:
|
||
yield db
|
||
finally:
|
||
db.close() |