From 33414dee65b10fc221d60bf04d33c91eaccc1131 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 6 Dec 2025 11:38:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=87=D0=B5=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B2=20=D1=84=D1=80=D0=BE=D0=BD=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 9 +- handlers/referrals.py | 113 +++------------------ handlers/start.py | 204 +++++++++++++++++++++----------------- handlers/subscriptions.py | 25 +++-- keyboard/keyboards.py | 21 ++-- 5 files changed, 152 insertions(+), 220 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index c1ef4bb..b060b4f 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -93,11 +93,12 @@ async def profile_callback_handler(callback: CallbackQuery): sub_status = "⚫ Нет активных" if sub_data: - expiry_date = sub_data.get("expiry_date") - if expiry_date: + end_date_str = sub_data.get("end_date") # Исправил + if end_date_str: try: - is_expired = datetime.fromisoformat( - expiry_date) < datetime.now() + is_expired = ( + datetime.fromisoformat(end_date_str) < datetime.now() + ) except ValueError: is_expired = True else: diff --git a/handlers/referrals.py b/handlers/referrals.py index 0a470aa..85e2ff6 100644 --- a/handlers/referrals.py +++ b/handlers/referrals.py @@ -2,126 +2,40 @@ from aiogram import Router, types from aiogram.filters import Command from aiogram.enums.parse_mode import ParseMode import logging -import aiohttp -from instences.config import BASE_URL_FASTAPI +from .start import call_api # используем уже готовый HTTP-хелпер router = Router() logger = logging.getLogger(__name__) -async def call_api(method, endpoint, data=None, base_url=BASE_URL_FASTAPI): - """ - Универсальный HTTP-запрос к FastAPI для рефералок. - - Ожидаем от бекенда: - GET /user/{telegram_id}/referrals -> { - "invited_count": int, - "bonus_total": float - } - """ - url = f"{base_url}{endpoint}" - logger.info( - f"[referrals] Инициализация запроса: {method} {url} с данными {data}") - - try: - async with aiohttp.ClientSession() as session: - async with session.request( - method, - url, - json=data, - headers={"Content-Type": "application/json"}, - ) as response: - logger.info( - f"[referrals] Получен ответ от {url}: статус {response.status}" - ) - - if response.status in {200, 201}: - result = await response.json() - logger.debug(f"[referrals] Ответ JSON: {result}") - return result - if response.status == 404: - logger.debug( - f"[referrals] Код {response.status}, возвращаю None" - ) - return None - logger.error( - f"[referrals] Ошибка в запросе: статус {response.status}, причина {response.reason}" - ) - return "ERROR" - except Exception as e: - logger.exception( - f"[referrals] Исключение при выполнении запроса к {url}: {e}") - return "ERROR" - - async def _build_referral_text(bot, user_id: int) -> str: - """ - Текст реферальной программы + статистика, если есть. - """ me = await bot.get_me() bot_username = me.username or "LarkVPN_bot" link = f"https://t.me/{bot_username}?start=ref_{user_id}" - # Базовый текст: без фейковых тире и нулевых рублей + invited_count = "—" + try: + data = await call_api("GET", f"/user/{user_id}/referrals") + if isinstance(data, dict): + invited_count = str(data.get("invited_count", 0)) + except Exception as e: + logger.exception( + f"Ошибка при получении количества рефералов для {user_id}: {e}" + ) + text = ( "👥 Реферальная программа\n\n" "Зови друзей в Lark VPN и получай бонусы на баланс.\n\n" f"🔗 Твоя ссылка:\n{link}\n\n" + f"👤 Приглашено: {invited_count}\n\n" ) - - invited_line = "" - bonus_line = "" - - stats = await call_api("GET", f"/user/{user_id}/referrals") - if isinstance(stats, dict): - invited = stats.get("invited_count") - bonus = stats.get("bonus_total") - - if invited is not None: - try: - invited_int = int(invited) - except (TypeError, ValueError): - invited_int = None - - if invited_int is not None: - invited_line = f"👤 Приглашено: {invited_int}\n" - - if bonus is not None: - try: - bonus_val = float(bonus) - except (TypeError, ValueError): - bonus_val = None - - # Строку с бонусами показываем только если реально что-то начислено - if bonus_val is not None and bonus_val > 0: - bonus_line = f"💰 Начислено бонусов: {bonus_val:.2f} ₽\n" - else: - logger.warning( - f"[referrals] Не удалось получить статистику для user_id={user_id}: {stats}" - ) - - if invited_line: - text += invited_line - if bonus_line: - text += bonus_line - - if invited_line or bonus_line: - text += "\n" - - text += ( - "Бонусы падают автоматически, когда приглашённые пополняют баланс." - ) - return text @router.message(Command("referrals")) async def referrals_command(message: types.Message): - """ - Команда /referrals — показывает текст реферальной программы. - """ logger.info(f"Получена команда /referrals от {message.from_user.id}") try: text = await _build_referral_text(message.bot, message.from_user.id) @@ -133,9 +47,6 @@ async def referrals_command(message: types.Message): @router.callback_query(lambda callback: callback.data == "referral") async def referrals_callback(callback: types.CallbackQuery): - """ - Кнопка «Реферальная программа» (если где-то есть). - """ try: text = await _build_referral_text( callback.message.bot, diff --git a/handlers/start.py b/handlers/start.py index 9570b62..3debb69 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,62 +1,52 @@ from aiogram import Router, types from aiogram.filters import Command -from aiogram.types import Message, CallbackQuery -import logging -from instences.config import BASE_URL_FASTAPI -import aiohttp -from keyboard.keyboards import main_keyboard from aiogram.enums.parse_mode import ParseMode +import logging +import aiohttp + +from aiogram.types import Message, CallbackQuery + +from instences.config import BASE_URL_FASTAPI +from keyboard.keyboards import main_keyboard from .referrals import _build_referral_text router = Router() logger = logging.getLogger(__name__) -async def call_api(method, endpoint, data=None): - """ - Выполняет HTTP-запрос к FastAPI. - :param method: HTTP метод (GET, POST, и т.д.) - :param endpoint: конечная точка API - :param data: тело запроса (если необходимо) - :return: JSON-ответ или "ERROR" при неуспехе - """ +async def call_api(method: str, endpoint: str, data: dict | None = None): + """Мини-обёртка для запросов к backend API.""" url = f"{BASE_URL_FASTAPI}{endpoint}" - logger.info(f"Инициализация запроса: {method} {url} с данными {data}") + logger.info(f"API {method} {url} data={data}") try: async with aiohttp.ClientSession() as session: - async with session.request(method, url, json=data) as response: - logger.info( - f"Получен ответ от {url}: статус {response.status}" - ) - - if response.status in {200, 201}: - result = await response.json() - logger.debug(f"Ответ JSON: {result}") - return result - if response.status == 404: - logger.debug(f"Код {response.status}, возвращаю ничего") - return None + async with session.request(method, url, json=data) as resp: + logger.info(f"API response {resp.status} {resp.reason}") + if resp.status in {200, 201}: + return await resp.json() + if resp.status in {400, 404}: + try: + return await resp.json() + except Exception: + return None logger.error( - f"Ошибка в запросе: статус {response.status}, причина {response.reason}" + f"Unexpected status {resp.status}: {await resp.text()}" ) return "ERROR" except Exception as e: - logger.exception(f"Исключение при выполнении запроса к {url}: {e}") + logger.exception(f"API exception {url}: {e}") return "ERROR" def _welcome_text(username: str | None) -> str: - """ - Текст приветствия в /start и в главном меню. - Имя пока не используем — оставляем сигнатуру на будущее. - """ + """Текст приветствия в /start и в главном меню.""" return "🥚 Lark Security\n\nВыберите действие из меню ниже." def _parse_referrer_id(message: Message) -> int | None: - """ - Достаём ref_ из /start. + """Достаём ref_ из /start. + Примеры: /start /start ref_123456789 @@ -82,86 +72,117 @@ def _parse_referrer_id(message: Message) -> int | None: @router.message(Command("start")) async def start_command(message: Message): - """ - Обработчик команды /start. - Добавлена обработка реферального payload'а ref_. - """ - logger.info( - f"Получена команда /start от пользователя: " - f"{message.from_user.id} ({message.from_user.username}) | text='{message.text}'" - ) + """/start с обработкой реферального параметра. + Логика: + - проверяем, есть ли пользователь в БД по telegram_id; + - если нет — создаём через /user/create; + - если есть корректный ref_ и пользователь новый — вызываем + /user/{referrer_id}/add_referral с invited_id = telegram_id. + """ user_id = message.from_user.id + username = message.from_user.username referrer_id = _parse_referrer_id(message) + logger.info( + f"[start] Команда /start от {user_id} (@{username}), " + f"text={message.text!r}, referrer_id={referrer_id}" + ) + try: - # 1. Проверяем/создаём пользователя в базе - user_data = await call_api("GET", f"/user/{user_id}") - if not user_data: - logger.debug( - "Пользователь не найден в базе, создаем новую запись." - ) - await call_api( + # 1. Проверяем, есть ли пользователь в БД + existing = await call_api("GET", f"/user/{user_id}") + user_exists = existing not in (None, "ERROR") + + # 2. Если пользователя нет — создаём + if not user_exists: + logger.debug(f"[start] Пользователь {user_id} не найден, создаю.") + create_result = await call_api( "POST", "/user/create", {"telegram_id": user_id}, ) - - # 2. Если есть реферер и он не сам пользователь — отправляем инфу в бекенд - if referrer_id and referrer_id != user_id: - payload = { - "referrer_id": referrer_id, - "telegram_id": user_id, - } - logger.info( - f"Отправка данных о реферале в бекенд: {payload}" - ) - result = await call_api( - "POST", - "/user/referrals/track", - payload, - ) - if result == "ERROR": + if create_result == "ERROR": logger.error( - f"Не удалось зафиксировать реферала в бекенде: {payload}" + f"[start] Не удалось создать пользователя {user_id} в БД" ) - elif referrer_id == user_id: - logger.info( - "Обнаружена попытка самореферала, запрос в бекенд не отправляем." - ) - # 3. Приветственное сообщение и главное меню - logger.debug("Отправка приветственного сообщения пользователю.") + # 3. Обработка рефералки, если параметр есть + if referrer_id is not None: + # 3.1. Самореферал + if referrer_id == user_id: + logger.info( + f"[start] Пользователь {user_id} попытался зайти " + f"по своей реферальной ссылке." + ) + await message.answer( + "Нельзя переходить по своей же реферальной ссылке." + ) + + # 3.2. Пользователь уже зарегистрирован в боте + elif user_exists: + logger.info( + f"[start] Пользователь {user_id} уже есть в БД, " + f"реферальная ссылка {referrer_id} не сработает." + ) + await message.answer( + "Вы уже зарегистрированы в боте, " + "реферальная ссылка не сработает." + ) + + # 3.3. Новый пользователь + чужая рефералка → регистрируем реферал + else: + payload = { + "invited_id": user_id, + } + logger.info( + f"[start] Фиксирую реферала в бекенде: " + f"referrer_id={referrer_id}, payload={payload}" + ) + result = await call_api( + "POST", + f"/user/{referrer_id}/add_referral", + payload, + ) + if result == "ERROR": + logger.error( + f"[start] Ошибка при фиксации реферала через " + f"/user/{referrer_id}/add_referral: referrer={referrer_id}, " + f"invited={user_id}" + ) + await message.answer("Вы вошли по реферальной ссылке.") + + # 4. В любом случае показываем главное меню await message.answer( - _welcome_text(message.from_user.username), + _welcome_text(username), reply_markup=main_keyboard(), ) - logger.info("Приветственное сообщение отправлено.") + logger.info(f"[start] Главное меню отправлено пользователю {user_id}.") + except Exception as e: logger.exception( - f"Ошибка при обработке команды /start для пользователя " - f"{user_id}: {e}" + f"[start] Ошибка при обработке /start для пользователя {user_id}: {e}" ) await message.answer("Произошла ошибка. Попробуйте позже.") @router.message(Command("referrals")) async def referrals_menu_command(message: Message): - """ - Команда /referrals из бокового меню Telegram. + """Команда /referrals из бокового меню Telegram. + Показывает текст реферальной программы. """ logger.info( - f"Получена команда /referrals от пользователя: " - f"{message.from_user.id} ({message.from_user.username})" + f"[start] Команда /referrals от {message.from_user.id} " + f"(@{message.from_user.username})" ) try: text = await _build_referral_text(message.bot, message.from_user.id) await message.answer(text, parse_mode=ParseMode.HTML) - logger.info("Реферальная программа отправлена пользователю.") + logger.info("[start] Реферальная программа отправлена пользователю.") except Exception as e: logger.exception( - f"Ошибка при обработке команды /referrals для пользователя " + f"[start] Ошибка при обработке /referrals для пользователя " f"{message.from_user.id}: {e}" ) await message.answer("Произошла ошибка. Попробуйте позже.") @@ -169,25 +190,26 @@ async def referrals_menu_command(message: Message): @router.callback_query(lambda callback: callback.data == "base") async def start_callback_handler(callback: CallbackQuery): - """ - Обработчик callback_query с data="base". - Возвращает пользователя в главное меню. - """ + """Callback с data="base" — возврат в главное меню.""" try: - user_data = await call_api("GET", f"/user/{callback.from_user.id}") - if not user_data: + user_id = callback.from_user.id + username = callback.from_user.username + logger.info(f"[start] callback 'base' от {user_id} (@{username})") + + user_data = await call_api("GET", f"/user/{user_id}") + if user_data in (None, "ERROR"): await call_api( "POST", "/user/create", - {"telegram_id": callback.from_user.id}, + {"telegram_id": user_id}, ) await callback.message.edit_text( - _welcome_text(callback.from_user.username), + _welcome_text(username), reply_markup=main_keyboard(), ) except Exception as e: - logger.exception(f"Ошибка при обработке callback с data='base': {e}") + logger.exception(f"[start] Ошибка при обработке callback 'base': {e}") await callback.message.answer("Произошла ошибка. Попробуйте позже.") finally: await callback.answer() diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index da28b62..2140ad9 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -129,15 +129,15 @@ async def _show_subscriptions_view( index = max(0, min(index, len(subs) - 1)) sub = subs[index] - plan_title = _plan_human_title(sub.get("plan")) - expiry_raw = sub.get("expiry_date") + plan_title = _plan_human_title(sub.get("plan_name")) + end_raw = sub.get("end_date") # Исправил date_str = "—" days_left = None status_emoji = "🟢" - if expiry_raw: + if end_raw: try: - dt = datetime.fromisoformat(expiry_raw) + dt = datetime.fromisoformat(end_raw) date_str = dt.strftime("%d.%m.%Y") now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now() days_left = (dt.date() - now.date()).days @@ -272,19 +272,22 @@ async def cb_sub_cfg(callback: types.CallbackQuery): await callback.answer() return - if not isinstance(detail, str) or not detail: + uri = None + if isinstance(detail, str): + uri = detail.strip() + elif isinstance(detail, list) and detail: + uri = str(detail[0]).strip() + + if not uri: await callback.message.answer( "Что-то пошло не так при выдаче конфига." ) await callback.answer() return - escaped = escape_markdown_v2(detail) - text = f"Твой конфиг:\n```{escaped}```" - await callback.message.answer( - text, - parse_mode=ParseMode.MARKDOWN_V2, + f"Твой конфиг:\n{uri}", + parse_mode=ParseMode.HTML, ) await callback.answer() @@ -310,7 +313,7 @@ async def cb_sub_renew(callback: types.CallbackQuery): await callback.answer() return - plan_id = target.get("plan") + plan_id = target.get("plan_name") if not plan_id: await callback.message.answer("Не удалось определить тариф для продления.") await callback.answer() diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index e7e9879..176c67a 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -121,7 +121,7 @@ def payment_methods_keyboard(amount: int): ) builder.row( InlineKeyboardButton( - text="💵 YooKassa", + text="💵 СБП", callback_data=f"method_ykassa_{amount}", ) ) @@ -317,10 +317,11 @@ def tarif_Lark_family_keyboard(): def subscriptions_card_keyboard(sub_id: str, index: int, total: int): """ Карточка подписки: - навигация, конфиг, продление, новая, назад. + навигация, конфиг, назад в главное меню. """ builder = InlineKeyboardBuilder() + # Навигация по подпискам nav = [] if index > 0: nav.append( @@ -339,28 +340,22 @@ def subscriptions_card_keyboard(sub_id: str, index: int, total: int): if nav: builder.row(*nav) + # Конфиг builder.row( InlineKeyboardButton( text="🔑 Конфиг", callback_data=f"sub_cfg:{sub_id}", - ), - InlineKeyboardButton( - text="🔁 Продлить", - callback_data=f"sub_renew:{sub_id}", - ), - ) - builder.row( - InlineKeyboardButton( - text="➕ Новая", - callback_data="buy_subscription", ) ) + + # Назад в главное меню builder.row( InlineKeyboardButton( text="🔙 Назад", - callback_data="profile", + callback_data="base", # было "profile" изменил ) ) + return builder.as_markup()