PostgreSQL + async refactoring, WebSocket server, Redis caching, minified assets
This commit is contained in:
+190
-234
@@ -1,27 +1,59 @@
|
||||
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 os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator, Generator, Optional
|
||||
|
||||
DATABASE_URL = "sqlite:///./cs2_simulator.db"
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
from sqlalchemy import (
|
||||
create_engine, Column, Integer, String, Float, Boolean,
|
||||
DateTime, ForeignKey, Text, text, inspect, Index
|
||||
)
|
||||
from sqlalchemy.orm import sessionmaker, relationship, DeclarativeBase
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
|
||||
# ─── Config ──────────────────────────────────────────────────────────────────
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./cs2_simulator.db")
|
||||
ASYNC_DATABASE_URL = os.getenv("ASYNC_DATABASE_URL", "sqlite+aiosqlite:///./cs2_simulator.db")
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
||||
USE_ASYNC = os.getenv("USE_ASYNC", "0") == "1" or "postgresql" in ASYNC_DATABASE_URL
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
# ─── Sync engine (SQLite fallback / admin) ──────────────────────────────────
|
||||
is_sqlite = DATABASE_URL.startswith("sqlite")
|
||||
sync_connect_args = {"check_same_thread": False} if is_sqlite else {}
|
||||
engine = create_engine(DATABASE_URL, connect_args=sync_connect_args, pool_pre_ping=True)
|
||||
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()
|
||||
if is_sqlite:
|
||||
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()
|
||||
|
||||
# ─── Async engine (PostgreSQL production) ───────────────────────────────────
|
||||
async_engine = create_async_engine(
|
||||
ASYNC_DATABASE_URL,
|
||||
pool_size=20,
|
||||
max_overflow=10,
|
||||
pool_pre_ping=True,
|
||||
echo=False,
|
||||
)
|
||||
AsyncSessionLocal = async_sessionmaker(async_engine, expire_on_commit=False)
|
||||
|
||||
# ─── Models ─────────────────────────────────────────────────────────────────
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
__table_args__ = (
|
||||
Index("ix_users_username", "username"),
|
||||
Index("ix_users_created_at", "created_at"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(50), unique=True, nullable=False)
|
||||
hashed_password = Column(String(200), nullable=False)
|
||||
balance = Column(Float, default=0.0)
|
||||
@@ -29,13 +61,11 @@ class User(Base):
|
||||
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
|
||||
total_deposited = Column(Float, default=0.0)
|
||||
total_withdrawn = Column(Float, default=0.0)
|
||||
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")
|
||||
@@ -44,39 +74,32 @@ class User(Base):
|
||||
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)
|
||||
__table_args__ = (Index("ix_upgrade_rpu_user_id", "user_id", unique=True),)
|
||||
|
||||
id = Column(Integer, primary_key=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) # Стоимость потерянных предметов
|
||||
|
||||
# Серии
|
||||
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)
|
||||
__table_args__ = (Index("ix_upgrades_user_id_created", "user_id", "created_at"),)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
input_item_id = Column(Integer, nullable=False)
|
||||
input_item_name = Column(String(200))
|
||||
@@ -85,19 +108,18 @@ class Upgrade(Base):
|
||||
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) # Шанс с учетом РПУ
|
||||
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)
|
||||
__table_args__ = (Index("ix_user_rpu_user_id", "user_id", unique=True),)
|
||||
|
||||
id = Column(Integer, primary_key=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)
|
||||
@@ -105,64 +127,44 @@ class UserRPU(Base):
|
||||
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) # стоимость последовательных проигрышей
|
||||
luck_budget = Column(Float, default=100.0)
|
||||
session_spent = Column(Float, default=0.0)
|
||||
session_won = Column(Float, default=0.0)
|
||||
session_reset_date = Column(DateTime, nullable=True)
|
||||
ceiling_multiplier = Column(Float, default=1.0)
|
||||
ceiling_break_count = Column(Integer, default=0)
|
||||
ceiling_break_session = Column(Integer, default=0)
|
||||
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 = холодный
|
||||
comeback_multiplier = Column(Float, default=1.0)
|
||||
hot_score = Column(Float, default=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
|
||||
__table_args__ = (Index("ix_promo_codes_code", "code"), Index("ix_promo_codes_active", "is_active"))
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
code = Column(String(50), unique=True, nullable=False)
|
||||
reward_type = Column(String(30), nullable=False)
|
||||
reward_amount = Column(Float, default=0.0)
|
||||
reward_data = Column(String(500), default="") # доп. параметры (JSON)
|
||||
reward_data = Column(String(500), default="")
|
||||
max_uses = Column(Integer, default=1)
|
||||
used_count = Column(Integer, default=0)
|
||||
start_date = Column(DateTime, default=datetime.utcnow)
|
||||
@@ -174,44 +176,57 @@ class PromoCode(Base):
|
||||
|
||||
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
|
||||
__table_args__ = (
|
||||
Index("ix_txlog_user_id", "user_id"),
|
||||
Index("ix_txlog_created", "created_at"),
|
||||
Index("ix_txlog_user_created", "user_id", "created_at"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
tx_type = Column(String(30), nullable=False)
|
||||
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) # при выводе скина
|
||||
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)
|
||||
__table_args__ = (
|
||||
Index("ix_inventory_user_id", "user_id"),
|
||||
Index("ix_inventory_user_obtained", "user_id", "obtained_at"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
item_id = Column(Integer, nullable=False) # ID из ALL_ITEMS
|
||||
item_id = Column(Integer, nullable=False)
|
||||
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_from = Column(String(100))
|
||||
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)
|
||||
__table_args__ = (
|
||||
Index("ix_case_openings_user_id", "user_id"),
|
||||
Index("ix_case_openings_opened_at", "opened_at"),
|
||||
Index("ix_case_openings_user_opened", "user_id", "opened_at"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
case_name = Column(String(100), nullable=False)
|
||||
item_id = Column(Integer, nullable=False)
|
||||
@@ -219,197 +234,138 @@ class CaseOpening(Base):
|
||||
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)
|
||||
__table_args__ = (
|
||||
Index("ix_contracts_user_id", "user_id"),
|
||||
Index("ix_contracts_user_created", "user_id", "created_at"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
input_item_ids = Column(Text, nullable=False) # JSON список ID из инвентаря
|
||||
input_item_ids = Column(Text, nullable=False)
|
||||
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)
|
||||
|
||||
id = Column(Integer, primary_key=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
|
||||
category = Column(String(50), default="general")
|
||||
requirement_type = Column(String(50), nullable=False)
|
||||
requirement_value = Column(Integer, nullable=False)
|
||||
reward_amount = Column(Float, default=0.0)
|
||||
sort_order = Column(Integer, default=0)
|
||||
hidden = Column(Boolean, default=False) # секретные достижения
|
||||
|
||||
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)
|
||||
__table_args__ = (
|
||||
Index("ix_user_achievements_user", "user_id"),
|
||||
Index("ix_user_achievements_ach", "achievement_id"),
|
||||
Index("ix_user_achievements_both", "user_id", "achievement_id", unique=True),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
achievement_id = Column(Integer, ForeignKey("achievements.id"), nullable=False)
|
||||
progress = Column(Integer, default=0) # текущий прогресс
|
||||
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)
|
||||
__table_args__ = (
|
||||
Index("ix_activity_user_id", "user_id"),
|
||||
Index("ix_activity_created", "created_at"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=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
|
||||
activity_type = Column(String(50), nullable=False)
|
||||
message = Column(String(500), nullable=False)
|
||||
data_json = Column(Text, default="{}") # дополнительные данные (item_name, price, etc.)
|
||||
data_json = Column(Text, default="{}")
|
||||
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}")
|
||||
# ─── Create tables ──────────────────────────────────────────────────────────
|
||||
def init_sync_db():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
_run_migrations(engine, is_sync=True)
|
||||
|
||||
# Миграция: добавляем колонку 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}")
|
||||
async def init_async_db():
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# Миграция: новые поля в 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}")
|
||||
def _run_migrations(engine_or_conn, is_sync: bool):
|
||||
try:
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
inspector = sa_inspect(engine_or_conn) if is_sync else sa_inspect(engine_or_conn)
|
||||
existing_tables = inspector.get_table_names()
|
||||
for tbl_name in Base.metadata.tables:
|
||||
if tbl_name not in existing_tables:
|
||||
continue
|
||||
model_cols = {c.name for c in Base.metadata.tables[tbl_name].columns}
|
||||
db_cols = {c["name"] for c in inspector.get_columns(tbl_name)}
|
||||
missing = model_cols - db_cols
|
||||
if missing:
|
||||
print(f"[DB] Migrating {tbl_name}: adding {missing}")
|
||||
except Exception as e:
|
||||
print(f"[DB] Migration check: {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():
|
||||
# ─── Session helpers ────────────────────────────────────────────────────────
|
||||
def get_db() -> Generator:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
db.close()
|
||||
|
||||
|
||||
async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
# ─── Thread-safe sync session wrapper for async endpoints ──────────────────
|
||||
# Use this as a bridge: runs sync DB ops in executor to avoid blocking event loop.
|
||||
# The session is closed when the context exits.
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_db_executor():
|
||||
"""Async context manager wrapping sync SessionLocal in executor thread.
|
||||
Use for endpoints that haven't been fully async-converted yet."""
|
||||
loop = asyncio.get_running_loop()
|
||||
db = await loop.run_in_executor(None, lambda: SessionLocal())
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
await loop.run_in_executor(None, db.close)
|
||||
|
||||
Reference in New Issue
Block a user