From 709e8f09eb3030630e1231ea46ccb28f401e9531 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Dec 2025 14:28:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A7=D0=B5=D1=82=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB,=20=D1=8F=20=D0=B7=D0=B0=D0=B1=D1=8B?= =?UTF-8?q?=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 144 +++++++++++++++++++++++++++++++++++++++--- handlers/referrals.py | 99 ++++++++++++++++++++++++++++- handlers/start.py | 64 +++++++++++++++++-- 3 files changed, 290 insertions(+), 17 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index 8a5061d..c1ef4bb 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,7 +1,12 @@ # Профиль. последнее изменение 24.11.2025 -from aiogram import Router, types -from aiogram.types import CallbackQuery +from aiogram import Router, types, F +from aiogram.types import ( + CallbackQuery, + LabeledPrice, + Message, + PreCheckoutQuery, +) import logging from datetime import datetime from aiogram.enums.parse_mode import ParseMode @@ -233,17 +238,54 @@ async def popup_confirm_callback_handler(callback: CallbackQuery): await callback.answer() +# ===== Telegram Stars ===== + @router.callback_query(lambda callback: callback.data.startswith("method_stars_")) async def method_stars_handler(callback: CallbackQuery): """ - Заглушка: оплата через Telegram Stars. + Оплата через Telegram Stars. + Формируем invoice прямо из бота, без отдельного биллинга. """ - amount = callback.data.split("_")[-1] - await callback.message.edit_text( - f"⭐ Оплата через Telegram Stars на {amount} ₽ пока в разработке.\n\n" - "Позже сюда подвяжем реальный платёж.", - ) - await callback.answer() + try: + amount_str = callback.data.split("_")[-1] + amount_rub = int(float(amount_str)) + except Exception: + await callback.message.answer("Некорректная сумма для оплаты.") + await callback.answer() + return + + # Внутренний payload, чтобы при успешной оплате понять, что это пополнение баланса + payload = f"stars_topup:{callback.from_user.id}:{amount_rub}" + + # 1 ₽ == 1 Star, если нужна другая конвертация — меняй тут + stars_amount = amount_rub + + prices = [ + LabeledPrice( + label=f"Пополнение баланса на {amount_rub} ₽", + amount=stars_amount, + ) + ] + + try: + await callback.message.answer_invoice( + title="Пополнение баланса Lark VPN", + description=( + f"Пополнение баланса на {amount_rub} ₽ через Telegram Stars.\n\n" + "После успешной оплаты баланс будет зачислен автоматически." + ), + payload=payload, + provider_token="", # для Stars провайдер пустой + currency="XTR", + prices=prices, + ) + await callback.answer() + except Exception as e: + logger.exception(f"Ошибка при отправке invoice Telegram Stars: {e}") + await callback.message.answer( + "Не удалось создать счёт в Telegram Stars. Попробуй позже или выбери другой способ оплаты." + ) + await callback.answer() @router.callback_query(lambda callback: callback.data.startswith("method_ykassa_")) @@ -383,3 +425,87 @@ async def guide_callback_handler(callback: CallbackQuery): reply_markup=guide_keyboard(), ) await callback.answer() + + +# ===== Служебные хендлеры для платежей Telegram Stars ===== + +@router.pre_checkout_query() +async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery): + """ + Обязательный шаг для Telegram Payments: + подтверждаем pre_checkout_query, иначе платёж не пройдёт. + """ + try: + await pre_checkout_query.bot.answer_pre_checkout_query( + pre_checkout_query.id, + ok=True, + ) + except Exception as e: + logger.exception(f"Ошибка при answer_pre_checkout_query: {e}") + + +async def _process_stars_topup(message: Message): + """ + Логика зачисления средств после успешной оплаты Stars. + """ + sp = message.successful_payment + if not sp: + return + + payload = sp.invoice_payload or "" + parts = payload.split(":") + if len(parts) != 3 or parts[0] != "stars_topup": + logger.info( + "successful_payment не относится к пополнению баланса Stars.") + return + + _, telegram_id_str, amount_str = parts + + try: + amount_rub = int(amount_str) + except ValueError: + # На всякий случай fallback к total_amount + amount_rub = sp.total_amount + + data = { + "telegram_id": telegram_id_str, + "amount": amount_rub, + "currency": sp.currency, + "provider": "telegram_stars", + "telegram_payment_charge_id": sp.telegram_payment_charge_id, + } + + logger.info( + f"Обработка успешного платежа Telegram Stars: " + f"user={telegram_id_str}, amount={amount_rub}, currency={sp.currency}" + ) + + result = await call_api("POST", "/user/deposit", data) + + if result == "ERROR" or result is None: + await message.answer( + "⭐ Оплата через Telegram Stars прошла, но не удалось автоматически обновить баланс.\n" + "Если баланс не изменился — напиши, пожалуйста, в поддержку и укажи время платежа." + ) + return + + await message.answer( + f"⭐ Оплата через Telegram Stars успешно проведена.\n" + f"На твой баланс зачислено {amount_rub} ₽." + ) + + +@router.message(F.successful_payment) +async def successful_payment_handler(message: Message): + """ + Глобальный хендлер успешных платежей. + Сейчас используем только для пополнения баланса через Stars. + """ + try: + await _process_stars_topup(message) + except Exception as e: + logger.exception(f"Ошибка при обработке successful_payment: {e}") + await message.answer( + "Оплата прошла, но произошла ошибка при обработке. " + "Если баланс не изменился — напиши, пожалуйста, в поддержку." + ) diff --git a/handlers/referrals.py b/handlers/referrals.py index 283891a..0a470aa 100644 --- a/handlers/referrals.py +++ b/handlers/referrals.py @@ -2,25 +2,118 @@ 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 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}" + # Базовый текст: без фейковых тире и нулевых рублей text = ( "👥 Реферальная программа\n\n" "Зови друзей в Lark VPN и получай бонусы на баланс.\n\n" f"🔗 Твоя ссылка:\n{link}\n\n" - "👤 Приглашено: —\n" - "💰 Начислено бонусов: — ₽\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 @@ -41,7 +134,7 @@ 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( diff --git a/handlers/start.py b/handlers/start.py index 69c20eb..9570b62 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -54,19 +54,49 @@ def _welcome_text(username: str | None) -> str: return "🥚 Lark Security\n\nВыберите действие из меню ниже." +def _parse_referrer_id(message: Message) -> int | None: + """ + Достаём ref_ из /start. + Примеры: + /start + /start ref_123456789 + """ + text = message.text or "" + parts = text.split(maxsplit=1) + if len(parts) < 2: + return None + + arg = parts[1].strip() + if not arg.startswith("ref_"): + return None + + raw_id = arg[4:] + if not raw_id.isdigit(): + return None + + try: + return int(raw_id) + except ValueError: + return None + + @router.message(Command("start")) async def start_command(message: Message): """ Обработчик команды /start. - Визуал и текст — обновлены, логика работы с API не тронута. + Добавлена обработка реферального payload'а ref_. """ logger.info( f"Получена команда /start от пользователя: " - f"{message.from_user.id} ({message.from_user.username})" + f"{message.from_user.id} ({message.from_user.username}) | text='{message.text}'" ) + user_id = message.from_user.id + referrer_id = _parse_referrer_id(message) + try: - user_data = await call_api("GET", f"/user/{message.from_user.id}") + # 1. Проверяем/создаём пользователя в базе + user_data = await call_api("GET", f"/user/{user_id}") if not user_data: logger.debug( "Пользователь не найден в базе, создаем новую запись." @@ -74,9 +104,33 @@ async def start_command(message: Message): await call_api( "POST", "/user/create", - {"telegram_id": message.from_user.id}, + {"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": + logger.error( + f"Не удалось зафиксировать реферала в бекенде: {payload}" + ) + elif referrer_id == user_id: + logger.info( + "Обнаружена попытка самореферала, запрос в бекенд не отправляем." + ) + + # 3. Приветственное сообщение и главное меню logger.debug("Отправка приветственного сообщения пользователю.") await message.answer( _welcome_text(message.from_user.username), @@ -86,7 +140,7 @@ async def start_command(message: Message): except Exception as e: logger.exception( f"Ошибка при обработке команды /start для пользователя " - f"{message.from_user.id}: {e}" + f"{user_id}: {e}" ) await message.answer("Произошла ошибка. Попробуйте позже.")