mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-04-28 16:50:08 +00:00
Add files via upload
This commit is contained in:
@@ -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}>"
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user