mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
7106 lines
335 KiB
Python
7106 lines
335 KiB
Python
import logging
|
||
from datetime import datetime
|
||
from typing import List, Tuple
|
||
|
||
from sqlalchemy import inspect, select, text
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import settings
|
||
from app.database.database import AsyncSessionLocal, engine
|
||
from app.database.models import WebApiToken
|
||
from app.utils.security import hash_api_token
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
async def get_database_type():
|
||
return engine.dialect.name
|
||
|
||
|
||
async def sync_postgres_sequences() -> bool:
|
||
"""Ensure PostgreSQL sequences match the current max values after restores."""
|
||
|
||
db_type = await get_database_type()
|
||
|
||
if db_type != "postgresql":
|
||
logger.debug("Пропускаем синхронизацию последовательностей: тип БД %s", db_type)
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
result = await conn.execute(
|
||
text(
|
||
"""
|
||
SELECT
|
||
cols.table_schema,
|
||
cols.table_name,
|
||
cols.column_name,
|
||
pg_get_serial_sequence(
|
||
format('%I.%I', cols.table_schema, cols.table_name),
|
||
cols.column_name
|
||
) AS sequence_path
|
||
FROM information_schema.columns AS cols
|
||
WHERE cols.column_default LIKE 'nextval(%'
|
||
AND cols.table_schema NOT IN ('pg_catalog', 'information_schema')
|
||
"""
|
||
)
|
||
)
|
||
|
||
sequences = result.fetchall()
|
||
|
||
if not sequences:
|
||
logger.info("ℹ️ Не найдено последовательностей PostgreSQL для синхронизации")
|
||
return True
|
||
|
||
for table_schema, table_name, column_name, sequence_path in sequences:
|
||
if not sequence_path:
|
||
continue
|
||
|
||
max_result = await conn.execute(
|
||
text(
|
||
f'SELECT COALESCE(MAX("{column_name}"), 0) '
|
||
f'FROM "{table_schema}"."{table_name}"'
|
||
)
|
||
)
|
||
max_value = max_result.scalar() or 0
|
||
|
||
parts = sequence_path.split('.')
|
||
if len(parts) == 2:
|
||
seq_schema, seq_name = parts
|
||
else:
|
||
seq_schema, seq_name = 'public', parts[-1]
|
||
|
||
seq_schema = seq_schema.strip('"')
|
||
seq_name = seq_name.strip('"')
|
||
current_result = await conn.execute(
|
||
text(
|
||
f'SELECT last_value, is_called FROM "{seq_schema}"."{seq_name}"'
|
||
)
|
||
)
|
||
current_row = current_result.fetchone()
|
||
|
||
if current_row:
|
||
current_last, is_called = current_row
|
||
current_next = current_last + 1 if is_called else current_last
|
||
if current_next > max_value:
|
||
continue
|
||
|
||
await conn.execute(
|
||
text(
|
||
"""
|
||
SELECT setval(:sequence_name, :new_value, TRUE)
|
||
"""
|
||
),
|
||
{"sequence_name": sequence_path, "new_value": max_value},
|
||
)
|
||
logger.info(
|
||
"🔄 Последовательность %s синхронизирована: MAX=%s, следующий ID=%s",
|
||
sequence_path,
|
||
max_value,
|
||
max_value + 1,
|
||
)
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error("❌ Ошибка синхронизации последовательностей PostgreSQL: %s", error)
|
||
return False
|
||
|
||
async def check_table_exists(table_name: str) -> bool:
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
result = await conn.execute(text(f"""
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name='{table_name}'
|
||
"""))
|
||
return result.fetchone() is not None
|
||
|
||
elif db_type == 'postgresql':
|
||
result = await conn.execute(text("""
|
||
SELECT table_name FROM information_schema.tables
|
||
WHERE table_schema = 'public' AND table_name = :table_name
|
||
"""), {"table_name": table_name})
|
||
return result.fetchone() is not None
|
||
|
||
elif db_type == 'mysql':
|
||
result = await conn.execute(text("""
|
||
SELECT table_name FROM information_schema.tables
|
||
WHERE table_schema = DATABASE() AND table_name = :table_name
|
||
"""), {"table_name": table_name})
|
||
return result.fetchone() is not None
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка проверки существования таблицы {table_name}: {e}")
|
||
return False
|
||
|
||
async def check_column_exists(table_name: str, column_name: str) -> bool:
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
result = await conn.execute(text(f"PRAGMA table_info({table_name})"))
|
||
columns = result.fetchall()
|
||
return any(col[1] == column_name for col in columns)
|
||
|
||
elif db_type == 'postgresql':
|
||
result = await conn.execute(text("""
|
||
SELECT column_name
|
||
FROM information_schema.columns
|
||
WHERE table_name = :table_name
|
||
AND column_name = :column_name
|
||
"""), {"table_name": table_name, "column_name": column_name})
|
||
return result.fetchone() is not None
|
||
|
||
elif db_type == 'mysql':
|
||
result = await conn.execute(text("""
|
||
SELECT COLUMN_NAME
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_NAME = :table_name
|
||
AND COLUMN_NAME = :column_name
|
||
"""), {"table_name": table_name, "column_name": column_name})
|
||
return result.fetchone() is not None
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка проверки существования колонки {column_name}: {e}")
|
||
return False
|
||
|
||
|
||
async def check_constraint_exists(table_name: str, constraint_name: str) -> bool:
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "postgresql":
|
||
result = await conn.execute(
|
||
text(
|
||
"""
|
||
SELECT 1
|
||
FROM information_schema.table_constraints
|
||
WHERE table_schema = 'public'
|
||
AND table_name = :table_name
|
||
AND constraint_name = :constraint_name
|
||
"""
|
||
),
|
||
{"table_name": table_name, "constraint_name": constraint_name},
|
||
)
|
||
return result.fetchone() is not None
|
||
|
||
if db_type == "mysql":
|
||
result = await conn.execute(
|
||
text(
|
||
"""
|
||
SELECT 1
|
||
FROM information_schema.table_constraints
|
||
WHERE table_schema = DATABASE()
|
||
AND table_name = :table_name
|
||
AND constraint_name = :constraint_name
|
||
"""
|
||
),
|
||
{"table_name": table_name, "constraint_name": constraint_name},
|
||
)
|
||
return result.fetchone() is not None
|
||
|
||
if db_type == "sqlite":
|
||
result = await conn.execute(text(f"PRAGMA foreign_key_list({table_name})"))
|
||
rows = result.fetchall()
|
||
return any(row[5] == constraint_name for row in rows)
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Ошибка проверки существования ограничения {constraint_name} для {table_name}: {e}"
|
||
)
|
||
return False
|
||
|
||
|
||
async def check_index_exists(table_name: str, index_name: str) -> bool:
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "postgresql":
|
||
result = await conn.execute(
|
||
text(
|
||
"""
|
||
SELECT 1
|
||
FROM pg_indexes
|
||
WHERE schemaname = 'public'
|
||
AND tablename = :table_name
|
||
AND indexname = :index_name
|
||
"""
|
||
),
|
||
{"table_name": table_name, "index_name": index_name},
|
||
)
|
||
return result.fetchone() is not None
|
||
|
||
if db_type == "mysql":
|
||
result = await conn.execute(
|
||
text(
|
||
"""
|
||
SELECT 1
|
||
FROM information_schema.statistics
|
||
WHERE table_schema = DATABASE()
|
||
AND table_name = :table_name
|
||
AND index_name = :index_name
|
||
"""
|
||
),
|
||
{"table_name": table_name, "index_name": index_name},
|
||
)
|
||
return result.fetchone() is not None
|
||
|
||
if db_type == "sqlite":
|
||
result = await conn.execute(text(f"PRAGMA index_list({table_name})"))
|
||
rows = result.fetchall()
|
||
return any(row[1] == index_name for row in rows)
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Ошибка проверки существования индекса {index_name} для {table_name}: {e}"
|
||
)
|
||
return False
|
||
|
||
|
||
async def fetch_duplicate_payment_links(conn) -> List[Tuple[str, int]]:
|
||
result = await conn.execute(
|
||
text(
|
||
"SELECT payment_link_id, COUNT(*) AS cnt "
|
||
"FROM wata_payments "
|
||
"WHERE payment_link_id IS NOT NULL AND payment_link_id <> '' "
|
||
"GROUP BY payment_link_id "
|
||
"HAVING COUNT(*) > 1"
|
||
)
|
||
)
|
||
return [(row[0], row[1]) for row in result.fetchall()]
|
||
|
||
|
||
def _build_dedup_suffix(base_suffix: str, record_id: int, max_length: int = 64) -> Tuple[str, int]:
|
||
suffix = f"{base_suffix}{record_id}"
|
||
trimmed_length = max_length - len(suffix)
|
||
if trimmed_length < 1:
|
||
# Fallback: use the record id only to stay within the limit.
|
||
suffix = f"dup-{record_id}"
|
||
trimmed_length = max_length - len(suffix)
|
||
return suffix, trimmed_length
|
||
|
||
|
||
async def resolve_duplicate_payment_links(conn, db_type: str) -> bool:
|
||
duplicates = await fetch_duplicate_payment_links(conn)
|
||
|
||
if not duplicates:
|
||
return True
|
||
|
||
logger.warning(
|
||
"Найдены дубликаты payment_link_id в wata_payments: %s",
|
||
", ".join(f"{link}×{count}" for link, count in duplicates[:5]),
|
||
)
|
||
|
||
for payment_link_id, _ in duplicates:
|
||
result = await conn.execute(
|
||
text(
|
||
"SELECT id, payment_link_id FROM wata_payments "
|
||
"WHERE payment_link_id = :payment_link_id "
|
||
"ORDER BY id"
|
||
),
|
||
{"payment_link_id": payment_link_id},
|
||
)
|
||
|
||
rows = result.fetchall()
|
||
|
||
if not rows:
|
||
continue
|
||
|
||
# Skip the first occurrence to preserve the original link value.
|
||
for duplicate_row in rows[1:]:
|
||
record_id = duplicate_row[0]
|
||
original_link = duplicate_row[1] or ""
|
||
suffix, trimmed_length = _build_dedup_suffix("-dup-", record_id)
|
||
new_base = original_link[:trimmed_length] if trimmed_length > 0 else ""
|
||
new_link = f"{new_base}{suffix}" if new_base else suffix
|
||
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE wata_payments SET payment_link_id = :new_link "
|
||
"WHERE id = :record_id"
|
||
),
|
||
{"new_link": new_link, "record_id": record_id},
|
||
)
|
||
|
||
remaining_duplicates = await fetch_duplicate_payment_links(conn)
|
||
|
||
if remaining_duplicates:
|
||
logger.error(
|
||
"Не удалось устранить дубликаты payment_link_id: %s",
|
||
", ".join(f"{link}×{count}" for link, count in remaining_duplicates[:5]),
|
||
)
|
||
return False
|
||
|
||
logger.info("✅ Дубликаты payment_link_id устранены")
|
||
return True
|
||
|
||
|
||
async def enforce_wata_payment_link_constraints(
|
||
conn,
|
||
db_type: str,
|
||
unique_index_exists: bool,
|
||
legacy_index_exists: bool,
|
||
) -> Tuple[bool, bool]:
|
||
try:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE wata_payments "
|
||
"SET payment_link_id = 'legacy-' || id "
|
||
"WHERE payment_link_id IS NULL OR payment_link_id = ''"
|
||
)
|
||
)
|
||
|
||
if not await resolve_duplicate_payment_links(conn, db_type):
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
if not unique_index_exists:
|
||
await conn.execute(
|
||
text(
|
||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_wata_payment_link "
|
||
"ON wata_payments(payment_link_id)"
|
||
)
|
||
)
|
||
logger.info("✅ Создан уникальный индекс uq_wata_payment_link для payment_link_id")
|
||
unique_index_exists = True
|
||
else:
|
||
logger.info("ℹ️ Уникальный индекс для payment_link_id уже существует")
|
||
|
||
if legacy_index_exists and unique_index_exists:
|
||
await conn.execute(text("DROP INDEX IF EXISTS idx_wata_link_id"))
|
||
logger.info("ℹ️ Удалён устаревший индекс idx_wata_link_id")
|
||
legacy_index_exists = False
|
||
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
if db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE wata_payments "
|
||
"SET payment_link_id = 'legacy-' || id::text "
|
||
"WHERE payment_link_id IS NULL OR payment_link_id = ''"
|
||
)
|
||
)
|
||
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE wata_payments "
|
||
"ALTER COLUMN payment_link_id SET NOT NULL"
|
||
)
|
||
)
|
||
logger.info("✅ Колонка payment_link_id теперь NOT NULL")
|
||
|
||
if not await resolve_duplicate_payment_links(conn, db_type):
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
if not unique_index_exists:
|
||
await conn.execute(
|
||
text(
|
||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_wata_payment_link "
|
||
"ON wata_payments(payment_link_id)"
|
||
)
|
||
)
|
||
logger.info("✅ Создан уникальный индекс uq_wata_payment_link для payment_link_id")
|
||
unique_index_exists = True
|
||
else:
|
||
logger.info("ℹ️ Уникальный индекс для payment_link_id уже существует")
|
||
|
||
if legacy_index_exists and unique_index_exists:
|
||
await conn.execute(text("DROP INDEX IF EXISTS idx_wata_link_id"))
|
||
logger.info("ℹ️ Удалён устаревший индекс idx_wata_link_id")
|
||
legacy_index_exists = False
|
||
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
if db_type == "mysql":
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE wata_payments "
|
||
"SET payment_link_id = CONCAT('legacy-', id) "
|
||
"WHERE payment_link_id IS NULL OR payment_link_id = ''"
|
||
)
|
||
)
|
||
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE wata_payments "
|
||
"MODIFY COLUMN payment_link_id VARCHAR(64) NOT NULL"
|
||
)
|
||
)
|
||
logger.info("✅ Колонка payment_link_id теперь NOT NULL")
|
||
|
||
if not await resolve_duplicate_payment_links(conn, db_type):
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
if not unique_index_exists:
|
||
await conn.execute(
|
||
text(
|
||
"CREATE UNIQUE INDEX uq_wata_payment_link "
|
||
"ON wata_payments(payment_link_id)"
|
||
)
|
||
)
|
||
logger.info("✅ Создан уникальный индекс uq_wata_payment_link для payment_link_id")
|
||
unique_index_exists = True
|
||
else:
|
||
logger.info("ℹ️ Уникальный индекс для payment_link_id уже существует")
|
||
|
||
if legacy_index_exists and unique_index_exists:
|
||
await conn.execute(text("DROP INDEX idx_wata_link_id ON wata_payments"))
|
||
logger.info("ℹ️ Удалён устаревший индекс idx_wata_link_id")
|
||
legacy_index_exists = False
|
||
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
logger.warning(
|
||
"⚠️ Неизвестный тип БД %s — не удалось усилить ограничения payment_link_id", db_type
|
||
)
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка настройки ограничений payment_link_id: {e}")
|
||
return unique_index_exists, legacy_index_exists
|
||
|
||
async def create_cryptobot_payments_table():
|
||
table_exists = await check_table_exists('cryptobot_payments')
|
||
if table_exists:
|
||
logger.info("Таблица cryptobot_payments уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE cryptobot_payments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
invoice_id VARCHAR(255) UNIQUE NOT NULL,
|
||
amount VARCHAR(50) NOT NULL,
|
||
asset VARCHAR(10) NOT NULL,
|
||
status VARCHAR(50) NOT NULL,
|
||
description TEXT NULL,
|
||
payload TEXT NULL,
|
||
bot_invoice_url TEXT NULL,
|
||
mini_app_invoice_url TEXT NULL,
|
||
web_app_invoice_url TEXT NULL,
|
||
paid_at DATETIME NULL,
|
||
transaction_id INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_cryptobot_payments_user_id ON cryptobot_payments(user_id);
|
||
CREATE INDEX idx_cryptobot_payments_invoice_id ON cryptobot_payments(invoice_id);
|
||
CREATE INDEX idx_cryptobot_payments_status ON cryptobot_payments(status);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE cryptobot_payments (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL,
|
||
invoice_id VARCHAR(255) UNIQUE NOT NULL,
|
||
amount VARCHAR(50) NOT NULL,
|
||
asset VARCHAR(10) NOT NULL,
|
||
status VARCHAR(50) NOT NULL,
|
||
description TEXT NULL,
|
||
payload TEXT NULL,
|
||
bot_invoice_url TEXT NULL,
|
||
mini_app_invoice_url TEXT NULL,
|
||
web_app_invoice_url TEXT NULL,
|
||
paid_at TIMESTAMP NULL,
|
||
transaction_id INTEGER NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_cryptobot_payments_user_id ON cryptobot_payments(user_id);
|
||
CREATE INDEX idx_cryptobot_payments_invoice_id ON cryptobot_payments(invoice_id);
|
||
CREATE INDEX idx_cryptobot_payments_status ON cryptobot_payments(status);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE cryptobot_payments (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
invoice_id VARCHAR(255) UNIQUE NOT NULL,
|
||
amount VARCHAR(50) NOT NULL,
|
||
asset VARCHAR(10) NOT NULL,
|
||
status VARCHAR(50) NOT NULL,
|
||
description TEXT NULL,
|
||
payload TEXT NULL,
|
||
bot_invoice_url TEXT NULL,
|
||
mini_app_invoice_url TEXT NULL,
|
||
web_app_invoice_url TEXT NULL,
|
||
paid_at DATETIME NULL,
|
||
transaction_id INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_cryptobot_payments_user_id ON cryptobot_payments(user_id);
|
||
CREATE INDEX idx_cryptobot_payments_invoice_id ON cryptobot_payments(invoice_id);
|
||
CREATE INDEX idx_cryptobot_payments_status ON cryptobot_payments(status);
|
||
"""
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для создания таблицы: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("Таблица cryptobot_payments успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы cryptobot_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def create_heleket_payments_table():
|
||
table_exists = await check_table_exists('heleket_payments')
|
||
if table_exists:
|
||
logger.info("Таблица heleket_payments уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE heleket_payments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
uuid VARCHAR(255) UNIQUE NOT NULL,
|
||
order_id VARCHAR(128) UNIQUE NOT NULL,
|
||
amount VARCHAR(50) NOT NULL,
|
||
currency VARCHAR(10) NOT NULL,
|
||
payer_amount VARCHAR(50) NULL,
|
||
payer_currency VARCHAR(10) NULL,
|
||
exchange_rate DOUBLE PRECISION NULL,
|
||
discount_percent INTEGER NULL,
|
||
status VARCHAR(50) NOT NULL,
|
||
payment_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
paid_at DATETIME NULL,
|
||
expires_at DATETIME NULL,
|
||
transaction_id INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_heleket_payments_user_id ON heleket_payments(user_id);
|
||
CREATE INDEX idx_heleket_payments_uuid ON heleket_payments(uuid);
|
||
CREATE INDEX idx_heleket_payments_order_id ON heleket_payments(order_id);
|
||
CREATE INDEX idx_heleket_payments_status ON heleket_payments(status);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE heleket_payments (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
uuid VARCHAR(255) UNIQUE NOT NULL,
|
||
order_id VARCHAR(128) UNIQUE NOT NULL,
|
||
amount VARCHAR(50) NOT NULL,
|
||
currency VARCHAR(10) NOT NULL,
|
||
payer_amount VARCHAR(50) NULL,
|
||
payer_currency VARCHAR(10) NULL,
|
||
exchange_rate DOUBLE PRECISION NULL,
|
||
discount_percent INTEGER NULL,
|
||
status VARCHAR(50) NOT NULL,
|
||
payment_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
paid_at TIMESTAMP NULL,
|
||
expires_at TIMESTAMP NULL,
|
||
transaction_id INTEGER NULL REFERENCES transactions(id),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX idx_heleket_payments_user_id ON heleket_payments(user_id);
|
||
CREATE INDEX idx_heleket_payments_uuid ON heleket_payments(uuid);
|
||
CREATE INDEX idx_heleket_payments_order_id ON heleket_payments(order_id);
|
||
CREATE INDEX idx_heleket_payments_status ON heleket_payments(status);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE heleket_payments (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
uuid VARCHAR(255) UNIQUE NOT NULL,
|
||
order_id VARCHAR(128) UNIQUE NOT NULL,
|
||
amount VARCHAR(50) NOT NULL,
|
||
currency VARCHAR(10) NOT NULL,
|
||
payer_amount VARCHAR(50) NULL,
|
||
payer_currency VARCHAR(10) NULL,
|
||
exchange_rate DOUBLE NULL,
|
||
discount_percent INT NULL,
|
||
status VARCHAR(50) NOT NULL,
|
||
payment_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
paid_at DATETIME NULL,
|
||
expires_at DATETIME NULL,
|
||
transaction_id INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_heleket_payments_user_id ON heleket_payments(user_id);
|
||
CREATE INDEX idx_heleket_payments_uuid ON heleket_payments(uuid);
|
||
CREATE INDEX idx_heleket_payments_order_id ON heleket_payments(order_id);
|
||
CREATE INDEX idx_heleket_payments_status ON heleket_payments(status);
|
||
"""
|
||
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для таблицы heleket_payments: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("Таблица heleket_payments успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы heleket_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def create_mulenpay_payments_table():
|
||
table_exists = await check_table_exists('mulenpay_payments')
|
||
if table_exists:
|
||
logger.info("Таблица mulenpay_payments уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE mulenpay_payments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
mulen_payment_id INTEGER NULL,
|
||
uuid VARCHAR(255) NOT NULL UNIQUE,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'created',
|
||
is_paid BOOLEAN DEFAULT 0,
|
||
paid_at DATETIME NULL,
|
||
payment_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
transaction_id INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_mulenpay_uuid ON mulenpay_payments(uuid);
|
||
CREATE INDEX idx_mulenpay_payment_id ON mulenpay_payments(mulen_payment_id);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE mulenpay_payments (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
mulen_payment_id INTEGER NULL,
|
||
uuid VARCHAR(255) NOT NULL UNIQUE,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'created',
|
||
is_paid BOOLEAN NOT NULL DEFAULT FALSE,
|
||
paid_at TIMESTAMP NULL,
|
||
payment_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
transaction_id INTEGER NULL REFERENCES transactions(id),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX idx_mulenpay_uuid ON mulenpay_payments(uuid);
|
||
CREATE INDEX idx_mulenpay_payment_id ON mulenpay_payments(mulen_payment_id);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE mulenpay_payments (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
mulen_payment_id INT NULL,
|
||
uuid VARCHAR(255) NOT NULL UNIQUE,
|
||
amount_kopeks INT NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'created',
|
||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||
paid_at DATETIME NULL,
|
||
payment_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
transaction_id INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_mulenpay_uuid ON mulenpay_payments(uuid);
|
||
CREATE INDEX idx_mulenpay_payment_id ON mulenpay_payments(mulen_payment_id);
|
||
"""
|
||
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для таблицы mulenpay_payments: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("Таблица mulenpay_payments успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы mulenpay_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_mulenpay_payment_schema() -> bool:
|
||
logger.info("=== ОБНОВЛЕНИЕ СХЕМЫ MULEN PAY ===")
|
||
|
||
table_exists = await check_table_exists("mulenpay_payments")
|
||
if not table_exists:
|
||
logger.warning("⚠️ Таблица mulenpay_payments отсутствует — создаём заново")
|
||
return await create_mulenpay_payments_table()
|
||
|
||
try:
|
||
column_exists = await check_column_exists("mulenpay_payments", "mulen_payment_id")
|
||
paid_at_column_exists = await check_column_exists("mulenpay_payments", "paid_at")
|
||
index_exists = await check_index_exists("mulenpay_payments", "idx_mulenpay_payment_id")
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not column_exists:
|
||
if db_type == "sqlite":
|
||
alter_sql = "ALTER TABLE mulenpay_payments ADD COLUMN mulen_payment_id INTEGER NULL"
|
||
elif db_type == "postgresql":
|
||
alter_sql = "ALTER TABLE mulenpay_payments ADD COLUMN mulen_payment_id INTEGER NULL"
|
||
elif db_type == "mysql":
|
||
alter_sql = "ALTER TABLE mulenpay_payments ADD COLUMN mulen_payment_id INT NULL"
|
||
else:
|
||
logger.error(
|
||
"Неподдерживаемый тип БД для добавления mulen_payment_id в mulenpay_payments: %s",
|
||
db_type,
|
||
)
|
||
return False
|
||
|
||
await conn.execute(text(alter_sql))
|
||
logger.info("✅ Добавлена колонка mulenpay_payments.mulen_payment_id")
|
||
else:
|
||
logger.info("ℹ️ Колонка mulenpay_payments.mulen_payment_id уже существует")
|
||
|
||
if not paid_at_column_exists:
|
||
if db_type == "sqlite":
|
||
alter_paid_at_sql = "ALTER TABLE mulenpay_payments ADD COLUMN paid_at DATETIME NULL"
|
||
elif db_type == "postgresql":
|
||
alter_paid_at_sql = "ALTER TABLE mulenpay_payments ADD COLUMN paid_at TIMESTAMP NULL"
|
||
elif db_type == "mysql":
|
||
alter_paid_at_sql = "ALTER TABLE mulenpay_payments ADD COLUMN paid_at DATETIME NULL"
|
||
else:
|
||
logger.error(
|
||
"Неподдерживаемый тип БД для добавления paid_at в mulenpay_payments: %s",
|
||
db_type,
|
||
)
|
||
return False
|
||
|
||
await conn.execute(text(alter_paid_at_sql))
|
||
logger.info("✅ Добавлена колонка mulenpay_payments.paid_at")
|
||
else:
|
||
logger.info("ℹ️ Колонка mulenpay_payments.paid_at уже существует")
|
||
|
||
if not index_exists:
|
||
if db_type == "sqlite":
|
||
create_index_sql = (
|
||
"CREATE INDEX IF NOT EXISTS idx_mulenpay_payment_id "
|
||
"ON mulenpay_payments(mulen_payment_id)"
|
||
)
|
||
elif db_type == "postgresql":
|
||
create_index_sql = (
|
||
"CREATE INDEX IF NOT EXISTS idx_mulenpay_payment_id "
|
||
"ON mulenpay_payments(mulen_payment_id)"
|
||
)
|
||
elif db_type == "mysql":
|
||
create_index_sql = (
|
||
"CREATE INDEX idx_mulenpay_payment_id "
|
||
"ON mulenpay_payments(mulen_payment_id)"
|
||
)
|
||
else:
|
||
logger.error(
|
||
"Неподдерживаемый тип БД для создания индекса mulenpay_payment_id: %s",
|
||
db_type,
|
||
)
|
||
return False
|
||
|
||
await conn.execute(text(create_index_sql))
|
||
logger.info("✅ Создан индекс idx_mulenpay_payment_id")
|
||
else:
|
||
logger.info("ℹ️ Индекс idx_mulenpay_payment_id уже существует")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления схемы mulenpay_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def create_pal24_payments_table():
|
||
table_exists = await check_table_exists('pal24_payments')
|
||
if table_exists:
|
||
logger.info("Таблица pal24_payments уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE pal24_payments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
bill_id VARCHAR(255) NOT NULL UNIQUE,
|
||
order_id VARCHAR(255) NULL,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
type VARCHAR(20) NOT NULL DEFAULT 'normal',
|
||
status VARCHAR(50) NOT NULL DEFAULT 'NEW',
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||
paid_at DATETIME NULL,
|
||
last_status VARCHAR(50) NULL,
|
||
last_status_checked_at DATETIME NULL,
|
||
link_url TEXT NULL,
|
||
link_page_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
payment_id VARCHAR(255) NULL,
|
||
payment_status VARCHAR(50) NULL,
|
||
payment_method VARCHAR(50) NULL,
|
||
balance_amount VARCHAR(50) NULL,
|
||
balance_currency VARCHAR(10) NULL,
|
||
payer_account VARCHAR(255) NULL,
|
||
ttl INTEGER NULL,
|
||
expires_at DATETIME NULL,
|
||
transaction_id INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_pal24_bill_id ON pal24_payments(bill_id);
|
||
CREATE INDEX idx_pal24_order_id ON pal24_payments(order_id);
|
||
CREATE INDEX idx_pal24_payment_id ON pal24_payments(payment_id);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE pal24_payments (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
bill_id VARCHAR(255) NOT NULL UNIQUE,
|
||
order_id VARCHAR(255) NULL,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
type VARCHAR(20) NOT NULL DEFAULT 'normal',
|
||
status VARCHAR(50) NOT NULL DEFAULT 'NEW',
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
is_paid BOOLEAN NOT NULL DEFAULT FALSE,
|
||
paid_at TIMESTAMP NULL,
|
||
last_status VARCHAR(50) NULL,
|
||
last_status_checked_at TIMESTAMP NULL,
|
||
link_url TEXT NULL,
|
||
link_page_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
payment_id VARCHAR(255) NULL,
|
||
payment_status VARCHAR(50) NULL,
|
||
payment_method VARCHAR(50) NULL,
|
||
balance_amount VARCHAR(50) NULL,
|
||
balance_currency VARCHAR(10) NULL,
|
||
payer_account VARCHAR(255) NULL,
|
||
ttl INTEGER NULL,
|
||
expires_at TIMESTAMP NULL,
|
||
transaction_id INTEGER NULL REFERENCES transactions(id),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX idx_pal24_bill_id ON pal24_payments(bill_id);
|
||
CREATE INDEX idx_pal24_order_id ON pal24_payments(order_id);
|
||
CREATE INDEX idx_pal24_payment_id ON pal24_payments(payment_id);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE pal24_payments (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
bill_id VARCHAR(255) NOT NULL UNIQUE,
|
||
order_id VARCHAR(255) NULL,
|
||
amount_kopeks INT NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
type VARCHAR(20) NOT NULL DEFAULT 'normal',
|
||
status VARCHAR(50) NOT NULL DEFAULT 'NEW',
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||
paid_at DATETIME NULL,
|
||
last_status VARCHAR(50) NULL,
|
||
last_status_checked_at DATETIME NULL,
|
||
link_url TEXT NULL,
|
||
link_page_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
payment_id VARCHAR(255) NULL,
|
||
payment_status VARCHAR(50) NULL,
|
||
payment_method VARCHAR(50) NULL,
|
||
balance_amount VARCHAR(50) NULL,
|
||
balance_currency VARCHAR(10) NULL,
|
||
payer_account VARCHAR(255) NULL,
|
||
ttl INT NULL,
|
||
expires_at DATETIME NULL,
|
||
transaction_id INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_pal24_bill_id ON pal24_payments(bill_id);
|
||
CREATE INDEX idx_pal24_order_id ON pal24_payments(order_id);
|
||
CREATE INDEX idx_pal24_payment_id ON pal24_payments(payment_id);
|
||
"""
|
||
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для таблицы pal24_payments: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("Таблица pal24_payments успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы pal24_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def create_wata_payments_table():
|
||
table_exists = await check_table_exists('wata_payments')
|
||
if table_exists:
|
||
logger.info("Таблица wata_payments уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE wata_payments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
payment_link_id VARCHAR(64) NOT NULL UNIQUE,
|
||
order_id VARCHAR(255) NULL,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
type VARCHAR(50) NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'Opened',
|
||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||
paid_at DATETIME NULL,
|
||
last_status VARCHAR(50) NULL,
|
||
terminal_public_id VARCHAR(64) NULL,
|
||
url TEXT NULL,
|
||
success_redirect_url TEXT NULL,
|
||
fail_redirect_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
expires_at DATETIME NULL,
|
||
transaction_id INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_wata_link_id ON wata_payments(payment_link_id);
|
||
CREATE INDEX idx_wata_order_id ON wata_payments(order_id);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE wata_payments (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
payment_link_id VARCHAR(64) NOT NULL UNIQUE,
|
||
order_id VARCHAR(255) NULL,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
type VARCHAR(50) NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'Opened',
|
||
is_paid BOOLEAN NOT NULL DEFAULT FALSE,
|
||
paid_at TIMESTAMP NULL,
|
||
last_status VARCHAR(50) NULL,
|
||
terminal_public_id VARCHAR(64) NULL,
|
||
url TEXT NULL,
|
||
success_redirect_url TEXT NULL,
|
||
fail_redirect_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
expires_at TIMESTAMP NULL,
|
||
transaction_id INTEGER NULL REFERENCES transactions(id),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_wata_link_id ON wata_payments(payment_link_id);
|
||
CREATE INDEX idx_wata_order_id ON wata_payments(order_id);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE wata_payments (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
payment_link_id VARCHAR(64) NOT NULL UNIQUE,
|
||
order_id VARCHAR(255) NULL,
|
||
amount_kopeks INT NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
type VARCHAR(50) NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'Opened',
|
||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||
paid_at DATETIME NULL,
|
||
last_status VARCHAR(50) NULL,
|
||
terminal_public_id VARCHAR(64) NULL,
|
||
url TEXT NULL,
|
||
success_redirect_url TEXT NULL,
|
||
fail_redirect_url TEXT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
expires_at DATETIME NULL,
|
||
transaction_id INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_wata_link_id ON wata_payments(payment_link_id);
|
||
CREATE INDEX idx_wata_order_id ON wata_payments(order_id);
|
||
"""
|
||
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для таблицы wata_payments: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("Таблица wata_payments успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы wata_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_wata_payment_schema() -> bool:
|
||
try:
|
||
table_exists = await check_table_exists("wata_payments")
|
||
if not table_exists:
|
||
logger.warning("⚠️ Таблица wata_payments отсутствует — создаём заново")
|
||
return await create_wata_payments_table()
|
||
|
||
db_type = await get_database_type()
|
||
|
||
legacy_link_index_exists = await check_index_exists(
|
||
"wata_payments", "idx_wata_link_id"
|
||
)
|
||
unique_link_index_exists = await check_index_exists(
|
||
"wata_payments", "uq_wata_payment_link"
|
||
)
|
||
builtin_unique_index_exists = await check_index_exists(
|
||
"wata_payments", "wata_payments_payment_link_id_key"
|
||
)
|
||
sqlite_auto_unique_exists = (
|
||
await check_index_exists("wata_payments", "sqlite_autoindex_wata_payments_1")
|
||
if db_type == "sqlite"
|
||
else False
|
||
)
|
||
order_index_exists = await check_index_exists("wata_payments", "idx_wata_order_id")
|
||
|
||
payment_link_column_exists = await check_column_exists(
|
||
"wata_payments", "payment_link_id"
|
||
)
|
||
order_id_column_exists = await check_column_exists("wata_payments", "order_id")
|
||
|
||
unique_index_exists = (
|
||
unique_link_index_exists
|
||
or builtin_unique_index_exists
|
||
or sqlite_auto_unique_exists
|
||
)
|
||
|
||
async with engine.begin() as conn:
|
||
if not payment_link_column_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE wata_payments "
|
||
"ADD COLUMN payment_link_id VARCHAR(64) NOT NULL DEFAULT ''"
|
||
)
|
||
)
|
||
payment_link_column_exists = True
|
||
unique_index_exists = False
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE wata_payments "
|
||
"ADD COLUMN IF NOT EXISTS payment_link_id VARCHAR(64)"
|
||
)
|
||
)
|
||
payment_link_column_exists = True
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text("ALTER TABLE wata_payments ADD COLUMN payment_link_id VARCHAR(64)")
|
||
)
|
||
payment_link_column_exists = True
|
||
else:
|
||
logger.warning(
|
||
"⚠️ Неизвестный тип БД %s — пропущено добавление payment_link_id",
|
||
db_type,
|
||
)
|
||
|
||
if payment_link_column_exists:
|
||
logger.info("✅ Добавлена колонка payment_link_id в wata_payments")
|
||
|
||
if payment_link_column_exists:
|
||
unique_index_exists, legacy_link_index_exists = (
|
||
await enforce_wata_payment_link_constraints(
|
||
conn,
|
||
db_type,
|
||
unique_index_exists,
|
||
legacy_link_index_exists,
|
||
)
|
||
)
|
||
|
||
if not order_id_column_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text("ALTER TABLE wata_payments ADD COLUMN order_id VARCHAR(255)")
|
||
)
|
||
order_id_column_exists = True
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE wata_payments "
|
||
"ADD COLUMN IF NOT EXISTS order_id VARCHAR(255)"
|
||
)
|
||
)
|
||
order_id_column_exists = True
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text("ALTER TABLE wata_payments ADD COLUMN order_id VARCHAR(255)")
|
||
)
|
||
order_id_column_exists = True
|
||
else:
|
||
logger.warning(
|
||
"⚠️ Неизвестный тип БД %s — пропущено добавление order_id",
|
||
db_type,
|
||
)
|
||
|
||
if order_id_column_exists:
|
||
logger.info("✅ Добавлена колонка order_id в wata_payments")
|
||
|
||
if not order_index_exists:
|
||
if not order_id_column_exists:
|
||
logger.warning(
|
||
"⚠️ Пропущено создание индекса idx_wata_order_id — колонка order_id отсутствует"
|
||
)
|
||
else:
|
||
index_created = False
|
||
if db_type in {"sqlite", "postgresql"}:
|
||
await conn.execute(
|
||
text(
|
||
"CREATE INDEX IF NOT EXISTS idx_wata_order_id ON wata_payments(order_id)"
|
||
)
|
||
)
|
||
index_created = True
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text("CREATE INDEX idx_wata_order_id ON wata_payments(order_id)")
|
||
)
|
||
index_created = True
|
||
else:
|
||
logger.warning(
|
||
"⚠️ Неизвестный тип БД %s — пропущено создание индекса idx_wata_order_id",
|
||
db_type,
|
||
)
|
||
|
||
if index_created:
|
||
logger.info("✅ Создан индекс idx_wata_order_id")
|
||
else:
|
||
logger.info("ℹ️ Индекс idx_wata_order_id уже существует")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления схемы wata_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def create_freekassa_payments_table():
|
||
"""Создаёт таблицу freekassa_payments для платежей через Freekassa."""
|
||
table_exists = await check_table_exists('freekassa_payments')
|
||
if table_exists:
|
||
logger.info("Таблица freekassa_payments уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE freekassa_payments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
order_id VARCHAR(64) NOT NULL UNIQUE,
|
||
freekassa_order_id VARCHAR(64) NULL UNIQUE,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||
payment_url TEXT NULL,
|
||
payment_system_id INTEGER NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
paid_at DATETIME NULL,
|
||
expires_at DATETIME NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
transaction_id INTEGER NULL,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_freekassa_user_id ON freekassa_payments(user_id);
|
||
CREATE UNIQUE INDEX idx_freekassa_order_id ON freekassa_payments(order_id);
|
||
CREATE UNIQUE INDEX idx_freekassa_fk_order_id ON freekassa_payments(freekassa_order_id);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE freekassa_payments (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
order_id VARCHAR(64) NOT NULL UNIQUE,
|
||
freekassa_order_id VARCHAR(64) NULL UNIQUE,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||
is_paid BOOLEAN NOT NULL DEFAULT FALSE,
|
||
payment_url TEXT NULL,
|
||
payment_system_id INTEGER NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
paid_at TIMESTAMP NULL,
|
||
expires_at TIMESTAMP NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
transaction_id INTEGER NULL REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_freekassa_user_id ON freekassa_payments(user_id);
|
||
CREATE UNIQUE INDEX idx_freekassa_order_id ON freekassa_payments(order_id);
|
||
CREATE UNIQUE INDEX idx_freekassa_fk_order_id ON freekassa_payments(freekassa_order_id);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE freekassa_payments (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
order_id VARCHAR(64) NOT NULL UNIQUE,
|
||
freekassa_order_id VARCHAR(64) NULL UNIQUE,
|
||
amount_kopeks INT NOT NULL,
|
||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||
description TEXT NULL,
|
||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||
payment_url TEXT NULL,
|
||
payment_system_id INT NULL,
|
||
metadata_json JSON NULL,
|
||
callback_payload JSON NULL,
|
||
paid_at DATETIME NULL,
|
||
expires_at DATETIME NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
transaction_id INT NULL,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||
);
|
||
|
||
CREATE INDEX idx_freekassa_user_id ON freekassa_payments(user_id);
|
||
CREATE UNIQUE INDEX idx_freekassa_order_id ON freekassa_payments(order_id);
|
||
CREATE UNIQUE INDEX idx_freekassa_fk_order_id ON freekassa_payments(freekassa_order_id);
|
||
"""
|
||
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для таблицы freekassa_payments: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("Таблица freekassa_payments успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы freekassa_payments: {e}")
|
||
return False
|
||
|
||
|
||
async def create_discount_offers_table():
|
||
table_exists = await check_table_exists('discount_offers')
|
||
if table_exists:
|
||
logger.info("Таблица discount_offers уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text("""
|
||
CREATE TABLE discount_offers (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
subscription_id INTEGER NULL,
|
||
notification_type VARCHAR(50) NOT NULL,
|
||
discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
expires_at DATETIME NOT NULL,
|
||
claimed_at DATETIME NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount',
|
||
extra_data TEXT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY(subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL
|
||
)
|
||
"""))
|
||
await conn.execute(text("""
|
||
CREATE INDEX IF NOT EXISTS ix_discount_offers_user_type
|
||
ON discount_offers (user_id, notification_type)
|
||
"""))
|
||
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text("""
|
||
CREATE TABLE IF NOT EXISTS discount_offers (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
subscription_id INTEGER NULL REFERENCES subscriptions(id) ON DELETE SET NULL,
|
||
notification_type VARCHAR(50) NOT NULL,
|
||
discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
expires_at TIMESTAMP NOT NULL,
|
||
claimed_at TIMESTAMP NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount',
|
||
extra_data JSON NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
await conn.execute(text("""
|
||
CREATE INDEX IF NOT EXISTS ix_discount_offers_user_type
|
||
ON discount_offers (user_id, notification_type)
|
||
"""))
|
||
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text("""
|
||
CREATE TABLE IF NOT EXISTS discount_offers (
|
||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
subscription_id INTEGER NULL,
|
||
notification_type VARCHAR(50) NOT NULL,
|
||
discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
expires_at DATETIME NOT NULL,
|
||
claimed_at DATETIME NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount',
|
||
extra_data JSON NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_discount_offers_user FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_discount_offers_subscription FOREIGN KEY(subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL
|
||
)
|
||
"""))
|
||
await conn.execute(text("""
|
||
CREATE INDEX ix_discount_offers_user_type
|
||
ON discount_offers (user_id, notification_type)
|
||
"""))
|
||
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Таблица discount_offers успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы discount_offers: {e}")
|
||
return False
|
||
|
||
|
||
async def create_referral_contests_table() -> bool:
|
||
table_exists = await check_table_exists("referral_contests")
|
||
if table_exists:
|
||
logger.info("Таблица referral_contests уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
await conn.execute(text("""
|
||
CREATE TABLE referral_contests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title VARCHAR(255) NOT NULL,
|
||
description TEXT NULL,
|
||
prize_text TEXT NULL,
|
||
contest_type VARCHAR(50) NOT NULL DEFAULT 'referral_paid',
|
||
start_at DATETIME NOT NULL,
|
||
end_at DATETIME NOT NULL,
|
||
daily_summary_time TIME NOT NULL DEFAULT '12:00:00',
|
||
daily_summary_times VARCHAR(255) NULL,
|
||
timezone VARCHAR(64) NOT NULL DEFAULT 'UTC',
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
last_daily_summary_date DATE NULL,
|
||
last_daily_summary_at DATETIME NULL,
|
||
final_summary_sent BOOLEAN NOT NULL DEFAULT 0,
|
||
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE referral_contests (
|
||
id SERIAL PRIMARY KEY,
|
||
title VARCHAR(255) NOT NULL,
|
||
description TEXT NULL,
|
||
prize_text TEXT NULL,
|
||
contest_type VARCHAR(50) NOT NULL DEFAULT 'referral_paid',
|
||
start_at TIMESTAMP NOT NULL,
|
||
end_at TIMESTAMP NOT NULL,
|
||
daily_summary_time TIME NOT NULL DEFAULT '12:00:00',
|
||
daily_summary_times VARCHAR(255) NULL,
|
||
timezone VARCHAR(64) NOT NULL DEFAULT 'UTC',
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
last_daily_summary_date DATE NULL,
|
||
last_daily_summary_at TIMESTAMP NULL,
|
||
final_summary_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
||
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
elif db_type == "mysql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE referral_contests (
|
||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||
title VARCHAR(255) NOT NULL,
|
||
description TEXT NULL,
|
||
prize_text TEXT NULL,
|
||
contest_type VARCHAR(50) NOT NULL DEFAULT 'referral_paid',
|
||
start_at DATETIME NOT NULL,
|
||
end_at DATETIME NOT NULL,
|
||
daily_summary_time TIME NOT NULL DEFAULT '12:00:00',
|
||
daily_summary_times VARCHAR(255) NULL,
|
||
timezone VARCHAR(64) NOT NULL DEFAULT 'UTC',
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
last_daily_summary_date DATE NULL,
|
||
last_daily_summary_at DATETIME NULL,
|
||
final_summary_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
||
created_by INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_referral_contest_creator FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
)
|
||
"""))
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Таблица referral_contests создана")
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"Ошибка создания таблицы referral_contests: {error}")
|
||
return False
|
||
|
||
|
||
async def create_referral_contest_events_table() -> bool:
|
||
table_exists = await check_table_exists("referral_contest_events")
|
||
if table_exists:
|
||
logger.info("Таблица referral_contest_events уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
await conn.execute(text("""
|
||
CREATE TABLE referral_contest_events (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
contest_id INTEGER NOT NULL,
|
||
referrer_id INTEGER NOT NULL,
|
||
referral_id INTEGER NOT NULL,
|
||
event_type VARCHAR(50) NOT NULL,
|
||
amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(contest_id) REFERENCES referral_contests(id) ON DELETE CASCADE,
|
||
FOREIGN KEY(referrer_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY(referral_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
UNIQUE(contest_id, referral_id)
|
||
)
|
||
"""))
|
||
await conn.execute(text("""
|
||
CREATE INDEX IF NOT EXISTS idx_referral_contest_referrer
|
||
ON referral_contest_events (contest_id, referrer_id)
|
||
"""))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE referral_contest_events (
|
||
id SERIAL PRIMARY KEY,
|
||
contest_id INTEGER NOT NULL REFERENCES referral_contests(id) ON DELETE CASCADE,
|
||
referrer_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
referral_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
event_type VARCHAR(50) NOT NULL,
|
||
amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
occurred_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT uq_referral_contest_referral UNIQUE (contest_id, referral_id)
|
||
)
|
||
"""))
|
||
await conn.execute(text("""
|
||
CREATE INDEX IF NOT EXISTS idx_referral_contest_referrer
|
||
ON referral_contest_events (contest_id, referrer_id)
|
||
"""))
|
||
elif db_type == "mysql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE referral_contest_events (
|
||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||
contest_id INTEGER NOT NULL,
|
||
referrer_id INTEGER NOT NULL,
|
||
referral_id INTEGER NOT NULL,
|
||
event_type VARCHAR(50) NOT NULL,
|
||
amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_referral_contest FOREIGN KEY(contest_id) REFERENCES referral_contests(id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_referral_contest_referrer FOREIGN KEY(referrer_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_referral_contest_referral FOREIGN KEY(referral_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
CONSTRAINT uq_referral_contest_referral UNIQUE (contest_id, referral_id)
|
||
)
|
||
"""))
|
||
await conn.execute(text("""
|
||
CREATE INDEX idx_referral_contest_referrer
|
||
ON referral_contest_events (contest_id, referrer_id)
|
||
"""))
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Таблица referral_contest_events создана")
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"Ошибка создания таблицы referral_contest_events: {error}")
|
||
return False
|
||
|
||
|
||
async def ensure_referral_contest_summary_columns() -> bool:
|
||
ok = True
|
||
for column in ["daily_summary_times", "last_daily_summary_at"]:
|
||
exists = await check_column_exists("referral_contests", column)
|
||
if exists:
|
||
logger.info("Колонка %s в referral_contests уже существует", column)
|
||
continue
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
if db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
f"ALTER TABLE referral_contests ADD COLUMN {column} "
|
||
+ ("VARCHAR(255)" if column == "daily_summary_times" else "TIMESTAMP")
|
||
)
|
||
)
|
||
else:
|
||
await conn.execute(
|
||
text(
|
||
f"ALTER TABLE referral_contests ADD COLUMN {column} "
|
||
+ ("VARCHAR(255)" if column == "daily_summary_times" else "DATETIME")
|
||
)
|
||
)
|
||
logger.info("✅ Колонка %s в referral_contests добавлена", column)
|
||
except Exception as error:
|
||
ok = False
|
||
logger.error("Ошибка добавления %s в referral_contests: %s", column, error)
|
||
return ok
|
||
|
||
|
||
async def create_contest_templates_table() -> bool:
|
||
table_exists = await check_table_exists("contest_templates")
|
||
if table_exists:
|
||
logger.info("Таблица contest_templates уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(100) NOT NULL,
|
||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||
description TEXT NULL,
|
||
prize_days INTEGER NOT NULL DEFAULT 1,
|
||
max_winners INTEGER NOT NULL DEFAULT 1,
|
||
attempts_per_user INTEGER NOT NULL DEFAULT 1,
|
||
times_per_day INTEGER NOT NULL DEFAULT 1,
|
||
schedule_times VARCHAR(255) NULL,
|
||
cooldown_hours INTEGER NOT NULL DEFAULT 24,
|
||
payload TEXT NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_templates (
|
||
id SERIAL PRIMARY KEY,
|
||
name VARCHAR(100) NOT NULL,
|
||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||
description TEXT NULL,
|
||
prize_days INTEGER NOT NULL DEFAULT 1,
|
||
max_winners INTEGER NOT NULL DEFAULT 1,
|
||
attempts_per_user INTEGER NOT NULL DEFAULT 1,
|
||
times_per_day INTEGER NOT NULL DEFAULT 1,
|
||
schedule_times VARCHAR(255) NULL,
|
||
cooldown_hours INTEGER NOT NULL DEFAULT 24,
|
||
payload JSON NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
elif db_type == "mysql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_templates (
|
||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||
name VARCHAR(100) NOT NULL,
|
||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||
description TEXT NULL,
|
||
prize_days INTEGER NOT NULL DEFAULT 1,
|
||
max_winners INTEGER NOT NULL DEFAULT 1,
|
||
attempts_per_user INTEGER NOT NULL DEFAULT 1,
|
||
times_per_day INTEGER NOT NULL DEFAULT 1,
|
||
schedule_times VARCHAR(255) NULL,
|
||
cooldown_hours INTEGER NOT NULL DEFAULT 24,
|
||
payload JSON NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Таблица contest_templates создана")
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"Ошибка создания таблицы contest_templates: {error}")
|
||
return False
|
||
|
||
|
||
async def create_contest_rounds_table() -> bool:
|
||
table_exists = await check_table_exists("contest_rounds")
|
||
if table_exists:
|
||
logger.info("Таблица contest_rounds уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_rounds (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
template_id INTEGER NOT NULL,
|
||
starts_at DATETIME NOT NULL,
|
||
ends_at DATETIME NOT NULL,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
payload TEXT NULL,
|
||
winners_count INTEGER NOT NULL DEFAULT 0,
|
||
max_winners INTEGER NOT NULL DEFAULT 1,
|
||
attempts_per_user INTEGER NOT NULL DEFAULT 1,
|
||
message_id BIGINT NULL,
|
||
chat_id BIGINT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(template_id) REFERENCES contest_templates(id) ON DELETE CASCADE
|
||
)
|
||
"""))
|
||
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_contest_round_status ON contest_rounds(status)"))
|
||
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_contest_round_template ON contest_rounds(template_id)"))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_rounds (
|
||
id SERIAL PRIMARY KEY,
|
||
template_id INTEGER NOT NULL REFERENCES contest_templates(id) ON DELETE CASCADE,
|
||
starts_at TIMESTAMP NOT NULL,
|
||
ends_at TIMESTAMP NOT NULL,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
payload JSON NULL,
|
||
winners_count INTEGER NOT NULL DEFAULT 0,
|
||
max_winners INTEGER NOT NULL DEFAULT 1,
|
||
attempts_per_user INTEGER NOT NULL DEFAULT 1,
|
||
message_id BIGINT NULL,
|
||
chat_id BIGINT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_contest_round_status ON contest_rounds(status)"))
|
||
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_contest_round_template ON contest_rounds(template_id)"))
|
||
elif db_type == "mysql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_rounds (
|
||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||
template_id INTEGER NOT NULL,
|
||
starts_at DATETIME NOT NULL,
|
||
ends_at DATETIME NOT NULL,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
payload JSON NULL,
|
||
winners_count INTEGER NOT NULL DEFAULT 0,
|
||
max_winners INTEGER NOT NULL DEFAULT 1,
|
||
attempts_per_user INTEGER NOT NULL DEFAULT 1,
|
||
message_id BIGINT NULL,
|
||
chat_id BIGINT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_contest_round_template FOREIGN KEY(template_id) REFERENCES contest_templates(id) ON DELETE CASCADE
|
||
)
|
||
"""))
|
||
await conn.execute(text("CREATE INDEX idx_contest_round_status ON contest_rounds(status)"))
|
||
await conn.execute(text("CREATE INDEX idx_contest_round_template ON contest_rounds(template_id)"))
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Таблица contest_rounds создана")
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"Ошибка создания таблицы contest_rounds: {error}")
|
||
return False
|
||
|
||
|
||
async def create_contest_attempts_table() -> bool:
|
||
table_exists = await check_table_exists("contest_attempts")
|
||
if table_exists:
|
||
logger.info("Таблица contest_attempts уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_attempts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
round_id INTEGER NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
answer TEXT NULL,
|
||
is_winner BOOLEAN NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(round_id) REFERENCES contest_rounds(id) ON DELETE CASCADE,
|
||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
UNIQUE(round_id, user_id)
|
||
)
|
||
"""))
|
||
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_contest_attempt_round ON contest_attempts(round_id)"))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_attempts (
|
||
id SERIAL PRIMARY KEY,
|
||
round_id INTEGER NOT NULL REFERENCES contest_rounds(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
answer TEXT NULL,
|
||
is_winner BOOLEAN NOT NULL DEFAULT FALSE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT uq_round_user_attempt UNIQUE(round_id, user_id)
|
||
)
|
||
"""))
|
||
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_contest_attempt_round ON contest_attempts(round_id)"))
|
||
elif db_type == "mysql":
|
||
await conn.execute(text("""
|
||
CREATE TABLE contest_attempts (
|
||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||
round_id INTEGER NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
answer TEXT NULL,
|
||
is_winner BOOLEAN NOT NULL DEFAULT FALSE,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_contest_attempt_round FOREIGN KEY(round_id) REFERENCES contest_rounds(id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_contest_attempt_user FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
CONSTRAINT uq_round_user_attempt UNIQUE(round_id, user_id)
|
||
)
|
||
"""))
|
||
await conn.execute(text("CREATE INDEX idx_contest_attempt_round ON contest_attempts(round_id)"))
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Таблица contest_attempts создана")
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"Ошибка создания таблицы contest_attempts: {error}")
|
||
return False
|
||
|
||
|
||
async def ensure_referral_contest_type_column() -> bool:
|
||
column_exists = await check_column_exists("referral_contests", "contest_type")
|
||
if column_exists:
|
||
logger.info("Колонка contest_type в referral_contests уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE referral_contests "
|
||
"ADD COLUMN contest_type VARCHAR(50) NOT NULL DEFAULT 'referral_paid'"
|
||
)
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE referral_contests "
|
||
"ADD COLUMN contest_type VARCHAR(50) NOT NULL DEFAULT 'referral_paid'"
|
||
)
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE referral_contests "
|
||
"ADD COLUMN contest_type VARCHAR(50) NOT NULL DEFAULT 'referral_paid'"
|
||
)
|
||
)
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Колонка contest_type в referral_contests добавлена")
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"Ошибка добавления contest_type в referral_contests: {error}")
|
||
return False
|
||
|
||
|
||
async def ensure_discount_offer_columns():
|
||
try:
|
||
effect_exists = await check_column_exists('discount_offers', 'effect_type')
|
||
extra_exists = await check_column_exists('discount_offers', 'extra_data')
|
||
|
||
if effect_exists and extra_exists:
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not effect_exists:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
|
||
))
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
|
||
))
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
if not extra_exists:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE discount_offers ADD COLUMN extra_data TEXT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE discount_offers ADD COLUMN extra_data JSON NULL"
|
||
))
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE discount_offers ADD COLUMN extra_data JSON NULL"
|
||
))
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
logger.info("✅ Колонки effect_type и extra_data для discount_offers проверены")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления колонок discount_offers: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_user_promo_offer_discount_columns():
|
||
try:
|
||
percent_exists = await check_column_exists('users', 'promo_offer_discount_percent')
|
||
source_exists = await check_column_exists('users', 'promo_offer_discount_source')
|
||
expires_exists = await check_column_exists('users', 'promo_offer_discount_expires_at')
|
||
|
||
if percent_exists and source_exists and expires_exists:
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not percent_exists:
|
||
column_def = 'INTEGER NOT NULL DEFAULT 0'
|
||
if db_type == 'mysql':
|
||
column_def = 'INT NOT NULL DEFAULT 0'
|
||
await conn.execute(text(
|
||
f"ALTER TABLE users ADD COLUMN promo_offer_discount_percent {column_def}"
|
||
))
|
||
|
||
if not source_exists:
|
||
if db_type == 'sqlite':
|
||
column_def = 'TEXT NULL'
|
||
elif db_type == 'postgresql':
|
||
column_def = 'VARCHAR(100) NULL'
|
||
elif db_type == 'mysql':
|
||
column_def = 'VARCHAR(100) NULL'
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
await conn.execute(text(
|
||
f"ALTER TABLE users ADD COLUMN promo_offer_discount_source {column_def}"
|
||
))
|
||
|
||
if not expires_exists:
|
||
if db_type == 'sqlite':
|
||
column_def = 'DATETIME NULL'
|
||
elif db_type == 'postgresql':
|
||
column_def = 'TIMESTAMP NULL'
|
||
elif db_type == 'mysql':
|
||
column_def = 'DATETIME NULL'
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
await conn.execute(text(
|
||
f"ALTER TABLE users ADD COLUMN promo_offer_discount_expires_at {column_def}"
|
||
))
|
||
|
||
logger.info("✅ Колонки promo_offer_discount_* для users проверены")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления колонок promo_offer_discount_*: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_user_notification_settings_column() -> bool:
|
||
"""Ensure notification_settings column exists in users table."""
|
||
try:
|
||
column_exists = await check_column_exists('users', 'notification_settings')
|
||
|
||
if column_exists:
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
column_def = 'TEXT NULL'
|
||
elif db_type == 'postgresql':
|
||
column_def = 'JSONB NULL'
|
||
elif db_type == 'mysql':
|
||
column_def = 'JSON NULL'
|
||
else:
|
||
column_def = 'TEXT NULL'
|
||
|
||
await conn.execute(text(
|
||
f"ALTER TABLE users ADD COLUMN notification_settings {column_def}"
|
||
))
|
||
|
||
logger.info("✅ Колонка notification_settings для users добавлена")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка добавления колонки notification_settings: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_promo_offer_template_active_duration_column() -> bool:
|
||
try:
|
||
column_exists = await check_column_exists('promo_offer_templates', 'active_discount_hours')
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not column_exists:
|
||
if db_type == 'sqlite':
|
||
column_def = 'INTEGER NULL'
|
||
elif db_type == 'postgresql':
|
||
column_def = 'INTEGER NULL'
|
||
elif db_type == 'mysql':
|
||
column_def = 'INT NULL'
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
await conn.execute(text(
|
||
f"ALTER TABLE promo_offer_templates ADD COLUMN active_discount_hours {column_def}"
|
||
))
|
||
|
||
await conn.execute(text(
|
||
"UPDATE promo_offer_templates "
|
||
"SET active_discount_hours = valid_hours "
|
||
"WHERE offer_type IN ('extend_discount', 'purchase_discount') "
|
||
"AND (active_discount_hours IS NULL OR active_discount_hours <= 0)"
|
||
))
|
||
|
||
logger.info("✅ Колонка active_discount_hours в promo_offer_templates актуальна")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления active_discount_hours в promo_offer_templates: {e}")
|
||
return False
|
||
|
||
|
||
async def migrate_discount_offer_effect_types():
|
||
try:
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"UPDATE discount_offers SET effect_type = 'percent_discount' "
|
||
"WHERE effect_type = 'balance_bonus'"
|
||
))
|
||
logger.info("✅ Типы эффектов discount_offers обновлены на percent_discount")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления типов эффектов discount_offers: {e}")
|
||
return False
|
||
|
||
|
||
async def reset_discount_offer_bonuses():
|
||
try:
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"UPDATE discount_offers SET bonus_amount_kopeks = 0 WHERE bonus_amount_kopeks <> 0"
|
||
))
|
||
await conn.execute(text(
|
||
"UPDATE promo_offer_templates SET bonus_amount_kopeks = 0 WHERE bonus_amount_kopeks <> 0"
|
||
))
|
||
logger.info("✅ Бонусы промо-предложений сброшены до нуля")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обнуления бонусов промо-предложений: {e}")
|
||
return False
|
||
|
||
|
||
async def create_promo_offer_templates_table():
|
||
table_exists = await check_table_exists('promo_offer_templates')
|
||
if table_exists:
|
||
logger.info("Таблица promo_offer_templates уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE promo_offer_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(255) NOT NULL,
|
||
offer_type VARCHAR(50) NOT NULL,
|
||
message_text TEXT NOT NULL,
|
||
button_text VARCHAR(255) NOT NULL,
|
||
valid_hours INTEGER NOT NULL DEFAULT 24,
|
||
discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
active_discount_hours INTEGER NULL,
|
||
test_duration_hours INTEGER NULL,
|
||
test_squad_uuids TEXT NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
created_by INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX ix_promo_offer_templates_type ON promo_offer_templates(offer_type);
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE IF NOT EXISTS promo_offer_templates (
|
||
id SERIAL PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
offer_type VARCHAR(50) NOT NULL,
|
||
message_text TEXT NOT NULL,
|
||
button_text VARCHAR(255) NOT NULL,
|
||
valid_hours INTEGER NOT NULL DEFAULT 24,
|
||
discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
active_discount_hours INTEGER NULL,
|
||
test_duration_hours INTEGER NULL,
|
||
test_squad_uuids JSON NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_promo_offer_templates_type ON promo_offer_templates(offer_type);
|
||
"""
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE IF NOT EXISTS promo_offer_templates (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
offer_type VARCHAR(50) NOT NULL,
|
||
message_text TEXT NOT NULL,
|
||
button_text VARCHAR(255) NOT NULL,
|
||
valid_hours INT NOT NULL DEFAULT 24,
|
||
discount_percent INT NOT NULL DEFAULT 0,
|
||
bonus_amount_kopeks INT NOT NULL DEFAULT 0,
|
||
active_discount_hours INT NULL,
|
||
test_duration_hours INT NULL,
|
||
test_squad_uuids JSON NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_by INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX ix_promo_offer_templates_type ON promo_offer_templates(offer_type);
|
||
"""
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
await conn.execute(text(create_sql))
|
||
|
||
logger.info("✅ Таблица promo_offer_templates успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы promo_offer_templates: {e}")
|
||
return False
|
||
|
||
|
||
async def create_main_menu_buttons_table() -> bool:
|
||
table_exists = await check_table_exists('main_menu_buttons')
|
||
if table_exists:
|
||
logger.info("Таблица main_menu_buttons уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE main_menu_buttons (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
text VARCHAR(64) NOT NULL,
|
||
action_type VARCHAR(20) NOT NULL,
|
||
action_value TEXT NOT NULL,
|
||
visibility VARCHAR(20) NOT NULL DEFAULT 'all',
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
display_order INTEGER NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_main_menu_buttons_order ON main_menu_buttons(display_order, id);
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE IF NOT EXISTS main_menu_buttons (
|
||
id SERIAL PRIMARY KEY,
|
||
text VARCHAR(64) NOT NULL,
|
||
action_type VARCHAR(20) NOT NULL,
|
||
action_value TEXT NOT NULL,
|
||
visibility VARCHAR(20) NOT NULL DEFAULT 'all',
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
display_order INTEGER NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_main_menu_buttons_order ON main_menu_buttons(display_order, id);
|
||
"""
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE IF NOT EXISTS main_menu_buttons (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
text VARCHAR(64) NOT NULL,
|
||
action_type VARCHAR(20) NOT NULL,
|
||
action_value TEXT NOT NULL,
|
||
visibility VARCHAR(20) NOT NULL DEFAULT 'all',
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
display_order INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX ix_main_menu_buttons_order ON main_menu_buttons(display_order, id);
|
||
"""
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для таблицы main_menu_buttons: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
|
||
logger.info("✅ Таблица main_menu_buttons успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы main_menu_buttons: {e}")
|
||
return False
|
||
|
||
|
||
async def create_promo_offer_logs_table() -> bool:
|
||
table_exists = await check_table_exists('promo_offer_logs')
|
||
if table_exists:
|
||
logger.info("Таблица promo_offer_logs уже существует")
|
||
return True
|
||
|
||
try:
|
||
db_type = await get_database_type()
|
||
async with engine.begin() as conn:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text("""
|
||
CREATE TABLE IF NOT EXISTS promo_offer_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
offer_id INTEGER NULL REFERENCES discount_offers(id) ON DELETE SET NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
source VARCHAR(100) NULL,
|
||
percent INTEGER NULL,
|
||
effect_type VARCHAR(50) NULL,
|
||
details JSON NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_promo_offer_logs_created_at ON promo_offer_logs(created_at DESC);
|
||
CREATE INDEX IF NOT EXISTS ix_promo_offer_logs_user_id ON promo_offer_logs(user_id);
|
||
"""))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text("""
|
||
CREATE TABLE IF NOT EXISTS promo_offer_logs (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||
offer_id INTEGER REFERENCES discount_offers(id) ON DELETE SET NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
source VARCHAR(100),
|
||
percent INTEGER,
|
||
effect_type VARCHAR(50),
|
||
details JSONB,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_promo_offer_logs_created_at ON promo_offer_logs(created_at DESC);
|
||
CREATE INDEX IF NOT EXISTS ix_promo_offer_logs_user_id ON promo_offer_logs(user_id);
|
||
"""))
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text("""
|
||
CREATE TABLE IF NOT EXISTS promo_offer_logs (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NULL,
|
||
offer_id INT NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
source VARCHAR(100) NULL,
|
||
percent INT NULL,
|
||
effect_type VARCHAR(50) NULL,
|
||
details JSON NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_promo_offer_logs_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||
CONSTRAINT fk_promo_offer_logs_offers FOREIGN KEY (offer_id) REFERENCES discount_offers(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX ix_promo_offer_logs_created_at ON promo_offer_logs(created_at DESC);
|
||
CREATE INDEX ix_promo_offer_logs_user_id ON promo_offer_logs(user_id);
|
||
"""))
|
||
else:
|
||
logger.warning("Неизвестный тип БД для создания promo_offer_logs: %s", db_type)
|
||
return False
|
||
|
||
logger.info("✅ Таблица promo_offer_logs успешно создана")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы promo_offer_logs: {e}")
|
||
return False
|
||
|
||
|
||
async def create_subscription_temporary_access_table():
|
||
table_exists = await check_table_exists('subscription_temporary_access')
|
||
if table_exists:
|
||
logger.info("Таблица subscription_temporary_access уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE subscription_temporary_access (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
subscription_id INTEGER NOT NULL,
|
||
offer_id INTEGER NOT NULL,
|
||
squad_uuid VARCHAR(255) NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
deactivated_at DATETIME NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
was_already_connected BOOLEAN NOT NULL DEFAULT 0,
|
||
FOREIGN KEY(subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE,
|
||
FOREIGN KEY(offer_id) REFERENCES discount_offers(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX ix_temp_access_subscription ON subscription_temporary_access(subscription_id);
|
||
CREATE INDEX ix_temp_access_offer ON subscription_temporary_access(offer_id);
|
||
CREATE INDEX ix_temp_access_active ON subscription_temporary_access(is_active, expires_at);
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE IF NOT EXISTS subscription_temporary_access (
|
||
id SERIAL PRIMARY KEY,
|
||
subscription_id INTEGER NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE,
|
||
offer_id INTEGER NOT NULL REFERENCES discount_offers(id) ON DELETE CASCADE,
|
||
squad_uuid VARCHAR(255) NOT NULL,
|
||
expires_at TIMESTAMP NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
deactivated_at TIMESTAMP NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
was_already_connected BOOLEAN NOT NULL DEFAULT FALSE
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_temp_access_subscription ON subscription_temporary_access(subscription_id);
|
||
CREATE INDEX IF NOT EXISTS ix_temp_access_offer ON subscription_temporary_access(offer_id);
|
||
CREATE INDEX IF NOT EXISTS ix_temp_access_active ON subscription_temporary_access(is_active, expires_at);
|
||
"""
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE IF NOT EXISTS subscription_temporary_access (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
subscription_id INT NOT NULL,
|
||
offer_id INT NOT NULL,
|
||
squad_uuid VARCHAR(255) NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
deactivated_at DATETIME NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
was_already_connected BOOLEAN NOT NULL DEFAULT FALSE,
|
||
FOREIGN KEY(subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE,
|
||
FOREIGN KEY(offer_id) REFERENCES discount_offers(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX ix_temp_access_subscription ON subscription_temporary_access(subscription_id);
|
||
CREATE INDEX ix_temp_access_offer ON subscription_temporary_access(offer_id);
|
||
CREATE INDEX ix_temp_access_active ON subscription_temporary_access(is_active, expires_at);
|
||
"""
|
||
else:
|
||
raise ValueError(f"Unsupported database type: {db_type}")
|
||
|
||
await conn.execute(text(create_sql))
|
||
|
||
logger.info("✅ Таблица subscription_temporary_access успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы subscription_temporary_access: {e}")
|
||
return False
|
||
|
||
async def create_user_messages_table():
|
||
table_exists = await check_table_exists('user_messages')
|
||
if table_exists:
|
||
logger.info("Таблица user_messages уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE user_messages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
message_text TEXT NOT NULL,
|
||
is_active BOOLEAN DEFAULT 1,
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_by INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_user_messages_active ON user_messages(is_active);
|
||
CREATE INDEX idx_user_messages_sort ON user_messages(sort_order, created_at);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE user_messages (
|
||
id SERIAL PRIMARY KEY,
|
||
message_text TEXT NOT NULL,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_by INTEGER NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_user_messages_active ON user_messages(is_active);
|
||
CREATE INDEX idx_user_messages_sort ON user_messages(sort_order, created_at);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE user_messages (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
message_text TEXT NOT NULL,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
sort_order INT DEFAULT 0,
|
||
created_by INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_user_messages_active ON user_messages(is_active);
|
||
CREATE INDEX idx_user_messages_sort ON user_messages(sort_order, created_at);
|
||
"""
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для создания таблицы: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("Таблица user_messages успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы user_messages: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_promo_groups_setup():
|
||
logger.info("=== НАСТРОЙКА ПРОМО ГРУПП ===")
|
||
|
||
try:
|
||
promo_table_exists = await check_table_exists("promo_groups")
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not promo_table_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS promo_groups (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(255) NOT NULL,
|
||
server_discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
traffic_discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
device_discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
is_default BOOLEAN NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
)
|
||
await conn.execute(
|
||
text(
|
||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_promo_groups_name ON promo_groups(name)"
|
||
)
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS promo_groups (
|
||
id SERIAL PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
server_discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
traffic_discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
device_discount_percent INTEGER NOT NULL DEFAULT 0,
|
||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT uq_promo_groups_name UNIQUE (name)
|
||
)
|
||
"""
|
||
)
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS promo_groups (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
server_discount_percent INT NOT NULL DEFAULT 0,
|
||
traffic_discount_percent INT NOT NULL DEFAULT 0,
|
||
device_discount_percent INT NOT NULL DEFAULT 0,
|
||
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uq_promo_groups_name (name)
|
||
) ENGINE=InnoDB
|
||
"""
|
||
)
|
||
)
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для promo_groups: {db_type}")
|
||
return False
|
||
|
||
logger.info("Создана таблица promo_groups")
|
||
|
||
if db_type == "postgresql" and not await check_constraint_exists(
|
||
"promo_groups", "uq_promo_groups_name"
|
||
):
|
||
try:
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD CONSTRAINT uq_promo_groups_name UNIQUE (name)"
|
||
)
|
||
)
|
||
except Exception as e:
|
||
logger.warning(
|
||
f"Не удалось добавить уникальное ограничение uq_promo_groups_name: {e}"
|
||
)
|
||
|
||
period_discounts_column_exists = await check_column_exists(
|
||
"promo_groups", "period_discounts"
|
||
)
|
||
|
||
if not period_discounts_column_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text("ALTER TABLE promo_groups ADD COLUMN period_discounts JSON")
|
||
)
|
||
await conn.execute(
|
||
text("UPDATE promo_groups SET period_discounts = '{}' WHERE period_discounts IS NULL")
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD COLUMN period_discounts JSONB"
|
||
)
|
||
)
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE promo_groups SET period_discounts = '{}'::jsonb WHERE period_discounts IS NULL"
|
||
)
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text("ALTER TABLE promo_groups ADD COLUMN period_discounts JSON")
|
||
)
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE promo_groups SET period_discounts = JSON_OBJECT() WHERE period_discounts IS NULL"
|
||
)
|
||
)
|
||
else:
|
||
logger.error(
|
||
f"Неподдерживаемый тип БД для promo_groups.period_discounts: {db_type}"
|
||
)
|
||
return False
|
||
|
||
logger.info("Добавлена колонка promo_groups.period_discounts")
|
||
|
||
auto_assign_column_exists = await check_column_exists(
|
||
"promo_groups", "auto_assign_total_spent_kopeks"
|
||
)
|
||
|
||
if not auto_assign_column_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD COLUMN auto_assign_total_spent_kopeks INTEGER DEFAULT 0"
|
||
)
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD COLUMN auto_assign_total_spent_kopeks INTEGER DEFAULT 0"
|
||
)
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD COLUMN auto_assign_total_spent_kopeks INT DEFAULT 0"
|
||
)
|
||
)
|
||
else:
|
||
logger.error(
|
||
f"Неподдерживаемый тип БД для promo_groups.auto_assign_total_spent_kopeks: {db_type}"
|
||
)
|
||
return False
|
||
|
||
logger.info(
|
||
"Добавлена колонка promo_groups.auto_assign_total_spent_kopeks"
|
||
)
|
||
|
||
addon_discount_column_exists = await check_column_exists(
|
||
"promo_groups", "apply_discounts_to_addons"
|
||
)
|
||
priority_column_exists = await check_column_exists(
|
||
"promo_groups", "priority"
|
||
)
|
||
|
||
if not addon_discount_column_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD COLUMN apply_discounts_to_addons BOOLEAN NOT NULL DEFAULT 1"
|
||
)
|
||
)
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE promo_groups SET apply_discounts_to_addons = 1 WHERE apply_discounts_to_addons IS NULL"
|
||
)
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD COLUMN apply_discounts_to_addons BOOLEAN NOT NULL DEFAULT TRUE"
|
||
)
|
||
)
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE promo_groups SET apply_discounts_to_addons = TRUE WHERE apply_discounts_to_addons IS NULL"
|
||
)
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE promo_groups ADD COLUMN apply_discounts_to_addons TINYINT(1) NOT NULL DEFAULT 1"
|
||
)
|
||
)
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE promo_groups SET apply_discounts_to_addons = 1 WHERE apply_discounts_to_addons IS NULL"
|
||
)
|
||
)
|
||
else:
|
||
logger.error(
|
||
f"Неподдерживаемый тип БД для promo_groups.apply_discounts_to_addons: {db_type}"
|
||
)
|
||
return False
|
||
|
||
logger.info(
|
||
"Добавлена колонка promo_groups.apply_discounts_to_addons"
|
||
)
|
||
addon_discount_column_exists = True
|
||
|
||
column_exists = await check_column_exists("users", "promo_group_id")
|
||
|
||
if not column_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(text("ALTER TABLE users ADD COLUMN promo_group_id INTEGER"))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text("ALTER TABLE users ADD COLUMN promo_group_id INTEGER"))
|
||
elif db_type == "mysql":
|
||
await conn.execute(text("ALTER TABLE users ADD COLUMN promo_group_id INT"))
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для promo_group_id: {db_type}")
|
||
return False
|
||
|
||
logger.info("Добавлена колонка users.promo_group_id")
|
||
|
||
auto_promo_flag_exists = await check_column_exists(
|
||
"users", "auto_promo_group_assigned"
|
||
)
|
||
|
||
if not auto_promo_flag_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users ADD COLUMN auto_promo_group_assigned BOOLEAN DEFAULT 0"
|
||
)
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users ADD COLUMN auto_promo_group_assigned BOOLEAN DEFAULT FALSE"
|
||
)
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users ADD COLUMN auto_promo_group_assigned TINYINT(1) DEFAULT 0"
|
||
)
|
||
)
|
||
else:
|
||
logger.error(
|
||
f"Неподдерживаемый тип БД для users.auto_promo_group_assigned: {db_type}"
|
||
)
|
||
return False
|
||
|
||
logger.info("Добавлена колонка users.auto_promo_group_assigned")
|
||
|
||
threshold_column_exists = await check_column_exists(
|
||
"users", "auto_promo_group_threshold_kopeks"
|
||
)
|
||
|
||
if not threshold_column_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users ADD COLUMN auto_promo_group_threshold_kopeks INTEGER NOT NULL DEFAULT 0"
|
||
)
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users ADD COLUMN auto_promo_group_threshold_kopeks BIGINT NOT NULL DEFAULT 0"
|
||
)
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users ADD COLUMN auto_promo_group_threshold_kopeks BIGINT NOT NULL DEFAULT 0"
|
||
)
|
||
)
|
||
else:
|
||
logger.error(
|
||
f"Неподдерживаемый тип БД для users.auto_promo_group_threshold_kopeks: {db_type}"
|
||
)
|
||
return False
|
||
|
||
logger.info(
|
||
"Добавлена колонка users.auto_promo_group_threshold_kopeks"
|
||
)
|
||
|
||
index_exists = await check_index_exists("users", "ix_users_promo_group_id")
|
||
|
||
if not index_exists:
|
||
try:
|
||
if db_type == "sqlite":
|
||
await conn.execute(
|
||
text("CREATE INDEX IF NOT EXISTS ix_users_promo_group_id ON users(promo_group_id)")
|
||
)
|
||
elif db_type == "postgresql":
|
||
await conn.execute(
|
||
text("CREATE INDEX IF NOT EXISTS ix_users_promo_group_id ON users(promo_group_id)")
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text("CREATE INDEX ix_users_promo_group_id ON users(promo_group_id)")
|
||
)
|
||
logger.info("Создан индекс ix_users_promo_group_id")
|
||
except Exception as e:
|
||
logger.warning(f"Не удалось создать индекс ix_users_promo_group_id: {e}")
|
||
|
||
default_group_name = "Базовый юзер"
|
||
default_group_id = None
|
||
|
||
result = await conn.execute(
|
||
text(
|
||
"SELECT id, is_default FROM promo_groups WHERE name = :name LIMIT 1"
|
||
),
|
||
{"name": default_group_name},
|
||
)
|
||
row = result.fetchone()
|
||
|
||
if row:
|
||
default_group_id = row[0]
|
||
if not row[1]:
|
||
await conn.execute(
|
||
text(
|
||
"UPDATE promo_groups SET is_default = :is_default WHERE id = :group_id"
|
||
),
|
||
{"is_default": True, "group_id": default_group_id},
|
||
)
|
||
else:
|
||
result = await conn.execute(
|
||
text(
|
||
"SELECT id FROM promo_groups WHERE is_default = :is_default LIMIT 1"
|
||
),
|
||
{"is_default": True},
|
||
)
|
||
existing_default = result.fetchone()
|
||
|
||
if existing_default:
|
||
default_group_id = existing_default[0]
|
||
else:
|
||
insert_params = {
|
||
"name": default_group_name,
|
||
"is_default": True,
|
||
}
|
||
|
||
if priority_column_exists:
|
||
insert_params["priority"] = 0
|
||
|
||
if addon_discount_column_exists and priority_column_exists:
|
||
insert_sql = """
|
||
INSERT INTO promo_groups (
|
||
name,
|
||
priority,
|
||
server_discount_percent,
|
||
traffic_discount_percent,
|
||
device_discount_percent,
|
||
apply_discounts_to_addons,
|
||
is_default
|
||
) VALUES (:name, :priority, 0, 0, 0, :apply_discounts_to_addons, :is_default)
|
||
"""
|
||
insert_params["apply_discounts_to_addons"] = True
|
||
elif addon_discount_column_exists:
|
||
insert_sql = """
|
||
INSERT INTO promo_groups (
|
||
name,
|
||
server_discount_percent,
|
||
traffic_discount_percent,
|
||
device_discount_percent,
|
||
apply_discounts_to_addons,
|
||
is_default
|
||
) VALUES (:name, 0, 0, 0, :apply_discounts_to_addons, :is_default)
|
||
"""
|
||
insert_params["apply_discounts_to_addons"] = True
|
||
elif priority_column_exists:
|
||
insert_sql = """
|
||
INSERT INTO promo_groups (
|
||
name,
|
||
priority,
|
||
server_discount_percent,
|
||
traffic_discount_percent,
|
||
device_discount_percent,
|
||
is_default
|
||
) VALUES (:name, :priority, 0, 0, 0, :is_default)
|
||
"""
|
||
else:
|
||
insert_sql = """
|
||
INSERT INTO promo_groups (
|
||
name,
|
||
server_discount_percent,
|
||
traffic_discount_percent,
|
||
device_discount_percent,
|
||
is_default
|
||
) VALUES (:name, 0, 0, 0, :is_default)
|
||
"""
|
||
|
||
await conn.execute(text(insert_sql), insert_params)
|
||
|
||
result = await conn.execute(
|
||
text(
|
||
"SELECT id FROM promo_groups WHERE name = :name LIMIT 1"
|
||
),
|
||
{"name": default_group_name},
|
||
)
|
||
row = result.fetchone()
|
||
default_group_id = row[0] if row else None
|
||
|
||
if default_group_id is None:
|
||
logger.error("Не удалось определить идентификатор базовой промо-группы")
|
||
return False
|
||
|
||
await conn.execute(
|
||
text(
|
||
"""
|
||
UPDATE users
|
||
SET promo_group_id = :group_id
|
||
WHERE promo_group_id IS NULL
|
||
"""
|
||
),
|
||
{"group_id": default_group_id},
|
||
)
|
||
|
||
if db_type == "postgresql":
|
||
constraint_exists = await check_constraint_exists(
|
||
"users", "fk_users_promo_group_id_promo_groups"
|
||
)
|
||
if not constraint_exists:
|
||
try:
|
||
await conn.execute(
|
||
text(
|
||
"""
|
||
ALTER TABLE users
|
||
ADD CONSTRAINT fk_users_promo_group_id_promo_groups
|
||
FOREIGN KEY (promo_group_id)
|
||
REFERENCES promo_groups(id)
|
||
ON DELETE RESTRICT
|
||
"""
|
||
)
|
||
)
|
||
logger.info("Добавлен внешний ключ users -> promo_groups")
|
||
except Exception as e:
|
||
logger.warning(
|
||
f"Не удалось добавить внешний ключ users.promo_group_id: {e}"
|
||
)
|
||
|
||
try:
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users ALTER COLUMN promo_group_id SET NOT NULL"
|
||
)
|
||
)
|
||
except Exception as e:
|
||
logger.warning(
|
||
f"Не удалось сделать users.promo_group_id NOT NULL: {e}"
|
||
)
|
||
|
||
elif db_type == "mysql":
|
||
constraint_exists = await check_constraint_exists(
|
||
"users", "fk_users_promo_group_id_promo_groups"
|
||
)
|
||
if not constraint_exists:
|
||
try:
|
||
await conn.execute(
|
||
text(
|
||
"""
|
||
ALTER TABLE users
|
||
ADD CONSTRAINT fk_users_promo_group_id_promo_groups
|
||
FOREIGN KEY (promo_group_id)
|
||
REFERENCES promo_groups(id)
|
||
ON DELETE RESTRICT
|
||
"""
|
||
)
|
||
)
|
||
logger.info("Добавлен внешний ключ users -> promo_groups")
|
||
except Exception as e:
|
||
logger.warning(
|
||
f"Не удалось добавить внешний ключ users.promo_group_id: {e}"
|
||
)
|
||
|
||
try:
|
||
await conn.execute(
|
||
text(
|
||
"ALTER TABLE users MODIFY promo_group_id INT NOT NULL"
|
||
)
|
||
)
|
||
except Exception as e:
|
||
logger.warning(
|
||
f"Не удалось сделать users.promo_group_id NOT NULL: {e}"
|
||
)
|
||
|
||
logger.info("✅ Промо группы настроены")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка настройки промо групп: {e}")
|
||
return False
|
||
|
||
async def add_welcome_text_is_enabled_column():
|
||
column_exists = await check_column_exists('welcome_texts', 'is_enabled')
|
||
if column_exists:
|
||
logger.info("Колонка is_enabled уже существует в таблице welcome_texts")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
alter_sql = "ALTER TABLE welcome_texts ADD COLUMN is_enabled BOOLEAN DEFAULT 1 NOT NULL"
|
||
elif db_type == 'postgresql':
|
||
alter_sql = "ALTER TABLE welcome_texts ADD COLUMN is_enabled BOOLEAN DEFAULT TRUE NOT NULL"
|
||
elif db_type == 'mysql':
|
||
alter_sql = "ALTER TABLE welcome_texts ADD COLUMN is_enabled BOOLEAN DEFAULT TRUE NOT NULL"
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для добавления колонки: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(alter_sql))
|
||
logger.info("✅ Поле is_enabled добавлено в таблицу welcome_texts")
|
||
|
||
if db_type == 'sqlite':
|
||
update_sql = "UPDATE welcome_texts SET is_enabled = 1 WHERE is_enabled IS NULL"
|
||
else:
|
||
update_sql = "UPDATE welcome_texts SET is_enabled = TRUE WHERE is_enabled IS NULL"
|
||
|
||
result = await conn.execute(text(update_sql))
|
||
updated_count = result.rowcount
|
||
logger.info(f"Обновлено {updated_count} существующих записей welcome_texts")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при добавлении поля is_enabled: {e}")
|
||
return False
|
||
|
||
async def create_welcome_texts_table():
|
||
table_exists = await check_table_exists('welcome_texts')
|
||
if table_exists:
|
||
logger.info("Таблица welcome_texts уже существует")
|
||
return await add_welcome_text_is_enabled_column()
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE welcome_texts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
text_content TEXT NOT NULL,
|
||
is_active BOOLEAN DEFAULT 1,
|
||
is_enabled BOOLEAN DEFAULT 1 NOT NULL,
|
||
created_by INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_welcome_texts_active ON welcome_texts(is_active);
|
||
CREATE INDEX idx_welcome_texts_enabled ON welcome_texts(is_enabled);
|
||
CREATE INDEX idx_welcome_texts_updated ON welcome_texts(updated_at);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE welcome_texts (
|
||
id SERIAL PRIMARY KEY,
|
||
text_content TEXT NOT NULL,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
is_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
||
created_by INTEGER NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_welcome_texts_active ON welcome_texts(is_active);
|
||
CREATE INDEX idx_welcome_texts_enabled ON welcome_texts(is_enabled);
|
||
CREATE INDEX idx_welcome_texts_updated ON welcome_texts(updated_at);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE welcome_texts (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
text_content TEXT NOT NULL,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
is_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
||
created_by INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_welcome_texts_active ON welcome_texts(is_active);
|
||
CREATE INDEX idx_welcome_texts_enabled ON welcome_texts(is_enabled);
|
||
CREATE INDEX idx_welcome_texts_updated ON welcome_texts(updated_at);
|
||
"""
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для создания таблицы: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица welcome_texts успешно создана с полем is_enabled")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы welcome_texts: {e}")
|
||
return False
|
||
|
||
|
||
async def create_pinned_messages_table():
|
||
table_exists = await check_table_exists("pinned_messages")
|
||
if table_exists:
|
||
logger.info("Таблица pinned_messages уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE pinned_messages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
content TEXT NOT NULL DEFAULT '',
|
||
media_type VARCHAR(32) NULL,
|
||
media_file_id VARCHAR(255) NULL,
|
||
send_before_menu BOOLEAN NOT NULL DEFAULT 1,
|
||
send_on_every_start BOOLEAN NOT NULL DEFAULT 1,
|
||
is_active BOOLEAN DEFAULT 1,
|
||
created_by INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_pinned_messages_active ON pinned_messages(is_active);
|
||
"""
|
||
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE pinned_messages (
|
||
id SERIAL PRIMARY KEY,
|
||
content TEXT NOT NULL DEFAULT '',
|
||
media_type VARCHAR(32) NULL,
|
||
media_file_id VARCHAR(255) NULL,
|
||
send_before_menu BOOLEAN NOT NULL DEFAULT TRUE,
|
||
send_on_every_start BOOLEAN NOT NULL DEFAULT TRUE,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_pinned_messages_active ON pinned_messages(is_active);
|
||
"""
|
||
|
||
elif db_type == "mysql":
|
||
create_sql = """
|
||
CREATE TABLE pinned_messages (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
content TEXT NOT NULL DEFAULT '',
|
||
media_type VARCHAR(32) NULL,
|
||
media_file_id VARCHAR(255) NULL,
|
||
send_before_menu BOOLEAN NOT NULL DEFAULT TRUE,
|
||
send_on_every_start BOOLEAN NOT NULL DEFAULT TRUE,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
created_by INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX ix_pinned_messages_active ON pinned_messages(is_active);
|
||
"""
|
||
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для создания таблицы pinned_messages: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
|
||
logger.info("✅ Таблица pinned_messages успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы pinned_messages: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_pinned_message_media_columns():
|
||
table_exists = await check_table_exists("pinned_messages")
|
||
if not table_exists:
|
||
logger.warning("⚠️ Таблица pinned_messages отсутствует — пропускаем обновление медиа полей")
|
||
return False
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not await check_column_exists("pinned_messages", "media_type"):
|
||
await conn.execute(
|
||
text("ALTER TABLE pinned_messages ADD COLUMN media_type VARCHAR(32)")
|
||
)
|
||
|
||
if not await check_column_exists("pinned_messages", "media_file_id"):
|
||
await conn.execute(
|
||
text("ALTER TABLE pinned_messages ADD COLUMN media_file_id VARCHAR(255)")
|
||
)
|
||
|
||
if not await check_column_exists("pinned_messages", "send_before_menu"):
|
||
default_value = "TRUE" if db_type != "sqlite" else "1"
|
||
await conn.execute(
|
||
text(
|
||
f"ALTER TABLE pinned_messages ADD COLUMN send_before_menu BOOLEAN NOT NULL DEFAULT {default_value}"
|
||
)
|
||
)
|
||
|
||
if not await check_column_exists("pinned_messages", "send_on_every_start"):
|
||
default_value = "TRUE" if db_type != "sqlite" else "1"
|
||
await conn.execute(
|
||
text(
|
||
f"ALTER TABLE pinned_messages ADD COLUMN send_on_every_start BOOLEAN NOT NULL DEFAULT {default_value}"
|
||
)
|
||
)
|
||
|
||
await conn.execute(text("UPDATE pinned_messages SET content = '' WHERE content IS NULL"))
|
||
|
||
if db_type == "postgresql":
|
||
await conn.execute(
|
||
text("ALTER TABLE pinned_messages ALTER COLUMN content SET DEFAULT ''")
|
||
)
|
||
elif db_type == "mysql":
|
||
await conn.execute(
|
||
text("ALTER TABLE pinned_messages MODIFY content TEXT NOT NULL DEFAULT ''")
|
||
)
|
||
else:
|
||
logger.info("ℹ️ Пропускаем установку DEFAULT для content в SQLite")
|
||
|
||
logger.info("✅ Медиа поля pinned_messages приведены в актуальное состояние")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления медиа полей pinned_messages: {e}")
|
||
return False
|
||
|
||
|
||
async def ensure_user_last_pinned_column():
|
||
try:
|
||
async with engine.begin() as conn:
|
||
if not await check_column_exists("users", "last_pinned_message_id"):
|
||
await conn.execute(
|
||
text("ALTER TABLE users ADD COLUMN last_pinned_message_id INTEGER")
|
||
)
|
||
logger.info("✅ Поле last_pinned_message_id у пользователей готово")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка добавления поля last_pinned_message_id: {e}")
|
||
return False
|
||
|
||
async def add_media_fields_to_broadcast_history():
|
||
logger.info("=== ДОБАВЛЕНИЕ ПОЛЕЙ МЕДИА В BROADCAST_HISTORY ===")
|
||
|
||
media_fields = {
|
||
'has_media': 'BOOLEAN DEFAULT FALSE',
|
||
'media_type': 'VARCHAR(20)',
|
||
'media_file_id': 'VARCHAR(255)',
|
||
'media_caption': 'TEXT'
|
||
}
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
for field_name, field_type in media_fields.items():
|
||
field_exists = await check_column_exists('broadcast_history', field_name)
|
||
|
||
if not field_exists:
|
||
logger.info(f"Добавление поля {field_name} в таблицу broadcast_history")
|
||
|
||
if db_type == 'sqlite':
|
||
if 'BOOLEAN' in field_type:
|
||
field_type = field_type.replace('BOOLEAN DEFAULT FALSE', 'BOOLEAN DEFAULT 0')
|
||
elif db_type == 'postgresql':
|
||
if 'BOOLEAN' in field_type:
|
||
field_type = field_type.replace('BOOLEAN DEFAULT FALSE', 'BOOLEAN DEFAULT FALSE')
|
||
elif db_type == 'mysql':
|
||
if 'BOOLEAN' in field_type:
|
||
field_type = field_type.replace('BOOLEAN DEFAULT FALSE', 'BOOLEAN DEFAULT FALSE')
|
||
|
||
alter_sql = f"ALTER TABLE broadcast_history ADD COLUMN {field_name} {field_type}"
|
||
await conn.execute(text(alter_sql))
|
||
logger.info(f"✅ Поле {field_name} успешно добавлено")
|
||
else:
|
||
logger.info(f"Поле {field_name} уже существует в broadcast_history")
|
||
|
||
logger.info("✅ Все поля медиа в broadcast_history готовы")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при добавлении полей медиа в broadcast_history: {e}")
|
||
return False
|
||
|
||
|
||
async def add_ticket_reply_block_columns():
|
||
try:
|
||
col_perm_exists = await check_column_exists('tickets', 'user_reply_block_permanent')
|
||
col_until_exists = await check_column_exists('tickets', 'user_reply_block_until')
|
||
|
||
if col_perm_exists and col_until_exists:
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not col_perm_exists:
|
||
if db_type == 'sqlite':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN user_reply_block_permanent BOOLEAN DEFAULT 0 NOT NULL"
|
||
elif db_type == 'postgresql':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN user_reply_block_permanent BOOLEAN DEFAULT FALSE NOT NULL"
|
||
elif db_type == 'mysql':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN user_reply_block_permanent BOOLEAN DEFAULT FALSE NOT NULL"
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для добавления user_reply_block_permanent: {db_type}")
|
||
return False
|
||
await conn.execute(text(alter_sql))
|
||
logger.info("✅ Добавлена колонка tickets.user_reply_block_permanent")
|
||
|
||
if not col_until_exists:
|
||
if db_type == 'sqlite':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN user_reply_block_until DATETIME NULL"
|
||
elif db_type == 'postgresql':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN user_reply_block_until TIMESTAMP NULL"
|
||
elif db_type == 'mysql':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN user_reply_block_until DATETIME NULL"
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для добавления user_reply_block_until: {db_type}")
|
||
return False
|
||
await conn.execute(text(alter_sql))
|
||
logger.info("✅ Добавлена колонка tickets.user_reply_block_until")
|
||
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка добавления колонок блокировок в tickets: {e}")
|
||
return False
|
||
|
||
|
||
async def add_ticket_sla_columns():
|
||
try:
|
||
col_exists = await check_column_exists('tickets', 'last_sla_reminder_at')
|
||
if col_exists:
|
||
return True
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
if db_type == 'sqlite':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN last_sla_reminder_at DATETIME NULL"
|
||
elif db_type == 'postgresql':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN last_sla_reminder_at TIMESTAMP NULL"
|
||
elif db_type == 'mysql':
|
||
alter_sql = "ALTER TABLE tickets ADD COLUMN last_sla_reminder_at DATETIME NULL"
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для добавления last_sla_reminder_at: {db_type}")
|
||
return False
|
||
await conn.execute(text(alter_sql))
|
||
logger.info("✅ Добавлена колонка tickets.last_sla_reminder_at")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка добавления SLA колонки в tickets: {e}")
|
||
return False
|
||
|
||
|
||
async def add_user_restriction_columns() -> bool:
|
||
"""Добавить колонки ограничений пользователей в таблицу users."""
|
||
try:
|
||
col_topup = await check_column_exists('users', 'restriction_topup')
|
||
col_sub = await check_column_exists('users', 'restriction_subscription')
|
||
col_reason = await check_column_exists('users', 'restriction_reason')
|
||
|
||
if col_topup and col_sub and col_reason:
|
||
logger.info("ℹ️ Колонки ограничений пользователей уже существуют")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not col_topup:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_topup BOOLEAN DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_topup BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_topup BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД: {db_type}")
|
||
return False
|
||
logger.info("✅ Добавлена колонка users.restriction_topup")
|
||
|
||
if not col_sub:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_subscription BOOLEAN DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_subscription BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_subscription BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД: {db_type}")
|
||
return False
|
||
logger.info("✅ Добавлена колонка users.restriction_subscription")
|
||
|
||
if not col_reason:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_reason VARCHAR(500) NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_reason VARCHAR(500) NULL"
|
||
))
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE users ADD COLUMN restriction_reason VARCHAR(500) NULL"
|
||
))
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД: {db_type}")
|
||
return False
|
||
logger.info("✅ Добавлена колонка users.restriction_reason")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка добавления колонок ограничений пользователей: {e}")
|
||
return False
|
||
|
||
|
||
async def add_user_cabinet_columns() -> bool:
|
||
"""Add cabinet (personal account) columns to users table."""
|
||
cabinet_columns = [
|
||
("email", "VARCHAR(255)", "VARCHAR(255)", "VARCHAR(255)"),
|
||
("email_verified", "BOOLEAN DEFAULT 0", "BOOLEAN DEFAULT FALSE", "TINYINT(1) DEFAULT 0"),
|
||
("email_verified_at", "DATETIME", "TIMESTAMP", "DATETIME"),
|
||
("password_hash", "VARCHAR(255)", "VARCHAR(255)", "VARCHAR(255)"),
|
||
("email_verification_token", "VARCHAR(255)", "VARCHAR(255)", "VARCHAR(255)"),
|
||
("email_verification_expires", "DATETIME", "TIMESTAMP", "DATETIME"),
|
||
("password_reset_token", "VARCHAR(255)", "VARCHAR(255)", "VARCHAR(255)"),
|
||
("password_reset_expires", "DATETIME", "TIMESTAMP", "DATETIME"),
|
||
("cabinet_last_login", "DATETIME", "TIMESTAMP", "DATETIME"),
|
||
]
|
||
|
||
try:
|
||
db_type = await get_database_type()
|
||
added_count = 0
|
||
|
||
for col_name, sqlite_type, pg_type, mysql_type in cabinet_columns:
|
||
if await check_column_exists('users', col_name):
|
||
continue
|
||
|
||
async with engine.begin() as conn:
|
||
if db_type == 'sqlite':
|
||
col_type = sqlite_type
|
||
elif db_type == 'postgresql':
|
||
col_type = pg_type
|
||
else:
|
||
col_type = mysql_type
|
||
|
||
await conn.execute(
|
||
text(f"ALTER TABLE users ADD COLUMN {col_name} {col_type}")
|
||
)
|
||
added_count += 1
|
||
logger.info(f"✅ Добавлена колонка users.{col_name}")
|
||
|
||
if added_count == 0:
|
||
logger.info("ℹ️ Все колонки cabinet уже существуют в таблице users")
|
||
else:
|
||
logger.info(f"✅ Добавлено {added_count} колонок cabinet в таблицу users")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка добавления колонок cabinet: {e}")
|
||
return False
|
||
|
||
|
||
async def add_subscription_crypto_link_column() -> bool:
|
||
column_exists = await check_column_exists('subscriptions', 'subscription_crypto_link')
|
||
if column_exists:
|
||
logger.info("ℹ️ Колонка subscription_crypto_link уже существует")
|
||
return True
|
||
|
||
try:
|
||
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 subscription_crypto_link TEXT"))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text("ALTER TABLE subscriptions ADD COLUMN subscription_crypto_link VARCHAR"))
|
||
elif db_type == 'mysql':
|
||
await conn.execute(text("ALTER TABLE subscriptions ADD COLUMN subscription_crypto_link VARCHAR(512)"))
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для добавления subscription_crypto_link: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(
|
||
"UPDATE subscriptions SET subscription_crypto_link = subscription_url "
|
||
"WHERE subscription_crypto_link IS NULL OR subscription_crypto_link = ''"
|
||
))
|
||
|
||
logger.info("✅ Добавлена колонка subscription_crypto_link в таблицу subscriptions")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка добавления колонки subscription_crypto_link: {e}")
|
||
return False
|
||
|
||
|
||
async def fix_foreign_keys_for_user_deletion():
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'postgresql':
|
||
try:
|
||
await conn.execute(text("""
|
||
ALTER TABLE user_messages
|
||
DROP CONSTRAINT IF EXISTS user_messages_created_by_fkey;
|
||
"""))
|
||
|
||
await conn.execute(text("""
|
||
ALTER TABLE user_messages
|
||
ADD CONSTRAINT user_messages_created_by_fkey
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
|
||
"""))
|
||
logger.info("Обновлен внешний ключ user_messages.created_by")
|
||
except Exception as e:
|
||
logger.warning(f"Ошибка обновления FK user_messages: {e}")
|
||
|
||
try:
|
||
await conn.execute(text("""
|
||
ALTER TABLE promocodes
|
||
DROP CONSTRAINT IF EXISTS promocodes_created_by_fkey;
|
||
"""))
|
||
|
||
await conn.execute(text("""
|
||
ALTER TABLE promocodes
|
||
ADD CONSTRAINT promocodes_created_by_fkey
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
|
||
"""))
|
||
logger.info("Обновлен внешний ключ promocodes.created_by")
|
||
except Exception as e:
|
||
logger.warning(f"Ошибка обновления FK promocodes: {e}")
|
||
|
||
logger.info("Внешние ключи обновлены для безопасного удаления пользователей")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обновления внешних ключей: {e}")
|
||
return False
|
||
|
||
async def add_referral_commission_percent_column() -> bool:
|
||
column_exists = await check_column_exists('users', 'referral_commission_percent')
|
||
if column_exists:
|
||
logger.info("ℹ️ Колонка referral_commission_percent уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
alter_sql = "ALTER TABLE users ADD COLUMN referral_commission_percent INTEGER NULL"
|
||
elif db_type == 'postgresql':
|
||
alter_sql = "ALTER TABLE users ADD COLUMN referral_commission_percent INTEGER NULL"
|
||
elif db_type == 'mysql':
|
||
alter_sql = "ALTER TABLE users ADD COLUMN referral_commission_percent INT NULL"
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для добавления referral_commission_percent: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(alter_sql))
|
||
logger.info("✅ Добавлена колонка referral_commission_percent в таблицу users")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"Ошибка добавления referral_commission_percent: {error}")
|
||
return False
|
||
|
||
|
||
async def add_referral_system_columns():
|
||
logger.info("=== МИГРАЦИЯ РЕФЕРАЛЬНОЙ СИСТЕМЫ ===")
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
column_exists = await check_column_exists('users', 'has_made_first_topup')
|
||
|
||
if not column_exists:
|
||
logger.info("Добавление колонки has_made_first_topup в таблицу users")
|
||
|
||
if db_type == 'sqlite':
|
||
column_def = 'BOOLEAN DEFAULT 0'
|
||
else:
|
||
column_def = 'BOOLEAN DEFAULT FALSE'
|
||
|
||
await conn.execute(text(f"ALTER TABLE users ADD COLUMN has_made_first_topup {column_def}"))
|
||
logger.info("Колонка has_made_first_topup успешно добавлена")
|
||
|
||
logger.info("Обновление существующих пользователей...")
|
||
|
||
if db_type == 'sqlite':
|
||
update_sql = """
|
||
UPDATE users
|
||
SET has_made_first_topup = 1
|
||
WHERE balance_kopeks > 0 OR has_had_paid_subscription = 1
|
||
"""
|
||
else:
|
||
update_sql = """
|
||
UPDATE users
|
||
SET has_made_first_topup = TRUE
|
||
WHERE balance_kopeks > 0 OR has_had_paid_subscription = TRUE
|
||
"""
|
||
|
||
result = await conn.execute(text(update_sql))
|
||
updated_count = result.rowcount
|
||
|
||
logger.info(f"Обновлено {updated_count} пользователей с has_made_first_topup = TRUE")
|
||
logger.info("✅ Миграция реферальной системы завершена")
|
||
|
||
return True
|
||
else:
|
||
logger.info("Колонка has_made_first_topup уже существует")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка миграции реферальной системы: {e}")
|
||
return False
|
||
|
||
async def create_subscription_conversions_table():
|
||
table_exists = await check_table_exists('subscription_conversions')
|
||
if table_exists:
|
||
logger.info("Таблица subscription_conversions уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE subscription_conversions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
converted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
trial_duration_days INTEGER NULL,
|
||
payment_method VARCHAR(50) NULL,
|
||
first_payment_amount_kopeks INTEGER NULL,
|
||
first_paid_period_days INTEGER NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
CREATE INDEX idx_subscription_conversions_user_id ON subscription_conversions(user_id);
|
||
CREATE INDEX idx_subscription_conversions_converted_at ON subscription_conversions(converted_at);
|
||
"""
|
||
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE subscription_conversions (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL,
|
||
converted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
trial_duration_days INTEGER NULL,
|
||
payment_method VARCHAR(50) NULL,
|
||
first_payment_amount_kopeks INTEGER NULL,
|
||
first_paid_period_days INTEGER NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
CREATE INDEX idx_subscription_conversions_user_id ON subscription_conversions(user_id);
|
||
CREATE INDEX idx_subscription_conversions_converted_at ON subscription_conversions(converted_at);
|
||
"""
|
||
|
||
elif db_type == 'mysql':
|
||
create_sql = """
|
||
CREATE TABLE subscription_conversions (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
converted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
trial_duration_days INT NULL,
|
||
payment_method VARCHAR(50) NULL,
|
||
first_payment_amount_kopeks INT NULL,
|
||
first_paid_period_days INT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
CREATE INDEX idx_subscription_conversions_user_id ON subscription_conversions(user_id);
|
||
CREATE INDEX idx_subscription_conversions_converted_at ON subscription_conversions(converted_at);
|
||
"""
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для создания таблицы: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица subscription_conversions успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы subscription_conversions: {e}")
|
||
return False
|
||
|
||
|
||
async def create_subscription_events_table():
|
||
table_exists = await check_table_exists("subscription_events")
|
||
if table_exists:
|
||
logger.info("Таблица subscription_events уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE subscription_events (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
event_type VARCHAR(50) NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
subscription_id INTEGER NULL,
|
||
transaction_id INTEGER NULL,
|
||
amount_kopeks INTEGER NULL,
|
||
currency VARCHAR(16) NULL,
|
||
message TEXT NULL,
|
||
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
extra JSON NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX ix_subscription_events_event_type ON subscription_events(event_type);
|
||
CREATE INDEX ix_subscription_events_user_id ON subscription_events(user_id);
|
||
"""
|
||
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE subscription_events (
|
||
id SERIAL PRIMARY KEY,
|
||
event_type VARCHAR(50) NOT NULL,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
subscription_id INTEGER NULL REFERENCES subscriptions(id) ON DELETE SET NULL,
|
||
transaction_id INTEGER NULL REFERENCES transactions(id) ON DELETE SET NULL,
|
||
amount_kopeks INTEGER NULL,
|
||
currency VARCHAR(16) NULL,
|
||
message TEXT NULL,
|
||
occurred_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
extra JSON NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX ix_subscription_events_event_type ON subscription_events(event_type);
|
||
CREATE INDEX ix_subscription_events_user_id ON subscription_events(user_id);
|
||
"""
|
||
|
||
elif db_type == "mysql":
|
||
create_sql = """
|
||
CREATE TABLE subscription_events (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
event_type VARCHAR(50) NOT NULL,
|
||
user_id INT NOT NULL,
|
||
subscription_id INT NULL,
|
||
transaction_id INT NULL,
|
||
amount_kopeks INT NULL,
|
||
currency VARCHAR(16) NULL,
|
||
message TEXT NULL,
|
||
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
extra JSON NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX ix_subscription_events_event_type ON subscription_events(event_type);
|
||
CREATE INDEX ix_subscription_events_user_id ON subscription_events(user_id);
|
||
"""
|
||
else:
|
||
logger.error(f"Неподдерживаемый тип БД для создания таблицы subscription_events: {db_type}")
|
||
return False
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица subscription_events успешно создана")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания таблицы subscription_events: {e}")
|
||
return False
|
||
|
||
async def fix_subscription_duplicates_universal():
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
logger.info(f"Обнаружен тип базы данных: {db_type}")
|
||
|
||
try:
|
||
result = await conn.execute(text("""
|
||
SELECT user_id, COUNT(*) as count
|
||
FROM subscriptions
|
||
GROUP BY user_id
|
||
HAVING COUNT(*) > 1
|
||
"""))
|
||
|
||
duplicates = result.fetchall()
|
||
|
||
if not duplicates:
|
||
logger.info("Дублирующихся подписок не найдено")
|
||
return 0
|
||
|
||
logger.info(f"Найдено {len(duplicates)} пользователей с дублирующимися подписками")
|
||
|
||
total_deleted = 0
|
||
|
||
for user_id_row, count in duplicates:
|
||
user_id = user_id_row
|
||
|
||
if db_type == 'sqlite':
|
||
delete_result = await conn.execute(text("""
|
||
DELETE FROM subscriptions
|
||
WHERE user_id = :user_id AND id NOT IN (
|
||
SELECT MAX(id)
|
||
FROM subscriptions
|
||
WHERE user_id = :user_id
|
||
)
|
||
"""), {"user_id": user_id})
|
||
|
||
elif db_type in ['postgresql', 'mysql']:
|
||
delete_result = await conn.execute(text("""
|
||
DELETE FROM subscriptions
|
||
WHERE user_id = :user_id AND id NOT IN (
|
||
SELECT max_id FROM (
|
||
SELECT MAX(id) as max_id
|
||
FROM subscriptions
|
||
WHERE user_id = :user_id
|
||
) as subquery
|
||
)
|
||
"""), {"user_id": user_id})
|
||
|
||
else:
|
||
subs_result = await conn.execute(text("""
|
||
SELECT id FROM subscriptions
|
||
WHERE user_id = :user_id
|
||
ORDER BY created_at DESC, id DESC
|
||
"""), {"user_id": user_id})
|
||
|
||
sub_ids = [row[0] for row in subs_result.fetchall()]
|
||
|
||
if len(sub_ids) > 1:
|
||
ids_to_delete = sub_ids[1:]
|
||
for sub_id in ids_to_delete:
|
||
await conn.execute(text("""
|
||
DELETE FROM subscriptions WHERE id = :id
|
||
"""), {"id": sub_id})
|
||
delete_result = type('Result', (), {'rowcount': len(ids_to_delete)})()
|
||
else:
|
||
delete_result = type('Result', (), {'rowcount': 0})()
|
||
|
||
deleted_count = delete_result.rowcount
|
||
total_deleted += deleted_count
|
||
logger.info(f"Удалено {deleted_count} дублирующихся подписок для пользователя {user_id}")
|
||
|
||
logger.info(f"Всего удалено дублирующихся подписок: {total_deleted}")
|
||
return total_deleted
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при очистке дублирующихся подписок: {e}")
|
||
raise
|
||
|
||
|
||
async def ensure_server_promo_groups_setup() -> bool:
|
||
logger.info("=== НАСТРОЙКА ДОСТУПА СЕРВЕРОВ К ПРОМОГРУППАМ ===")
|
||
|
||
try:
|
||
table_exists = await check_table_exists("server_squad_promo_groups")
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not table_exists:
|
||
if db_type == "sqlite":
|
||
create_table_sql = """
|
||
CREATE TABLE server_squad_promo_groups (
|
||
server_squad_id INTEGER NOT NULL,
|
||
promo_group_id INTEGER NOT NULL,
|
||
PRIMARY KEY (server_squad_id, promo_group_id),
|
||
FOREIGN KEY (server_squad_id) REFERENCES server_squads(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
|
||
);
|
||
"""
|
||
create_index_sql = """
|
||
CREATE INDEX IF NOT EXISTS idx_server_squad_promo_groups_promo ON server_squad_promo_groups(promo_group_id);
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_table_sql = """
|
||
CREATE TABLE server_squad_promo_groups (
|
||
server_squad_id INTEGER NOT NULL REFERENCES server_squads(id) ON DELETE CASCADE,
|
||
promo_group_id INTEGER NOT NULL REFERENCES promo_groups(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (server_squad_id, promo_group_id)
|
||
);
|
||
"""
|
||
create_index_sql = """
|
||
CREATE INDEX IF NOT EXISTS idx_server_squad_promo_groups_promo ON server_squad_promo_groups(promo_group_id);
|
||
"""
|
||
else:
|
||
create_table_sql = """
|
||
CREATE TABLE server_squad_promo_groups (
|
||
server_squad_id INT NOT NULL,
|
||
promo_group_id INT NOT NULL,
|
||
PRIMARY KEY (server_squad_id, promo_group_id),
|
||
FOREIGN KEY (server_squad_id) REFERENCES server_squads(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
|
||
);
|
||
"""
|
||
create_index_sql = """
|
||
CREATE INDEX IF NOT EXISTS idx_server_squad_promo_groups_promo ON server_squad_promo_groups(promo_group_id);
|
||
"""
|
||
|
||
await conn.execute(text(create_table_sql))
|
||
await conn.execute(text(create_index_sql))
|
||
logger.info("✅ Таблица server_squad_promo_groups создана")
|
||
else:
|
||
logger.info("ℹ️ Таблица server_squad_promo_groups уже существует")
|
||
|
||
default_query = (
|
||
"SELECT id FROM promo_groups WHERE is_default IS TRUE LIMIT 1"
|
||
if db_type == "postgresql"
|
||
else "SELECT id FROM promo_groups WHERE is_default = 1 LIMIT 1"
|
||
)
|
||
default_result = await conn.execute(text(default_query))
|
||
default_row = default_result.fetchone()
|
||
|
||
if not default_row:
|
||
logger.warning("⚠️ Не найдена базовая промогруппа для назначения серверам")
|
||
return True
|
||
|
||
default_group_id = default_row[0]
|
||
|
||
servers_result = await conn.execute(text("SELECT id FROM server_squads"))
|
||
server_ids = [row[0] for row in servers_result.fetchall()]
|
||
|
||
assigned_count = 0
|
||
for server_id in server_ids:
|
||
existing = await conn.execute(
|
||
text(
|
||
"SELECT 1 FROM server_squad_promo_groups WHERE server_squad_id = :sid LIMIT 1"
|
||
),
|
||
{"sid": server_id},
|
||
)
|
||
if existing.fetchone():
|
||
continue
|
||
|
||
await conn.execute(
|
||
text(
|
||
"INSERT INTO server_squad_promo_groups (server_squad_id, promo_group_id) "
|
||
"VALUES (:sid, :gid)"
|
||
),
|
||
{"sid": server_id, "gid": default_group_id},
|
||
)
|
||
assigned_count += 1
|
||
|
||
if assigned_count:
|
||
logger.info(
|
||
f"✅ Базовая промогруппа назначена {assigned_count} серверам"
|
||
)
|
||
else:
|
||
logger.info("ℹ️ Все серверы уже имеют назначенные промогруппы")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Ошибка настройки таблицы server_squad_promo_groups: {e}"
|
||
)
|
||
return False
|
||
|
||
|
||
async def add_server_trial_flag_column() -> bool:
|
||
column_exists = await check_column_exists('server_squads', 'is_trial_eligible')
|
||
if column_exists:
|
||
logger.info("Колонка is_trial_eligible уже существует в server_squads")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
column_def = 'BOOLEAN NOT NULL DEFAULT 0'
|
||
elif db_type == 'postgresql':
|
||
column_def = 'BOOLEAN NOT NULL DEFAULT FALSE'
|
||
else:
|
||
column_def = 'BOOLEAN NOT NULL DEFAULT FALSE'
|
||
|
||
await conn.execute(
|
||
text(f"ALTER TABLE server_squads ADD COLUMN is_trial_eligible {column_def}")
|
||
)
|
||
|
||
if db_type == 'postgresql':
|
||
await conn.execute(
|
||
text("ALTER TABLE server_squads ALTER COLUMN is_trial_eligible SET DEFAULT FALSE")
|
||
)
|
||
|
||
logger.info("✅ Добавлена колонка is_trial_eligible в server_squads")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"Ошибка добавления колонки is_trial_eligible: {error}")
|
||
return False
|
||
|
||
|
||
async def create_system_settings_table() -> bool:
|
||
table_exists = await check_table_exists("system_settings")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица system_settings уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE system_settings (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
key VARCHAR(255) NOT NULL UNIQUE,
|
||
value TEXT NULL,
|
||
description TEXT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE system_settings (
|
||
id SERIAL PRIMARY KEY,
|
||
key VARCHAR(255) NOT NULL UNIQUE,
|
||
value TEXT NULL,
|
||
description TEXT NULL,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
"""
|
||
else:
|
||
create_sql = """
|
||
CREATE TABLE system_settings (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
key VARCHAR(255) NOT NULL UNIQUE,
|
||
value TEXT NULL,
|
||
description TEXT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица system_settings создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"Ошибка создания таблицы system_settings: {error}")
|
||
return False
|
||
|
||
|
||
async def create_menu_layout_history_table() -> bool:
|
||
"""Создаёт таблицу для хранения истории изменений конфигурации меню."""
|
||
table_exists = await check_table_exists("menu_layout_history")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица menu_layout_history уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_table_sql = """
|
||
CREATE TABLE menu_layout_history (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
config_json TEXT NOT NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
changes_summary TEXT NULL,
|
||
user_info VARCHAR(255) NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_table_sql = """
|
||
CREATE TABLE menu_layout_history (
|
||
id SERIAL PRIMARY KEY,
|
||
config_json TEXT NOT NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
changes_summary TEXT NULL,
|
||
user_info VARCHAR(255) NULL,
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
)
|
||
"""
|
||
else:
|
||
create_table_sql = """
|
||
CREATE TABLE menu_layout_history (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
config_json TEXT NOT NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
changes_summary TEXT NULL,
|
||
user_info VARCHAR(255) NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB
|
||
"""
|
||
|
||
await conn.execute(text(create_table_sql))
|
||
await conn.execute(text(
|
||
"CREATE INDEX ix_menu_layout_history_created ON menu_layout_history(created_at)"
|
||
))
|
||
logger.info("✅ Таблица menu_layout_history создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы menu_layout_history: {error}")
|
||
return False
|
||
|
||
|
||
async def create_button_click_logs_table() -> bool:
|
||
"""Создаёт таблицу для логирования кликов по кнопкам меню."""
|
||
table_exists = await check_table_exists("button_click_logs")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица button_click_logs уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_table_sql = """
|
||
CREATE TABLE button_click_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
button_id VARCHAR(100) NOT NULL,
|
||
user_id BIGINT NULL REFERENCES users(telegram_id) ON DELETE SET NULL,
|
||
callback_data VARCHAR(255) NULL,
|
||
clicked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
button_type VARCHAR(20) NULL,
|
||
button_text VARCHAR(255) NULL
|
||
)
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_table_sql = """
|
||
CREATE TABLE button_click_logs (
|
||
id SERIAL PRIMARY KEY,
|
||
button_id VARCHAR(100) NOT NULL,
|
||
user_id BIGINT NULL REFERENCES users(telegram_id) ON DELETE SET NULL,
|
||
callback_data VARCHAR(255) NULL,
|
||
clicked_at TIMESTAMP DEFAULT NOW(),
|
||
button_type VARCHAR(20) NULL,
|
||
button_text VARCHAR(255) NULL
|
||
)
|
||
"""
|
||
else:
|
||
create_table_sql = """
|
||
CREATE TABLE button_click_logs (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
button_id VARCHAR(100) NOT NULL,
|
||
user_id BIGINT NULL,
|
||
callback_data VARCHAR(255) NULL,
|
||
clicked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
button_type VARCHAR(20) NULL,
|
||
button_text VARCHAR(255) NULL,
|
||
FOREIGN KEY (user_id) REFERENCES users(telegram_id) ON DELETE SET NULL
|
||
) ENGINE=InnoDB
|
||
"""
|
||
|
||
await conn.execute(text(create_table_sql))
|
||
|
||
# Создаём индексы отдельными запросами
|
||
index_statements = [
|
||
"CREATE INDEX ix_button_click_logs_button_id ON button_click_logs(button_id)",
|
||
"CREATE INDEX ix_button_click_logs_user_id ON button_click_logs(user_id)",
|
||
"CREATE INDEX ix_button_click_logs_clicked_at ON button_click_logs(clicked_at)",
|
||
"CREATE INDEX ix_button_click_logs_button_date ON button_click_logs(button_id, clicked_at)",
|
||
"CREATE INDEX ix_button_click_logs_user_date ON button_click_logs(user_id, clicked_at)",
|
||
]
|
||
for stmt in index_statements:
|
||
await conn.execute(text(stmt))
|
||
|
||
logger.info("✅ Таблица button_click_logs создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы button_click_logs: {error}")
|
||
return False
|
||
|
||
|
||
async def create_web_api_tokens_table() -> bool:
|
||
table_exists = await check_table_exists("web_api_tokens")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица web_api_tokens уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE web_api_tokens (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(255) NOT NULL,
|
||
token_hash VARCHAR(128) NOT NULL UNIQUE,
|
||
token_prefix VARCHAR(32) NOT NULL,
|
||
description TEXT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at DATETIME NULL,
|
||
last_used_at DATETIME NULL,
|
||
last_used_ip VARCHAR(64) NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
created_by VARCHAR(255) NULL
|
||
);
|
||
CREATE INDEX idx_web_api_tokens_active ON web_api_tokens(is_active);
|
||
CREATE INDEX idx_web_api_tokens_prefix ON web_api_tokens(token_prefix);
|
||
CREATE INDEX idx_web_api_tokens_last_used ON web_api_tokens(last_used_at);
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE web_api_tokens (
|
||
id SERIAL PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
token_hash VARCHAR(128) NOT NULL UNIQUE,
|
||
token_prefix VARCHAR(32) NOT NULL,
|
||
description TEXT NULL,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW(),
|
||
expires_at TIMESTAMP NULL,
|
||
last_used_at TIMESTAMP NULL,
|
||
last_used_ip VARCHAR(64) NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_by VARCHAR(255) NULL
|
||
);
|
||
CREATE INDEX idx_web_api_tokens_active ON web_api_tokens(is_active);
|
||
CREATE INDEX idx_web_api_tokens_prefix ON web_api_tokens(token_prefix);
|
||
CREATE INDEX idx_web_api_tokens_last_used ON web_api_tokens(last_used_at);
|
||
"""
|
||
else:
|
||
create_sql = """
|
||
CREATE TABLE web_api_tokens (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
token_hash VARCHAR(128) NOT NULL UNIQUE,
|
||
token_prefix VARCHAR(32) NOT NULL,
|
||
description TEXT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMP NULL,
|
||
last_used_at TIMESTAMP NULL,
|
||
last_used_ip VARCHAR(64) NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_by VARCHAR(255) NULL
|
||
) ENGINE=InnoDB;
|
||
CREATE INDEX idx_web_api_tokens_active ON web_api_tokens(is_active);
|
||
CREATE INDEX idx_web_api_tokens_prefix ON web_api_tokens(token_prefix);
|
||
CREATE INDEX idx_web_api_tokens_last_used ON web_api_tokens(last_used_at);
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица web_api_tokens создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы web_api_tokens: {error}")
|
||
return False
|
||
|
||
|
||
async def create_privacy_policies_table() -> bool:
|
||
table_exists = await check_table_exists("privacy_policies")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица privacy_policies уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE privacy_policies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
content TEXT NOT NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE privacy_policies (
|
||
id SERIAL PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
content TEXT NOT NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
"""
|
||
else:
|
||
create_sql = """
|
||
CREATE TABLE privacy_policies (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
content TEXT NOT NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB;
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица privacy_policies создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы privacy_policies: {error}")
|
||
return False
|
||
|
||
|
||
async def create_public_offers_table() -> bool:
|
||
table_exists = await check_table_exists("public_offers")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица public_offers уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE public_offers (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
content TEXT NOT NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE public_offers (
|
||
id SERIAL PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
content TEXT NOT NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
"""
|
||
else:
|
||
create_sql = """
|
||
CREATE TABLE public_offers (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
content TEXT NOT NULL,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB;
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица public_offers создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы public_offers: {error}")
|
||
return False
|
||
|
||
|
||
async def create_faq_settings_table() -> bool:
|
||
table_exists = await check_table_exists("faq_settings")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица faq_settings уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE faq_settings (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE faq_settings (
|
||
id SERIAL PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
"""
|
||
else:
|
||
create_sql = """
|
||
CREATE TABLE faq_settings (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL UNIQUE,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB;
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица faq_settings создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы faq_settings: {error}")
|
||
return False
|
||
|
||
|
||
async def create_faq_pages_table() -> bool:
|
||
table_exists = await check_table_exists("faq_pages")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица faq_pages уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE faq_pages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
language VARCHAR(10) NOT NULL,
|
||
title VARCHAR(255) NOT NULL,
|
||
content TEXT NOT NULL,
|
||
display_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
CREATE INDEX idx_faq_pages_language ON faq_pages(language);
|
||
"""
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE faq_pages (
|
||
id SERIAL PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL,
|
||
title VARCHAR(255) NOT NULL,
|
||
content TEXT NOT NULL,
|
||
display_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
CREATE INDEX idx_faq_pages_language ON faq_pages(language);
|
||
CREATE INDEX idx_faq_pages_order ON faq_pages(language, display_order);
|
||
"""
|
||
else:
|
||
create_sql = """
|
||
CREATE TABLE faq_pages (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
language VARCHAR(10) NOT NULL,
|
||
title VARCHAR(255) NOT NULL,
|
||
content TEXT NOT NULL,
|
||
display_order INT NOT NULL DEFAULT 0,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB;
|
||
CREATE INDEX idx_faq_pages_language ON faq_pages(language);
|
||
CREATE INDEX idx_faq_pages_order ON faq_pages(language, display_order);
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица faq_pages создана")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы faq_pages: {error}")
|
||
return False
|
||
|
||
|
||
async def ensure_default_web_api_token() -> bool:
|
||
default_token = (settings.WEB_API_DEFAULT_TOKEN or "").strip()
|
||
if not default_token:
|
||
return True
|
||
|
||
token_name = (settings.WEB_API_DEFAULT_TOKEN_NAME or "Bootstrap Token").strip()
|
||
|
||
try:
|
||
async with AsyncSessionLocal() as session:
|
||
token_hash = hash_api_token(default_token, settings.WEB_API_TOKEN_HASH_ALGORITHM)
|
||
result = await session.execute(
|
||
select(WebApiToken).where(WebApiToken.token_hash == token_hash)
|
||
)
|
||
existing = result.scalar_one_or_none()
|
||
|
||
if existing:
|
||
updated = False
|
||
|
||
if not existing.is_active:
|
||
existing.is_active = True
|
||
updated = True
|
||
|
||
if token_name and existing.name != token_name:
|
||
existing.name = token_name
|
||
updated = True
|
||
|
||
if updated:
|
||
existing.updated_at = datetime.utcnow()
|
||
await session.commit()
|
||
return True
|
||
|
||
token = WebApiToken(
|
||
name=token_name or "Bootstrap Token",
|
||
token_hash=token_hash,
|
||
token_prefix=default_token[:12],
|
||
description="Автоматически создан при миграции",
|
||
created_by="migration",
|
||
is_active=True,
|
||
)
|
||
session.add(token)
|
||
await session.commit()
|
||
logger.info("✅ Создан дефолтный токен веб-API из конфигурации")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания дефолтного веб-API токена: {error}")
|
||
return False
|
||
|
||
|
||
async def add_promo_group_priority_column() -> bool:
|
||
"""Добавляет колонку priority в таблицу promo_groups."""
|
||
column_exists = await check_column_exists('promo_groups', 'priority')
|
||
if column_exists:
|
||
logger.info("Колонка priority уже существует в promo_groups")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
column_def = 'INTEGER NOT NULL DEFAULT 0'
|
||
elif db_type == 'postgresql':
|
||
column_def = 'INTEGER NOT NULL DEFAULT 0'
|
||
else:
|
||
column_def = 'INT NOT NULL DEFAULT 0'
|
||
|
||
await conn.execute(
|
||
text(f"ALTER TABLE promo_groups ADD COLUMN priority {column_def}")
|
||
)
|
||
|
||
# Создаем индекс для оптимизации сортировки
|
||
if db_type == 'postgresql':
|
||
await conn.execute(
|
||
text("CREATE INDEX IF NOT EXISTS idx_promo_groups_priority ON promo_groups(priority DESC)")
|
||
)
|
||
elif db_type == 'sqlite':
|
||
await conn.execute(
|
||
text("CREATE INDEX IF NOT EXISTS idx_promo_groups_priority ON promo_groups(priority DESC)")
|
||
)
|
||
else: # MySQL
|
||
await conn.execute(
|
||
text("CREATE INDEX idx_promo_groups_priority ON promo_groups(priority DESC)")
|
||
)
|
||
|
||
logger.info("✅ Добавлена колонка priority в promo_groups с индексом")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"Ошибка добавления колонки priority: {error}")
|
||
return False
|
||
|
||
|
||
async def create_user_promo_groups_table() -> bool:
|
||
"""Создает таблицу user_promo_groups для связи Many-to-Many между users и promo_groups."""
|
||
table_exists = await check_table_exists("user_promo_groups")
|
||
if table_exists:
|
||
logger.info("ℹ️ Таблица user_promo_groups уже существует")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
create_sql = """
|
||
CREATE TABLE user_promo_groups (
|
||
user_id INTEGER NOT NULL,
|
||
promo_group_id INTEGER NOT NULL,
|
||
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
assigned_by VARCHAR(50) DEFAULT 'system',
|
||
PRIMARY KEY (user_id, promo_group_id),
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
|
||
);
|
||
"""
|
||
index_sql = "CREATE INDEX idx_user_promo_groups_user_id ON user_promo_groups(user_id);"
|
||
elif db_type == "postgresql":
|
||
create_sql = """
|
||
CREATE TABLE user_promo_groups (
|
||
user_id INTEGER NOT NULL,
|
||
promo_group_id INTEGER NOT NULL,
|
||
assigned_at TIMESTAMP DEFAULT NOW(),
|
||
assigned_by VARCHAR(50) DEFAULT 'system',
|
||
PRIMARY KEY (user_id, promo_group_id),
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
|
||
);
|
||
"""
|
||
index_sql = "CREATE INDEX idx_user_promo_groups_user_id ON user_promo_groups(user_id);"
|
||
else: # MySQL
|
||
create_sql = """
|
||
CREATE TABLE user_promo_groups (
|
||
user_id INT NOT NULL,
|
||
promo_group_id INT NOT NULL,
|
||
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
assigned_by VARCHAR(50) DEFAULT 'system',
|
||
PRIMARY KEY (user_id, promo_group_id),
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
|
||
);
|
||
"""
|
||
index_sql = "CREATE INDEX idx_user_promo_groups_user_id ON user_promo_groups(user_id);"
|
||
|
||
await conn.execute(text(create_sql))
|
||
await conn.execute(text(index_sql))
|
||
logger.info("✅ Таблица user_promo_groups создана с индексом")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы user_promo_groups: {error}")
|
||
return False
|
||
|
||
|
||
async def migrate_existing_user_promo_groups_data() -> bool:
|
||
"""Переносит существующие связи users.promo_group_id в таблицу user_promo_groups."""
|
||
try:
|
||
table_exists = await check_table_exists("user_promo_groups")
|
||
if not table_exists:
|
||
logger.warning("⚠️ Таблица user_promo_groups не существует, пропускаем миграцию данных")
|
||
return False
|
||
|
||
column_exists = await check_column_exists('users', 'promo_group_id')
|
||
if not column_exists:
|
||
logger.warning("⚠️ Колонка users.promo_group_id не существует, пропускаем миграцию данных")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
# Проверяем есть ли уже данные в user_promo_groups
|
||
result = await conn.execute(text("SELECT COUNT(*) FROM user_promo_groups"))
|
||
count = result.scalar()
|
||
|
||
if count > 0:
|
||
logger.info(f"ℹ️ В таблице user_promo_groups уже есть {count} записей, пропускаем миграцию")
|
||
return True
|
||
|
||
# Переносим данные из users.promo_group_id
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == "sqlite":
|
||
migrate_sql = """
|
||
INSERT INTO user_promo_groups (user_id, promo_group_id, assigned_at, assigned_by)
|
||
SELECT id, promo_group_id, CURRENT_TIMESTAMP, 'system'
|
||
FROM users
|
||
WHERE promo_group_id IS NOT NULL
|
||
"""
|
||
else: # PostgreSQL and MySQL
|
||
migrate_sql = """
|
||
INSERT INTO user_promo_groups (user_id, promo_group_id, assigned_at, assigned_by)
|
||
SELECT id, promo_group_id, NOW(), 'system'
|
||
FROM users
|
||
WHERE promo_group_id IS NOT NULL
|
||
"""
|
||
|
||
result = await conn.execute(text(migrate_sql))
|
||
migrated_count = result.rowcount if hasattr(result, 'rowcount') else 0
|
||
|
||
logger.info(f"✅ Перенесено {migrated_count} связей пользователей с промогруппами")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка миграции данных user_promo_groups: {error}")
|
||
return False
|
||
|
||
|
||
async def add_promocode_promo_group_column() -> bool:
|
||
"""Добавляет колонку promo_group_id в таблицу promocodes."""
|
||
column_exists = await check_column_exists('promocodes', 'promo_group_id')
|
||
if column_exists:
|
||
logger.info("Колонка promo_group_id уже существует в promocodes")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
# Add column
|
||
if db_type == 'sqlite':
|
||
await conn.execute(
|
||
text("ALTER TABLE promocodes ADD COLUMN promo_group_id INTEGER")
|
||
)
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(
|
||
text("ALTER TABLE promocodes ADD COLUMN promo_group_id INTEGER")
|
||
)
|
||
# Add foreign key
|
||
await conn.execute(
|
||
text("""
|
||
ALTER TABLE promocodes
|
||
ADD CONSTRAINT fk_promocodes_promo_group
|
||
FOREIGN KEY (promo_group_id)
|
||
REFERENCES promo_groups(id)
|
||
ON DELETE SET NULL
|
||
""")
|
||
)
|
||
# Add index
|
||
await conn.execute(
|
||
text("CREATE INDEX IF NOT EXISTS idx_promocodes_promo_group_id ON promocodes(promo_group_id)")
|
||
)
|
||
elif db_type == 'mysql':
|
||
await conn.execute(
|
||
text("""
|
||
ALTER TABLE promocodes
|
||
ADD COLUMN promo_group_id INT,
|
||
ADD CONSTRAINT fk_promocodes_promo_group
|
||
FOREIGN KEY (promo_group_id)
|
||
REFERENCES promo_groups(id)
|
||
ON DELETE SET NULL
|
||
""")
|
||
)
|
||
await conn.execute(
|
||
text("CREATE INDEX idx_promocodes_promo_group_id ON promocodes(promo_group_id)")
|
||
)
|
||
|
||
logger.info("✅ Добавлена колонка promo_group_id в promocodes")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления promo_group_id в promocodes: {error}")
|
||
return False
|
||
|
||
|
||
async def add_promocode_first_purchase_only_column() -> bool:
|
||
"""Добавляет колонку first_purchase_only в таблицу promocodes."""
|
||
column_exists = await check_column_exists('promocodes', 'first_purchase_only')
|
||
if column_exists:
|
||
logger.info("Колонка first_purchase_only уже существует в promocodes")
|
||
return True
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(
|
||
text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT 0")
|
||
)
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(
|
||
text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT FALSE")
|
||
)
|
||
elif db_type == 'mysql':
|
||
await conn.execute(
|
||
text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT FALSE")
|
||
)
|
||
|
||
logger.info("✅ Добавлена колонка first_purchase_only в promocodes")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления first_purchase_only в promocodes: {error}")
|
||
return False
|
||
|
||
|
||
async def migrate_contest_templates_prize_columns() -> bool:
|
||
"""Миграция contest_templates: prize_days -> prize_type + prize_value."""
|
||
try:
|
||
prize_type_exists = await check_column_exists("contest_templates", "prize_type")
|
||
prize_value_exists = await check_column_exists("contest_templates", "prize_value")
|
||
|
||
if prize_type_exists and prize_value_exists:
|
||
logger.info("Колонки prize_type и prize_value уже существуют в contest_templates")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
# Добавляем prize_type
|
||
if not prize_type_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(text(
|
||
"ALTER TABLE contest_templates ADD COLUMN prize_type VARCHAR(20) NOT NULL DEFAULT 'days'"
|
||
))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text(
|
||
"ALTER TABLE contest_templates ADD COLUMN prize_type VARCHAR(20) NOT NULL DEFAULT 'days'"
|
||
))
|
||
else:
|
||
await conn.execute(text(
|
||
"ALTER TABLE contest_templates ADD COLUMN prize_type VARCHAR(20) NOT NULL DEFAULT 'days'"
|
||
))
|
||
logger.info("✅ Добавлена колонка prize_type в contest_templates")
|
||
|
||
# Добавляем prize_value
|
||
if not prize_value_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(text(
|
||
"ALTER TABLE contest_templates ADD COLUMN prize_value VARCHAR(50) NOT NULL DEFAULT '1'"
|
||
))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text(
|
||
"ALTER TABLE contest_templates ADD COLUMN prize_value VARCHAR(50) NOT NULL DEFAULT '1'"
|
||
))
|
||
else:
|
||
await conn.execute(text(
|
||
"ALTER TABLE contest_templates ADD COLUMN prize_value VARCHAR(50) NOT NULL DEFAULT '1'"
|
||
))
|
||
logger.info("✅ Добавлена колонка prize_value в contest_templates")
|
||
|
||
# Мигрируем данные из prize_days в prize_value (если prize_days существует)
|
||
prize_days_exists = await check_column_exists("contest_templates", "prize_days")
|
||
if prize_days_exists:
|
||
await conn.execute(text(
|
||
"UPDATE contest_templates SET prize_value = CAST(prize_days AS VARCHAR) WHERE prize_type = 'days'"
|
||
))
|
||
logger.info("✅ Данные из prize_days перенесены в prize_value")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка миграции prize_type/prize_value в contest_templates: {error}")
|
||
return False
|
||
|
||
|
||
async def add_subscription_modem_enabled_column() -> bool:
|
||
"""Добавить колонку modem_enabled в subscriptions."""
|
||
try:
|
||
column_exists = await check_column_exists("subscriptions", "modem_enabled")
|
||
if column_exists:
|
||
logger.info("Колонка modem_enabled уже существует в 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 modem_enabled BOOLEAN DEFAULT 0"
|
||
))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN modem_enabled BOOLEAN DEFAULT FALSE"
|
||
))
|
||
else:
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN modem_enabled TINYINT(1) DEFAULT 0"
|
||
))
|
||
|
||
logger.info("✅ Добавлена колонка modem_enabled в subscriptions")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления modem_enabled в subscriptions: {error}")
|
||
return False
|
||
|
||
|
||
async def add_subscription_purchased_traffic_column() -> bool:
|
||
"""Добавить колонку purchased_traffic_gb в subscriptions."""
|
||
try:
|
||
column_exists = await check_column_exists("subscriptions", "purchased_traffic_gb")
|
||
if column_exists:
|
||
logger.info("Колонка purchased_traffic_gb уже существует в 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 purchased_traffic_gb INTEGER DEFAULT 0"
|
||
))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN purchased_traffic_gb INTEGER DEFAULT 0"
|
||
))
|
||
else:
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN purchased_traffic_gb INT DEFAULT 0"
|
||
))
|
||
|
||
logger.info("✅ Добавлена колонка purchased_traffic_gb в subscriptions")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления purchased_traffic_gb в subscriptions: {error}")
|
||
return False
|
||
|
||
|
||
async def add_transaction_receipt_columns() -> bool:
|
||
"""Добавить колонки receipt_uuid и receipt_created_at в transactions."""
|
||
try:
|
||
receipt_uuid_exists = await check_column_exists("transactions", "receipt_uuid")
|
||
receipt_created_at_exists = await check_column_exists("transactions", "receipt_created_at")
|
||
|
||
if receipt_uuid_exists and receipt_created_at_exists:
|
||
logger.info("Колонки receipt_uuid и receipt_created_at уже существуют в transactions")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if not receipt_uuid_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(text(
|
||
"ALTER TABLE transactions ADD COLUMN receipt_uuid VARCHAR(255)"
|
||
))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text(
|
||
"ALTER TABLE transactions ADD COLUMN receipt_uuid VARCHAR(255)"
|
||
))
|
||
else:
|
||
await conn.execute(text(
|
||
"ALTER TABLE transactions ADD COLUMN receipt_uuid VARCHAR(255)"
|
||
))
|
||
logger.info("✅ Добавлена колонка receipt_uuid в transactions")
|
||
|
||
if not receipt_created_at_exists:
|
||
if db_type == "sqlite":
|
||
await conn.execute(text(
|
||
"ALTER TABLE transactions ADD COLUMN receipt_created_at DATETIME"
|
||
))
|
||
elif db_type == "postgresql":
|
||
await conn.execute(text(
|
||
"ALTER TABLE transactions ADD COLUMN receipt_created_at TIMESTAMP"
|
||
))
|
||
else:
|
||
await conn.execute(text(
|
||
"ALTER TABLE transactions ADD COLUMN receipt_created_at DATETIME"
|
||
))
|
||
logger.info("✅ Добавлена колонка receipt_created_at в transactions")
|
||
|
||
# Создаём индекс на receipt_uuid
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
if db_type == "postgresql":
|
||
await conn.execute(text(
|
||
"CREATE INDEX IF NOT EXISTS ix_transactions_receipt_uuid "
|
||
"ON transactions (receipt_uuid)"
|
||
))
|
||
elif db_type == "sqlite":
|
||
await conn.execute(text(
|
||
"CREATE INDEX IF NOT EXISTS ix_transactions_receipt_uuid "
|
||
"ON transactions (receipt_uuid)"
|
||
))
|
||
else:
|
||
await conn.execute(text(
|
||
"CREATE INDEX ix_transactions_receipt_uuid "
|
||
"ON transactions (receipt_uuid)"
|
||
))
|
||
except Exception as idx_error:
|
||
logger.warning(f"Индекс на receipt_uuid возможно уже существует: {idx_error}")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонок чеков в transactions: {error}")
|
||
return False
|
||
|
||
|
||
async def create_withdrawal_requests_table() -> bool:
|
||
"""Создаёт таблицу для заявок на вывод реферального баланса."""
|
||
try:
|
||
if await check_table_exists('withdrawal_requests'):
|
||
logger.debug("Таблица withdrawal_requests уже существует")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE withdrawal_requests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||
payment_details TEXT,
|
||
risk_score INTEGER DEFAULT 0,
|
||
risk_analysis TEXT,
|
||
processed_by INTEGER,
|
||
processed_at DATETIME,
|
||
admin_comment TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (processed_by) REFERENCES users(id) ON DELETE SET NULL
|
||
)
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE withdrawal_requests (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
amount_kopeks INTEGER NOT NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||
payment_details TEXT,
|
||
risk_score INTEGER DEFAULT 0,
|
||
risk_analysis TEXT,
|
||
processed_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||
processed_at TIMESTAMP,
|
||
admin_comment TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
else: # mysql
|
||
create_sql = """
|
||
CREATE TABLE withdrawal_requests (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
amount_kopeks INT NOT NULL,
|
||
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||
payment_details TEXT,
|
||
risk_score INT DEFAULT 0,
|
||
risk_analysis TEXT,
|
||
processed_by INT,
|
||
processed_at DATETIME,
|
||
admin_comment TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (processed_by) REFERENCES users(id) ON DELETE SET NULL
|
||
)
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица withdrawal_requests создана")
|
||
|
||
# Создаём индексы
|
||
try:
|
||
await conn.execute(text(
|
||
"CREATE INDEX idx_withdrawal_requests_user_id ON withdrawal_requests(user_id)"
|
||
))
|
||
await conn.execute(text(
|
||
"CREATE INDEX idx_withdrawal_requests_status ON withdrawal_requests(status)"
|
||
))
|
||
except Exception:
|
||
pass # Индексы могут уже существовать
|
||
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы withdrawal_requests: {error}")
|
||
return False
|
||
|
||
|
||
# =============================================================================
|
||
# МИГРАЦИЯ ДЛЯ ИНДИВИДУАЛЬНЫХ ДОКУПОК ТРАФИКА
|
||
# =============================================================================
|
||
|
||
async def create_traffic_purchases_table() -> bool:
|
||
"""Создаёт таблицу для индивидуальных докупок трафика с отдельными датами истечения."""
|
||
try:
|
||
if await check_table_exists('traffic_purchases'):
|
||
logger.info("ℹ️ Таблица traffic_purchases уже существует")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE traffic_purchases (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
subscription_id INTEGER NOT NULL,
|
||
traffic_gb INTEGER NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE
|
||
);
|
||
CREATE INDEX idx_traffic_purchases_subscription_id ON traffic_purchases(subscription_id);
|
||
CREATE INDEX idx_traffic_purchases_expires_at ON traffic_purchases(expires_at);
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE traffic_purchases (
|
||
id SERIAL PRIMARY KEY,
|
||
subscription_id INTEGER NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE,
|
||
traffic_gb INTEGER NOT NULL,
|
||
expires_at TIMESTAMP NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
CREATE INDEX idx_traffic_purchases_subscription_id ON traffic_purchases(subscription_id);
|
||
CREATE INDEX idx_traffic_purchases_expires_at ON traffic_purchases(expires_at);
|
||
"""
|
||
else: # mysql
|
||
create_sql = """
|
||
CREATE TABLE traffic_purchases (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
subscription_id INT NOT NULL,
|
||
traffic_gb INT NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE,
|
||
INDEX idx_traffic_purchases_subscription_id (subscription_id),
|
||
INDEX idx_traffic_purchases_expires_at (expires_at)
|
||
);
|
||
"""
|
||
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица traffic_purchases создана")
|
||
|
||
return True
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблицы traffic_purchases: {error}")
|
||
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 ensure_tariff_max_device_limit_column() -> bool:
|
||
"""Добавляет колонку max_device_limit в таблицу tariffs."""
|
||
try:
|
||
column_exists = await check_column_exists('tariffs', 'max_device_limit')
|
||
if column_exists:
|
||
logger.info("ℹ️ Колонка max_device_limit в tariffs уже существует")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_device_limit INTEGER NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_device_limit INTEGER NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_device_limit INT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка max_device_limit добавлена в tariffs")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонки max_device_limit: {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 add_campaign_tariff_columns() -> bool:
|
||
"""Добавляет колонки tariff_id и tariff_duration_days в таблицы рекламных кампаний."""
|
||
try:
|
||
campaigns_tariff_id_exists = await check_column_exists('advertising_campaigns', 'tariff_id')
|
||
campaigns_duration_exists = await check_column_exists('advertising_campaigns', 'tariff_duration_days')
|
||
registrations_tariff_id_exists = await check_column_exists('advertising_campaign_registrations', 'tariff_id')
|
||
registrations_duration_exists = await check_column_exists('advertising_campaign_registrations', 'tariff_duration_days')
|
||
|
||
if campaigns_tariff_id_exists and campaigns_duration_exists and registrations_tariff_id_exists and registrations_duration_exists:
|
||
logger.info("ℹ️ Колонки tariff в рекламных кампаниях уже существуют")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
# Добавляем колонки в advertising_campaigns
|
||
if not campaigns_tariff_id_exists:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaigns ADD COLUMN tariff_id INTEGER REFERENCES tariffs(id)"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaigns ADD COLUMN tariff_id INTEGER REFERENCES tariffs(id) ON DELETE SET NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaigns ADD COLUMN tariff_id INT NULL"
|
||
))
|
||
logger.info("✅ Колонка tariff_id добавлена в advertising_campaigns")
|
||
|
||
if not campaigns_duration_exists:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaigns ADD COLUMN tariff_duration_days INTEGER NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaigns ADD COLUMN tariff_duration_days INTEGER NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaigns ADD COLUMN tariff_duration_days INT NULL"
|
||
))
|
||
logger.info("✅ Колонка tariff_duration_days добавлена в advertising_campaigns")
|
||
|
||
# Добавляем колонки в advertising_campaign_registrations
|
||
if not registrations_tariff_id_exists:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaign_registrations ADD COLUMN tariff_id INTEGER REFERENCES tariffs(id)"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaign_registrations ADD COLUMN tariff_id INTEGER REFERENCES tariffs(id) ON DELETE SET NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaign_registrations ADD COLUMN tariff_id INT NULL"
|
||
))
|
||
logger.info("✅ Колонка tariff_id добавлена в advertising_campaign_registrations")
|
||
|
||
if not registrations_duration_exists:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaign_registrations ADD COLUMN tariff_duration_days INTEGER NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaign_registrations ADD COLUMN tariff_duration_days INTEGER NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE advertising_campaign_registrations ADD COLUMN tariff_duration_days INT NULL"
|
||
))
|
||
logger.info("✅ Колонка tariff_duration_days добавлена в advertising_campaign_registrations")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонок tariff в рекламные кампании: {error}")
|
||
return False
|
||
|
||
|
||
async def add_tariff_device_price_column() -> bool:
|
||
"""Добавляет колонку device_price_kopeks в таблицу tariffs."""
|
||
try:
|
||
if await check_column_exists('tariffs', 'device_price_kopeks'):
|
||
logger.info("ℹ️ Колонка device_price_kopeks уже существует в tariffs")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN device_price_kopeks INTEGER DEFAULT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN device_price_kopeks INTEGER DEFAULT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN device_price_kopeks INT DEFAULT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка device_price_kopeks добавлена в tariffs")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонки device_price_kopeks: {error}")
|
||
return False
|
||
|
||
|
||
async def add_tariff_server_traffic_limits_column() -> bool:
|
||
"""Добавляет колонку server_traffic_limits в таблицу tariffs."""
|
||
try:
|
||
if await check_column_exists('tariffs', 'server_traffic_limits'):
|
||
logger.info("ℹ️ Колонка server_traffic_limits уже существует в tariffs")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN server_traffic_limits TEXT DEFAULT '{}'"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN server_traffic_limits JSONB DEFAULT '{}'"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN server_traffic_limits JSON DEFAULT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка server_traffic_limits добавлена в tariffs")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонки server_traffic_limits: {error}")
|
||
return False
|
||
|
||
|
||
async def add_tariff_allow_traffic_topup_column() -> bool:
|
||
"""Добавляет колонку allow_traffic_topup в таблицу tariffs."""
|
||
try:
|
||
if await check_column_exists('tariffs', 'allow_traffic_topup'):
|
||
logger.info("ℹ️ Колонка allow_traffic_topup уже существует в tariffs")
|
||
return True
|
||
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN allow_traffic_topup INTEGER NOT NULL DEFAULT 1"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN allow_traffic_topup BOOLEAN NOT NULL DEFAULT TRUE"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN allow_traffic_topup BOOLEAN NOT NULL DEFAULT TRUE"
|
||
))
|
||
|
||
logger.info("✅ Колонка allow_traffic_topup добавлена в tariffs")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонки allow_traffic_topup: {error}")
|
||
return False
|
||
|
||
|
||
async def create_wheel_tables() -> bool:
|
||
"""Создаёт таблицы для колеса удачи: wheel_config, wheel_prizes, wheel_spins."""
|
||
try:
|
||
db_type = await get_database_type()
|
||
|
||
# Создание wheel_config
|
||
if not await check_table_exists('wheel_config'):
|
||
async with engine.begin() as conn:
|
||
if db_type == 'sqlite':
|
||
create_config_sql = """
|
||
CREATE TABLE wheel_config (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT 0,
|
||
name VARCHAR(255) NOT NULL DEFAULT 'Колесо удачи',
|
||
spin_cost_stars INTEGER NOT NULL DEFAULT 50,
|
||
spin_cost_days INTEGER NOT NULL DEFAULT 3,
|
||
spin_cost_stars_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||
spin_cost_days_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||
rtp_percent REAL NOT NULL DEFAULT 85.0,
|
||
daily_spin_limit INTEGER NOT NULL DEFAULT 5,
|
||
min_subscription_days_for_day_payment INTEGER NOT NULL DEFAULT 7,
|
||
promo_prefix VARCHAR(50) NOT NULL DEFAULT 'WHEEL',
|
||
promo_validity_days INTEGER NOT NULL DEFAULT 30,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_config_sql = """
|
||
CREATE TABLE wheel_config (
|
||
id SERIAL PRIMARY KEY,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||
name VARCHAR(255) NOT NULL DEFAULT 'Колесо удачи',
|
||
spin_cost_stars INTEGER NOT NULL DEFAULT 50,
|
||
spin_cost_days INTEGER NOT NULL DEFAULT 3,
|
||
spin_cost_stars_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
spin_cost_days_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
rtp_percent REAL NOT NULL DEFAULT 85.0,
|
||
daily_spin_limit INTEGER NOT NULL DEFAULT 5,
|
||
min_subscription_days_for_day_payment INTEGER NOT NULL DEFAULT 7,
|
||
promo_prefix VARCHAR(50) NOT NULL DEFAULT 'WHEEL',
|
||
promo_validity_days INTEGER NOT NULL DEFAULT 30,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
else: # mysql
|
||
create_config_sql = """
|
||
CREATE TABLE wheel_config (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||
name VARCHAR(255) NOT NULL DEFAULT 'Колесо удачи',
|
||
spin_cost_stars INT NOT NULL DEFAULT 50,
|
||
spin_cost_days INT NOT NULL DEFAULT 3,
|
||
spin_cost_stars_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
spin_cost_days_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||
rtp_percent FLOAT NOT NULL DEFAULT 85.0,
|
||
daily_spin_limit INT NOT NULL DEFAULT 5,
|
||
min_subscription_days_for_day_payment INT NOT NULL DEFAULT 7,
|
||
promo_prefix VARCHAR(50) NOT NULL DEFAULT 'WHEEL',
|
||
promo_validity_days INT NOT NULL DEFAULT 30,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
await conn.execute(text(create_config_sql))
|
||
logger.info("✅ Таблица wheel_config создана")
|
||
else:
|
||
logger.debug("ℹ️ Таблица wheel_config уже существует")
|
||
|
||
# Создание wheel_prizes
|
||
if not await check_table_exists('wheel_prizes'):
|
||
async with engine.begin() as conn:
|
||
if db_type == 'sqlite':
|
||
create_prizes_sql = """
|
||
CREATE TABLE wheel_prizes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
config_id INTEGER NOT NULL,
|
||
prize_type VARCHAR(50) NOT NULL,
|
||
prize_value INTEGER NOT NULL DEFAULT 0,
|
||
display_name VARCHAR(255) NOT NULL,
|
||
emoji VARCHAR(10) NOT NULL DEFAULT '🎁',
|
||
color VARCHAR(20) NOT NULL DEFAULT '#3B82F6',
|
||
prize_value_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
manual_probability REAL,
|
||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||
promo_balance_bonus_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
promo_subscription_days INTEGER NOT NULL DEFAULT 0,
|
||
promo_traffic_gb INTEGER NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (config_id) REFERENCES wheel_config(id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_prizes_sql = """
|
||
CREATE TABLE wheel_prizes (
|
||
id SERIAL PRIMARY KEY,
|
||
config_id INTEGER NOT NULL REFERENCES wheel_config(id) ON DELETE CASCADE,
|
||
prize_type VARCHAR(50) NOT NULL,
|
||
prize_value INTEGER NOT NULL DEFAULT 0,
|
||
display_name VARCHAR(255) NOT NULL,
|
||
emoji VARCHAR(10) NOT NULL DEFAULT '🎁',
|
||
color VARCHAR(20) NOT NULL DEFAULT '#3B82F6',
|
||
prize_value_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
manual_probability REAL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
promo_balance_bonus_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
promo_subscription_days INTEGER NOT NULL DEFAULT 0,
|
||
promo_traffic_gb INTEGER NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
else: # mysql
|
||
create_prizes_sql = """
|
||
CREATE TABLE wheel_prizes (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
config_id INT NOT NULL,
|
||
prize_type VARCHAR(50) NOT NULL,
|
||
prize_value INT NOT NULL DEFAULT 0,
|
||
display_name VARCHAR(255) NOT NULL,
|
||
emoji VARCHAR(10) NOT NULL DEFAULT '🎁',
|
||
color VARCHAR(20) NOT NULL DEFAULT '#3B82F6',
|
||
prize_value_kopeks INT NOT NULL DEFAULT 0,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
manual_probability FLOAT,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
promo_balance_bonus_kopeks INT NOT NULL DEFAULT 0,
|
||
promo_subscription_days INT NOT NULL DEFAULT 0,
|
||
promo_traffic_gb INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (config_id) REFERENCES wheel_config(id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
await conn.execute(text(create_prizes_sql))
|
||
# Индексы
|
||
try:
|
||
await conn.execute(text(
|
||
"CREATE INDEX idx_wheel_prizes_config_id ON wheel_prizes(config_id)"
|
||
))
|
||
except Exception:
|
||
pass
|
||
logger.info("✅ Таблица wheel_prizes создана")
|
||
else:
|
||
logger.debug("ℹ️ Таблица wheel_prizes уже существует")
|
||
|
||
# Создание wheel_spins
|
||
if not await check_table_exists('wheel_spins'):
|
||
async with engine.begin() as conn:
|
||
if db_type == 'sqlite':
|
||
create_spins_sql = """
|
||
CREATE TABLE wheel_spins (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
config_id INTEGER NOT NULL,
|
||
prize_id INTEGER,
|
||
payment_type VARCHAR(50) NOT NULL,
|
||
payment_amount INTEGER NOT NULL,
|
||
payment_value_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
prize_type VARCHAR(50) NOT NULL,
|
||
prize_value INTEGER NOT NULL DEFAULT 0,
|
||
prize_value_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
promocode_id INTEGER,
|
||
is_applied BOOLEAN NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (config_id) REFERENCES wheel_config(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (prize_id) REFERENCES wheel_prizes(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (promocode_id) REFERENCES promocodes(id) ON DELETE SET NULL
|
||
)
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_spins_sql = """
|
||
CREATE TABLE wheel_spins (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
config_id INTEGER NOT NULL REFERENCES wheel_config(id) ON DELETE CASCADE,
|
||
prize_id INTEGER REFERENCES wheel_prizes(id) ON DELETE SET NULL,
|
||
payment_type VARCHAR(50) NOT NULL,
|
||
payment_amount INTEGER NOT NULL,
|
||
payment_value_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
prize_type VARCHAR(50) NOT NULL,
|
||
prize_value INTEGER NOT NULL DEFAULT 0,
|
||
prize_value_kopeks INTEGER NOT NULL DEFAULT 0,
|
||
promocode_id INTEGER REFERENCES promocodes(id) ON DELETE SET NULL,
|
||
is_applied BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
else: # mysql
|
||
create_spins_sql = """
|
||
CREATE TABLE wheel_spins (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
config_id INT NOT NULL,
|
||
prize_id INT,
|
||
payment_type VARCHAR(50) NOT NULL,
|
||
payment_amount INT NOT NULL,
|
||
payment_value_kopeks INT NOT NULL DEFAULT 0,
|
||
prize_type VARCHAR(50) NOT NULL,
|
||
prize_value INT NOT NULL DEFAULT 0,
|
||
prize_value_kopeks INT NOT NULL DEFAULT 0,
|
||
promocode_id INT,
|
||
is_applied BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (config_id) REFERENCES wheel_config(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (prize_id) REFERENCES wheel_prizes(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (promocode_id) REFERENCES promocodes(id) ON DELETE SET NULL
|
||
)
|
||
"""
|
||
await conn.execute(text(create_spins_sql))
|
||
# Индексы
|
||
try:
|
||
await conn.execute(text(
|
||
"CREATE INDEX idx_wheel_spins_user_id ON wheel_spins(user_id)"
|
||
))
|
||
await conn.execute(text(
|
||
"CREATE INDEX idx_wheel_spins_created_at ON wheel_spins(created_at)"
|
||
))
|
||
except Exception:
|
||
pass
|
||
logger.info("✅ Таблица wheel_spins создана")
|
||
else:
|
||
logger.debug("ℹ️ Таблица wheel_spins уже существует")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка создания таблиц для колеса удачи: {error}")
|
||
return False
|
||
|
||
|
||
async def add_tariff_traffic_topup_columns() -> bool:
|
||
"""Добавляет колонки для докупки трафика в тарифах."""
|
||
try:
|
||
columns_added = 0
|
||
|
||
# Колонка traffic_topup_enabled
|
||
if not await check_column_exists('tariffs', 'traffic_topup_enabled'):
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_topup_enabled INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_topup_enabled BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_topup_enabled TINYINT(1) DEFAULT 0 NOT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка traffic_topup_enabled добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка traffic_topup_enabled уже существует в tariffs")
|
||
|
||
# Колонка traffic_topup_packages (JSON)
|
||
if not await check_column_exists('tariffs', 'traffic_topup_packages'):
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_topup_packages TEXT DEFAULT '{}'"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_topup_packages JSONB DEFAULT '{}'"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_topup_packages JSON DEFAULT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка traffic_topup_packages добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка traffic_topup_packages уже существует в tariffs")
|
||
|
||
# Колонка max_topup_traffic_gb (максимальный лимит трафика после докупок)
|
||
if not await check_column_exists('tariffs', 'max_topup_traffic_gb'):
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_topup_traffic_gb INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_topup_traffic_gb INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_topup_traffic_gb INT DEFAULT 0 NOT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка max_topup_traffic_gb добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка max_topup_traffic_gb уже существует в tariffs")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонок для докупки трафика: {error}")
|
||
return False
|
||
|
||
|
||
async def add_tariff_daily_columns() -> bool:
|
||
"""Добавляет колонки для суточных тарифов."""
|
||
try:
|
||
columns_added = 0
|
||
|
||
# Колонка is_daily
|
||
if not await check_column_exists('tariffs', 'is_daily'):
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN is_daily INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN is_daily BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN is_daily TINYINT(1) DEFAULT 0 NOT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка is_daily добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка is_daily уже существует в tariffs")
|
||
|
||
# Колонка daily_price_kopeks
|
||
if not await check_column_exists('tariffs', 'daily_price_kopeks'):
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN daily_price_kopeks INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN daily_price_kopeks INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN daily_price_kopeks INT DEFAULT 0 NOT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка daily_price_kopeks добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка daily_price_kopeks уже существует в tariffs")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонок суточного тарифа: {error}")
|
||
return False
|
||
|
||
|
||
async def add_tariff_custom_days_traffic_columns() -> bool:
|
||
"""Добавляет колонки для произвольных дней и трафика в тарифы."""
|
||
try:
|
||
columns_added = 0
|
||
db_type = await get_database_type()
|
||
|
||
# === ПРОИЗВОЛЬНОЕ КОЛИЧЕСТВО ДНЕЙ ===
|
||
# custom_days_enabled
|
||
if not await check_column_exists('tariffs', 'custom_days_enabled'):
|
||
async with engine.begin() as conn:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN custom_days_enabled INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN custom_days_enabled BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN custom_days_enabled TINYINT(1) DEFAULT 0 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка custom_days_enabled добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка custom_days_enabled уже существует в tariffs")
|
||
|
||
# price_per_day_kopeks
|
||
if not await check_column_exists('tariffs', 'price_per_day_kopeks'):
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN price_per_day_kopeks INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка price_per_day_kopeks добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка price_per_day_kopeks уже существует в tariffs")
|
||
|
||
# min_days
|
||
if not await check_column_exists('tariffs', 'min_days'):
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN min_days INTEGER DEFAULT 1 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка min_days добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка min_days уже существует в tariffs")
|
||
|
||
# max_days
|
||
if not await check_column_exists('tariffs', 'max_days'):
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_days INTEGER DEFAULT 365 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка max_days добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка max_days уже существует в tariffs")
|
||
|
||
# === ПРОИЗВОЛЬНЫЙ ТРАФИК ПРИ ПОКУПКЕ ===
|
||
# custom_traffic_enabled
|
||
if not await check_column_exists('tariffs', 'custom_traffic_enabled'):
|
||
async with engine.begin() as conn:
|
||
if db_type == 'sqlite':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN custom_traffic_enabled INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN custom_traffic_enabled BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN custom_traffic_enabled TINYINT(1) DEFAULT 0 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка custom_traffic_enabled добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка custom_traffic_enabled уже существует в tariffs")
|
||
|
||
# traffic_price_per_gb_kopeks
|
||
if not await check_column_exists('tariffs', 'traffic_price_per_gb_kopeks'):
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_price_per_gb_kopeks INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка traffic_price_per_gb_kopeks добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка traffic_price_per_gb_kopeks уже существует в tariffs")
|
||
|
||
# min_traffic_gb
|
||
if not await check_column_exists('tariffs', 'min_traffic_gb'):
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN min_traffic_gb INTEGER DEFAULT 1 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка min_traffic_gb добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка min_traffic_gb уже существует в tariffs")
|
||
|
||
# max_traffic_gb
|
||
if not await check_column_exists('tariffs', 'max_traffic_gb'):
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN max_traffic_gb INTEGER DEFAULT 1000 NOT NULL"
|
||
))
|
||
logger.info("✅ Колонка max_traffic_gb добавлена в tariffs")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка max_traffic_gb уже существует в tariffs")
|
||
|
||
if columns_added > 0:
|
||
logger.info(f"✅ Добавлено {columns_added} колонок для произвольных дней/трафика")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонок произвольных дней/трафика: {error}")
|
||
return False
|
||
|
||
|
||
async def add_tariff_traffic_reset_mode_column() -> bool:
|
||
"""Добавляет колонку traffic_reset_mode в tariffs для настройки режима сброса трафика.
|
||
|
||
Значения: DAY, WEEK, MONTH, NO_RESET (NULL = использовать глобальную настройку)
|
||
"""
|
||
try:
|
||
if not await check_column_exists('tariffs', 'traffic_reset_mode'):
|
||
async with engine.begin() as conn:
|
||
await conn.execute(text(
|
||
"ALTER TABLE tariffs ADD COLUMN traffic_reset_mode VARCHAR(20) NULL"
|
||
))
|
||
logger.info("✅ Колонка traffic_reset_mode добавлена в tariffs")
|
||
return True
|
||
else:
|
||
logger.info("ℹ️ Колонка traffic_reset_mode уже существует в tariffs")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонки traffic_reset_mode: {error}")
|
||
return False
|
||
|
||
|
||
async def add_subscription_daily_columns() -> bool:
|
||
"""Добавляет колонки для суточных подписок."""
|
||
try:
|
||
columns_added = 0
|
||
|
||
# Колонка is_daily_paused
|
||
if not await check_column_exists('subscriptions', 'is_daily_paused'):
|
||
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 is_daily_paused INTEGER DEFAULT 0 NOT NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN is_daily_paused BOOLEAN DEFAULT FALSE NOT NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN is_daily_paused TINYINT(1) DEFAULT 0 NOT NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка is_daily_paused добавлена в subscriptions")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка is_daily_paused уже существует в subscriptions")
|
||
|
||
# Колонка last_daily_charge_at
|
||
if not await check_column_exists('subscriptions', 'last_daily_charge_at'):
|
||
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 last_daily_charge_at DATETIME NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN last_daily_charge_at TIMESTAMP NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN last_daily_charge_at DATETIME NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка last_daily_charge_at добавлена в subscriptions")
|
||
columns_added += 1
|
||
else:
|
||
logger.info("ℹ️ Колонка last_daily_charge_at уже существует в subscriptions")
|
||
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонок суточной подписки: {error}")
|
||
return False
|
||
|
||
|
||
async def add_subscription_traffic_reset_at_column() -> bool:
|
||
"""Добавляет колонку traffic_reset_at в subscriptions для сброса докупленного трафика через 30 дней."""
|
||
try:
|
||
if not await check_column_exists('subscriptions', 'traffic_reset_at'):
|
||
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 traffic_reset_at DATETIME NULL"
|
||
))
|
||
elif db_type == 'postgresql':
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN traffic_reset_at TIMESTAMP NULL"
|
||
))
|
||
else: # MySQL
|
||
await conn.execute(text(
|
||
"ALTER TABLE subscriptions ADD COLUMN traffic_reset_at DATETIME NULL"
|
||
))
|
||
|
||
logger.info("✅ Колонка traffic_reset_at добавлена в subscriptions")
|
||
return True
|
||
else:
|
||
logger.info("ℹ️ Колонка traffic_reset_at уже существует в subscriptions")
|
||
return True
|
||
|
||
except Exception as error:
|
||
logger.error(f"❌ Ошибка добавления колонки traffic_reset_at: {error}")
|
||
return False
|
||
|
||
|
||
async def run_universal_migration():
|
||
logger.info("=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===")
|
||
|
||
try:
|
||
db_type = await get_database_type()
|
||
logger.info(f"Тип базы данных: {db_type}")
|
||
|
||
if db_type == 'postgresql':
|
||
logger.info("=== СИНХРОНИЗАЦИЯ ПОСЛЕДОВАТЕЛЬНОСТЕЙ PostgreSQL ===")
|
||
sequences_synced = await sync_postgres_sequences()
|
||
if sequences_synced:
|
||
logger.info("✅ Последовательности PostgreSQL синхронизированы")
|
||
else:
|
||
logger.warning("⚠️ Не удалось синхронизировать последовательности PostgreSQL")
|
||
|
||
referral_migration_success = await add_referral_system_columns()
|
||
if not referral_migration_success:
|
||
logger.warning("⚠️ Проблемы с миграцией реферальной системы")
|
||
|
||
commission_column_ready = await add_referral_commission_percent_column()
|
||
if commission_column_ready:
|
||
logger.info("✅ Колонка referral_commission_percent готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой referral_commission_percent")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ SYSTEM_SETTINGS ===")
|
||
system_settings_ready = await create_system_settings_table()
|
||
if system_settings_ready:
|
||
logger.info("✅ Таблица system_settings готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей system_settings")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ WEB_API_TOKENS ===")
|
||
web_api_tokens_ready = await create_web_api_tokens_table()
|
||
if web_api_tokens_ready:
|
||
logger.info("✅ Таблица web_api_tokens готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей web_api_tokens")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ MENU_LAYOUT_HISTORY ===")
|
||
menu_layout_history_ready = await create_menu_layout_history_table()
|
||
if menu_layout_history_ready:
|
||
logger.info("✅ Таблица menu_layout_history готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей menu_layout_history")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ BUTTON_CLICK_LOGS ===")
|
||
button_click_logs_ready = await create_button_click_logs_table()
|
||
if button_click_logs_ready:
|
||
logger.info("✅ Таблица button_click_logs готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей button_click_logs")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ ДЛЯ ТРИАЛЬНЫХ СКВАДОВ ===")
|
||
trial_column_ready = await add_server_trial_flag_column()
|
||
if trial_column_ready:
|
||
logger.info("✅ Колонка is_trial_eligible готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой is_trial_eligible")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PRIVACY_POLICIES ===")
|
||
privacy_policies_ready = await create_privacy_policies_table()
|
||
if privacy_policies_ready:
|
||
logger.info("✅ Таблица privacy_policies готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей privacy_policies")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PUBLIC_OFFERS ===")
|
||
public_offers_ready = await create_public_offers_table()
|
||
if public_offers_ready:
|
||
logger.info("✅ Таблица public_offers готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей public_offers")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ FAQ_SETTINGS ===")
|
||
faq_settings_ready = await create_faq_settings_table()
|
||
if faq_settings_ready:
|
||
logger.info("✅ Таблица faq_settings готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей faq_settings")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ FAQ_PAGES ===")
|
||
faq_pages_ready = await create_faq_pages_table()
|
||
if faq_pages_ready:
|
||
logger.info("✅ Таблица faq_pages готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей faq_pages")
|
||
|
||
logger.info("=== ПРОВЕРКА БАЗОВЫХ ТОКЕНОВ ВЕБ-API ===")
|
||
default_token_ready = await ensure_default_web_api_token()
|
||
if default_token_ready:
|
||
logger.info("✅ Бутстрап токен веб-API готов")
|
||
else:
|
||
logger.warning("⚠️ Не удалось создать бутстрап токен веб-API")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ CRYPTOBOT ===")
|
||
cryptobot_created = await create_cryptobot_payments_table()
|
||
if cryptobot_created:
|
||
logger.info("✅ Таблица CryptoBot payments готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей CryptoBot payments")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ HELEKET ===")
|
||
heleket_created = await create_heleket_payments_table()
|
||
if heleket_created:
|
||
logger.info("✅ Таблица Heleket payments готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей Heleket payments")
|
||
|
||
mulenpay_name = settings.get_mulenpay_display_name()
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ %s ===", mulenpay_name)
|
||
mulenpay_created = await create_mulenpay_payments_table()
|
||
if mulenpay_created:
|
||
logger.info("✅ Таблица %s payments готова", mulenpay_name)
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей %s payments", mulenpay_name)
|
||
|
||
mulenpay_schema_ok = await ensure_mulenpay_payment_schema()
|
||
if mulenpay_schema_ok:
|
||
logger.info("✅ Схема %s payments актуальна", mulenpay_name)
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить схему %s payments", mulenpay_name)
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PAL24 ===")
|
||
pal24_created = await create_pal24_payments_table()
|
||
if pal24_created:
|
||
logger.info("✅ Таблица Pal24 payments готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей Pal24 payments")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ WATA ===")
|
||
wata_created = await create_wata_payments_table()
|
||
if wata_created:
|
||
logger.info("✅ Таблица Wata payments готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей Wata payments")
|
||
|
||
wata_schema_ok = await ensure_wata_payment_schema()
|
||
if wata_schema_ok:
|
||
logger.info("✅ Схема Wata payments актуальна")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить схему Wata payments")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ FREEKASSA ===")
|
||
freekassa_created = await create_freekassa_payments_table()
|
||
if freekassa_created:
|
||
logger.info("✅ Таблица Freekassa payments готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей Freekassa payments")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ DISCOUNT_OFFERS ===")
|
||
discount_created = await create_discount_offers_table()
|
||
if discount_created:
|
||
logger.info("✅ Таблица discount_offers готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей discount_offers")
|
||
|
||
discount_columns_ready = await ensure_discount_offer_columns()
|
||
if discount_columns_ready:
|
||
logger.info("✅ Колонки discount_offers в актуальном состоянии")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить колонки discount_offers")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦ ДЛЯ РЕФЕРАЛЬНЫХ КОНКУРСОВ ===")
|
||
contests_table_ready = await create_referral_contests_table()
|
||
if contests_table_ready:
|
||
logger.info("✅ Таблица referral_contests готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей referral_contests")
|
||
|
||
contest_events_ready = await create_referral_contest_events_table()
|
||
if contest_events_ready:
|
||
logger.info("✅ Таблица referral_contest_events готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей referral_contest_events")
|
||
|
||
contest_type_ready = await ensure_referral_contest_type_column()
|
||
if contest_type_ready:
|
||
logger.info("✅ Колонка contest_type для referral_contests готова")
|
||
else:
|
||
logger.warning("⚠️ Не удалось добавить contest_type в referral_contests")
|
||
|
||
contest_summary_ready = await ensure_referral_contest_summary_columns()
|
||
if contest_summary_ready:
|
||
logger.info("✅ Колонки daily_summary_times/last_daily_summary_at готовы")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить колонки сводок для referral_contests")
|
||
|
||
contest_templates_ready = await create_contest_templates_table()
|
||
if contest_templates_ready:
|
||
logger.info("✅ Таблица contest_templates готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей contest_templates")
|
||
|
||
logger.info("=== МИГРАЦИЯ КОЛОНОК ПРИЗА В CONTEST_TEMPLATES ===")
|
||
prize_columns_ready = await migrate_contest_templates_prize_columns()
|
||
if prize_columns_ready:
|
||
logger.info("✅ Колонки prize_type и prize_value готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с миграцией prize_type/prize_value")
|
||
|
||
contest_rounds_ready = await create_contest_rounds_table()
|
||
if contest_rounds_ready:
|
||
logger.info("✅ Таблица contest_rounds готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей contest_rounds")
|
||
|
||
contest_attempts_ready = await create_contest_attempts_table()
|
||
if contest_attempts_ready:
|
||
logger.info("✅ Таблица contest_attempts готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей contest_attempts")
|
||
|
||
user_discount_columns_ready = await ensure_user_promo_offer_discount_columns()
|
||
if user_discount_columns_ready:
|
||
logger.info("✅ Колонки пользовательских промо-скидок готовы")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить пользовательские промо-скидки")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ NOTIFICATION_SETTINGS ===")
|
||
notification_settings_ready = await ensure_user_notification_settings_column()
|
||
if notification_settings_ready:
|
||
logger.info("✅ Колонка notification_settings готова")
|
||
else:
|
||
logger.warning("⚠️ Не удалось добавить колонку notification_settings")
|
||
|
||
effect_types_updated = await migrate_discount_offer_effect_types()
|
||
if effect_types_updated:
|
||
logger.info("✅ Типы эффектов промо-предложений обновлены")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить типы эффектов промо-предложений")
|
||
|
||
bonuses_reset = await reset_discount_offer_bonuses()
|
||
if bonuses_reset:
|
||
logger.info("✅ Бонусные начисления промо-предложений отключены")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обнулить бонусы промо-предложений")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PROMO_OFFER_TEMPLATES ===")
|
||
promo_templates_created = await create_promo_offer_templates_table()
|
||
if promo_templates_created:
|
||
logger.info("✅ Таблица promo_offer_templates готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей promo_offer_templates")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ ПРИОРИТЕТА В ПРОМОГРУППЫ ===")
|
||
priority_column_ready = await add_promo_group_priority_column()
|
||
if priority_column_ready:
|
||
logger.info("✅ Колонка priority в promo_groups готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением priority в promo_groups")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ USER_PROMO_GROUPS ===")
|
||
user_promo_groups_ready = await create_user_promo_groups_table()
|
||
if user_promo_groups_ready:
|
||
logger.info("✅ Таблица user_promo_groups готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей user_promo_groups")
|
||
|
||
logger.info("=== МИГРАЦИЯ ДАННЫХ В USER_PROMO_GROUPS ===")
|
||
data_migrated = await migrate_existing_user_promo_groups_data()
|
||
if data_migrated:
|
||
logger.info("✅ Данные перенесены в user_promo_groups")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с миграцией данных в user_promo_groups")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ PROMO_GROUP_ID В PROMOCODES ===")
|
||
promocode_column_ready = await add_promocode_promo_group_column()
|
||
if promocode_column_ready:
|
||
logger.info("✅ Колонка promo_group_id в promocodes готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением promo_group_id в promocodes")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ FIRST_PURCHASE_ONLY В PROMOCODES ===")
|
||
first_purchase_ready = await add_promocode_first_purchase_only_column()
|
||
if first_purchase_ready:
|
||
logger.info("✅ Колонка first_purchase_only в promocodes готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением first_purchase_only в promocodes")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ MAIN_MENU_BUTTONS ===")
|
||
main_menu_buttons_created = await create_main_menu_buttons_table()
|
||
if main_menu_buttons_created:
|
||
logger.info("✅ Таблица main_menu_buttons готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей main_menu_buttons")
|
||
|
||
template_columns_ready = await ensure_promo_offer_template_active_duration_column()
|
||
if template_columns_ready:
|
||
logger.info("✅ Колонка active_discount_hours промо-предложений готова")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить колонку active_discount_hours промо-предложений")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PROMO_OFFER_LOGS ===")
|
||
promo_logs_created = await create_promo_offer_logs_table()
|
||
if promo_logs_created:
|
||
logger.info("✅ Таблица promo_offer_logs готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей promo_offer_logs")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ SUBSCRIPTION_TEMPORARY_ACCESS ===")
|
||
temp_access_created = await create_subscription_temporary_access_table()
|
||
if temp_access_created:
|
||
logger.info("✅ Таблица subscription_temporary_access готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей subscription_temporary_access")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ USER_MESSAGES ===")
|
||
user_messages_created = await create_user_messages_table()
|
||
if user_messages_created:
|
||
logger.info("✅ Таблица user_messages готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей user_messages")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PINNED_MESSAGES ===")
|
||
pinned_messages_created = await create_pinned_messages_table()
|
||
if pinned_messages_created:
|
||
logger.info("✅ Таблица pinned_messages готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей pinned_messages")
|
||
|
||
logger.info("=== СОЗДАНИЕ/ОБНОВЛЕНИЕ ТАБЛИЦЫ WELCOME_TEXTS ===")
|
||
welcome_texts_created = await create_welcome_texts_table()
|
||
if welcome_texts_created:
|
||
logger.info("✅ Таблица welcome_texts готова с полем is_enabled")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей welcome_texts")
|
||
|
||
logger.info("=== ОБНОВЛЕНИЕ СХЕМЫ PINNED_MESSAGES ===")
|
||
pinned_media_ready = await ensure_pinned_message_media_columns()
|
||
if pinned_media_ready:
|
||
logger.info("✅ Медиа поля для pinned_messages готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с медиа полями pinned_messages")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ СЛЕДА ОТПРАВКИ ЗАКРЕПА ДЛЯ ПОЛЬЗОВАТЕЛЕЙ ===")
|
||
last_pinned_ready = await ensure_user_last_pinned_column()
|
||
if last_pinned_ready:
|
||
logger.info("✅ Колонка last_pinned_message_id добавлена")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить колонку last_pinned_message_id")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ МЕДИА ПОЛЕЙ В BROADCAST_HISTORY ===")
|
||
media_fields_added = await add_media_fields_to_broadcast_history()
|
||
if media_fields_added:
|
||
logger.info("✅ Медиа поля в broadcast_history готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением медиа полей")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ ПОЛЕЙ БЛОКИРОВКИ В TICKETS ===")
|
||
tickets_block_cols_added = await add_ticket_reply_block_columns()
|
||
if tickets_block_cols_added:
|
||
logger.info("✅ Поля блокировок в tickets готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением полей блокировок в tickets")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ ПОЛЕЙ SLA В TICKETS ===")
|
||
sla_cols_added = await add_ticket_sla_columns()
|
||
if sla_cols_added:
|
||
logger.info("✅ Поля SLA в tickets готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением полей SLA в tickets")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ CRYPTO LINK ДЛЯ ПОДПИСОК ===")
|
||
crypto_link_added = await add_subscription_crypto_link_column()
|
||
if crypto_link_added:
|
||
logger.info("✅ Колонка subscription_crypto_link готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением колонки subscription_crypto_link")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ MODEM_ENABLED ДЛЯ ПОДПИСОК ===")
|
||
modem_enabled_added = await add_subscription_modem_enabled_column()
|
||
if modem_enabled_added:
|
||
logger.info("✅ Колонка modem_enabled готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением колонки modem_enabled")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ PURCHASED_TRAFFIC_GB ДЛЯ ПОДПИСОК ===")
|
||
purchased_traffic_added = await add_subscription_purchased_traffic_column()
|
||
if purchased_traffic_added:
|
||
logger.info("✅ Колонка purchased_traffic_gb готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением колонки purchased_traffic_gb")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНОК ОГРАНИЧЕНИЙ ПОЛЬЗОВАТЕЛЕЙ ===")
|
||
restrictions_added = await add_user_restriction_columns()
|
||
if restrictions_added:
|
||
logger.info("✅ Колонки ограничений пользователей готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением колонок ограничений пользователей")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНОК ЛИЧНОГО КАБИНЕТА ===")
|
||
cabinet_added = await add_user_cabinet_columns()
|
||
if cabinet_added:
|
||
logger.info("✅ Колонки личного кабинета готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с добавлением колонок личного кабинета")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ АУДИТА ПОДДЕРЖКИ ===")
|
||
try:
|
||
async with engine.begin() as conn:
|
||
db_type = await get_database_type()
|
||
if not await check_table_exists('support_audit_logs'):
|
||
if db_type == 'sqlite':
|
||
create_sql = """
|
||
CREATE TABLE support_audit_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
actor_user_id INTEGER NULL,
|
||
actor_telegram_id BIGINT NOT NULL,
|
||
is_moderator BOOLEAN NOT NULL DEFAULT 0,
|
||
action VARCHAR(50) NOT NULL,
|
||
ticket_id INTEGER NULL,
|
||
target_user_id INTEGER NULL,
|
||
details JSON NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (actor_user_id) REFERENCES users(id),
|
||
FOREIGN KEY (ticket_id) REFERENCES tickets(id),
|
||
FOREIGN KEY (target_user_id) REFERENCES users(id)
|
||
);
|
||
CREATE INDEX idx_support_audit_logs_ticket ON support_audit_logs(ticket_id);
|
||
CREATE INDEX idx_support_audit_logs_actor ON support_audit_logs(actor_telegram_id);
|
||
CREATE INDEX idx_support_audit_logs_action ON support_audit_logs(action);
|
||
"""
|
||
elif db_type == 'postgresql':
|
||
create_sql = """
|
||
CREATE TABLE support_audit_logs (
|
||
id SERIAL PRIMARY KEY,
|
||
actor_user_id INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
actor_telegram_id BIGINT NOT NULL,
|
||
is_moderator BOOLEAN NOT NULL DEFAULT FALSE,
|
||
action VARCHAR(50) NOT NULL,
|
||
ticket_id INTEGER NULL REFERENCES tickets(id) ON DELETE SET NULL,
|
||
target_user_id INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
details JSON NULL,
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
CREATE INDEX idx_support_audit_logs_ticket ON support_audit_logs(ticket_id);
|
||
CREATE INDEX idx_support_audit_logs_actor ON support_audit_logs(actor_telegram_id);
|
||
CREATE INDEX idx_support_audit_logs_action ON support_audit_logs(action);
|
||
"""
|
||
else:
|
||
create_sql = """
|
||
CREATE TABLE support_audit_logs (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
actor_user_id INT NULL,
|
||
actor_telegram_id BIGINT NOT NULL,
|
||
is_moderator BOOLEAN NOT NULL DEFAULT 0,
|
||
action VARCHAR(50) NOT NULL,
|
||
ticket_id INT NULL,
|
||
target_user_id INT NULL,
|
||
details JSON NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
CREATE INDEX idx_support_audit_logs_ticket ON support_audit_logs(ticket_id);
|
||
CREATE INDEX idx_support_audit_logs_actor ON support_audit_logs(actor_telegram_id);
|
||
CREATE INDEX idx_support_audit_logs_action ON support_audit_logs(action);
|
||
"""
|
||
await conn.execute(text(create_sql))
|
||
logger.info("✅ Таблица support_audit_logs создана")
|
||
else:
|
||
logger.info("ℹ️ Таблица support_audit_logs уже существует")
|
||
except Exception as e:
|
||
logger.warning(f"⚠️ Проблемы с созданием таблицы support_audit_logs: {e}")
|
||
|
||
logger.info("=== НАСТРОЙКА ПРОМО ГРУПП ===")
|
||
promo_groups_ready = await ensure_promo_groups_setup()
|
||
if promo_groups_ready:
|
||
logger.info("✅ Промо группы готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с настройкой промо групп")
|
||
|
||
server_promo_groups_ready = await ensure_server_promo_groups_setup()
|
||
if server_promo_groups_ready:
|
||
logger.info("✅ Доступ серверов по промогруппам настроен")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с настройкой доступа серверов к промогруппам")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ ДОКУПОК ТРАФИКА ===")
|
||
traffic_purchases_ready = await create_traffic_purchases_table()
|
||
if traffic_purchases_ready:
|
||
logger.info("✅ Таблица traffic_purchases готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей traffic_purchases")
|
||
|
||
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("=== ДОБАВЛЕНИЕ КОЛОНОК ТАРИФОВ В РЕКЛАМНЫЕ КАМПАНИИ ===")
|
||
campaign_tariff_columns_ready = await add_campaign_tariff_columns()
|
||
if campaign_tariff_columns_ready:
|
||
logger.info("✅ Колонки tariff в рекламных кампаниях готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонками tariff в рекламных кампаниях")
|
||
|
||
device_price_column_ready = await add_tariff_device_price_column()
|
||
if device_price_column_ready:
|
||
logger.info("✅ Колонка device_price_kopeks в tariffs готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой device_price_kopeks в tariffs")
|
||
|
||
max_device_limit_ready = await ensure_tariff_max_device_limit_column()
|
||
if max_device_limit_ready:
|
||
logger.info("✅ Колонка max_device_limit в tariffs готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой max_device_limit в tariffs")
|
||
|
||
server_traffic_limits_ready = await add_tariff_server_traffic_limits_column()
|
||
if server_traffic_limits_ready:
|
||
logger.info("✅ Колонка server_traffic_limits в tariffs готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой server_traffic_limits в tariffs")
|
||
|
||
allow_traffic_topup_ready = await add_tariff_allow_traffic_topup_column()
|
||
if allow_traffic_topup_ready:
|
||
logger.info("✅ Колонка allow_traffic_topup в tariffs готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой allow_traffic_topup в tariffs")
|
||
|
||
traffic_topup_columns_ready = await add_tariff_traffic_topup_columns()
|
||
if traffic_topup_columns_ready:
|
||
logger.info("✅ Колонки докупки трафика в tariffs готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонками докупки трафика в tariffs")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНОК СУТОЧНЫХ ТАРИФОВ ===")
|
||
daily_tariff_columns_ready = await add_tariff_daily_columns()
|
||
if daily_tariff_columns_ready:
|
||
logger.info("✅ Колонки суточных тарифов в tariffs готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонками суточных тарифов в tariffs")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНОК ПРОИЗВОЛЬНЫХ ДНЕЙ/ТРАФИКА ===")
|
||
custom_days_traffic_ready = await add_tariff_custom_days_traffic_columns()
|
||
if custom_days_traffic_ready:
|
||
logger.info("✅ Колонки произвольных дней/трафика в tariffs готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонками произвольных дней/трафика в tariffs")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ РЕЖИМА СБРОСА ТРАФИКА В ТАРИФАХ ===")
|
||
traffic_reset_mode_ready = await add_tariff_traffic_reset_mode_column()
|
||
if traffic_reset_mode_ready:
|
||
logger.info("✅ Колонка traffic_reset_mode в tariffs готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой traffic_reset_mode в tariffs")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНОК СУТОЧНЫХ ПОДПИСОК ===")
|
||
daily_subscription_columns_ready = await add_subscription_daily_columns()
|
||
if daily_subscription_columns_ready:
|
||
logger.info("✅ Колонки суточных подписок в subscriptions готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонками суточных подписок в subscriptions")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ СБРОСА ТРАФИКА ===")
|
||
traffic_reset_column_ready = await add_subscription_traffic_reset_at_column()
|
||
if traffic_reset_column_ready:
|
||
logger.info("✅ Колонка traffic_reset_at в subscriptions готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонкой traffic_reset_at в subscriptions")
|
||
|
||
logger.info("=== ОБНОВЛЕНИЕ ВНЕШНИХ КЛЮЧЕЙ ===")
|
||
fk_updated = await fix_foreign_keys_for_user_deletion()
|
||
if fk_updated:
|
||
logger.info("✅ Внешние ключи обновлены")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с обновлением внешних ключей")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ КОНВЕРСИЙ ПОДПИСОК ===")
|
||
conversions_created = await create_subscription_conversions_table()
|
||
if conversions_created:
|
||
logger.info("✅ Таблица subscription_conversions готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей subscription_conversions")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ SUBSCRIPTION_EVENTS ===")
|
||
events_created = await create_subscription_events_table()
|
||
if events_created:
|
||
logger.info("✅ Таблица subscription_events готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей subscription_events")
|
||
|
||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНОК ЧЕКОВ В TRANSACTIONS ===")
|
||
receipt_columns_ready = await add_transaction_receipt_columns()
|
||
if receipt_columns_ready:
|
||
logger.info("✅ Колонки receipt_uuid и receipt_created_at готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с колонками чеков в transactions")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ WITHDRAWAL_REQUESTS ===")
|
||
withdrawal_requests_ready = await create_withdrawal_requests_table()
|
||
if withdrawal_requests_ready:
|
||
logger.info("✅ Таблица withdrawal_requests готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей withdrawal_requests")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦ КОЛЕСА УДАЧИ ===")
|
||
wheel_tables_ready = await create_wheel_tables()
|
||
if wheel_tables_ready:
|
||
logger.info("✅ Таблицы колеса удачи готовы")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицами колеса удачи")
|
||
|
||
async with engine.begin() as conn:
|
||
total_subs = await conn.execute(text("SELECT COUNT(*) FROM subscriptions"))
|
||
unique_users = await conn.execute(text("SELECT COUNT(DISTINCT user_id) FROM subscriptions"))
|
||
|
||
total_count = total_subs.fetchone()[0]
|
||
unique_count = unique_users.fetchone()[0]
|
||
|
||
logger.info(f"Всего подписок: {total_count}")
|
||
logger.info(f"Уникальных пользователей: {unique_count}")
|
||
|
||
if total_count == unique_count:
|
||
logger.info("База данных уже в корректном состоянии")
|
||
logger.info("=== МИГРАЦИЯ ЗАВЕРШЕНА УСПЕШНО ===")
|
||
return True
|
||
|
||
deleted_count = await fix_subscription_duplicates_universal()
|
||
|
||
async with engine.begin() as conn:
|
||
final_check = await conn.execute(text("""
|
||
SELECT user_id, COUNT(*) as count
|
||
FROM subscriptions
|
||
GROUP BY user_id
|
||
HAVING COUNT(*) > 1
|
||
"""))
|
||
|
||
remaining_duplicates = final_check.fetchall()
|
||
|
||
if remaining_duplicates:
|
||
logger.warning(f"Остались дубликаты у {len(remaining_duplicates)} пользователей")
|
||
return False
|
||
else:
|
||
logger.info("=== МИГРАЦИЯ ЗАВЕРШЕНА УСПЕШНО ===")
|
||
logger.info("✅ Реферальная система обновлена")
|
||
logger.info("✅ CryptoBot таблица готова")
|
||
logger.info("✅ Heleket таблица готова")
|
||
logger.info("✅ Таблица конверсий подписок создана")
|
||
logger.info("✅ Таблица событий подписок создана")
|
||
logger.info("✅ Таблица welcome_texts с полем is_enabled готова")
|
||
logger.info("✅ Медиа поля в broadcast_history добавлены")
|
||
logger.info("✅ Дубликаты подписок исправлены")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"=== ОШИБКА ВЫПОЛНЕНИЯ МИГРАЦИИ: {e} ===")
|
||
return False
|
||
|
||
async def check_migration_status():
|
||
logger.info("=== ПРОВЕРКА СТАТУСА МИГРАЦИЙ ===")
|
||
|
||
try:
|
||
status = {
|
||
"has_made_first_topup_column": False,
|
||
"cryptobot_table": False,
|
||
"heleket_table": False,
|
||
"user_messages_table": False,
|
||
"pinned_messages_table": False,
|
||
"welcome_texts_table": False,
|
||
"welcome_texts_is_enabled_column": False,
|
||
"pinned_messages_media_columns": False,
|
||
"pinned_messages_position_column": False,
|
||
"pinned_messages_start_mode_column": False,
|
||
"users_last_pinned_column": False,
|
||
"broadcast_history_media_fields": False,
|
||
"subscription_duplicates": False,
|
||
"subscription_conversions_table": False,
|
||
"subscription_events_table": False,
|
||
"promo_groups_table": False,
|
||
"server_promo_groups_table": False,
|
||
"server_squads_trial_column": False,
|
||
"privacy_policies_table": False,
|
||
"public_offers_table": False,
|
||
"users_promo_group_column": False,
|
||
"promo_groups_period_discounts_column": False,
|
||
"promo_groups_auto_assign_column": False,
|
||
"promo_groups_addon_discount_column": False,
|
||
"users_auto_promo_group_assigned_column": False,
|
||
"users_auto_promo_group_threshold_column": False,
|
||
"users_promo_offer_discount_percent_column": False,
|
||
"users_promo_offer_discount_source_column": False,
|
||
"users_promo_offer_discount_expires_column": False,
|
||
"users_referral_commission_percent_column": False,
|
||
"users_notification_settings_column": False,
|
||
"subscription_crypto_link_column": False,
|
||
"subscription_modem_enabled_column": False,
|
||
"subscription_purchased_traffic_column": False,
|
||
"users_restriction_topup_column": False,
|
||
"users_restriction_subscription_column": False,
|
||
"users_restriction_reason_column": False,
|
||
"contest_templates_prize_type_column": False,
|
||
"contest_templates_prize_value_column": False,
|
||
"discount_offers_table": False,
|
||
"discount_offers_effect_column": False,
|
||
"discount_offers_extra_column": False,
|
||
"referral_contests_table": False,
|
||
"referral_contest_events_table": False,
|
||
"referral_contest_type_column": False,
|
||
"referral_contest_summary_times_column": False,
|
||
"referral_contest_last_summary_at_column": False,
|
||
"contest_templates_table": False,
|
||
"contest_rounds_table": False,
|
||
"contest_attempts_table": False,
|
||
"promo_offer_templates_table": False,
|
||
"promo_offer_templates_active_discount_column": False,
|
||
"promo_offer_logs_table": False,
|
||
"subscription_temporary_access_table": False,
|
||
"campaign_tariff_id_column": False,
|
||
"campaign_tariff_duration_days_column": False,
|
||
"campaign_registration_tariff_id_column": False,
|
||
"campaign_registration_tariff_duration_days_column": False,
|
||
}
|
||
|
||
status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup')
|
||
|
||
status["cryptobot_table"] = await check_table_exists('cryptobot_payments')
|
||
status["heleket_table"] = await check_table_exists('heleket_payments')
|
||
status["user_messages_table"] = await check_table_exists('user_messages')
|
||
status["pinned_messages_table"] = await check_table_exists('pinned_messages')
|
||
status["welcome_texts_table"] = await check_table_exists('welcome_texts')
|
||
status["privacy_policies_table"] = await check_table_exists('privacy_policies')
|
||
status["public_offers_table"] = await check_table_exists('public_offers')
|
||
status["subscription_conversions_table"] = await check_table_exists('subscription_conversions')
|
||
status["subscription_events_table"] = await check_table_exists('subscription_events')
|
||
status["promo_groups_table"] = await check_table_exists('promo_groups')
|
||
status["server_promo_groups_table"] = await check_table_exists('server_squad_promo_groups')
|
||
status["server_squads_trial_column"] = await check_column_exists('server_squads', 'is_trial_eligible')
|
||
|
||
status["discount_offers_table"] = await check_table_exists('discount_offers')
|
||
status["discount_offers_effect_column"] = await check_column_exists('discount_offers', 'effect_type')
|
||
status["discount_offers_extra_column"] = await check_column_exists('discount_offers', 'extra_data')
|
||
status["referral_contests_table"] = await check_table_exists('referral_contests')
|
||
status["referral_contest_events_table"] = await check_table_exists('referral_contest_events')
|
||
status["referral_contest_type_column"] = await check_column_exists('referral_contests', 'contest_type')
|
||
status["referral_contest_summary_times_column"] = await check_column_exists('referral_contests', 'daily_summary_times')
|
||
status["referral_contest_last_summary_at_column"] = await check_column_exists('referral_contests', 'last_daily_summary_at')
|
||
status["contest_templates_table"] = await check_table_exists('contest_templates')
|
||
status["contest_rounds_table"] = await check_table_exists('contest_rounds')
|
||
status["contest_attempts_table"] = await check_table_exists('contest_attempts')
|
||
status["promo_offer_templates_table"] = await check_table_exists('promo_offer_templates')
|
||
status["promo_offer_templates_active_discount_column"] = await check_column_exists('promo_offer_templates', 'active_discount_hours')
|
||
status["promo_offer_logs_table"] = await check_table_exists('promo_offer_logs')
|
||
status["subscription_temporary_access_table"] = await check_table_exists('subscription_temporary_access')
|
||
|
||
# Проверяем колонки tariff в рекламных кампаниях
|
||
status["campaign_tariff_id_column"] = await check_column_exists('advertising_campaigns', 'tariff_id')
|
||
status["campaign_tariff_duration_days_column"] = await check_column_exists('advertising_campaigns', 'tariff_duration_days')
|
||
status["campaign_registration_tariff_id_column"] = await check_column_exists('advertising_campaign_registrations', 'tariff_id')
|
||
status["campaign_registration_tariff_duration_days_column"] = await check_column_exists('advertising_campaign_registrations', 'tariff_duration_days')
|
||
|
||
status["welcome_texts_is_enabled_column"] = await check_column_exists('welcome_texts', 'is_enabled')
|
||
status["users_promo_group_column"] = await check_column_exists('users', 'promo_group_id')
|
||
status["promo_groups_period_discounts_column"] = await check_column_exists('promo_groups', 'period_discounts')
|
||
status["promo_groups_auto_assign_column"] = await check_column_exists('promo_groups', 'auto_assign_total_spent_kopeks')
|
||
status["promo_groups_addon_discount_column"] = await check_column_exists('promo_groups', 'apply_discounts_to_addons')
|
||
status["users_auto_promo_group_assigned_column"] = await check_column_exists('users', 'auto_promo_group_assigned')
|
||
status["users_auto_promo_group_threshold_column"] = await check_column_exists('users', 'auto_promo_group_threshold_kopeks')
|
||
status["users_promo_offer_discount_percent_column"] = await check_column_exists('users', 'promo_offer_discount_percent')
|
||
status["users_promo_offer_discount_source_column"] = await check_column_exists('users', 'promo_offer_discount_source')
|
||
status["users_promo_offer_discount_expires_column"] = await check_column_exists('users', 'promo_offer_discount_expires_at')
|
||
status["users_referral_commission_percent_column"] = await check_column_exists('users', 'referral_commission_percent')
|
||
status["users_notification_settings_column"] = await check_column_exists('users', 'notification_settings')
|
||
status["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link')
|
||
status["subscription_modem_enabled_column"] = await check_column_exists('subscriptions', 'modem_enabled')
|
||
status["subscription_purchased_traffic_column"] = await check_column_exists('subscriptions', 'purchased_traffic_gb')
|
||
status["users_restriction_topup_column"] = await check_column_exists('users', 'restriction_topup')
|
||
status["users_restriction_subscription_column"] = await check_column_exists('users', 'restriction_subscription')
|
||
status["users_restriction_reason_column"] = await check_column_exists('users', 'restriction_reason')
|
||
status["contest_templates_prize_type_column"] = await check_column_exists('contest_templates', 'prize_type')
|
||
status["contest_templates_prize_value_column"] = await check_column_exists('contest_templates', 'prize_value')
|
||
|
||
media_fields_exist = (
|
||
await check_column_exists('broadcast_history', 'has_media') and
|
||
await check_column_exists('broadcast_history', 'media_type') and
|
||
await check_column_exists('broadcast_history', 'media_file_id') and
|
||
await check_column_exists('broadcast_history', 'media_caption')
|
||
)
|
||
status["broadcast_history_media_fields"] = media_fields_exist
|
||
|
||
pinned_media_columns_exist = (
|
||
status["pinned_messages_table"]
|
||
and await check_column_exists('pinned_messages', 'media_type')
|
||
and await check_column_exists('pinned_messages', 'media_file_id')
|
||
)
|
||
status["pinned_messages_media_columns"] = pinned_media_columns_exist
|
||
|
||
status["pinned_messages_position_column"] = (
|
||
status["pinned_messages_table"]
|
||
and await check_column_exists('pinned_messages', 'send_before_menu')
|
||
)
|
||
|
||
status["pinned_messages_start_mode_column"] = (
|
||
status["pinned_messages_table"]
|
||
and await check_column_exists('pinned_messages', 'send_on_every_start')
|
||
)
|
||
|
||
status["users_last_pinned_column"] = await check_column_exists('users', 'last_pinned_message_id')
|
||
|
||
# Колонки чеков в transactions
|
||
status["transactions_receipt_uuid_column"] = await check_column_exists('transactions', 'receipt_uuid')
|
||
status["transactions_receipt_created_at_column"] = await check_column_exists('transactions', 'receipt_created_at')
|
||
|
||
async with engine.begin() as conn:
|
||
duplicates_check = await conn.execute(text("""
|
||
SELECT COUNT(*) FROM (
|
||
SELECT user_id, COUNT(*) as count
|
||
FROM subscriptions
|
||
GROUP BY user_id
|
||
HAVING COUNT(*) > 1
|
||
) as dups
|
||
"""))
|
||
duplicates_count = duplicates_check.fetchone()[0]
|
||
status["subscription_duplicates"] = (duplicates_count == 0)
|
||
|
||
check_names = {
|
||
"has_made_first_topup_column": "Колонка реферальной системы",
|
||
"cryptobot_table": "Таблица CryptoBot payments",
|
||
"heleket_table": "Таблица Heleket payments",
|
||
"user_messages_table": "Таблица пользовательских сообщений",
|
||
"pinned_messages_table": "Таблица закреплённых сообщений",
|
||
"welcome_texts_table": "Таблица приветственных текстов",
|
||
"privacy_policies_table": "Таблица политик конфиденциальности",
|
||
"public_offers_table": "Таблица публичных оферт",
|
||
"welcome_texts_is_enabled_column": "Поле is_enabled в welcome_texts",
|
||
"pinned_messages_media_columns": "Медиа поля в pinned_messages",
|
||
"pinned_messages_position_column": "Позиция закрепа (до/после меню)",
|
||
"pinned_messages_start_mode_column": "Режим отправки закрепа при /start",
|
||
"users_last_pinned_column": "Колонка last_pinned_message_id у пользователей",
|
||
"broadcast_history_media_fields": "Медиа поля в broadcast_history",
|
||
"subscription_conversions_table": "Таблица конверсий подписок",
|
||
"subscription_events_table": "Таблица событий подписок",
|
||
"subscription_duplicates": "Отсутствие дубликатов подписок",
|
||
"promo_groups_table": "Таблица промо-групп",
|
||
"server_promo_groups_table": "Связи серверов и промогрупп",
|
||
"server_squads_trial_column": "Колонка триального назначения у серверов",
|
||
"users_promo_group_column": "Колонка promo_group_id у пользователей",
|
||
"promo_groups_period_discounts_column": "Колонка period_discounts у промо-групп",
|
||
"promo_groups_auto_assign_column": "Колонка auto_assign_total_spent_kopeks у промо-групп",
|
||
"promo_groups_addon_discount_column": "Колонка apply_discounts_to_addons у промо-групп",
|
||
"users_auto_promo_group_assigned_column": "Флаг автоназначения промогруппы у пользователей",
|
||
"users_auto_promo_group_threshold_column": "Порог последней авто-промогруппы у пользователей",
|
||
"users_promo_offer_discount_percent_column": "Колонка процента промо-скидки у пользователей",
|
||
"users_promo_offer_discount_source_column": "Колонка источника промо-скидки у пользователей",
|
||
"users_promo_offer_discount_expires_column": "Колонка срока действия промо-скидки у пользователей",
|
||
"users_referral_commission_percent_column": "Колонка процента реферальной комиссии у пользователей",
|
||
"users_notification_settings_column": "Колонка notification_settings у пользователей",
|
||
"subscription_crypto_link_column": "Колонка subscription_crypto_link в subscriptions",
|
||
"subscription_modem_enabled_column": "Колонка modem_enabled в subscriptions",
|
||
"subscription_purchased_traffic_column": "Колонка purchased_traffic_gb в subscriptions",
|
||
"contest_templates_prize_type_column": "Колонка prize_type в contest_templates",
|
||
"contest_templates_prize_value_column": "Колонка prize_value в contest_templates",
|
||
"discount_offers_table": "Таблица discount_offers",
|
||
"discount_offers_effect_column": "Колонка effect_type в discount_offers",
|
||
"discount_offers_extra_column": "Колонка extra_data в discount_offers",
|
||
"referral_contests_table": "Таблица referral_contests",
|
||
"referral_contest_events_table": "Таблица referral_contest_events",
|
||
"referral_contest_type_column": "Колонка contest_type в referral_contests",
|
||
"referral_contest_summary_times_column": "Колонка daily_summary_times в referral_contests",
|
||
"referral_contest_last_summary_at_column": "Колонка last_daily_summary_at в referral_contests",
|
||
"contest_templates_table": "Таблица contest_templates",
|
||
"contest_rounds_table": "Таблица contest_rounds",
|
||
"contest_attempts_table": "Таблица contest_attempts",
|
||
"promo_offer_templates_table": "Таблица promo_offer_templates",
|
||
"promo_offer_templates_active_discount_column": "Колонка active_discount_hours в promo_offer_templates",
|
||
"promo_offer_logs_table": "Таблица promo_offer_logs",
|
||
"subscription_temporary_access_table": "Таблица subscription_temporary_access",
|
||
"transactions_receipt_uuid_column": "Колонка receipt_uuid в transactions",
|
||
"transactions_receipt_created_at_column": "Колонка receipt_created_at в transactions",
|
||
}
|
||
|
||
for check_key, check_status in status.items():
|
||
check_name = check_names.get(check_key, check_key)
|
||
icon = "✅" if check_status else "❌"
|
||
logger.info(f"{icon} {check_name}: {'OK' if check_status else 'ТРЕБУЕТ ВНИМАНИЯ'}")
|
||
|
||
all_good = all(status.values())
|
||
if all_good:
|
||
logger.info("🎉 Все миграции выполнены успешно!")
|
||
|
||
try:
|
||
async with engine.begin() as conn:
|
||
conversions_count = await conn.execute(text("SELECT COUNT(*) FROM subscription_conversions"))
|
||
users_count = await conn.execute(text("SELECT COUNT(*) FROM users"))
|
||
welcome_texts_count = await conn.execute(text("SELECT COUNT(*) FROM welcome_texts"))
|
||
broadcasts_count = await conn.execute(text("SELECT COUNT(*) FROM broadcast_history"))
|
||
|
||
conv_count = conversions_count.fetchone()[0]
|
||
usr_count = users_count.fetchone()[0]
|
||
welcome_count = welcome_texts_count.fetchone()[0]
|
||
broadcast_count = broadcasts_count.fetchone()[0]
|
||
|
||
logger.info(f"📊 Статистика: {usr_count} пользователей, {conv_count} конверсий, {welcome_count} приветственных текстов, {broadcast_count} рассылок")
|
||
except Exception as stats_error:
|
||
logger.debug(f"Не удалось получить дополнительную статистику: {stats_error}")
|
||
|
||
else:
|
||
logger.warning("⚠️ Некоторые миграции требуют внимания")
|
||
missing_migrations = [check_names[k] for k, v in status.items() if not v]
|
||
logger.warning(f"Требуют выполнения: {', '.join(missing_migrations)}")
|
||
|
||
return status
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка проверки статуса миграций: {e}")
|
||
return None
|