diff --git a/database.py b/database.py index 688f0ebf..a833e664 100644 --- a/database.py +++ b/database.py @@ -75,18 +75,26 @@ async def init_db(file_path: str = "assets/schema.sql"): await conn.close() -async def check_unique_server_name(server_name: str) -> bool: +async def check_unique_server_name(server_name: str, session: Any, cluster_name: str | None = None) -> bool: """ Проверяет уникальность имени сервера. :param server_name: Имя сервера. + :param session: Сессия базы данных. + :param cluster_name: Имя кластера (опционально). :return: True, если имя сервера уникально, False, если уже существует. """ - conn = await asyncpg.connect(DATABASE_URL) - - result = await conn.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) - - await conn.close() + if cluster_name: + result = await session.fetchrow( + "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", + server_name, + cluster_name + ) + else: + result = await session.fetchrow( + "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", + server_name + ) return result is None @@ -122,6 +130,39 @@ async def create_coupon(coupon_code: str, amount: float, usage_limit: int, sessi logger.error(f"Ошибка при создании купона {coupon_code}: {e}") raise +async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None: + """ + Получает информацию о купоне по его коду. + + Args: + coupon_code (str): Код купона для поиска + session (Any): Сессия базы данных + + Returns: + dict | None: Словарь с информацией о купоне или None, если купон не найден + - id (int): ID купона + - usage_limit (int): Лимит использований + - usage_count (int): Текущее количество использований + - is_used (bool): Флаг использования + - amount (float): Сумма купона + + Raises: + Exception: В случае ошибки при выполнении запроса + """ + try: + result = await session.fetchrow( + """ + SELECT id, usage_limit, usage_count, is_used, amount + FROM coupons + WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE + """, + coupon_code, + ) + return dict(result) if result else None + except Exception as e: + logger.error(f"Ошибка при получении купона {coupon_code}: {e}") + raise + async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): """ @@ -1259,3 +1300,110 @@ async def add_server_to_db( except Exception as e: logger.error(f"Ошибка при добавлении сервера {server_name} в кластер {cluster_name}: {e}") raise + +async def delete_server(server_name: str, session: Any): + """ + Удаляет сервер из базы данных по его названию. + + Args: + server_name (str): Название сервера для удаления + session (Any): Сессия базы данных + + Raises: + Exception: В случае ошибки при удалении сервера + """ + try: + await session.execute( + """ + DELETE FROM servers WHERE server_name = $1 + """, + server_name, + ) + logger.info(f"Сервер {server_name} успешно удалён из базы данных") + except Exception as e: + logger.error(f"Ошибка при удалении сервера {server_name} из базы данных: {e}") + raise + + +async def create_coupon_usage(coupon_id: int, user_id: int, session: Any): + """ + Создаёт запись об использовании купона в базе данных. + + Args: + coupon_id (int): ID купона + user_id (int): ID пользователя + session (Any): Сессия базы данных + + Raises: + Exception: В случае ошибки при создании записи + """ + try: + await session.execute( + """ + INSERT INTO coupon_usages (coupon_id, user_id, used_at) + VALUES ($1, $2, $3) + """, + coupon_id, + user_id, + datetime.utcnow(), + ) + logger.info(f"Создана запись об использовании купона {coupon_id} пользователем {user_id}") + except Exception as e: + logger.error(f"Ошибка при создании записи об использовании купона {coupon_id} пользователем {user_id}: {e}") + raise + +async def check_coupon_usage(coupon_id: int, user_id: int, session: Any) -> bool: + """ + Проверяет, использовал ли пользователь данный купон. + + Args: + coupon_id (int): ID купона для проверки + user_id (int): ID пользователя для проверки + session (Any): Сессия базы данных + + Returns: + bool: True если купон уже использован, False если нет + + Raises: + Exception: В случае ошибки при выполнении запроса + """ + try: + result = await session.fetchrow( + """ + SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2 + """, + coupon_id, + user_id, + ) + return result is not None + except Exception as e: + logger.error(f"Ошибка при проверке использования купона {coupon_id} пользователем {user_id}: {e}") + raise + + +async def update_coupon_usage_count(coupon_id: int, session: Any): + """ + Обновляет счетчик использования купона и его статус. + + Args: + coupon_id (int): ID купона для обновления + session (Any): Сессия базы данных + + Raises: + Exception: В случае ошибки при обновлении данных купона + """ + try: + await session.execute( + """ + UPDATE coupons + SET usage_count = usage_count + 1, + is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END + WHERE id = $1 + """, + coupon_id, + ) + logger.info(f"Успешно обновлен счетчик использования купона {coupon_id}") + except Exception as e: + logger.error(f"Ошибка при обновлении счетчика использования купона {coupon_id}: {e}") + raise + diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index c110fda7..fd429596 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -11,7 +11,7 @@ from py3xui import AsyncApi from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from database import add_server_to_db, check_unique_server_name, get_keys_by_server, get_servers_from_db +from database import add_server_to_db, check_unique_server_name, delete_server, get_keys_by_server, get_servers_from_db from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster from logger import logger @@ -93,7 +93,7 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): @router.message(UserEditorState.waiting_for_server_name, IsAdminFilter()) -async def handle_server_name_input(message: types.Message, state: FSMContext): +async def handle_server_name_input(message: types.Message, state: FSMContext, session: Any): server_name = message.text.strip() if server_name == "❌ Отменить": @@ -110,7 +110,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.") return - server_unique = await check_unique_server_name(server_name) + server_unique = await check_unique_server_name(server_name, session) if not server_unique: await message.answer("❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.") return @@ -432,17 +432,10 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo @router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) -async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext, session: Any): server_name = callback_query.data.split("|")[1] - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - DELETE FROM servers WHERE server_name = $1 - """, - server_name, - ) - await conn.close() + await delete_server(server_name, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) diff --git a/handlers/coupons.py b/handlers/coupons.py index 6c340090..6c74359e 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -7,7 +7,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import update_balance +from database import check_coupon_usage, create_coupon_usage, get_coupon_by_code, update_balance, update_coupon_usage_count class CouponActivationState(StatesGroup): @@ -43,51 +43,21 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session async def activate_coupon(user_id: int, coupon_code: str, session: Any): - coupon_record = await session.fetchrow( - """ - SELECT id, usage_limit, usage_count, is_used, amount - FROM coupons - WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE - """, - coupon_code, - ) + coupon_record = await get_coupon_by_code(coupon_code, session) if not coupon_record: return "❌ Купон не найден 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍" - usage_exists = await session.fetchrow( - """ - SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2 - """, - coupon_record["id"], - user_id, - ) + usage_exists = await check_coupon_usage(coupon_record["id"], user_id, session) + if usage_exists: return "❌ Вы уже активировали этот купон. 🚫 Купоны могут быть активированы только один раз. 🔒" coupon_amount = coupon_record["amount"] - async with session.transaction(): - await session.execute( - """ - UPDATE coupons - SET usage_count = usage_count + 1, - is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END - WHERE id = $1 - """, - coupon_record["id"], - ) - - await session.execute( - """ - INSERT INTO coupon_usages (coupon_id, user_id, used_at) - VALUES ($1, $2, $3) - """, - coupon_record["id"], - user_id, - datetime.utcnow(), - ) + await update_coupon_usage_count(coupon_record["id"], session) + await create_coupon_usage(coupon_record["id"], user_id, session) await update_balance(user_id, coupon_amount, session) return f"✅ Купон успешно активирован! 🎉\n\nНа ваш баланс добавлено {coupon_amount} рублей 💰." diff --git a/servers.py b/servers.py index af439a0b..f97b7bbb 100644 --- a/servers.py +++ b/servers.py @@ -9,7 +9,7 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL, PING_TIME -from database import add_server_to_db, get_servers_from_db +from database import add_server_to_db, check_unique_server_name, get_servers_from_db from logger import logger try: @@ -34,14 +34,7 @@ async def sync_servers_with_db(): for cluster_name, servers in CLUSTERS.items(): for server_key, server_info in servers.items(): - exists = await conn.fetchval( - """ - SELECT 1 FROM servers - WHERE cluster_name = $1 AND server_name = $2 - """, - cluster_name, - server_info["name"], - ) + exists = await check_unique_server_name(server_info["name"], conn, cluster_name) if not exists: await add_server_to_db(