mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
3130 lines
147 KiB
Python
3130 lines
147 KiB
Python
import logging
|
||
from datetime import datetime
|
||
|
||
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 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_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_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 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_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_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"
|
||
)
|
||
|
||
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 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
|
||
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 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_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_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 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 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_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 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 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("⚠️ Проблемы с миграцией реферальной системы")
|
||
|
||
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("=== СОЗДАНИЕ ТАБЛИЦЫ 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("=== ПРОВЕРКА БАЗОВЫХ ТОКЕНОВ ВЕБ-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("=== СОЗДАНИЕ ТАБЛИЦЫ MULEN PAY ===")
|
||
mulenpay_created = await create_mulenpay_payments_table()
|
||
if mulenpay_created:
|
||
logger.info("✅ Таблица Mulen Pay payments готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей Mulen Pay payments")
|
||
|
||
mulenpay_schema_ok = await ensure_mulenpay_payment_schema()
|
||
if mulenpay_schema_ok:
|
||
logger.info("✅ Схема Mulen Pay payments актуальна")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить схему Mulen Pay payments")
|
||
|
||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PAL24 ===")
|
||
pal24_created = await create_pal24_payments_table()
|
||
if pal24_created:
|
||
logger.info("✅ Таблица Pal24 payments готова")
|
||
else:
|
||
logger.warning("⚠️ Проблемы с таблицей Pal24 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")
|
||
|
||
user_discount_columns_ready = await ensure_user_promo_offer_discount_columns()
|
||
if user_discount_columns_ready:
|
||
logger.info("✅ Колонки пользовательских промо-скидок готовы")
|
||
else:
|
||
logger.warning("⚠️ Не удалось обновить пользовательские промо-скидки")
|
||
|
||
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")
|
||
|
||
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("=== СОЗДАНИЕ/ОБНОВЛЕНИЕ ТАБЛИЦЫ 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("=== ДОБАВЛЕНИЕ МЕДИА ПОЛЕЙ В 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("=== СОЗДАНИЕ ТАБЛИЦЫ АУДИТА ПОДДЕРЖКИ ===")
|
||
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("=== ОБНОВЛЕНИЕ ВНЕШНИХ КЛЮЧЕЙ ===")
|
||
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")
|
||
|
||
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("✅ Таблица конверсий подписок создана")
|
||
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,
|
||
"user_messages_table": False,
|
||
"welcome_texts_table": False,
|
||
"welcome_texts_is_enabled_column": False,
|
||
"broadcast_history_media_fields": False,
|
||
"subscription_duplicates": False,
|
||
"subscription_conversions_table": False,
|
||
"promo_groups_table": False,
|
||
"server_promo_groups_table": 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,
|
||
"subscription_crypto_link_column": False,
|
||
"discount_offers_table": False,
|
||
"discount_offers_effect_column": False,
|
||
"discount_offers_extra_column": False,
|
||
"promo_offer_templates_table": False,
|
||
"promo_offer_templates_active_discount_column": False,
|
||
"promo_offer_logs_table": False,
|
||
"subscription_temporary_access_table": 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["user_messages_table"] = await check_table_exists('user_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["promo_groups_table"] = await check_table_exists('promo_groups')
|
||
status["server_promo_groups_table"] = await check_table_exists('server_squad_promo_groups')
|
||
|
||
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["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')
|
||
|
||
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["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link')
|
||
|
||
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
|
||
|
||
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",
|
||
"user_messages_table": "Таблица пользовательских сообщений",
|
||
"welcome_texts_table": "Таблица приветственных текстов",
|
||
"privacy_policies_table": "Таблица политик конфиденциальности",
|
||
"public_offers_table": "Таблица публичных оферт",
|
||
"welcome_texts_is_enabled_column": "Поле is_enabled в welcome_texts",
|
||
"broadcast_history_media_fields": "Медиа поля в broadcast_history",
|
||
"subscription_conversions_table": "Таблица конверсий подписок",
|
||
"subscription_duplicates": "Отсутствие дубликатов подписок",
|
||
"promo_groups_table": "Таблица промо-групп",
|
||
"server_promo_groups_table": "Связи серверов и промогрупп",
|
||
"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": "Колонка срока действия промо-скидки у пользователей",
|
||
"subscription_crypto_link_column": "Колонка subscription_crypto_link в subscriptions",
|
||
"discount_offers_table": "Таблица discount_offers",
|
||
"discount_offers_effect_column": "Колонка effect_type в discount_offers",
|
||
"discount_offers_extra_column": "Колонка extra_data в discount_offers",
|
||
"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",
|
||
}
|
||
|
||
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
|