Add files via upload

This commit is contained in:
Egor
2025-08-30 23:21:04 +03:00
committed by GitHub
parent 541b44b933
commit b09b7a0c84
10 changed files with 138 additions and 691 deletions

View File

@@ -51,13 +51,17 @@ async def get_transaction_by_id(db: AsyncSession, transaction_id: int) -> Option
async def get_transaction_by_external_id(
db: AsyncSession,
external_id: str
external_id: str,
payment_method: PaymentMethod
) -> Optional[Transaction]:
"""Получить транзакцию по external_id"""
result = await db.execute(
select(Transaction)
.options(selectinload(Transaction.user))
.where(Transaction.external_id == external_id)
.where(
and_(
Transaction.external_id == external_id,
Transaction.payment_method == payment_method.value
)
)
)
return result.scalar_one_or_none()
@@ -273,17 +277,16 @@ async def get_revenue_by_period(
return [{"date": row.date, "amount_kopeks": row.amount} for row in result]
async def find_tribute_transactions_by_payment_id(
db: AsyncSession,
payment_id: str,
user_telegram_id: Optional[int] = None
) -> List[Transaction]:
"""Найти все Tribute транзакции по payment_id"""
query = select(Transaction).options(selectinload(Transaction.user))
conditions = [
Transaction.external_id == f"tribute_{payment_id}",
Transaction.external_id == f"donation_{payment_id}",
Transaction.external_id == payment_id,
Transaction.external_id.like(f"%{payment_id}%")
@@ -331,14 +334,15 @@ async def create_unique_tribute_transaction(
amount_kopeks: int,
description: str
) -> Transaction:
"""Создать уникальную Tribute транзакцию с защитой от дубликатов"""
external_id = f"tribute_{payment_id}"
external_id = f"donation_{payment_id}"
existing = await get_transaction_by_external_id(db, external_id)
existing = await get_transaction_by_external_id(db, external_id, PaymentMethod.TRIBUTE)
if existing:
timestamp = int(datetime.utcnow().timestamp())
external_id = f"tribute_{payment_id}_{amount_kopeks}_{timestamp}"
external_id = f"donation_{payment_id}_{amount_kopeks}_{timestamp}"
logger.info(f"Создан уникальный external_id для избежания дубликатов: {external_id}")
@@ -352,70 +356,3 @@ async def create_unique_tribute_transaction(
external_id=external_id,
is_completed=True
)
class TransactionCRUD:
async def create_transaction(
self,
db: AsyncSession,
transaction_data: dict
) -> Optional[Transaction]:
try:
transaction = Transaction(
user_id=transaction_data['user_id'],
type=transaction_data['transaction_type'],
amount_kopeks=transaction_data['amount_kopeks'],
description=transaction_data['description'],
external_id=transaction_data.get('external_id'),
payment_method=transaction_data.get('payment_system'),
is_completed=transaction_data.get('status') == 'completed'
)
db.add(transaction)
await db.commit()
await db.refresh(transaction)
logger.info(f"Создана транзакция: {transaction.id} на {transaction.amount_kopeks} коп.")
return transaction
except Exception as e:
logger.error(f"Ошибка создания транзакции: {e}")
await db.rollback()
return None
async def get_transaction_by_external_id(
self,
db: AsyncSession,
external_id: str
) -> Optional[Transaction]:
return await get_transaction_by_external_id(db, external_id)
async def update_transaction_status(
self,
db: AsyncSession,
transaction_id: int,
status: str
) -> bool:
try:
result = await db.execute(
select(Transaction).where(Transaction.id == transaction_id)
)
transaction = result.scalar_one_or_none()
if not transaction:
logger.error(f"Транзакция {transaction_id} не найдена")
return False
transaction.is_completed = (status == 'completed')
if transaction.is_completed:
transaction.completed_at = datetime.utcnow()
await db.commit()
logger.info(f"Статус транзакции {transaction_id} обновлен на {status}")
return True
except Exception as e:
logger.error(f"Ошибка обновления статуса транзакции: {e}")
await db.rollback()
return False

View File

@@ -156,25 +156,25 @@ async def add_user_balance(
async def add_user_balance_by_id(
db: AsyncSession,
user_id: int,
telegram_id: int,
amount_kopeks: int,
description: str = "Пополнение баланса"
) -> bool:
"""Пополнить баланс пользователя по ID"""
try:
user = await get_user_by_id(db, user_id)
user = await get_user_by_telegram_id(db, telegram_id)
if not user:
user = await get_user_by_telegram_id(db, user_id)
if not user:
logger.error(f"Пользователь с ID {user_id} не найден")
logger.error(f"Пользователь с telegram_id {telegram_id} не найден")
return False
return await add_user_balance(db, user, amount_kopeks, description)
except Exception as e:
logger.error(f"Ошибка пополнения баланса пользователя {user_id}: {e}")
logger.error(f"Ошибка пополнения баланса пользователя {telegram_id}: {e}")
return False
except Exception as e:
logger.error(f"❌ Ошибка пополнения баланса пользователя {user_id}: {e}")
await db.rollback()
return False
async def subtract_user_balance(
@@ -356,14 +356,3 @@ async def get_users_statistics(db: AsyncSession) -> dict:
"new_week": new_week,
"new_month": new_month
}
class UserCRUD:
async def get_user(self, db: AsyncSession, user_id: int) -> Optional[User]:
return await get_user_by_id(db, user_id)
async def get_user_by_telegram_id(self, db: AsyncSession, telegram_id: int) -> Optional[User]:
return await get_user_by_telegram_id(db, telegram_id)
async def add_balance(self, db: AsyncSession, user_id: int, amount_kopeks: int) -> bool:
return await add_user_balance_by_id(db, user_id, amount_kopeks)

View File

@@ -28,16 +28,10 @@ class TributeService:
return None
try:
payment_url = f"{self.donate_link}?user_id={user_id}"
if amount_kopeks > 0:
amount_rubles = amount_kopeks / 100
payment_url += f"&amount={amount_rubles:.2f}"
payment_url = f"{self.donate_link}&user_id={user_id}"
if description:
payment_url += f"&description={description}"
logger.info(f"Создана ссылка Tribute для пользователя {user_id}: {amount_kopeks} коп.")
logger.info(f"Создана ссылка Tribute для пользователя {user_id}")
return payment_url
except Exception as e:
@@ -45,28 +39,19 @@ class TributeService:
return None
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
"""Проверяет подпись webhook"""
if not self.webhook_secret:
logger.warning("Webhook secret не настроен, пропускаем проверку подписи")
logger.warning("Webhook secret не настроен")
return True
try:
if signature.startswith('sha256='):
signature = signature[7:]
expected_signature = hmac.new(
self.webhook_secret.encode('utf-8'),
payload.encode('utf-8'),
self.webhook_secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
result = hmac.compare_digest(signature, expected_signature)
if not result:
logger.warning(f"Неверная подпись webhook. Получено: {signature[:10]}..., ожидалось: {expected_signature[:10]}...")
return result
return hmac.compare_digest(signature, expected_signature)
except Exception as e:
logger.error(f"Ошибка проверки подписи webhook: {e}")
@@ -75,104 +60,49 @@ class TributeService:
async def process_webhook(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
try:
logger.debug(f"Обработка Tribute webhook: {json.dumps(payload, ensure_ascii=False, indent=2)}")
payment_id = None
status = None
amount_kopeks = 0
telegram_user_id = None
payment_id = payload.get("id") or payload.get("payment_id") or payload.get("donation_id")
payment_id = payload.get("id") or payload.get("payment_id")
status = payload.get("status")
amount_kopeks = payload.get("amount", 0)
amount_kopeks = payload.get("amount", 0)
telegram_user_id = payload.get("telegram_user_id") or payload.get("user_id")
if not payment_id and "payload" in payload:
data = payload["payload"]
payment_id = data.get("id") or data.get("payment_id") or data.get("donation_id")
payment_id = data.get("id") or data.get("payment_id")
status = data.get("status")
amount_kopeks = data.get("amount", 0)
amount_kopeks = data.get("amount", 0)
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
if not payment_id and "name" in payload:
event_name = payload.get("name")
data = payload.get("payload", {})
payment_id = (
data.get("donation_request_id") or
data.get("donation_id") or
data.get("id") or
data.get("payment_id")
)
amount_kopeks = data.get("amount", 0)
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
payment_id = str(data.get("donation_request_id"))
amount_kopeks = data.get("amount", 0)
telegram_user_id = data.get("telegram_user_id")
if event_name == "new_donation":
status = "paid"
elif event_name == "donation_completed":
status = "completed"
elif event_name == "cancelled_subscription":
status = "cancelled"
else:
status = "unknown"
logger.warning(f"Неизвестное событие Tribute: {event_name}")
if not payment_id and "data" in payload:
data = payload["data"]
payment_id = data.get("id") or data.get("donation_id")
status = data.get("status", "paid")
amount_kopeks = data.get("amount", 0)
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
if isinstance(amount_kopeks, (int, float)):
if amount_kopeks > 1000:
amount_kopeks = int(amount_kopeks)
else:
amount_kopeks = int(amount_kopeks * 100)
else:
amount_kopeks = 0
logger.info(f"Извлеченные данные Tribute webhook:")
logger.info(f" - payment_id: {payment_id}")
logger.info(f" - status: {status}")
logger.info(f" - amount_kopeks: {amount_kopeks}")
logger.info(f" - telegram_user_id: {telegram_user_id}")
logger.info(f"Обработка Tribute webhook: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}")
if not telegram_user_id:
logger.error("Не найден telegram_user_id в webhook данных")
logger.error(f"Полные данные webhook: {json.dumps(payload, ensure_ascii=False)}")
return None
if amount_kopeks <= 0:
logger.error(f"Неверная сумма платежа: {amount_kopeks}")
return None
if not payment_id:
import time
payment_id = f"tribute_{telegram_user_id}_{int(time.time())}"
logger.info(f"Сгенерирован payment_id: {payment_id}")
else:
payment_id = str(payment_id)
if not status:
status = "paid"
result = {
return {
"event_type": "payment",
"payment_id": payment_id,
"payment_id": payment_id or f"tribute_{telegram_user_id}_{amount_kopeks}",
"user_id": int(telegram_user_id),
"amount_kopeks": amount_kopeks,
"status": status,
"external_id": f"tribute_{payment_id}",
"provider": "tribute"
"status": status or "paid",
"external_id": f"donation_{payment_id}"
}
logger.info(f"✅ Успешно обработан Tribute webhook: {result}")
return result
except Exception as e:
logger.error(f"Ошибка обработки Tribute webhook: {e}", exc_info=True)
logger.error(f"Ошибка обработки Tribute webhook: {e}")
logger.error(f"Webhook payload: {json.dumps(payload, ensure_ascii=False)}")
return None

View File

@@ -1,247 +1,98 @@
import logging
import asyncio
from typing import Optional, List
from typing import Optional
from aiohttp import web
from aiogram import Bot
from app.config import settings
from app.external.tribute import TributeService
from app.services.payment_service import PaymentService
from app.external.yookassa_webhook import YooKassaWebhookHandler
from app.services.tribute_service import TributeService
logger = logging.getLogger(__name__)
class WebhookServer:
def __init__(self, bot: Bot, payment_service: PaymentService):
def __init__(self, bot: Bot):
self.bot = bot
self.payment_service = payment_service
self.tribute_service = TributeService()
self.yookassa_handler = YooKassaWebhookHandler(payment_service)
self.tribute_app = None
self.tribute_runner = None
self.tribute_site = None
self.yookassa_app = None
self.yookassa_runner = None
self.yookassa_site = None
self.app = None
self.runner = None
self.site = None
self.tribute_service = TributeService(bot)
def _create_logging_middleware(self):
@web.middleware
async def logging_middleware(request, handler):
start_time = request.loop.time()
try:
response = await handler(request)
process_time = request.loop.time() - start_time
logger.info(f"{request.method} {request.path_qs} -> {response.status} ({process_time:.3f}s)")
return response
except Exception as e:
process_time = request.loop.time() - start_time
logger.error(f"{request.method} {request.path_qs} -> ERROR ({process_time:.3f}s): {e}")
raise
async def create_app(self) -> web.Application:
return logging_middleware
self.app = web.Application()
self.app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
self.app.router.add_get('/health', self._health_check)
logger.info(f"Webhook сервер настроен:")
logger.info(f" - Tribute webhook: {settings.TRIBUTE_WEBHOOK_PATH}")
logger.info(f" - Health check: /health")
return self.app
async def create_tribute_app(self) -> web.Application:
self.tribute_app = web.Application()
self.tribute_app.middlewares.append(self._create_logging_middleware())
self.tribute_app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
self.tribute_app.router.add_get('/health', self._tribute_health_check)
logger.info(f"Tribute webhook настроен:")
logger.info(f" - Webhook path: {settings.TRIBUTE_WEBHOOK_PATH}")
logger.info(f" - Port: {settings.TRIBUTE_WEBHOOK_PORT}")
return self.tribute_app
async def create_yookassa_app(self) -> web.Application:
self.yookassa_app = web.Application()
self.yookassa_app.middlewares.append(self._create_logging_middleware())
self.yookassa_app.router.add_post(settings.YOOKASSA_WEBHOOK_PATH, self.yookassa_handler.handle_webhook)
self.yookassa_app.router.add_get('/health', self._yookassa_health_check)
logger.info(f"YooKassa webhook настроен:")
logger.info(f" - Webhook path: {settings.YOOKASSA_WEBHOOK_PATH}")
logger.info(f" - Port: {settings.YOOKASSA_WEBHOOK_PORT}")
return self.yookassa_app
async def start_tribute_server(self):
if not settings.TRIBUTE_ENABLED:
logger.info("Tribute отключен, сервер не запускается")
return
async def start(self):
try:
await self.create_tribute_app()
if not self.app:
await self.create_app()
self.tribute_runner = web.AppRunner(self.tribute_app)
await self.tribute_runner.setup()
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.tribute_site = web.TCPSite(
self.tribute_runner,
host='0.0.0.0',
self.site = web.TCPSite(
self.runner,
host='0.0.0.0',
port=settings.TRIBUTE_WEBHOOK_PORT
)
await self.tribute_site.start()
await self.site.start()
logger.info(f"Tribute webhook сервер запущен на 0.0.0.0:{settings.TRIBUTE_WEBHOOK_PORT}")
logger.info(f"Webhook сервер запущен на порту {settings.TRIBUTE_WEBHOOK_PORT}")
logger.info(f"🎯 Tribute webhook URL: http://your-server:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
except Exception as e:
logger.error(f"❌ Ошибка запуска Tribute webhook сервера: {e}")
logger.error(f"❌ Ошибка запуска webhook сервера: {e}")
raise
async def start_yookassa_server(self):
if not settings.is_yookassa_enabled():
logger.info("YooKassa отключен, сервер не запускается")
return
try:
await self.create_yookassa_app()
self.yookassa_runner = web.AppRunner(self.yookassa_app)
await self.yookassa_runner.setup()
self.yookassa_site = web.TCPSite(
self.yookassa_runner,
host='0.0.0.0',
port=settings.YOOKASSA_WEBHOOK_PORT
)
await self.yookassa_site.start()
logger.info(f"✅ YooKassa webhook сервер запущен на 0.0.0.0:{settings.YOOKASSA_WEBHOOK_PORT}")
logger.info(f"🎯 YooKassa webhook URL: http://your-server:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
except Exception as e:
logger.error(f"❌ Ошибка запуска YooKassa webhook сервера: {e}")
raise
async def start(self):
tasks = []
if settings.TRIBUTE_ENABLED:
tasks.append(self.start_tribute_server())
if settings.is_yookassa_enabled():
tasks.append(self.start_yookassa_server())
if not tasks:
logger.warning("Ни один платежный провайдер не включен!")
return
await asyncio.gather(*tasks)
logger.info("🚀 Все webhook серверы запущены!")
async def stop(self):
tasks = []
if self.tribute_site:
tasks.append(self.tribute_site.stop())
if self.tribute_runner:
tasks.append(self.tribute_runner.cleanup())
if self.yookassa_site:
tasks.append(self.yookassa_site.stop())
if self.yookassa_runner:
tasks.append(self.yookassa_runner.cleanup())
if tasks:
try:
await asyncio.gather(*tasks, return_exceptions=True)
logger.info("🛑 Все webhook серверы остановлены")
except Exception as e:
logger.error(f"Ошибка остановки webhook серверов: {e}")
try:
if self.site:
await self.site.stop()
logger.info("Webhook сайт остановлен")
if self.runner:
await self.runner.cleanup()
logger.info("Webhook runner очищен")
except Exception as e:
logger.error(f"Ошибка остановки webhook сервера: {e}")
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
try:
raw_body = await request.read()
if not raw_body:
logger.warning("Получен пустой Tribute webhook")
return web.json_response(
{"status": "error", "reason": "empty_body"},
status=400
)
try:
payload = raw_body.decode('utf-8')
import json
webhook_data = json.loads(payload)
except (UnicodeDecodeError, json.JSONDecodeError) as e:
logger.error(f"Ошибка декодирования Tribute webhook: {e}")
return web.json_response(
{"status": "error", "reason": "invalid_payload"},
status=400
)
payload = raw_body.decode('utf-8')
signature = request.headers.get('X-Tribute-Signature')
if signature and not self.tribute_service.verify_webhook_signature(payload, signature):
logger.error("Неверная подпись Tribute webhook")
return web.json_response(
{"status": "error", "reason": "invalid_signature"},
status=400
)
result = await self.tribute_service.process_webhook(webhook_data)
result = await self.tribute_service.process_webhook(payload, signature)
if result:
from app.database.database import get_db
async with get_db() as db:
success = await self.payment_service.process_tribute_payment(
db=db,
user_id=result['user_id'],
amount_kopeks=result['amount_kopeks'],
payment_id=result['payment_id']
)
if success:
logger.info(f"✅ Успешно обработан Tribute платеж: {result['payment_id']}")
return web.json_response({"status": "ok"}, status=200)
else:
logger.error(f"❌ Ошибка обработки Tribute платежа: {result['payment_id']}")
return web.json_response(
{"status": "error", "reason": "processing_failed"},
status=500
)
else:
logger.error("Не удалось обработать Tribute webhook данные")
return web.json_response(
{"status": "error", "reason": "invalid_data"},
status=400
)
return web.json_response(result, status=200)
except Exception as e:
logger.error(f"Критическая ошибка обработки Tribute webhook: {e}", exc_info=True)
logger.error(f"Ошибка обработки Tribute webhook: {e}")
return web.json_response(
{"status": "error", "reason": "internal_error"},
{"status": "error", "reason": "internal_error"},
status=500
)
async def _tribute_health_check(self, request: web.Request) -> web.Response:
async def _health_check(self, request: web.Request) -> web.Response:
return web.json_response({
"status": "ok",
"service": "tribute-webhooks",
"tribute_enabled": settings.TRIBUTE_ENABLED,
"port": settings.TRIBUTE_WEBHOOK_PORT
})
async def _yookassa_health_check(self, request: web.Request) -> web.Response:
return web.json_response({
"status": "ok",
"service": "yookassa-webhooks",
"yookassa_enabled": settings.is_yookassa_enabled(),
"port": settings.YOOKASSA_WEBHOOK_PORT
})
"tribute_enabled": settings.TRIBUTE_ENABLED
})

View File

@@ -500,7 +500,7 @@ async def check_yookassa_payment_status(
emoji = status_emoji.get(payment.status, "")
status = status_text.get(payment.status, "Неизвестно")
message_text = (f"💳 Статус платежа\n\n"
message_text = (f"💳 <b>Статус платежа</b>\n\n"
f"🆔 ID: {payment.yookassa_payment_id[:8]}...\n"
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n"
f"📊 Статус: {emoji} {status}\n"

View File

@@ -342,26 +342,6 @@ class RussianTexts(Texts):
💳 Требуется: {required}
Пополните баланс и продлите подписку вручную.
"""
PAYMENT_SUCCESS_TRIBUTE = """
✅ <b>Пополнение выполнено!</b>
💰 Сумма: {amount}
🎯 Способ: Tribute
🆔 ID платежа: {payment_id}
Средства зачислены на ваш баланс!
"""
PAYMENT_SUCCESS_YOOKASSA = """
✅ <b>Пополнение выполнено!</b>
💰 Сумма: {amount}
🏦 Способ: Банковская карта (YooKassa)
🆔 ID платежа: {payment_id}
Средства зачислены на ваш баланс!
"""
SUPPORT_INFO = f"""
@@ -411,6 +391,29 @@ class RussianTexts(Texts):
Не забудьте продлить подписку, чтобы не потерять доступ к серверам.
"""
SUBSCRIPTION_EXPIRED = """
❌ <b>Подписка истекла</b>
Ваша подписка истекла. Для восстановления доступа продлите подписку.
"""
AUTOPAY_SUCCESS = """
✅ <b>Автоплатеж выполнен</b>
Ваша подписка автоматически продлена на {days} дней.
Списано с баланса: {amount}
"""
AUTOPAY_FAILED = """
❌ <b>Ошибка автоплатежа</b>
Не удалось списать средства для продления подписки.
Недостаточно средств на балансе: {balance}
Требуется: {required}
Пополните баланс и продлите подписку вручную.
"""
class EnglishTexts(Texts):
@@ -504,20 +507,3 @@ def clear_rules_cache():
global _cached_rules
_cached_rules.clear()
print("✅ Кеш правил очищен")
def get_text(key: str, language: str = "ru") -> str:
texts = get_texts(language)
text_mapping = {
"payment_success_tribute": getattr(texts, "PAYMENT_SUCCESS_TRIBUTE",
"✅ Пополнение через Tribute выполнено! Сумма: {amount} ₽, ID: {payment_id}"),
"payment_success_yookassa": getattr(texts, "PAYMENT_SUCCESS_YOOKASSA",
"✅ Пополнение через YooKassa выполнено! Сумма: {amount} ₽, ID: {payment_id}"),
"welcome": texts.WELCOME,
"back": texts.BACK,
"cancel": texts.CANCEL,
"confirm": texts.CONFIRM,
"continue": texts.CONTINUE,
}
return text_mapping.get(key, f"Текст для ключа '{key}' не найден")

View File

@@ -35,14 +35,12 @@ class MaintenanceService:
return self._status
def is_maintenance_active(self) -> bool:
"""Проверяет, активен ли режим техработ"""
return self._status.is_active
def get_maintenance_message(self) -> str:
"""Получает сообщение о техработах"""
if self._status.auto_enabled:
return f"""
🔧 Технические работы
🔧 Технические работы!
Сервис временно недоступен из-за проблем с подключением к серверам.
@@ -67,10 +65,6 @@ class MaintenanceService:
await self._save_status_to_cache()
logger.warning(f"🔧 Режим техработ ВКЛЮЧЕН. Причина: {self._status.reason}")
if auto:
await self._notify_admins_maintenance_enabled(reason)
return True
except Exception as e:
@@ -83,8 +77,6 @@ class MaintenanceService:
logger.info("Режим техработ уже выключен")
return True
was_auto_enabled = self._status.auto_enabled
self._status.is_active = False
self._status.enabled_at = None
self._status.reason = None
@@ -94,10 +86,6 @@ class MaintenanceService:
await self._save_status_to_cache()
logger.info("✅ Режим техработ ВЫКЛЮЧЕН")
if was_auto_enabled:
await self._notify_admins_maintenance_disabled()
return True
except Exception as e:
@@ -105,7 +93,6 @@ class MaintenanceService:
return False
async def start_monitoring(self) -> bool:
"""Запускает мониторинг API RemnaWave"""
try:
if self._check_task and not self._check_task.done():
logger.warning("Мониторинг уже запущен")
@@ -194,10 +181,9 @@ class MaintenanceService:
break
except Exception as e:
logger.error(f"Ошибка в цикле мониторинга: {e}")
await asyncio.sleep(30)
await asyncio.sleep(30)
async def _save_status_to_cache(self):
"""Сохраняет состояние в кеше"""
try:
status_data = {
"is_active": self._status.is_active,
@@ -278,84 +264,5 @@ class MaintenanceService:
"consecutive_failures": self._status.consecutive_failures
}
def set_bot(self, bot):
self._bot = bot
async def _notify_admins_maintenance_enabled(self, reason: Optional[str] = None):
if not hasattr(self, '_bot') or not self._bot:
return
try:
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
admin_ids = settings.get_admin_ids()
if not admin_ids:
return
enabled_time = self._status.enabled_at.strftime("%d.%m.%Y %H:%M:%S") if self._status.enabled_at else "неизвестно"
message = f"""
🔧 <b>Режим техработ автоматически ВКЛЮЧЕН</b>
⏰ <b>Время:</b> {enabled_time}
📝 <b>Причина:</b> {reason or 'Недоступность API'}
🔄 <b>Неудачных проверок:</b> {self._status.consecutive_failures}
Обычные пользователи заблокированы до восстановления API.
⚙️ Админы имеют полный доступ к боту.
🔍 Для управления используйте админ-панель → Техработы
"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔧 Панель техработ", callback_data="maintenance_panel")]
])
for admin_id in admin_ids:
try:
await self._bot.send_message(
admin_id,
message,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
except Exception as e:
logger.error(f"Ошибка отправки уведомлений админам о включении техработ: {e}")
async def _notify_admins_maintenance_disabled(self):
if not hasattr(self, '_bot') or not self._bot:
return
try:
admin_ids = settings.get_admin_ids()
if not admin_ids:
return
disabled_time = datetime.utcnow().strftime("%d.%m.%Y %H:%M:%S")
message = f"""
✅ <b>Режим техработ автоматически ОТКЛЮЧЕН</b>
⏰ <b>Время:</b> {disabled_time}
🔄 <b>API восстановлено:</b> Подключение к Remnawave работает
Пользователи снова могут использовать бота.
"""
for admin_id in admin_ids:
try:
await self._bot.send_message(
admin_id,
message,
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
except Exception as e:
logger.error(f"Ошибка отправки уведомлений админам о выключении техработ: {e}")
maintenance_service = MaintenanceService()

View File

@@ -131,81 +131,6 @@ class PaymentService:
except Exception as e:
logger.error(f"Ошибка создания платежа YooKassa: {e}")
return None
async def process_tribute_payment(
self,
db: AsyncSession,
user_id: int,
amount_kopeks: int,
payment_id: str
) -> bool:
try:
logger.info(f"Обработка Tribute платежа: telegram_id={user_id}, amount={amount_kopeks} коп., payment_id={payment_id}")
from app.database.crud.user import UserCRUD
user_crud = UserCRUD()
user = await user_crud.get_user_by_telegram_id(db, user_id)
if not user:
logger.error(f"Пользователь с telegram_id {user_id} не найден для обработки Tribute платежа")
return False
from app.database.crud.transaction import TransactionCRUD
transaction_crud = TransactionCRUD()
existing_transaction = await transaction_crud.get_transaction_by_external_id(
db, f"tribute_{payment_id}"
)
if existing_transaction:
logger.info(f"Платеж {payment_id} уже был обработан ранее")
return True
transaction_data = {
"user_id": user.id,
"amount_kopeks": amount_kopeks,
"transaction_type": "deposit",
"status": "completed",
"external_id": f"tribute_{payment_id}",
"payment_system": "tribute",
"description": f"Пополнение через Tribute: {payment_id}"
}
transaction = await transaction_crud.create_transaction(db, transaction_data)
if not transaction:
logger.error(f"Не удалось создать транзакцию для Tribute платежа {payment_id}")
return False
success = await user_crud.add_balance(db, user.id, amount_kopeks)
if not success:
logger.error(f"Не удалось пополнить баланс пользователя {user.id}")
await transaction_crud.update_transaction_status(db, transaction.id, "failed")
return False
try:
from app.localization.texts import get_text
amount_rubles = amount_kopeks / 100
message = get_text("payment_success_tribute").format(
amount=f"{amount_rubles:.2f}",
payment_id=payment_id
)
if hasattr(self, 'bot') and self.bot:
await self.bot.send_message(user.telegram_id, message)
except Exception as e:
logger.warning(f"Не удалось отправить уведомление о платеже пользователю {user.telegram_id}: {e}")
logger.info(f"✅ Успешно обработан Tribute платеж {payment_id} для пользователя {user.telegram_id}")
return True
except Exception as e:
logger.error(f"❌ Ошибка обработки Tribute платежа {payment_id}: {e}", exc_info=True)
return False
async def process_yookassa_webhook(self, db: AsyncSession, webhook_data: dict) -> bool:
try:

View File

@@ -20,7 +20,7 @@ async def process_referral_registration(
referrer = await get_user_by_id(db, referrer_id)
if not new_user or not referrer:
logger.error(f"Пользователи не найдены: new_user_id={new_user_id}, referrer_id={referrer_id}")
logger.error(f"Пользователи не найдены: {new_user_id}, {referrer_id}")
return False
if new_user.referred_by_id != referrer_id:
@@ -81,13 +81,9 @@ async def process_referral_purchase(
logger.info(f"🔍 Покупка реферала {user_id}: первая = {is_first_purchase}, сумма = {purchase_amount_kopeks/100}")
if is_first_purchase and settings.REFERRAL_REGISTRATION_REWARD > 0:
if is_first_purchase:
reward_amount = settings.REFERRAL_REGISTRATION_REWARD
if reward_amount > 1000000:
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: reward_amount = {reward_amount} слишком большой! Проверьте настройки REFERRAL_REGISTRATION_REWARD")
reward_amount = 10000
await add_user_balance(
db, referrer, reward_amount,
f"Реферальная награда за первую покупку {user.full_name}"
@@ -104,18 +100,12 @@ async def process_referral_purchase(
logger.info(f"🎉 Первая покупка реферала: {referrer.telegram_id} получил {reward_amount/100}")
if not (0 <= settings.REFERRAL_COMMISSION_PERCENT <= 100):
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: REFERRAL_COMMISSION_PERCENT = {settings.REFERRAL_COMMISSION_PERCENT} некорректный! Должен быть от 0 до 100")
commission_percent = 10
else:
commission_percent = settings.REFERRAL_COMMISSION_PERCENT
commission_amount = int(purchase_amount_kopeks * commission_percent / 100)
commission_amount = int(purchase_amount_kopeks * settings.REFERRAL_COMMISSION_PERCENT / 100)
if commission_amount > 0:
await add_user_balance(
db, referrer, commission_amount,
f"Комиссия {commission_percent}% с покупки {user.full_name}"
f"Комиссия {settings.REFERRAL_COMMISSION_PERCENT}% с покупки {user.full_name}"
)
await create_referral_earning(
@@ -138,8 +128,6 @@ async def process_referral_purchase(
except Exception as e:
logger.error(f"Ошибка обработки покупки реферала: {e}")
import traceback
logger.error(f"Полный traceback: {traceback.format_exc()}")
return False
@@ -153,7 +141,7 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
invited_count_result = await db.execute(
select(func.count(User.id)).where(User.referred_by_id == user_id)
)
invited_count = invited_count_result.scalar() or 0
invited_count = invited_count_result.scalar()
paid_referrals_result = await db.execute(
select(func.count(User.id)).where(
@@ -161,13 +149,13 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
User.has_had_paid_subscription == True
)
)
paid_referrals_count = paid_referrals_result.scalar() or 0
paid_referrals_count = paid_referrals_result.scalar()
total_earned = await get_referral_earnings_sum(db, user_id) or 0
total_earned = await get_referral_earnings_sum(db, user_id)
from datetime import datetime, timedelta
month_ago = datetime.utcnow() - timedelta(days=30)
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago) or 0
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago)
return {
"invited_count": invited_count,
@@ -183,4 +171,4 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
"paid_referrals_count": 0,
"total_earned_kopeks": 0,
"month_earned_kopeks": 0
}
}

View File

@@ -306,109 +306,43 @@ class SubscriptionService:
try:
needs_cleanup = False
if not isinstance(subscription.connected_squads, list):
logger.warning(f"Исправляем connected_squads для пользователя {user.telegram_id}")
subscription.connected_squads = []
needs_cleanup = True
if subscription.connected_squads:
unique_squads = list(set([squad for squad in subscription.connected_squads if squad and isinstance(squad, str)]))
if len(unique_squads) != len(subscription.connected_squads):
logger.info(f"Очищены дубликаты в connected_squads для пользователя {user.telegram_id}")
subscription.connected_squads = unique_squads
needs_cleanup = True
if subscription.traffic_limit_gb < 0:
logger.warning(f"Отрицательный traffic_limit_gb исправлен на 0 для пользователя {user.telegram_id}")
subscription.traffic_limit_gb = 0
needs_cleanup = True
if subscription.traffic_used_gb < 0:
logger.warning(f"Отрицательный traffic_used_gb исправлен на 0 для пользователя {user.telegram_id}")
subscription.traffic_used_gb = 0.0
needs_cleanup = True
if subscription.device_limit < 1:
logger.warning(f"device_limit < 1 исправлен на 1 для пользователя {user.telegram_id}")
subscription.device_limit = 1
needs_cleanup = True
elif subscription.device_limit > 10:
logger.warning(f"device_limit > 10 исправлен на 10 для пользователя {user.telegram_id}")
subscription.device_limit = 10
needs_cleanup = True
from datetime import datetime
current_time = datetime.utcnow()
if subscription.start_date > current_time + timedelta(days=1):
logger.warning(f"Некорректная start_date исправлена для пользователя {user.telegram_id}")
subscription.start_date = current_time
needs_cleanup = True
if subscription.end_date < subscription.start_date:
logger.warning(f"end_date раньше start_date исправлено для пользователя {user.telegram_id}")
subscription.end_date = subscription.start_date + timedelta(days=1)
needs_cleanup = True
if user.remnawave_uuid:
try:
async with self.api as api:
remnawave_user = await api.get_user_by_uuid(user.remnawave_uuid)
if not remnawave_user:
logger.warning(f"Пользователь {user.telegram_id} имеет UUID {user.remnawave_uuid}, но не найден в панели")
logger.warning(f"⚠️ Пользователь {user.telegram_id} имеет UUID {user.remnawave_uuid}, но не найден в панели")
needs_cleanup = True
else:
if remnawave_user.telegram_id != user.telegram_id:
logger.warning(f"Несоответствие telegram_id для пользователя {user.telegram_id}")
logger.warning(f"⚠️ Несоответствие telegram_id для пользователя {user.telegram_id}")
needs_cleanup = True
if remnawave_user.subscription_url and not subscription.subscription_url:
subscription.subscription_url = remnawave_user.subscription_url
logger.info(f"Восстановлена subscription_url из панели для пользователя {user.telegram_id}")
needs_cleanup = True
except Exception as api_error:
logger.error(f"Ошибка проверки пользователя в панели: {api_error}")
logger.error(f"Ошибка проверки пользователя в панели: {api_error}")
needs_cleanup = True
if subscription.remnawave_short_uuid and not user.remnawave_uuid:
logger.warning(f"У подписки есть short_uuid, но у пользователя нет remnawave_uuid")
logger.warning(f"⚠️ У подписки есть short_uuid, но у пользователя нет remnawave_uuid")
needs_cleanup = True
if subscription.subscription_url:
if not subscription.subscription_url.startswith(('http://', 'https://')):
logger.warning(f"Некорректный subscription_url для пользователя {user.telegram_id}")
subscription.subscription_url = ""
needs_cleanup = True
if needs_cleanup and (
not user.remnawave_uuid or
subscription.remnawave_short_uuid and not user.remnawave_uuid
):
logger.info(f"Очищаем критические мусорные данные подписки для пользователя {user.telegram_id}")
if needs_cleanup:
logger.info(f"🧹 Очищаем мусорные данные подписки для пользователя {user.telegram_id}")
subscription.remnawave_short_uuid = None
subscription.subscription_url = ""
subscription.connected_squads = []
user.remnawave_uuid = None
if needs_cleanup:
subscription.updated_at = current_time
await db.commit()
logger.info(f"Исправления применены для пользователя {user.telegram_id}")
logger.info(f"✅ Мусорные данные очищены для пользователя {user.telegram_id}")
return True
except Exception as e:
logger.error(f"Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
import traceback
logger.error(f"Полный traceback: {traceback.format_exc()}")
try:
await db.rollback()
except:
pass
logger.error(f"Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
await db.rollback()
return False
async def get_countries_price_by_uuids(