Add files via upload

This commit is contained in:
Egor
2026-01-07 02:16:00 +03:00
committed by GitHub
parent a981bf2ae0
commit cff00eb515
2 changed files with 285 additions and 2 deletions

View File

@@ -46,6 +46,25 @@ server_squad_promo_groups = Table(
)
# M2M таблица для связи тарифов с промогруппами (доступ к тарифу)
tariff_promo_groups = Table(
"tariff_promo_groups",
Base.metadata,
Column(
"tariff_id",
Integer,
ForeignKey("tariffs.id", ondelete="CASCADE"),
primary_key=True,
),
Column(
"promo_group_id",
Integer,
ForeignKey("promo_groups.id", ondelete="CASCADE"),
primary_key=True,
),
)
class UserStatus(Enum):
ACTIVE = "active"
BLOCKED = "blocked"
@@ -714,6 +733,81 @@ class UserPromoGroup(Base):
return f"<UserPromoGroup(user_id={self.user_id}, promo_group_id={self.promo_group_id}, assigned_by='{self.assigned_by}')>"
class Tariff(Base):
"""Тарифный план для режима продаж 'Тарифы'."""
__tablename__ = "tariffs"
id = Column(Integer, primary_key=True, index=True)
# Основная информация
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
display_order = Column(Integer, default=0, nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
# Параметры тарифа
traffic_limit_gb = Column(Integer, nullable=False, default=100) # 0 = безлимит
device_limit = Column(Integer, nullable=False, default=1)
# Сквады (серверы) доступные в тарифе
allowed_squads = Column(JSON, default=list) # список UUID сквадов
# Цены на периоды в копейках (JSON: {"14": 30000, "30": 50000, "90": 120000, ...})
period_prices = Column(JSON, nullable=False, default=dict)
# Уровень тарифа (для визуального отображения, 1 = базовый)
tier_level = Column(Integer, default=1, nullable=False)
# Дополнительные настройки
is_trial_available = Column(Boolean, default=False, nullable=False) # Можно ли взять триал на этом тарифе
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
# M2M связь с промогруппами (какие промогруппы имеют доступ к тарифу)
allowed_promo_groups = relationship(
"PromoGroup",
secondary=tariff_promo_groups,
lazy="selectin",
)
# Подписки на этом тарифе
subscriptions = relationship("Subscription", back_populates="tariff")
@property
def is_unlimited_traffic(self) -> bool:
"""Проверяет, безлимитный ли трафик."""
return self.traffic_limit_gb == 0
def get_price_for_period(self, period_days: int) -> Optional[int]:
"""Возвращает цену в копейках для указанного периода."""
prices = self.period_prices or {}
return prices.get(str(period_days))
def get_available_periods(self) -> List[int]:
"""Возвращает список доступных периодов в днях."""
prices = self.period_prices or {}
return sorted([int(p) for p in prices.keys()])
def get_price_rubles(self, period_days: int) -> Optional[float]:
"""Возвращает цену в рублях для указанного периода."""
price_kopeks = self.get_price_for_period(period_days)
if price_kopeks is not None:
return price_kopeks / 100
return None
def is_available_for_promo_group(self, promo_group_id: Optional[int]) -> bool:
"""Проверяет, доступен ли тариф для указанной промогруппы."""
if not self.allowed_promo_groups:
return True # Если нет ограничений - доступен всем
if promo_group_id is None:
return True # Если у пользователя нет группы - доступен
return any(pg.id == promo_group_id for pg in self.allowed_promo_groups)
def __repr__(self):
return f"<Tariff(id={self.id}, name='{self.name}', tier={self.tier_level}, active={self.is_active})>"
class User(Base):
__tablename__ = "users"
@@ -860,10 +954,14 @@ class Subscription(Base):
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
remnawave_short_uuid = Column(String(255), nullable=True)
# Тариф (для режима продаж "Тарифы")
tariff_id = Column(Integer, ForeignKey("tariffs.id", ondelete="SET NULL"), nullable=True, index=True)
user = relationship("User", back_populates="subscription")
tariff = relationship("Tariff", back_populates="subscriptions")
discount_offers = relationship("DiscountOffer", back_populates="subscription")
temporary_accesses = relationship("SubscriptionTemporaryAccess", back_populates="subscription")
@@ -2108,4 +2206,4 @@ class CabinetRefreshToken(Base):
def __repr__(self) -> str:
status = "valid" if self.is_valid else ("revoked" if self.is_revoked else "expired")
return f"<CabinetRefreshToken id={self.id} user_id={self.user_id} status={status}>"
return f"<CabinetRefreshToken id={self.id} user_id={self.user_id} status={status}>"

View File

@@ -5049,6 +5049,172 @@ async def add_transaction_receipt_columns() -> bool:
return False
# =============================================================================
# МИГРАЦИИ ДЛЯ РЕЖИМА ТАРИФОВ
# =============================================================================
async def create_tariffs_table() -> bool:
"""Создаёт таблицу тарифов для режима продаж 'Тарифы'."""
try:
if await check_table_exists('tariffs'):
logger.info(" Таблица tariffs уже существует")
return True
async with engine.begin() as conn:
db_type = await get_database_type()
if db_type == 'sqlite':
await conn.execute(text("""
CREATE TABLE tariffs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
display_order INTEGER DEFAULT 0 NOT NULL,
is_active BOOLEAN DEFAULT 1 NOT NULL,
traffic_limit_gb INTEGER DEFAULT 100 NOT NULL,
device_limit INTEGER DEFAULT 1 NOT NULL,
allowed_squads JSON DEFAULT '[]',
period_prices JSON DEFAULT '{}' NOT NULL,
tier_level INTEGER DEFAULT 1 NOT NULL,
is_trial_available BOOLEAN DEFAULT 0 NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""))
elif db_type == 'postgresql':
await conn.execute(text("""
CREATE TABLE tariffs (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
display_order INTEGER DEFAULT 0 NOT NULL,
is_active BOOLEAN DEFAULT TRUE NOT NULL,
traffic_limit_gb INTEGER DEFAULT 100 NOT NULL,
device_limit INTEGER DEFAULT 1 NOT NULL,
allowed_squads JSON DEFAULT '[]',
period_prices JSON DEFAULT '{}' NOT NULL,
tier_level INTEGER DEFAULT 1 NOT NULL,
is_trial_available BOOLEAN DEFAULT FALSE NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
"""))
else: # MySQL
await conn.execute(text("""
CREATE TABLE tariffs (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
display_order INT DEFAULT 0 NOT NULL,
is_active BOOLEAN DEFAULT TRUE NOT NULL,
traffic_limit_gb INT DEFAULT 100 NOT NULL,
device_limit INT DEFAULT 1 NOT NULL,
allowed_squads JSON DEFAULT (JSON_ARRAY()),
period_prices JSON NOT NULL,
tier_level INT DEFAULT 1 NOT NULL,
is_trial_available BOOLEAN DEFAULT FALSE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
"""))
logger.info("✅ Таблица tariffs создана")
return True
except Exception as error:
logger.error(f"❌ Ошибка создания таблицы tariffs: {error}")
return False
async def create_tariff_promo_groups_table() -> bool:
"""Создаёт связующую таблицу tariff_promo_groups для M2M связи тарифов и промогрупп."""
try:
if await check_table_exists('tariff_promo_groups'):
logger.info(" Таблица tariff_promo_groups уже существует")
return True
async with engine.begin() as conn:
db_type = await get_database_type()
if db_type == 'sqlite':
await conn.execute(text("""
CREATE TABLE tariff_promo_groups (
tariff_id INTEGER NOT NULL,
promo_group_id INTEGER NOT NULL,
PRIMARY KEY (tariff_id, promo_group_id),
FOREIGN KEY (tariff_id) REFERENCES tariffs(id) ON DELETE CASCADE,
FOREIGN KEY (promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
)
"""))
elif db_type == 'postgresql':
await conn.execute(text("""
CREATE TABLE tariff_promo_groups (
tariff_id INTEGER NOT NULL REFERENCES tariffs(id) ON DELETE CASCADE,
promo_group_id INTEGER NOT NULL REFERENCES promo_groups(id) ON DELETE CASCADE,
PRIMARY KEY (tariff_id, promo_group_id)
)
"""))
else: # MySQL
await conn.execute(text("""
CREATE TABLE tariff_promo_groups (
tariff_id INT NOT NULL,
promo_group_id INT NOT NULL,
PRIMARY KEY (tariff_id, promo_group_id),
FOREIGN KEY (tariff_id) REFERENCES tariffs(id) ON DELETE CASCADE,
FOREIGN KEY (promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
)
"""))
logger.info("✅ Таблица tariff_promo_groups создана")
return True
except Exception as error:
logger.error(f"❌ Ошибка создания таблицы tariff_promo_groups: {error}")
return False
async def add_subscription_tariff_id_column() -> bool:
"""Добавляет колонку tariff_id в таблицу subscriptions."""
try:
if await check_column_exists('subscriptions', 'tariff_id'):
logger.info(" Колонка tariff_id уже существует в subscriptions")
return True
async with engine.begin() as conn:
db_type = await get_database_type()
if db_type == 'sqlite':
await conn.execute(text(
"ALTER TABLE subscriptions ADD COLUMN tariff_id INTEGER REFERENCES tariffs(id)"
))
elif db_type == 'postgresql':
await conn.execute(text(
"ALTER TABLE subscriptions ADD COLUMN tariff_id INTEGER REFERENCES tariffs(id) ON DELETE SET NULL"
))
# Создаём индекс
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS ix_subscriptions_tariff_id ON subscriptions(tariff_id)"
))
else: # MySQL
await conn.execute(text(
"ALTER TABLE subscriptions ADD COLUMN tariff_id INT NULL"
))
await conn.execute(text(
"ALTER TABLE subscriptions ADD CONSTRAINT fk_subscriptions_tariff "
"FOREIGN KEY (tariff_id) REFERENCES tariffs(id) ON DELETE SET NULL"
))
await conn.execute(text(
"CREATE INDEX ix_subscriptions_tariff_id ON subscriptions(tariff_id)"
))
logger.info("✅ Колонка tariff_id добавлена в subscriptions")
return True
except Exception as error:
logger.error(f"❌ Ошибка добавления колонки tariff_id: {error}")
return False
async def run_universal_migration():
logger.info("=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===")
@@ -5526,6 +5692,25 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с настройкой доступа серверов к промогруппам")
logger.info("=== СОЗДАНИЕ ТАБЛИЦ ДЛЯ РЕЖИМА ТАРИФОВ ===")
tariffs_table_ready = await create_tariffs_table()
if tariffs_table_ready:
logger.info("✅ Таблица tariffs готова")
else:
logger.warning("⚠️ Проблемы с таблицей tariffs")
tariff_promo_groups_ready = await create_tariff_promo_groups_table()
if tariff_promo_groups_ready:
logger.info("✅ Таблица tariff_promo_groups готова")
else:
logger.warning("⚠️ Проблемы с таблицей tariff_promo_groups")
tariff_id_column_ready = await add_subscription_tariff_id_column()
if tariff_id_column_ready:
logger.info("✅ Колонка tariff_id в subscriptions готова")
else:
logger.warning("⚠️ Проблемы с колонкой tariff_id в subscriptions")
logger.info("=== ОБНОВЛЕНИЕ ВНЕШНИХ КЛЮЧЕЙ ===")
fk_updated = await fix_foreign_keys_for_user_deletion()
if fk_updated: