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()