diff --git a/bot.py b/bot.py index e0488422..c01c2d8d 100644 --- a/bot.py +++ b/bot.py @@ -10,15 +10,17 @@ storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) router = Router() -from handlers import commands, notifications, pay, profile, start -from handlers.admin import admin_commands, admin_panel, admin_user_editor +from handlers import commands, coupons, notifications, pay, profile, start +from handlers.admin import admin_commands, admin_coupons, admin_panel, admin_user_editor from handlers.keys import key_management, keys from handlers.payments import cryprobot_pay, freekassa_pay, stars_pay, yookassa_pay dp.include_router(admin_commands.router) +dp.include_router(admin_coupons.router) dp.include_router(admin_panel.router) dp.include_router(admin_user_editor.router) dp.include_router(commands.router) +dp.include_router(coupons.router) dp.include_router(start.router) dp.include_router(profile.router) dp.include_router(keys.router) diff --git a/database.py b/database.py index 1459cecc..c1fcc078 100644 --- a/database.py +++ b/database.py @@ -9,6 +9,7 @@ from config import BONUS_PERCENT, DATABASE_URL async def init_db(): conn = await asyncpg.connect(DATABASE_URL) + # Таблица для хранения информации о пользователях await conn.execute( """ CREATE TABLE IF NOT EXISTS connections ( @@ -16,9 +17,10 @@ async def init_db(): balance REAL NOT NULL DEFAULT 0.0, trial INTEGER NOT NULL DEFAULT 0 ) - """ + """ ) + # Таблица для хранения ключей await conn.execute( """ CREATE TABLE IF NOT EXISTS keys ( @@ -28,54 +30,123 @@ async def init_db(): created_at BIGINT NOT NULL, expiry_time BIGINT NOT NULL, key TEXT NOT NULL, - server_id TEXT NOT NULL DEFAULT 'server1', -- поле для идентификатора сервера - notified BOOLEAN NOT NULL DEFAULT FALSE, -- новое поле для статуса уведомления + server_id TEXT NOT NULL DEFAULT 'cluster1', + notified BOOLEAN NOT NULL DEFAULT FALSE, + notified_24h BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY (tg_id, client_id) ) - """ + """ ) + + # Таблица для хранения рефералов await conn.execute( """ CREATE TABLE IF NOT EXISTS referrals ( - referred_tg_id BIGINT PRIMARY KEY NOT NULL, -- ID приглашенного пользователя - referrer_tg_id BIGINT NOT NULL, -- ID пригласившего пользователя - reward_issued BOOLEAN DEFAULT FALSE -- Был ли начислен бонус + referred_tg_id BIGINT PRIMARY KEY NOT NULL, + referrer_tg_id BIGINT NOT NULL, + reward_issued BOOLEAN DEFAULT FALSE ) - """ + """ ) - try: - await conn.execute( - """ - ALTER TABLE keys - ADD COLUMN server_id TEXT NOT NULL DEFAULT 'server1' + # Таблица для хранения купонов + await conn.execute( """ + CREATE TABLE IF NOT EXISTS coupons ( + id SERIAL PRIMARY KEY, + code TEXT UNIQUE NOT NULL, + amount INTEGER NOT NULL, + usage_limit INTEGER NOT NULL DEFAULT 1, + usage_count INTEGER NOT NULL DEFAULT 0, + is_used BOOLEAN NOT NULL DEFAULT FALSE ) - except asyncpg.exceptions.DuplicateColumnError: - pass + """ + ) - try: - await conn.execute( - """ - ALTER TABLE keys - ADD COLUMN notified BOOLEAN NOT NULL DEFAULT FALSE + # Таблица для отслеживания использований купонов пользователями + await conn.execute( """ + CREATE TABLE IF NOT EXISTS coupon_usages ( + coupon_id INTEGER NOT NULL REFERENCES coupons(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL, + used_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (coupon_id, user_id) ) - except asyncpg.exceptions.DuplicateColumnError: - pass - try: - await conn.execute( - """ - ALTER TABLE keys - ADD COLUMN notified_24h BOOLEAN NOT NULL DEFAULT FALSE """ - ) - except asyncpg.exceptions.DuplicateColumnError: - pass + ) await conn.close() +async def create_coupon(coupon_code: str, amount: float, usage_limit: int): + conn = await asyncpg.connect(DATABASE_URL) + + try: + await conn.execute( + """ + INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) + VALUES ($1, $2, $3, 0, FALSE) + """, + coupon_code, + amount, + usage_limit, + ) + except Exception as e: + print(f"Ошибка при создании купона: {e}") + raise + finally: + await conn.close() + + +async def get_all_coupons(): + """Получить список всех купонов""" + conn = await asyncpg.connect(DATABASE_URL) + try: + coupons = await conn.fetch( + """ + SELECT code, amount, usage_limit, usage_count + FROM coupons + """ + ) + return coupons + except Exception as e: + print(f"Ошибка при получении купонов: {e}") + return [] + finally: + await conn.close() + + +async def delete_coupon_from_db(coupon_code: str): + """Удалить купон из базы данных по его коду""" + conn = await asyncpg.connect(DATABASE_URL) + try: + + coupon_record = await conn.fetchrow( + """ + SELECT id FROM coupons WHERE code = $1 + """, + coupon_code, + ) + + if not coupon_record: + return False + + await conn.execute( + """ + DELETE FROM coupons WHERE code = $1 + """, + coupon_code, + ) + + return True + + except Exception as e: + print(f"Ошибка при удалении купона: {e}") + return False + finally: + await conn.close() + + async def restore_trial(tg_id: int): conn = await asyncpg.connect(DATABASE_URL) try: diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py new file mode 100644 index 00000000..00764539 --- /dev/null +++ b/handlers/admin/admin_coupons.py @@ -0,0 +1,207 @@ +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder +from loguru import logger + +from database import create_coupon, delete_coupon_from_db, get_all_coupons + + +class AdminCouponsState(StatesGroup): + waiting_for_coupon_data = State() + + +router = Router() + + +@router.callback_query(F.data == "coupons_editor") +async def show_coupon_management_menu(callback_query: types.CallbackQuery): + try: + await callback_query.message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") + + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon") + ) + builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons")) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_admin_menu") + ) + + markup = builder.as_markup() + await callback_query.message.answer( + "🛠 Меню управления купонами:", reply_markup=markup + ) + await callback_query.answer() + + +@router.callback_query(F.data == "coupons") +async def show_coupon_list(callback_query: types.CallbackQuery): + try: + try: + await callback_query.message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") + + coupons = await get_all_coupons() + + if not coupons: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) + markup = builder.as_markup() + + await callback_query.message.answer( + "❌ На данный момент нет доступных купонов.\n" + "Вы можете вернуться в меню управления.", + parse_mode="HTML", + reply_markup=markup, + ) + await callback_query.answer() + return + + coupon_list = "📜 Список всех купонов:\n\n" + builder = InlineKeyboardBuilder() + + for coupon in coupons: + coupon_list += ( + f"Код: {coupon['code']}\n" + f"Сумма: {coupon['amount']} рублей\n" + f"Лимит использования: {coupon['usage_limit']} раз\n" + f"Использовано: {coupon['usage_count']} раз\n\n" + ) + + builder.row( + InlineKeyboardButton( + text=f"❌ Удалить {coupon['code']}", + callback_data=f"delete_coupon_{coupon['code']}", + ) + ) + + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) + + markup = builder.as_markup() + await callback_query.message.answer( + coupon_list, parse_mode="HTML", reply_markup=markup + ) + + except Exception as e: + logger.error(f"Ошибка при получении списка купонов: {e}") + await callback_query.message.answer( + f"❌ Произошла ошибка при получении списка купонов: {e}", parse_mode="HTML" + ) + await callback_query.answer() + + +@router.callback_query(F.data.startswith("delete_coupon_")) +async def handle_delete_coupon(callback_query: types.CallbackQuery): + coupon_code = callback_query.data[len("delete_coupon_") :] + + try: + result = await delete_coupon_from_db(coupon_code) + + if result: + try: + await callback_query.message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") + + await show_coupon_list(callback_query) + else: + await callback_query.message.answer( + f"❌ Купон с кодом {coupon_code} не найден.", parse_mode="HTML" + ) + await show_coupon_list(callback_query) + + except Exception as e: + logger.error(f"Ошибка при удалении купона: {e}") + await callback_query.message.answer( + f"❌ Произошла ошибка при удалении купона: {e}", parse_mode="HTML" + ) + await callback_query.answer() + + +@router.callback_query(F.data == "create_coupon") +async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext): + try: + await callback_query.message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") + + await callback_query.message.answer( + "Введите данные для создания купона в формате:\n\n" + "код сумма лимит\n\n" + "Пример: 'COUPON1 50 5'\n\n", + parse_mode="HTML", + ) + await state.set_state(AdminCouponsState.waiting_for_coupon_data) + await callback_query.answer() + + +@router.message(AdminCouponsState.waiting_for_coupon_data) +async def process_coupon_data(message: types.Message, state: FSMContext): + text = message.text.strip() + + parts = text.split() + if len(parts) != 3: + await message.answer( + "❌ Некорректный формат! Пожалуйста, введите данные в формате:\n" + "код сумма лимит\n" + "Пример: 'COUPON1 50 5'", + parse_mode="HTML", + ) + return + + try: + coupon_code = parts[0] + coupon_amount = float(parts[1]) + usage_limit = int(parts[2]) + except ValueError: + await message.answer( + "⚠️ Проверьте правильность введенных данных.\n" + "Сумма должна быть числом, а лимит — целым числом.", + parse_mode="HTML", + ) + return + + try: + await create_coupon(coupon_code, coupon_amount, usage_limit) + + result_message = ( + f"✅ Купон с кодом {coupon_code} успешно создан!\n" + f"Сумма: {coupon_amount} рублей\n" + f"Лимит использования: {usage_limit} раз." + ) + + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) + markup = builder.as_markup() + + try: + await message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") + + await message.answer(result_message, parse_mode="HTML", reply_markup=markup) + + await state.clear() + + except Exception as e: + logger.error(f"Ошибка при создании купона: {e}") + await message.answer( + f"❌ Ошибка при создании купона: {e}", parse_mode="HTML" + ) + + +@router.callback_query(F.data == "back_to_coupons_menu") +async def back_to_coupons_menu(callback_query: types.CallbackQuery): + """Возвращаем пользователя в меню управления купонами""" + await show_coupon_management_menu(callback_query) diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 08e5c907..c840524f 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -36,6 +36,11 @@ async def handle_admin_command(message: types.Message): text="👥 Управление пользователями", callback_data="user_editor" ) ) + builder.row( + InlineKeyboardButton( + text="🎟️ Управление купонами", callback_data="coupons_editor" + ) + ) builder.row( InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls") ) @@ -164,6 +169,11 @@ async def back_to_admin_menu(callback_query: CallbackQuery): builder.row( InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls") ) + builder.row( + InlineKeyboardButton( + text="🎟️ Управление купонами", callback_data="coupons_editor" + ) + ) builder.row( InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups") ) diff --git a/handlers/coupons.py b/handlers/coupons.py new file mode 100644 index 00000000..86aed0e4 --- /dev/null +++ b/handlers/coupons.py @@ -0,0 +1,122 @@ +from datetime import datetime + +import asyncpg +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder +from loguru import logger + +from config import DATABASE_URL +from database import update_balance + + +class CouponActivationState(StatesGroup): + waiting_for_coupon_code = State() + + +router = Router() + + +@router.callback_query(F.data == "activate_coupon") +async def handle_activate_coupon( + callback_query: types.CallbackQuery, state: FSMContext +): + try: + await callback_query.message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") + + await callback_query.message.answer( + "Введите код купона:\n\n" + "Пожалуйста, введите действующий код купона, который вы хотите активировать.", + parse_mode="HTML", + ) + await state.set_state(CouponActivationState.waiting_for_coupon_code) + await callback_query.answer() + + +@router.message(CouponActivationState.waiting_for_coupon_code) +async def process_coupon_code(message: types.Message, state: FSMContext): + try: + await message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") + + coupon_code = message.text.strip() + activation_result = await activate_coupon(message.from_user.id, coupon_code) + + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile") + ) + + markup = builder.as_markup() + + await message.answer(activation_result, reply_markup=markup, parse_mode="HTML") + await state.clear() + + +async def activate_coupon(user_id: int, coupon_code: str): + """Функция для активации купона""" + conn = await asyncpg.connect(DATABASE_URL) + + try: + coupon_record = await conn.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, + ) + + if not coupon_record: + return "❌ Купон не найден или его использование ограничено. Пожалуйста, проверьте код и попробуйте снова." + + usage_exists = await conn.fetchrow( + """ + SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2 + """, + coupon_record["id"], + user_id, + ) + + if usage_exists: + return "❌ Вы уже активировали этот купон. Купоны могут быть активированы только один раз." + + coupon_amount = coupon_record["amount"] + + async with conn.transaction(): + await conn.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 conn.execute( + """ + INSERT INTO coupon_usages (coupon_id, user_id, used_at) + VALUES ($1, $2, $3) + """, + coupon_record["id"], + user_id, + datetime.utcnow(), + ) + + await update_balance(user_id, coupon_amount) + return f"✅ Купон успешно активирован!\n\nНа ваш баланс добавлено {coupon_amount} рублей." + + except Exception as e: + logger.error(f"Ошибка при активации купона: {e}") + return ( + "⚠️ Произошла ошибка при активации купона.\nПопробуйте ещё раз позже." + ) + + finally: + await conn.close() diff --git a/handlers/pay.py b/handlers/pay.py index c0a21de2..42ebc494 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -17,7 +17,7 @@ async def handle_pay(callback_query: CallbackQuery): if YOOKASSA_ENABLE: builder.row( InlineKeyboardButton( - text="💳 ЯКасса: быстрый перевод", + text="💳 ЮКасса: быстрый перевод", callback_data="pay_yookassa", ) ) @@ -42,6 +42,11 @@ async def handle_pay(callback_query: CallbackQuery): callback_data="pay_stars", ) ) + builder.row( + InlineKeyboardButton( + text="🎟️ Активировать купон", callback_data="activate_coupon" + ) + ) builder.row( InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile") diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index 08346329..1bd35f3a 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -83,13 +83,11 @@ async def process_callback_pay_yookassa( ) ) builder.row( - InlineKeyboardButton( - text="💰 Ввести свою сумму", callback_data="enter_custom_amount" - ) - ) - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile") + InlineKeyboardButton( + text="💰 Ввести свою сумму", callback_data="enter_custom_amount" ) + ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile")) key_count = await get_key_count(tg_id) diff --git a/handlers/start.py b/handlers/start.py index 37ffe227..a06d602d 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -133,7 +133,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session): async def handle_about_vpn(callback_query: CallbackQuery): await callback_query.message.delete() - about_vpn_message = get_about_vpn("3.0.5_preStable") + about_vpn_message = get_about_vpn("3.0.6_preStable") builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu"))