Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b7d8d41f6 | |||
| 4ca46537f9 | |||
| 75f2d0f1ce | |||
| 51424bf256 | |||
| 4cce2f1efb | |||
| 5102286963 | |||
| 24c8f25e6e | |||
| de4796ac1c | |||
| 95a8da5f19 | |||
| 76f1313550 | |||
| 58d22200cf | |||
| eb8b09e658 | |||
| 25cf635b6f | |||
| a54c1001f9 | |||
| 6140d7042a | |||
| d0135a2328 | |||
| 1048734548 | |||
| 62116fd5ba | |||
| 37a905f056 | |||
| bce9061401 | |||
| 118b282a2b | |||
| 6c250ed36f | |||
| 3842aeacca | |||
| 1d1afbe1da | |||
| d4db55ea66 | |||
| 354964060c | |||
| 5bc8d8e52c | |||
| 8f3e8bacc4 | |||
| 61c94e17a4 | |||
| 77c52c1197 | |||
| c65c703e00 | |||
| 7560e802cb | |||
| a5a90a806b | |||
| aa0a271d14 | |||
| 5d4e51c862 | |||
| e6057bb93c | |||
| 714bdd0d7c | |||
| 41f3eb76c1 | |||
| e9c58b3f76 | |||
| 3d6976ff6e | |||
| ee978dff7b | |||
| 0bcf5c5f43 | |||
| 2ceedd6587 | |||
| 2ded1a4a9e | |||
| d551a7267f | |||
| 3ddf6b35c6 | |||
| c0d3c70683 | |||
| 7267433d99 | |||
| 09f3c4d27f | |||
| 3083915bc8 | |||
| 5c72c04d27 | |||
| 826149da3d | |||
| f090be328f | |||
| ecbfcd2b54 | |||
| 2d21b1c542 | |||
| c63438af5e | |||
| 8e47013ae2 | |||
| 92fa0ebcc1 | |||
| f1d23616f1 | |||
| d554f2d801 | |||
| 1fe4608604 | |||
| bea534b352 | |||
| e993c19a34 | |||
| 2deb5d1bc1 | |||
| fb1b0bbcd5 | |||
| 0308dd6c58 | |||
| 860bf7fc7a | |||
| c24024db18 | |||
| 7d330838e0 | |||
| feed3fff0d | |||
| 4ddaafd052 | |||
| 140194a0ad | |||
| 7c6adae87f | |||
| 03deb91a25 | |||
| ea2caa98ae | |||
| dab69b9229 | |||
| 0ffa86ff36 | |||
| 3b08b71472 | |||
| 6c7b1353df | |||
| 9e009d2ded | |||
| dc6a829f0f | |||
| 3a3a4aa9aa | |||
| 99cd22d76b |
@@ -11,6 +11,17 @@ REDIS_URL=redis://localhost:6379/0
|
||||
REMNAWAVE_API_URL=
|
||||
REMNAWAVE_API_KEY=
|
||||
|
||||
# === NEW: Traffic Selection Mode Settings ===
|
||||
# Режим выбора трафика:
|
||||
# "selectable" - пользователи выбирают пакеты трафика (по умолчанию)
|
||||
# "fixed" - фиксированный лимит трафика для всех подписок, доступно 5/10/25/50/100/250/0 (0 безлимит) гб
|
||||
TRAFFIC_SELECTION_MODE=selectable
|
||||
|
||||
# Фиксированный лимит трафика в ГБ (используется только в режиме "fixed")
|
||||
# 0 = безлимит
|
||||
# для "fixed" обязательно должы быть проставлены цены на пакеты 5/10/25/50/100/250/0 можно постать 0 руб - будет беслпатно
|
||||
FIXED_TRAFFIC_LIMIT_GB=0
|
||||
|
||||
# TRIAL SUBSCRIPTION SETTINGS
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_LIMIT_GB=10
|
||||
@@ -43,6 +54,14 @@ REFERRAL_REGISTRATION_REWARD=5000
|
||||
REFERRED_USER_REWARD=10000
|
||||
REFERRAL_COMMISSION_PERCENT=25
|
||||
|
||||
# Режим работы кнопки "Подключиться"
|
||||
# guide - открывает гайд подключения (режим 1)
|
||||
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
|
||||
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
|
||||
CONNECT_BUTTON_MODE=miniapp_subscription
|
||||
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
|
||||
# MINIAPP_CUSTOM_URL=
|
||||
|
||||
# AUTO-PAYMENT SETTINGS
|
||||
AUTOPAY_WARNING_DAYS=3,1
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
[alembic]
|
||||
script_location = migrations/alembic
|
||||
prepend_sys_path = .
|
||||
version_path_separator = os
|
||||
sqlalchemy.url = postgresql+asyncpg://vpn_user:your_password@localhost:5432/vpn_bot
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
+12
-2
@@ -8,6 +8,7 @@ from app.config import settings
|
||||
from app.middlewares.auth import AuthMiddleware
|
||||
from app.middlewares.logging import LoggingMiddleware
|
||||
from app.middlewares.throttling import ThrottlingMiddleware
|
||||
from app.middlewares.subscription_checker import SubscriptionStatusMiddleware
|
||||
from app.utils.cache import cache
|
||||
|
||||
from app.handlers import (
|
||||
@@ -40,7 +41,13 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Кеш не инициализирован: {e}")
|
||||
|
||||
bot = Bot(token=settings.BOT_TOKEN, parse_mode="HTML")
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
|
||||
bot = Bot(
|
||||
token=settings.BOT_TOKEN,
|
||||
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||
)
|
||||
|
||||
try:
|
||||
redis_client = redis.from_url(settings.REDIS_URL)
|
||||
@@ -60,6 +67,9 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
dp.callback_query.middleware(AuthMiddleware())
|
||||
dp.message.middleware(ThrottlingMiddleware())
|
||||
dp.callback_query.middleware(ThrottlingMiddleware())
|
||||
dp.message.middleware(SubscriptionStatusMiddleware())
|
||||
dp.callback_query.middleware(SubscriptionStatusMiddleware())
|
||||
|
||||
|
||||
start.register_handlers(dp)
|
||||
menu.register_handlers(dp)
|
||||
@@ -85,4 +95,4 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
|
||||
logger.info("✅ Бот успешно настроен")
|
||||
|
||||
return bot, dp
|
||||
return bot, dp
|
||||
|
||||
+16
-1
@@ -49,6 +49,9 @@ class Settings(BaseSettings):
|
||||
|
||||
PRICE_PER_DEVICE: int = 5000
|
||||
|
||||
TRAFFIC_SELECTION_MODE: str = "selectable"
|
||||
FIXED_TRAFFIC_LIMIT_GB: int = 100
|
||||
|
||||
REFERRAL_REGISTRATION_REWARD: int = 5000
|
||||
REFERRED_USER_REWARD: int = 2500
|
||||
REFERRAL_COMMISSION_PERCENT: int = 10
|
||||
@@ -69,6 +72,9 @@ class Settings(BaseSettings):
|
||||
TRIBUTE_DONATE_LINK: Optional[str] = None
|
||||
TRIBUTE_WEBHOOK_PATH: str = "/tribute-webhook"
|
||||
TRIBUTE_WEBHOOK_PORT: int = 8081
|
||||
|
||||
CONNECT_BUTTON_MODE: str = "guide"
|
||||
MINIAPP_CUSTOM_URL: str = ""
|
||||
|
||||
DEFAULT_LANGUAGE: str = "ru"
|
||||
AVAILABLE_LANGUAGES: str = "ru,en"
|
||||
@@ -82,7 +88,7 @@ class Settings(BaseSettings):
|
||||
|
||||
APP_CONFIG_PATH: str = "app-config.json"
|
||||
ENABLE_DEEP_LINKS: bool = True
|
||||
APP_CONFIG_CACHE_TTL: int = 3600 # 1 час
|
||||
APP_CONFIG_CACHE_TTL: int = 3600
|
||||
|
||||
@field_validator('LOG_FILE', mode='before')
|
||||
@classmethod
|
||||
@@ -159,6 +165,15 @@ class Settings(BaseSettings):
|
||||
def get_app_config_cache_ttl(self) -> int:
|
||||
return self.APP_CONFIG_CACHE_TTL
|
||||
|
||||
def is_traffic_selectable(self) -> bool:
|
||||
return self.TRAFFIC_SELECTION_MODE.lower() == "selectable"
|
||||
|
||||
def is_traffic_fixed(self) -> bool:
|
||||
return self.TRAFFIC_SELECTION_MODE.lower() == "fixed"
|
||||
|
||||
def get_fixed_traffic_limit(self) -> int:
|
||||
return self.FIXED_TRAFFIC_LIMIT_GB
|
||||
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
"env_file_encoding": "utf-8"
|
||||
|
||||
@@ -20,7 +20,12 @@ async def get_subscription_by_user_id(db: AsyncSession, user_id: int) -> Optiona
|
||||
.options(selectinload(Subscription.user))
|
||||
.where(Subscription.user_id == user_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
subscription = result.scalar_one_or_none()
|
||||
|
||||
if subscription:
|
||||
subscription = await check_and_update_subscription_status(db, subscription)
|
||||
|
||||
return subscription
|
||||
|
||||
|
||||
async def create_trial_subscription(
|
||||
@@ -444,6 +449,107 @@ async def get_subscription_servers(
|
||||
|
||||
return servers_info
|
||||
|
||||
async def remove_subscription_servers(
|
||||
db: AsyncSession,
|
||||
subscription_id: int,
|
||||
server_squad_ids: List[int]
|
||||
) -> bool:
|
||||
try:
|
||||
from app.database.models import SubscriptionServer
|
||||
from sqlalchemy import delete
|
||||
|
||||
await db.execute(
|
||||
delete(SubscriptionServer)
|
||||
.where(
|
||||
SubscriptionServer.subscription_id == subscription_id,
|
||||
SubscriptionServer.server_squad_id.in_(server_squad_ids)
|
||||
)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"🗑️ Удалены серверы {server_squad_ids} из подписки {subscription_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления серверов из подписки: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
async def get_subscription_renewal_cost(
|
||||
db: AsyncSession,
|
||||
subscription_id: int,
|
||||
period_days: int
|
||||
) -> int:
|
||||
try:
|
||||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES, settings
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
servers_info = await get_subscription_servers(db, subscription_id)
|
||||
servers_cost = sum(server_info['paid_price_kopeks'] for server_info in servers_info)
|
||||
|
||||
subscription = await db.get(Subscription, subscription_id)
|
||||
if not subscription:
|
||||
return base_price
|
||||
|
||||
traffic_cost = 0
|
||||
if subscription.traffic_limit_gb > 0:
|
||||
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
|
||||
|
||||
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
|
||||
|
||||
total_cost = base_price + servers_cost + traffic_cost + devices_cost
|
||||
|
||||
logger.info(f"💰 Расчет продления подписки {subscription_id} на {period_days} дней:")
|
||||
logger.info(f" 📅 Период: {base_price/100}₽")
|
||||
logger.info(f" 🌍 Серверы: {servers_cost/100}₽")
|
||||
logger.info(f" 📊 Трафик: {traffic_cost/100}₽")
|
||||
logger.info(f" 📱 Устройства: {devices_cost/100}₽")
|
||||
logger.info(f" 💎 ИТОГО: {total_cost/100}₽")
|
||||
|
||||
return total_cost
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка расчета стоимости продления: {e}")
|
||||
from app.config import PERIOD_PRICES
|
||||
return PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
async def expire_subscription(
|
||||
db: AsyncSession,
|
||||
subscription: Subscription
|
||||
) -> Subscription:
|
||||
|
||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||
subscription.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f"⏰ Подписка пользователя {subscription.user_id} помечена как истёкшая")
|
||||
return subscription
|
||||
|
||||
|
||||
async def check_and_update_subscription_status(
|
||||
db: AsyncSession,
|
||||
subscription: Subscription
|
||||
) -> Subscription:
|
||||
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
if (subscription.status == SubscriptionStatus.ACTIVE.value and
|
||||
subscription.end_date <= current_time):
|
||||
|
||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||
subscription.updated_at = current_time
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f"⏰ Статус подписки пользователя {subscription.user_id} изменен на 'expired'")
|
||||
|
||||
return subscription
|
||||
|
||||
async def create_subscription(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
@@ -482,4 +588,4 @@ async def create_subscription(
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f"✅ Создана подписка для пользователя {user_id}")
|
||||
return subscription
|
||||
return subscription
|
||||
|
||||
+27
-24
@@ -150,33 +150,36 @@ async def add_user_balance(
|
||||
|
||||
|
||||
async def subtract_user_balance(
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
amount_kopeks: int,
|
||||
description: str = "Списание с баланса"
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
amount_kopeks: int,
|
||||
description: str
|
||||
) -> bool:
|
||||
logger.error(f"💸 ОТЛАДКА subtract_user_balance:")
|
||||
logger.error(f" 👤 User ID: {user.id} (TG: {user.telegram_id})")
|
||||
logger.error(f" 💰 Баланс до списания: {user.balance_kopeks} копеек")
|
||||
logger.error(f" 💸 Сумма к списанию: {amount_kopeks} копеек")
|
||||
logger.error(f" 📝 Описание: {description}")
|
||||
|
||||
if not user.subtract_balance(amount_kopeks):
|
||||
if user.balance_kopeks < amount_kopeks:
|
||||
logger.error(f" ❌ НЕДОСТАТОЧНО СРЕДСТВ!")
|
||||
return False
|
||||
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
from app.database.crud.transaction import create_transaction
|
||||
from app.database.models import TransactionType
|
||||
|
||||
await create_transaction(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
type=TransactionType.WITHDRAWAL,
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=description
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
logger.info(f"💸 Списан баланс пользователя {user.telegram_id}: -{amount_kopeks/100}₽")
|
||||
return True
|
||||
try:
|
||||
old_balance = user.balance_kopeks
|
||||
user.balance_kopeks -= amount_kopeks
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
logger.error(f" ✅ Средства списаны: {old_balance} → {user.balance_kopeks}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ ОШИБКА СПИСАНИЯ: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
async def get_users_list(
|
||||
@@ -324,4 +327,4 @@ async def get_users_statistics(db: AsyncSession) -> dict:
|
||||
"new_today": new_today,
|
||||
"new_week": new_week,
|
||||
"new_month": new_month
|
||||
}
|
||||
}
|
||||
|
||||
+97
-7
@@ -31,7 +31,7 @@ class TransactionType(Enum):
|
||||
DEPOSIT = "deposit"
|
||||
WITHDRAWAL = "withdrawal"
|
||||
SUBSCRIPTION_PAYMENT = "subscription_payment"
|
||||
REFUND = "refund" # Возврат
|
||||
REFUND = "refund"
|
||||
REFERRAL_REWARD = "referral_reward"
|
||||
|
||||
|
||||
@@ -133,21 +133,111 @@ class Subscription(Base):
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
current_time = datetime.utcnow()
|
||||
return (
|
||||
self.status == SubscriptionStatus.ACTIVE.value and
|
||||
self.end_date > datetime.utcnow()
|
||||
self.end_date > current_time
|
||||
)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Проверяет, истёк ли срок подписки"""
|
||||
return self.end_date <= datetime.utcnow()
|
||||
|
||||
|
||||
@property
|
||||
def should_be_expired(self) -> bool:
|
||||
current_time = datetime.utcnow()
|
||||
return (
|
||||
self.status == SubscriptionStatus.ACTIVE.value and
|
||||
self.end_date <= current_time
|
||||
)
|
||||
|
||||
@property
|
||||
def actual_status(self) -> str:
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
if self.status == SubscriptionStatus.EXPIRED.value:
|
||||
return "expired"
|
||||
|
||||
if self.status == SubscriptionStatus.DISABLED.value:
|
||||
return "disabled"
|
||||
|
||||
if self.status == SubscriptionStatus.ACTIVE.value:
|
||||
if self.end_date <= current_time:
|
||||
return "expired"
|
||||
else:
|
||||
return "active"
|
||||
|
||||
if self.status == SubscriptionStatus.TRIAL.value:
|
||||
if self.end_date <= current_time:
|
||||
return "expired"
|
||||
else:
|
||||
return "trial"
|
||||
|
||||
return self.status
|
||||
|
||||
@property
|
||||
def status_display(self) -> str:
|
||||
actual_status = self.actual_status
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
if actual_status == "expired":
|
||||
return "🔴 Истекла"
|
||||
elif actual_status == "active":
|
||||
if self.is_trial:
|
||||
return "🎯 Тестовая"
|
||||
else:
|
||||
return "🟢 Активна"
|
||||
elif actual_status == "disabled":
|
||||
return "⚫ Отключена"
|
||||
elif actual_status == "trial":
|
||||
return "🎯 Тестовая"
|
||||
|
||||
return "❓ Неизвестно"
|
||||
|
||||
@property
|
||||
def status_emoji(self) -> str:
|
||||
actual_status = self.actual_status
|
||||
|
||||
if actual_status == "expired":
|
||||
return "🔴"
|
||||
elif actual_status == "active":
|
||||
if self.is_trial:
|
||||
return "🎁"
|
||||
else:
|
||||
return "💎"
|
||||
elif actual_status == "disabled":
|
||||
return "⚫"
|
||||
elif actual_status == "trial":
|
||||
return "🎁"
|
||||
|
||||
return "❓"
|
||||
|
||||
@property
|
||||
def days_left(self) -> int:
|
||||
if self.is_expired:
|
||||
current_time = datetime.utcnow()
|
||||
if self.end_date <= current_time:
|
||||
return 0
|
||||
delta = self.end_date - datetime.utcnow()
|
||||
return delta.days
|
||||
delta = self.end_date - current_time
|
||||
return max(0, delta.days)
|
||||
|
||||
@property
|
||||
def time_left_display(self) -> str:
|
||||
current_time = datetime.utcnow()
|
||||
if self.end_date <= current_time:
|
||||
return "истёк"
|
||||
|
||||
delta = self.end_date - current_time
|
||||
days = delta.days
|
||||
hours = delta.seconds // 3600
|
||||
minutes = (delta.seconds % 3600) // 60
|
||||
|
||||
if days > 0:
|
||||
return f"{days} дн."
|
||||
elif hours > 0:
|
||||
return f"{hours} ч."
|
||||
else:
|
||||
return f"{minutes} мин."
|
||||
|
||||
@property
|
||||
def traffic_used_percent(self) -> float:
|
||||
@@ -418,4 +508,4 @@ class SubscriptionServer(Base):
|
||||
paid_price_kopeks = Column(Integer, default=0)
|
||||
|
||||
subscription = relationship("Subscription", backref="subscription_servers")
|
||||
server_squad = relationship("ServerSquad", backref="subscription_servers")
|
||||
server_squad = relationship("ServerSquad", backref="subscription_servers")
|
||||
|
||||
Vendored
+108
-57
@@ -1,7 +1,9 @@
|
||||
import asyncio
|
||||
import json
|
||||
import ssl
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Union, Any
|
||||
from urllib.parse import urlparse
|
||||
import aiohttp
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
@@ -85,13 +87,65 @@ class RemnaWaveAPI:
|
||||
self.api_key = api_key
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
def _detect_connection_type(self) -> str:
|
||||
parsed = urlparse(self.base_url)
|
||||
|
||||
local_hosts = [
|
||||
'localhost', '127.0.0.1', 'remnawave',
|
||||
'remnawave-backend', 'app', 'api'
|
||||
]
|
||||
|
||||
if parsed.hostname in local_hosts:
|
||||
return "local"
|
||||
|
||||
if parsed.hostname:
|
||||
if (parsed.hostname.startswith('192.168.') or
|
||||
parsed.hostname.startswith('10.') or
|
||||
parsed.hostname.startswith('172.') or
|
||||
parsed.hostname.endswith('.local')):
|
||||
return "local"
|
||||
|
||||
return "external"
|
||||
|
||||
async def __aenter__(self):
|
||||
conn_type = self._detect_connection_type()
|
||||
|
||||
logger.info(f"🔗 Подключение к RemnaWave: {self.base_url} (тип: {conn_type})")
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
connector_kwargs = {}
|
||||
|
||||
if conn_type == "local":
|
||||
logger.debug("🏠 Использую локальные заголовки proxy")
|
||||
headers.update({
|
||||
'X-Forwarded-For': '127.0.0.1',
|
||||
'X-Forwarded-Proto': 'https',
|
||||
'X-Forwarded-Host': 'localhost',
|
||||
'X-Real-IP': '127.0.0.1',
|
||||
'Host': 'localhost'
|
||||
})
|
||||
|
||||
if self.base_url.startswith('https://'):
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
connector_kwargs['ssl'] = ssl_context
|
||||
logger.debug("🔓 SSL проверка отключена для локального HTTPS")
|
||||
|
||||
elif conn_type == "external":
|
||||
logger.debug("🌐 Использую внешнее подключение с полной SSL проверкой")
|
||||
pass
|
||||
|
||||
connector = aiohttp.TCPConnector(**connector_kwargs)
|
||||
|
||||
self.session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=30),
|
||||
headers={
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
headers=headers,
|
||||
connector=connector
|
||||
)
|
||||
return self
|
||||
|
||||
@@ -375,6 +429,56 @@ class RemnaWaveAPI:
|
||||
return response['response']
|
||||
|
||||
|
||||
async def get_user_devices(self, user_uuid: str) -> Dict[str, Any]:
|
||||
try:
|
||||
response = await self._make_request('GET', f'/api/hwid/devices/{user_uuid}')
|
||||
return response['response']
|
||||
except RemnaWaveAPIError as e:
|
||||
if e.status_code == 404:
|
||||
return {'total': 0, 'devices': []}
|
||||
raise
|
||||
|
||||
async def reset_user_devices(self, user_uuid: str) -> bool:
|
||||
try:
|
||||
devices_info = await self.get_user_devices(user_uuid)
|
||||
devices = devices_info.get('devices', [])
|
||||
|
||||
if not devices:
|
||||
return True
|
||||
|
||||
failed_count = 0
|
||||
for device in devices:
|
||||
device_hwid = device.get('hwid')
|
||||
if device_hwid:
|
||||
try:
|
||||
delete_data = {
|
||||
"userUuid": user_uuid,
|
||||
"hwid": device_hwid
|
||||
}
|
||||
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
|
||||
except Exception as device_error:
|
||||
logger.error(f"Ошибка удаления устройства {device_hwid}: {device_error}")
|
||||
failed_count += 1
|
||||
|
||||
return failed_count < len(devices) / 2
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сбросе устройств: {e}")
|
||||
return False
|
||||
|
||||
async def remove_device(self, user_uuid: str, device_hwid: str) -> bool:
|
||||
try:
|
||||
delete_data = {
|
||||
"userUuid": user_uuid,
|
||||
"hwid": device_hwid
|
||||
}
|
||||
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления устройства {device_hwid}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _parse_user(self, user_data: Dict) -> RemnaWaveUser:
|
||||
return RemnaWaveUser(
|
||||
uuid=user_data['uuid'],
|
||||
@@ -422,7 +526,6 @@ class RemnaWaveAPI:
|
||||
)
|
||||
|
||||
|
||||
|
||||
def format_bytes(bytes_value: int) -> str:
|
||||
if bytes_value == 0:
|
||||
return "0 B"
|
||||
@@ -467,55 +570,3 @@ async def test_api_connection(api: RemnaWaveAPI) -> bool:
|
||||
except Exception as e:
|
||||
logger.error(f"API connection test failed: {e}")
|
||||
return False
|
||||
|
||||
async def get_user_devices(self, user_uuid: str) -> Dict[str, Any]:
|
||||
try:
|
||||
response = await self._make_request('GET', f'/api/hwid/devices/{user_uuid}')
|
||||
return response['response']
|
||||
except RemnaWaveAPIError as e:
|
||||
if e.status_code == 404:
|
||||
return {'total': 0, 'devices': []}
|
||||
raise
|
||||
|
||||
|
||||
async def reset_user_devices(self, user_uuid: str) -> bool:
|
||||
try:
|
||||
devices_info = await self.get_user_devices(user_uuid)
|
||||
devices = devices_info.get('devices', [])
|
||||
|
||||
if not devices:
|
||||
return True
|
||||
|
||||
failed_count = 0
|
||||
for device in devices:
|
||||
device_hwid = device.get('hwid')
|
||||
if device_hwid:
|
||||
try:
|
||||
delete_data = {
|
||||
"userUuid": user_uuid,
|
||||
"hwid": device_hwid
|
||||
}
|
||||
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
|
||||
except Exception as device_error:
|
||||
logger.error(f"Ошибка удаления устройства {device_hwid}: {device_error}")
|
||||
failed_count += 1
|
||||
|
||||
return failed_count < len(devices) / 2
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сбросе устройств: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
async def remove_device(self, user_uuid: str, device_hwid: str) -> bool:
|
||||
try:
|
||||
delete_data = {
|
||||
"userUuid": user_uuid,
|
||||
"hwid": device_hwid
|
||||
}
|
||||
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления устройства {device_hwid}: {e}")
|
||||
return False
|
||||
+144
-19
@@ -1315,11 +1315,11 @@ async def restart_all_nodes(
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_sync_options(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
text = """
|
||||
text = """
|
||||
🔄 <b>Синхронизация с RemnaWave</b>
|
||||
|
||||
Выберите тип синхронизации:
|
||||
@@ -1342,24 +1342,41 @@ async def show_sync_options(
|
||||
• Обновление подключенных сквадов
|
||||
• ⏱️ Время выполнения: 1-3 минуты
|
||||
|
||||
🔍 <b>Валидация подписок</b>
|
||||
• Проверка и исправление проблем в данных
|
||||
• Восстановление отсутствующих полей
|
||||
• Исправление некорректных статусов
|
||||
|
||||
🧹 <b>Мягкая очистка</b>
|
||||
• Деактивация подписок отсутствующих в панели
|
||||
• Сохранение транзакций и истории
|
||||
|
||||
🗑️ <b>ПРИНУДИТЕЛЬНАЯ ОЧИСТКА</b>
|
||||
• ⚠️ ОПАСНО: Полное удаление данных пользователей
|
||||
• Удаление транзакций, балансов, рефералов
|
||||
• Только при серьезных проблемах синхронизации
|
||||
|
||||
⚠️ <b>Важно:</b>
|
||||
• Во время синхронизации не выполняйте другие операции
|
||||
• При полной синхронизации подписки пользователей, отсутствующих в панели, будут деактивированы
|
||||
• Рекомендуется делать полную синхронизацию ежедневно
|
||||
"""
|
||||
|
||||
keyboard = [
|
||||
[types.InlineKeyboardButton(text="🔄 Синхронизировать всех", callback_data="sync_all_users")],
|
||||
[types.InlineKeyboardButton(text="🆕 Только новых", callback_data="sync_new_users")],
|
||||
[types.InlineKeyboardButton(text="📈 Обновить данные", callback_data="sync_update_data")],
|
||||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
keyboard = [
|
||||
[types.InlineKeyboardButton(text="🔄 Синхронизировать всех", callback_data="sync_all_users")],
|
||||
[types.InlineKeyboardButton(text="🆕 Только новых", callback_data="sync_new_users")],
|
||||
[types.InlineKeyboardButton(text="📈 Обновить данные", callback_data="sync_update_data")],
|
||||
[types.InlineKeyboardButton(text="🔍 Валидация подписок", callback_data="sync_validate")],
|
||||
[types.InlineKeyboardButton(text="🧹 Мягкая очистка", callback_data="sync_cleanup")],
|
||||
[types.InlineKeyboardButton(text="🗑️ ПРИНУДИТЕЛЬНАЯ ОЧИСТКА", callback_data="confirm_force_cleanup")],
|
||||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
@@ -1442,7 +1459,6 @@ async def validate_subscriptions(
|
||||
remnawave_service = RemnaWaveService()
|
||||
stats = await remnawave_service.validate_and_fix_subscriptions(db)
|
||||
|
||||
# Формируем отчет
|
||||
if stats['errors'] == 0:
|
||||
status_emoji = "✅"
|
||||
status_text = "успешно завершена"
|
||||
@@ -1498,7 +1514,6 @@ async def cleanup_subscriptions(
|
||||
remnawave_service = RemnaWaveService()
|
||||
stats = await remnawave_service.cleanup_orphaned_subscriptions(db)
|
||||
|
||||
# Формируем отчет
|
||||
if stats['errors'] == 0:
|
||||
status_emoji = "✅"
|
||||
status_text = "успешно завершена"
|
||||
@@ -1537,6 +1552,114 @@ async def cleanup_subscriptions(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def force_cleanup_all_orphaned_users(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🗑️ Выполняется принудительная очистка всех пользователей, отсутствующих в панели...\n\n"
|
||||
"⚠️ ВНИМАНИЕ: Это полностью удалит ВСЕ данные пользователей!\n"
|
||||
"📊 Включая: транзакции, реферальные доходы, промокоды, серверы, балансы\n\n"
|
||||
"⏳ Пожалуйста, подождите...",
|
||||
reply_markup=None
|
||||
)
|
||||
|
||||
remnawave_service = RemnaWaveService()
|
||||
stats = await remnawave_service.cleanup_orphaned_subscriptions(db)
|
||||
|
||||
if stats['errors'] == 0:
|
||||
status_emoji = "✅"
|
||||
status_text = "успешно завершена"
|
||||
else:
|
||||
status_emoji = "⚠️"
|
||||
status_text = "завершена с ошибками"
|
||||
|
||||
text = f"""
|
||||
{status_emoji} <b>Принудительная очистка {status_text}</b>
|
||||
|
||||
📊 <b>Результаты:</b>
|
||||
• 🔍 Проверено подписок: {stats['checked']}
|
||||
• 🗑️ Полностью очищено: {stats['deactivated']}
|
||||
• ❌ Ошибок: {stats['errors']}
|
||||
"""
|
||||
|
||||
if stats['deactivated'] > 0:
|
||||
text += f"""
|
||||
|
||||
🗑️ <b>Полностью очищенные данные:</b>
|
||||
• Подписки сброшены к начальному состоянию
|
||||
• Удалены ВСЕ транзакции пользователей
|
||||
• Удалены ВСЕ реферальные доходы
|
||||
• Удалены использования промокодов
|
||||
• Сброшены балансы к нулю
|
||||
• Удалены подключенные серверы
|
||||
• Сброшены HWID устройства в RemnaWave
|
||||
• Очищены RemnaWave UUID
|
||||
"""
|
||||
else:
|
||||
text += f"\n✅ Неактуальных подписок не найдено!\nВсе пользователи синхронизированы с панелью."
|
||||
|
||||
if stats['errors'] > 0:
|
||||
text += f"\n⚠️ Обнаружены ошибки при обработке.\nПроверьте логи для подробной информации."
|
||||
|
||||
keyboard = [
|
||||
[types.InlineKeyboardButton(text="🔄 Повторить очистку", callback_data="force_cleanup_orphaned")],
|
||||
[types.InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_all_users")],
|
||||
[types.InlineKeyboardButton(text="⬅️ К синхронизации", callback_data="admin_rw_sync")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def confirm_force_cleanup(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
text = """
|
||||
⚠️ <b>ВНИМАНИЕ! ОПАСНАЯ ОПЕРАЦИЯ!</b>
|
||||
|
||||
🗑️ <b>Принудительная очистка полностью удалит:</b>
|
||||
• ВСЕ транзакции пользователей отсутствующих в панели
|
||||
• ВСЕ реферальные доходы и связи
|
||||
• ВСЕ использования промокодов
|
||||
• ВСЕ подключенные серверы подписок
|
||||
• ВСЕ балансы (сброс к нулю)
|
||||
• ВСЕ HWID устройства в RemnaWave
|
||||
• ВСЕ RemnaWave UUID и ссылки
|
||||
|
||||
⚡ <b>Это действие НЕОБРАТИМО!</b>
|
||||
|
||||
Используйте только если:
|
||||
• Обычная синхронизация не помогает
|
||||
• Нужно полностью очистить "мусорные" данные
|
||||
• После массового удаления пользователей из панели
|
||||
|
||||
❓ <b>Вы действительно хотите продолжить?</b>
|
||||
"""
|
||||
|
||||
keyboard = [
|
||||
[types.InlineKeyboardButton(text="🗑️ ДА, ОЧИСТИТЬ ВСЕ", callback_data="force_cleanup_orphaned")],
|
||||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_rw_sync")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
@@ -1725,6 +1848,8 @@ def register_handlers(dp: Dispatcher):
|
||||
dp.callback_query.register(show_sync_recommendations, F.data == "sync_recommendations")
|
||||
dp.callback_query.register(validate_subscriptions, F.data == "sync_validate")
|
||||
dp.callback_query.register(cleanup_subscriptions, F.data == "sync_cleanup")
|
||||
dp.callback_query.register(confirm_force_cleanup, F.data == "confirm_force_cleanup")
|
||||
dp.callback_query.register(force_cleanup_all_orphaned_users, F.data == "force_cleanup_orphaned")
|
||||
dp.callback_query.register(show_squads_management, F.data == "admin_rw_squads")
|
||||
|
||||
dp.callback_query.register(show_squad_details, F.data.startswith("admin_squad_manage_"))
|
||||
|
||||
+815
-21
@@ -274,7 +274,7 @@ async def show_user_subscription(
|
||||
text += f"<b>Окончание:</b> {format_datetime(subscription.end_date)}\n"
|
||||
text += f"<b>Трафик:</b> {subscription.traffic_used_gb:.1f}/{subscription.traffic_limit_gb} ГБ\n"
|
||||
text += f"<b>Устройства:</b> {subscription.device_limit}\n"
|
||||
text += f"<b>Подключенных устройств:</b> {len(subscription.connected_devices) if subscription.connected_devices else 0}\n"
|
||||
text += f"<b>Подключенных устройств:</b> {subscription.device_limit}\n"
|
||||
|
||||
if subscription.is_active:
|
||||
days_left = (subscription.end_date - datetime.utcnow()).days
|
||||
@@ -521,7 +521,14 @@ async def show_user_management(
|
||||
user = profile["user"]
|
||||
subscription = profile["subscription"]
|
||||
|
||||
status_text = "✅ Активен" if user.status == UserStatus.ACTIVE.value else "❌ Заблокирован"
|
||||
if user.status == UserStatus.ACTIVE.value:
|
||||
status_text = "✅ Активен"
|
||||
elif user.status == UserStatus.BLOCKED.value:
|
||||
status_text = "🚫 Заблокирован"
|
||||
elif user.status == UserStatus.DELETED.value:
|
||||
status_text = "🗑️ Удален"
|
||||
else:
|
||||
status_text = "❓ Неизвестно"
|
||||
|
||||
text = f"""
|
||||
👤 <b>Управление пользователем</b>
|
||||
@@ -558,7 +565,7 @@ async def show_user_management(
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=get_user_management_keyboard(user.id, db_user.language)
|
||||
reply_markup=get_user_management_keyboard(user.id, user.status, db_user.language)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@@ -746,6 +753,712 @@ async def show_inactive_users(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def confirm_user_unblock(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User
|
||||
):
|
||||
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await callback.message.edit_text(
|
||||
"✅ <b>Разблокировка пользователя</b>\n\n"
|
||||
"Вы уверены, что хотите разблокировать этого пользователя?\n"
|
||||
"Пользователь снова получит доступ к боту.",
|
||||
reply_markup=get_confirmation_keyboard(
|
||||
f"admin_user_unblock_confirm_{user_id}",
|
||||
f"admin_user_manage_{user_id}",
|
||||
db_user.language
|
||||
)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def unblock_user(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
user_service = UserService()
|
||||
success = await user_service.unblock_user(db, user_id, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Пользователь разблокирован",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_manage_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка разблокировки пользователя",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_manage_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_user_statistics(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
user_service = UserService()
|
||||
profile = await user_service.get_user_profile(db, user_id)
|
||||
|
||||
if not profile:
|
||||
await callback.answer("❌ Пользователь не найден", show_alert=True)
|
||||
return
|
||||
|
||||
user = profile["user"]
|
||||
subscription = profile["subscription"]
|
||||
|
||||
text = f"📊 <b>Статистика пользователя</b>\n\n"
|
||||
text += f"👤 {user.full_name} (ID: <code>{user.telegram_id}</code>)\n\n"
|
||||
|
||||
text += f"<b>Основная информация:</b>\n"
|
||||
text += f"• Дней с регистрации: {profile['registration_days']}\n"
|
||||
text += f"• Баланс: {settings.format_price(user.balance_kopeks)}\n"
|
||||
text += f"• Транзакций: {profile['transactions_count']}\n"
|
||||
text += f"• Язык: {user.language}\n\n"
|
||||
|
||||
text += f"<b>Подписка:</b>\n"
|
||||
if subscription:
|
||||
sub_status = "✅ Активна" if subscription.is_active else "❌ Неактивна"
|
||||
sub_type = " (триал)" if subscription.is_trial else " (платная)"
|
||||
text += f"• Статус: {sub_status}{sub_type}\n"
|
||||
text += f"• Трафик: {subscription.traffic_used_gb:.1f}/{subscription.traffic_limit_gb} ГБ\n"
|
||||
text += f"• Устройства: {subscription.device_limit}\n"
|
||||
text += f"• Стран: {len(subscription.connected_squads)}\n"
|
||||
else:
|
||||
text += f"• Отсутствует\n"
|
||||
|
||||
text += f"\n<b>Реферальная программа:</b>\n"
|
||||
if user.referred_by_id:
|
||||
text += f"• Пришел по рефералке\n"
|
||||
else:
|
||||
text += f"• Прямая регистрация\n"
|
||||
text += f"• Реферальный код: <code>{user.referral_code}</code>\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="⬅️ К пользователю", callback_data=f"admin_user_manage_{user_id}")]
|
||||
])
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def extend_user_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(extending_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"⏰ <b>Продление подписки</b>\n\n"
|
||||
"Введите количество дней для продления:\n"
|
||||
"• Например: 30, 7, 90\n"
|
||||
"• Максимум: 365 дней\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="7 дней", callback_data=f"admin_sub_extend_days_{user_id}_7"),
|
||||
types.InlineKeyboardButton(text="30 дней", callback_data=f"admin_sub_extend_days_{user_id}_30")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="90 дней", callback_data=f"admin_sub_extend_days_{user_id}_90"),
|
||||
types.InlineKeyboardButton(text="180 дней", callback_data=f"admin_sub_extend_days_{user_id}_180")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_subscription_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.extending_subscription)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_extension_days(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
days = int(parts[-1])
|
||||
|
||||
success = await _extend_subscription_by_days(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
f"✅ Подписка пользователя продлена на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка продления подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_extension_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("extending_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
days = int(message.text.strip())
|
||||
|
||||
if days <= 0 or days > 365:
|
||||
await message.answer("❌ Количество дней должно быть от 1 до 365")
|
||||
return
|
||||
|
||||
success = await _extend_subscription_by_days(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Подписка пользователя продлена на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка продления подписки")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число дней")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def add_subscription_traffic(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(traffic_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📊 <b>Добавление трафика</b>\n\n"
|
||||
"Введите количество ГБ для добавления:\n"
|
||||
"• Например: 50, 100, 500\n"
|
||||
"• Максимум: 10000 ГБ\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="50 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_50"),
|
||||
types.InlineKeyboardButton(text="100 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_100")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="500 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_500"),
|
||||
types.InlineKeyboardButton(text="1000 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_1000")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="♾️ Безлимит", callback_data=f"admin_sub_traffic_add_{user_id}_0"),
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_subscription_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.adding_traffic)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_traffic_addition_button(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
gb = int(parts[-1])
|
||||
|
||||
success = await _add_subscription_traffic(db, user_id, gb, db_user.id)
|
||||
|
||||
if success:
|
||||
traffic_text = "♾️ безлимитный" if gb == 0 else f"{gb} ГБ"
|
||||
await callback.message.edit_text(
|
||||
f"✅ К подписке пользователя добавлен трафик: {traffic_text}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка добавления трафика",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_traffic_addition_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("traffic_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
gb = int(message.text.strip())
|
||||
|
||||
if gb < 0 or gb > 10000:
|
||||
await message.answer("❌ Количество ГБ должно быть от 0 до 10000 (0 = безлимит)")
|
||||
return
|
||||
|
||||
success = await _add_subscription_traffic(db, user_id, gb, db_user.id)
|
||||
|
||||
if success:
|
||||
traffic_text = "♾️ безлимитный" if gb == 0 else f"{gb} ГБ"
|
||||
await message.answer(
|
||||
f"✅ К подписке пользователя добавлен трафик: {traffic_text}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка добавления трафика")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число ГБ")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def deactivate_user_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🚫 <b>Деактивация подписки</b>\n\n"
|
||||
"Вы уверены, что хотите деактивировать подписку этого пользователя?\n"
|
||||
"Пользователь потеряет доступ к сервису.",
|
||||
reply_markup=get_confirmation_keyboard(
|
||||
f"admin_sub_deactivate_confirm_{user_id}",
|
||||
f"admin_user_subscription_{user_id}",
|
||||
db_user.language
|
||||
)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def confirm_subscription_deactivation(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
success = await _deactivate_user_subscription(db, user_id, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Подписка пользователя деактивирована",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка деактивации подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def activate_user_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
success = await _activate_user_subscription(db, user_id, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Подписка пользователя активирована",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка активации подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def grant_trial_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
success = await _grant_trial_subscription(db, user_id, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Пользователю выдан триальный период",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка выдачи триального периода",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def grant_paid_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(granting_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"💎 <b>Выдача подписки</b>\n\n"
|
||||
"Введите количество дней подписки:\n"
|
||||
"• Например: 30, 90, 180, 365\n"
|
||||
"• Максимум: 730 дней\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="30 дней", callback_data=f"admin_sub_grant_days_{user_id}_30"),
|
||||
types.InlineKeyboardButton(text="90 дней", callback_data=f"admin_sub_grant_days_{user_id}_90")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="180 дней", callback_data=f"admin_sub_grant_days_{user_id}_180"),
|
||||
types.InlineKeyboardButton(text="365 дней", callback_data=f"admin_sub_grant_days_{user_id}_365")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_subscription_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.granting_subscription)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_grant_days(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
days = int(parts[-1])
|
||||
|
||||
success = await _grant_paid_subscription(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
f"✅ Пользователю выдана подписка на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка выдачи подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_grant_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("granting_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
days = int(message.text.strip())
|
||||
|
||||
if days <= 0 or days > 730:
|
||||
await message.answer("❌ Количество дней должно быть от 1 до 730")
|
||||
return
|
||||
|
||||
success = await _grant_paid_subscription(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Пользователю выдана подписка на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка выдачи подписки")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число дней")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
async def _extend_subscription_by_days(db: AsyncSession, user_id: int, days: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, extend_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
await extend_subscription(db, subscription, days)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} продлил подписку пользователя {user_id} на {days} дней")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка продления подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _add_subscription_traffic(db: AsyncSession, user_id: int, gb: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, add_subscription_traffic
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
if gb == 0:
|
||||
subscription.traffic_limit_gb = 0
|
||||
await db.commit()
|
||||
else:
|
||||
await add_subscription_traffic(db, subscription, gb)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
traffic_text = "безлимитный" if gb == 0 else f"{gb} ГБ"
|
||||
logger.info(f"Админ {admin_id} добавил трафик {traffic_text} пользователю {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления трафика: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _deactivate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, deactivate_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
await deactivate_subscription(db, subscription)
|
||||
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if user and user.remnawave_uuid:
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
|
||||
logger.info(f"Админ {admin_id} деактивировал подписку пользователя {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка деактивации подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.database.models import SubscriptionStatus
|
||||
from datetime import datetime
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||
if subscription.end_date <= datetime.utcnow():
|
||||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} активировал подписку пользователя {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка активации подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _grant_trial_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, create_trial_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
existing_subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if existing_subscription:
|
||||
logger.error(f"У пользователя {user_id} уже есть подписка")
|
||||
return False
|
||||
|
||||
subscription = await create_trial_subscription(db, user_id)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} выдал триальную подписку пользователю {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка выдачи триальной подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _grant_paid_subscription(db: AsyncSession, user_id: int, days: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, create_paid_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.config import settings
|
||||
|
||||
existing_subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if existing_subscription:
|
||||
logger.error(f"У пользователя {user_id} уже есть подписка")
|
||||
return False
|
||||
|
||||
subscription = await create_paid_subscription(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
duration_days=days,
|
||||
traffic_limit_gb=settings.DEFAULT_TRAFFIC_LIMIT_GB,
|
||||
device_limit=settings.DEFAULT_DEVICE_LIMIT,
|
||||
connected_squads=[settings.TRIAL_SQUAD_UUID] if settings.TRIAL_SQUAD_UUID else []
|
||||
)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} выдал платную подписку на {days} дней пользователю {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка выдачи платной подписки: {e}")
|
||||
return False
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
@@ -787,23 +1500,48 @@ def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
show_user_subscription,
|
||||
F.data.startswith("admin_user_sub_")
|
||||
F.data.startswith("admin_user_subscription_")
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
show_user_transactions,
|
||||
F.data.startswith("admin_user_trans_")
|
||||
F.data.startswith("admin_user_transactions_")
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_user_delete,
|
||||
F.data.startswith("admin_user_delete_")
|
||||
show_user_statistics,
|
||||
F.data.startswith("admin_user_statistics_")
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
block_user,
|
||||
F.data.startswith("admin_user_block_confirm_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
delete_user_account,
|
||||
F.data.startswith("admin_user_delete_confirm_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_user_block,
|
||||
F.data.startswith("admin_user_block_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
unblock_user,
|
||||
F.data.startswith("admin_user_unblock_confirm_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_user_unblock,
|
||||
F.data.startswith("admin_user_unblock_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_user_delete,
|
||||
F.data.startswith("admin_user_delete_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_users_list_pagination_fixed,
|
||||
@@ -835,16 +1573,6 @@ def register_handlers(dp: Dispatcher):
|
||||
AdminStates.editing_user_balance
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_user_block,
|
||||
F.data.startswith("admin_user_block_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
block_user,
|
||||
F.data.startswith("admin_user_block_confirm_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_inactive_users,
|
||||
F.data == "admin_users_inactive"
|
||||
@@ -853,4 +1581,70 @@ def register_handlers(dp: Dispatcher):
|
||||
dp.callback_query.register(
|
||||
cleanup_inactive_users,
|
||||
F.data == "admin_cleanup_inactive"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
extend_user_subscription,
|
||||
F.data.startswith("admin_sub_extend_") & ~F.data.contains("days") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_subscription_extension_days,
|
||||
F.data.startswith("admin_sub_extend_days_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_subscription_extension_text,
|
||||
AdminStates.extending_subscription
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
add_subscription_traffic,
|
||||
F.data.startswith("admin_sub_traffic_") & ~F.data.contains("add")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_traffic_addition_button,
|
||||
F.data.startswith("admin_sub_traffic_add_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_traffic_addition_text,
|
||||
AdminStates.adding_traffic
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
deactivate_user_subscription,
|
||||
F.data.startswith("admin_sub_deactivate_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_subscription_deactivation,
|
||||
F.data.startswith("admin_sub_deactivate_confirm_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
activate_user_subscription,
|
||||
F.data.startswith("admin_sub_activate_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
grant_trial_subscription,
|
||||
F.data.startswith("admin_sub_grant_trial_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
grant_paid_subscription,
|
||||
F.data.startswith("admin_sub_grant_") & ~F.data.contains("trial") & ~F.data.contains("days")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_subscription_grant_days,
|
||||
F.data.startswith("admin_sub_grant_days_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_subscription_grant_text,
|
||||
AdminStates.granting_subscription
|
||||
)
|
||||
|
||||
+130
-99
@@ -96,7 +96,8 @@ async def show_balance_history(
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@@ -112,16 +113,43 @@ async def handle_balance_history_pagination(
|
||||
|
||||
|
||||
@error_handler
|
||||
async def start_balance_topup(
|
||||
async def show_payment_methods(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
payment_text = """
|
||||
💳 <b>Способы пополнения баланса</b>
|
||||
|
||||
Выберите удобный для вас способ оплаты:
|
||||
|
||||
⭐ <b>Telegram Stars</b> - быстро и удобно
|
||||
💎 <b>Банковская карта</b> - через Tribute
|
||||
🛠️ <b>Через поддержку</b> - другие способы
|
||||
|
||||
Выберите способ пополнения:
|
||||
"""
|
||||
|
||||
await callback.message.edit_text(
|
||||
payment_text,
|
||||
reply_markup=get_payment_methods_keyboard(0, db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@error_handler
|
||||
async def start_stars_payment(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
"""Начать пополнение - только для Telegram Stars"""
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.TELEGRAM_STARS_ENABLED:
|
||||
await callback.answer("❌ Пополнение временно недоступно", show_alert=True)
|
||||
await callback.answer("❌ Пополнение через Stars временно недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
@@ -130,96 +158,15 @@ async def start_balance_topup(
|
||||
)
|
||||
|
||||
await state.set_state(BalanceStates.waiting_for_amount)
|
||||
await state.update_data(payment_method="stars")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_topup_amount(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
"""Обработка введенной суммы - только для Telegram Stars"""
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
try:
|
||||
amount_rubles = float(message.text.replace(',', '.'))
|
||||
|
||||
if amount_rubles < 1:
|
||||
await message.answer("❌ Минимальная сумма пополнения: 1 ₽")
|
||||
return
|
||||
|
||||
if amount_rubles > 50000:
|
||||
await message.answer("❌ Максимальная сумма пополнения: 50,000 ₽")
|
||||
return
|
||||
|
||||
amount_kopeks = int(amount_rubles * 100)
|
||||
|
||||
await state.update_data(amount_kopeks=amount_kopeks)
|
||||
|
||||
payment_text = texts.TOP_UP_METHODS.format(
|
||||
amount=texts.format_price(amount_kopeks)
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
payment_text,
|
||||
reply_markup=get_payment_methods_keyboard(amount_kopeks, db_user.language)
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
texts.INVALID_AMOUNT,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_stars_payment(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.TELEGRAM_STARS_ENABLED:
|
||||
await callback.answer("❌ Оплата Stars временно недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
amount_kopeks = int(callback.data.split('_')[-1])
|
||||
|
||||
try:
|
||||
payment_service = PaymentService(callback.bot)
|
||||
invoice_link = await payment_service.create_stars_invoice(
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}",
|
||||
payload=f"balance_{db_user.id}_{amount_kopeks}"
|
||||
)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
|
||||
f"Сумма: {texts.format_price(amount_kopeks)}\n\n"
|
||||
f"Нажмите кнопку ниже для оплаты:",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания Stars invoice: {e}")
|
||||
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_tribute_quick_payment(
|
||||
async def start_tribute_payment(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User
|
||||
):
|
||||
"""Быстрое пополнение через Tribute - без выбора суммы"""
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.TRIBUTE_ENABLED:
|
||||
@@ -242,17 +189,19 @@ async def process_tribute_quick_payment(
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")]
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"💳 <b>Пополнение банковской картой</b>\n\n"
|
||||
f"• Введите любую сумму от 50 ₽\n"
|
||||
f"• Введите любую сумму от 100₽\n"
|
||||
f"• Безопасная оплата через Tribute\n"
|
||||
f"• Мгновенное зачисление на баланс\n"
|
||||
f"• Принимаем карты Visa, MasterCard, МИР\n\n"
|
||||
f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n"
|
||||
f"Нажмите кнопку для перехода к оплате:",
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -281,6 +230,11 @@ async def request_support_topup(
|
||||
• Способ оплаты
|
||||
|
||||
⏰ Время обработки: 1-24 часа
|
||||
|
||||
<b>Доступные способы:</b>
|
||||
• Криптовалюта
|
||||
• Переводы между банками
|
||||
• Другие платежные системы
|
||||
"""
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
@@ -288,16 +242,93 @@ async def request_support_topup(
|
||||
text="💬 Написать в поддержку",
|
||||
url=f"https://t.me/{settings.SUPPORT_USERNAME.lstrip('@')}"
|
||||
)],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")]
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
support_text,
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_topup_amount(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
try:
|
||||
amount_rubles = float(message.text.replace(',', '.'))
|
||||
|
||||
if amount_rubles < 1:
|
||||
await message.answer("❌ Минимальная сумма пополнения: 1 ₽")
|
||||
return
|
||||
|
||||
if amount_rubles > 50000:
|
||||
await message.answer("❌ Максимальная сумма пополнения: 50,000 ₽")
|
||||
return
|
||||
|
||||
amount_kopeks = int(amount_rubles * 100)
|
||||
data = await state.get_data()
|
||||
payment_method = data.get("payment_method", "stars")
|
||||
|
||||
if payment_method == "stars":
|
||||
await process_stars_payment_amount(message, db_user, amount_kopeks, state)
|
||||
else:
|
||||
await message.answer("❌ Неизвестный способ оплаты")
|
||||
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
texts.INVALID_AMOUNT,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_stars_payment_amount(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
amount_kopeks: int,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.TELEGRAM_STARS_ENABLED:
|
||||
await message.answer("❌ Оплата Stars временно недоступна")
|
||||
return
|
||||
|
||||
try:
|
||||
payment_service = PaymentService(message.bot)
|
||||
invoice_link = await payment_service.create_stars_invoice(
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}",
|
||||
payload=f"balance_{db_user.id}_{amount_kopeks}"
|
||||
)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
|
||||
])
|
||||
|
||||
await message.answer(
|
||||
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
|
||||
f"Сумма: {texts.format_price(amount_kopeks)}\n\n"
|
||||
f"Нажмите кнопку ниже для оплаты:",
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания Stars invoice: {e}")
|
||||
await message.answer("❌ Ошибка создания платежа")
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
@@ -316,26 +347,26 @@ def register_handlers(dp: Dispatcher):
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
start_balance_topup,
|
||||
show_payment_methods,
|
||||
F.data == "balance_topup"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_stars_payment,
|
||||
F.data.startswith("pay_stars_")
|
||||
start_stars_payment,
|
||||
F.data == "topup_stars"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_tribute_quick_payment,
|
||||
F.data == "tribute_quick_pay"
|
||||
start_tribute_payment,
|
||||
F.data == "topup_tribute"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
request_support_topup,
|
||||
F.data == "balance_support"
|
||||
F.data == "topup_support"
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_topup_amount,
|
||||
BalanceStates.waiting_for_amount
|
||||
)
|
||||
)
|
||||
|
||||
+28
-9
@@ -2,6 +2,7 @@ import logging
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.config import settings
|
||||
from app.database.crud.user import get_user_by_telegram_id, update_user
|
||||
@@ -108,15 +109,33 @@ async def handle_back_to_menu(
|
||||
|
||||
def _get_subscription_status(user: User, texts) -> str:
|
||||
if not user.subscription:
|
||||
return texts.SUBSCRIPTION_NONE
|
||||
return "⌐ Отсутствует"
|
||||
|
||||
if user.subscription.is_trial:
|
||||
return f"{texts.SUBSCRIPTION_TRIAL} (до {user.subscription.end_date.strftime('%d.%m.%Y')})"
|
||||
elif user.subscription.is_active:
|
||||
days_left = user.subscription.days_left
|
||||
return f"{texts.SUBSCRIPTION_ACTIVE} ({days_left} дн.)"
|
||||
else:
|
||||
return texts.SUBSCRIPTION_EXPIRED
|
||||
subscription = user.subscription
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
if subscription.end_date <= current_time:
|
||||
return f"🔴 Истекла\n📅 {subscription.end_date.strftime('%d.%m.%Y')}"
|
||||
|
||||
days_left = (subscription.end_date - current_time).days
|
||||
|
||||
if subscription.is_trial:
|
||||
if days_left > 1:
|
||||
return f"🎁 Тестовая подписка\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
|
||||
elif days_left == 1:
|
||||
return f"🎁 Тестовая подписка\n⚠️ истекает завтра!"
|
||||
else:
|
||||
return f"🎁 Тестовая подписка\n⚠️ истекает сегодня!"
|
||||
|
||||
else:
|
||||
if days_left > 7:
|
||||
return f"💎 Активна\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
|
||||
elif days_left > 1:
|
||||
return f"💎 Активна\n⚠️ истекает через {days_left} дн."
|
||||
elif days_left == 1:
|
||||
return f"💎 Активна\n⚠️ истекает завтра!"
|
||||
else:
|
||||
return f"💎 Активна\n⚠️ истекает сегодня!"
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
@@ -129,4 +148,4 @@ def register_handlers(dp: Dispatcher):
|
||||
dp.callback_query.register(
|
||||
show_service_rules,
|
||||
F.data == "menu_rules"
|
||||
)
|
||||
)
|
||||
|
||||
+162
-52
@@ -9,6 +9,7 @@ from app.states import RegistrationStates
|
||||
from app.database.crud.user import (
|
||||
get_user_by_telegram_id, create_user, get_user_by_referral_code
|
||||
)
|
||||
from app.database.models import UserStatus
|
||||
from app.keyboards.inline import (
|
||||
get_rules_keyboard, get_main_menu_keyboard
|
||||
)
|
||||
@@ -19,7 +20,7 @@ from app.utils.user_utils import generate_unique_referral_code
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession):
|
||||
async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession, db_user=None):
|
||||
logger.info(f"🚀 START: Обработка /start от {message.from_user.id}")
|
||||
|
||||
referral_code = None
|
||||
@@ -31,10 +32,10 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession)
|
||||
if referral_code:
|
||||
await state.set_data({'referral_code': referral_code})
|
||||
|
||||
user = await get_user_by_telegram_id(db, message.from_user.id)
|
||||
user = db_user if db_user else await get_user_by_telegram_id(db, message.from_user.id)
|
||||
|
||||
if user:
|
||||
logger.info(f"✅ Пользователь найден: {user.telegram_id}")
|
||||
if user and user.status != UserStatus.DELETED.value:
|
||||
logger.info(f"✅ Активный пользователь найден: {user.telegram_id}")
|
||||
texts = get_texts(user.language)
|
||||
|
||||
if referral_code and not user.referred_by_id:
|
||||
@@ -60,26 +61,82 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession)
|
||||
subscription_is_active=subscription_is_active
|
||||
)
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
if user and user.status == UserStatus.DELETED.value:
|
||||
logger.info(f"🔄 Удаленный пользователь {user.telegram_id} начинает повторную регистрацию")
|
||||
|
||||
try:
|
||||
from app.services.user_service import UserService
|
||||
from app.database.models import (
|
||||
Subscription, Transaction, PromoCodeUse,
|
||||
ReferralEarning, SubscriptionServer
|
||||
)
|
||||
from sqlalchemy import delete
|
||||
|
||||
if user.subscription:
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены записи SubscriptionServer")
|
||||
|
||||
if user.subscription:
|
||||
await db.delete(user.subscription)
|
||||
logger.info(f"🗑️ Удалена подписка пользователя")
|
||||
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
|
||||
)
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user.id)
|
||||
)
|
||||
|
||||
user.status = UserStatus.ACTIVE.value
|
||||
user.balance_kopeks = 0
|
||||
user.remnawave_uuid = None
|
||||
user.has_had_paid_subscription = False
|
||||
user.referred_by_id = None
|
||||
from app.utils.user_utils import generate_unique_referral_code
|
||||
user.referral_code = await generate_unique_referral_code(db, user.telegram_id)
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"✅ Пользователь {user.telegram_id} подготовлен к восстановлению")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка подготовки к восстановлению: {e}")
|
||||
await db.rollback()
|
||||
else:
|
||||
logger.info(f"🆕 Новый пользователь, начинаем регистрацию")
|
||||
|
||||
language = 'ru'
|
||||
texts = get_texts(language)
|
||||
|
||||
data = await state.get_data() or {}
|
||||
data['language'] = language
|
||||
await state.set_data(data)
|
||||
logger.info(f"💾 Установлен русский язык по умолчанию")
|
||||
|
||||
await message.answer(
|
||||
texts.RULES_TEXT,
|
||||
reply_markup=get_rules_keyboard(language)
|
||||
)
|
||||
logger.info(f"📋 Правила отправлены")
|
||||
|
||||
await state.set_state(RegistrationStates.waiting_for_rules_accept)
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"📊 Установлено состояние: {current_state}")
|
||||
|
||||
language = 'ru'
|
||||
texts = get_texts(language)
|
||||
|
||||
data = await state.get_data() or {}
|
||||
data['language'] = language
|
||||
await state.set_data(data)
|
||||
logger.info(f"💾 Установлен русский язык по умолчанию")
|
||||
|
||||
await message.answer(
|
||||
texts.RULES_TEXT,
|
||||
reply_markup=get_rules_keyboard(language)
|
||||
)
|
||||
logger.info(f"📋 Правила отправлены")
|
||||
|
||||
await state.set_state(RegistrationStates.waiting_for_rules_accept)
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"📊 Установлено состояние: {current_state}")
|
||||
|
||||
|
||||
async def process_rules_accept(
|
||||
@@ -125,7 +182,7 @@ async def process_rules_accept(
|
||||
if referrer:
|
||||
data['referrer_id'] = referrer.id
|
||||
await state.set_data(data)
|
||||
logger.info(f"✅ Референс найден: {referrer.id}")
|
||||
logger.info(f"✅ Реферер найден: {referrer.id}")
|
||||
|
||||
await complete_registration_from_callback(callback, state, db)
|
||||
else:
|
||||
@@ -237,8 +294,9 @@ async def complete_registration_from_callback(
|
||||
logger.info(f"🏁 COMPLETE: Завершение регистрации для пользователя {callback.from_user.id}")
|
||||
|
||||
existing_user = await get_user_by_telegram_id(db, callback.from_user.id)
|
||||
if existing_user:
|
||||
logger.warning(f"⚠️ Пользователь {callback.from_user.id} уже существует! Показываем главное меню.")
|
||||
|
||||
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
|
||||
logger.warning(f"⚠️ Пользователь {callback.from_user.id} уже активен! Показываем главное меню.")
|
||||
texts = get_texts(existing_user.language)
|
||||
|
||||
has_active_subscription = existing_user.subscription is not None
|
||||
@@ -282,18 +340,43 @@ async def complete_registration_from_callback(
|
||||
if referrer:
|
||||
referrer_id = referrer.id
|
||||
|
||||
referral_code = await generate_unique_referral_code(db, callback.from_user.id)
|
||||
|
||||
user = await create_user(
|
||||
db=db,
|
||||
telegram_id=callback.from_user.id,
|
||||
username=callback.from_user.username,
|
||||
first_name=callback.from_user.first_name,
|
||||
last_name=callback.from_user.last_name,
|
||||
language=language,
|
||||
referred_by_id=referrer_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
if existing_user and existing_user.status == UserStatus.DELETED.value:
|
||||
logger.info(f"🔄 Восстанавливаем удаленного пользователя {callback.from_user.id}")
|
||||
|
||||
existing_user.username = callback.from_user.username
|
||||
existing_user.first_name = callback.from_user.first_name
|
||||
existing_user.last_name = callback.from_user.last_name
|
||||
existing_user.language = language
|
||||
existing_user.referred_by_id = referrer_id
|
||||
existing_user.status = UserStatus.ACTIVE.value
|
||||
existing_user.balance_kopeks = 0
|
||||
existing_user.has_had_paid_subscription = False
|
||||
|
||||
from datetime import datetime
|
||||
existing_user.updated_at = datetime.utcnow()
|
||||
existing_user.last_activity = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(existing_user)
|
||||
|
||||
user = existing_user
|
||||
logger.info(f"✅ Пользователь {callback.from_user.id} восстановлен")
|
||||
|
||||
else:
|
||||
logger.info(f"🆕 Создаем нового пользователя {callback.from_user.id}")
|
||||
|
||||
referral_code = await generate_unique_referral_code(db, callback.from_user.id)
|
||||
|
||||
user = await create_user(
|
||||
db=db,
|
||||
telegram_id=callback.from_user.id,
|
||||
username=callback.from_user.username,
|
||||
first_name=callback.from_user.first_name,
|
||||
last_name=callback.from_user.last_name,
|
||||
language=language,
|
||||
referred_by_id=referrer_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
|
||||
if referrer_id:
|
||||
try:
|
||||
@@ -355,7 +438,7 @@ async def complete_registration_from_callback(
|
||||
except Exception as final_error:
|
||||
logger.error(f"❌ Критическая ошибка при отправке простого сообщения: {final_error}")
|
||||
|
||||
logger.info(f"✅ Зарегистрирован новый пользователь: {user_telegram_id}")
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user_telegram_id}")
|
||||
|
||||
|
||||
async def complete_registration(
|
||||
@@ -366,6 +449,8 @@ async def complete_registration(
|
||||
|
||||
logger.info(f"🏁 COMPLETE: Завершение регистрации для пользователя {message.from_user.id}")
|
||||
|
||||
existing_user = await get_user_by_telegram_id(db, message.from_user.id)
|
||||
|
||||
data = await state.get_data()
|
||||
language = data.get('language', 'ru')
|
||||
texts = get_texts(language)
|
||||
@@ -376,18 +461,43 @@ async def complete_registration(
|
||||
if referrer:
|
||||
referrer_id = referrer.id
|
||||
|
||||
referral_code = await generate_unique_referral_code(db, message.from_user.id)
|
||||
|
||||
user = await create_user(
|
||||
db=db,
|
||||
telegram_id=message.from_user.id,
|
||||
username=message.from_user.username,
|
||||
first_name=message.from_user.first_name,
|
||||
last_name=message.from_user.last_name,
|
||||
language=language,
|
||||
referred_by_id=referrer_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
if existing_user and existing_user.status == UserStatus.DELETED.value:
|
||||
logger.info(f"🔄 Восстанавливаем удаленного пользователя {message.from_user.id}")
|
||||
|
||||
existing_user.username = message.from_user.username
|
||||
existing_user.first_name = message.from_user.first_name
|
||||
existing_user.last_name = message.from_user.last_name
|
||||
existing_user.language = language
|
||||
existing_user.referred_by_id = referrer_id
|
||||
existing_user.status = UserStatus.ACTIVE.value
|
||||
existing_user.balance_kopeks = 0
|
||||
existing_user.has_had_paid_subscription = False
|
||||
|
||||
from datetime import datetime
|
||||
existing_user.updated_at = datetime.utcnow()
|
||||
existing_user.last_activity = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(existing_user)
|
||||
|
||||
user = existing_user
|
||||
logger.info(f"✅ Пользователь {message.from_user.id} восстановлен")
|
||||
|
||||
else:
|
||||
logger.info(f"🆕 Создаем нового пользователя {message.from_user.id}")
|
||||
|
||||
referral_code = await generate_unique_referral_code(db, message.from_user.id)
|
||||
|
||||
user = await create_user(
|
||||
db=db,
|
||||
telegram_id=message.from_user.id,
|
||||
username=message.from_user.username,
|
||||
first_name=message.from_user.first_name,
|
||||
last_name=message.from_user.last_name,
|
||||
language=language,
|
||||
referred_by_id=referrer_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
|
||||
if referrer_id:
|
||||
try:
|
||||
@@ -449,7 +559,7 @@ async def complete_registration(
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.info(f"✅ Зарегистрирован новый пользователь: {user_telegram_id}")
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user_telegram_id}")
|
||||
|
||||
|
||||
def _get_subscription_status(user, texts):
|
||||
@@ -504,4 +614,4 @@ def register_handlers(dp: Dispatcher):
|
||||
)
|
||||
logger.info("✅ Зарегистрирован process_referral_code_input")
|
||||
|
||||
logger.info("🔧 === КОНЕЦ регистрации обработчиков start.py ===")
|
||||
logger.info("🔧 === КОНЕЦ регистрации обработчиков start.py ===")
|
||||
|
||||
+561
-181
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,171 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from app.database.models import User, Subscription
|
||||
from app.database.crud.user import get_user_by_id, subtract_user_balance
|
||||
from app.database.crud.subscription import get_expiring_subscriptions, extend_subscription
|
||||
from app.database.crud.transaction import create_transaction
|
||||
from app.database.models import TransactionType
|
||||
from app.keyboards.inline import get_autopay_notification_keyboard, get_subscription_expiring_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def send_subscription_expiring_notification(
|
||||
bot: Bot,
|
||||
db: AsyncSession,
|
||||
subscription: Subscription,
|
||||
days_left: int
|
||||
) -> bool:
|
||||
try:
|
||||
user = await get_user_by_id(db, subscription.user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
texts = get_texts(user.language)
|
||||
|
||||
if subscription.is_trial:
|
||||
text = texts.TRIAL_ENDING_SOON.format(
|
||||
price=texts.format_price(30000)
|
||||
)
|
||||
else:
|
||||
autopay_status = texts.AUTOPAY_ENABLED_TEXT if subscription.autopay_enabled else texts.AUTOPAY_DISABLED_TEXT
|
||||
|
||||
if subscription.autopay_enabled:
|
||||
action_text = f"💰 Убедитесь, что на балансе достаточно средств: {texts.format_price(user.balance_kopeks)}"
|
||||
else:
|
||||
action_text = "💡 Включите автоплатеж или продлите подписку вручную"
|
||||
|
||||
text = texts.SUBSCRIPTION_EXPIRING_PAID.format(
|
||||
days=days_left,
|
||||
end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M"),
|
||||
autopay_status=autopay_status,
|
||||
action_text=action_text
|
||||
)
|
||||
|
||||
keyboard = get_subscription_expiring_keyboard(subscription.id, user.language)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user.telegram_id,
|
||||
text=text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
logger.info(f"✅ Отправлено уведомление об истечении подписки пользователю {user.telegram_id}")
|
||||
return True
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
logger.warning(f"⚠️ Не удалось отправить уведомление пользователю {user.telegram_id}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка отправки уведомления об истечении подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def send_autopay_failed_notification(
|
||||
bot: Bot,
|
||||
db: AsyncSession,
|
||||
subscription: Subscription,
|
||||
required_amount: int
|
||||
) -> bool:
|
||||
try:
|
||||
user = await get_user_by_id(db, subscription.user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
texts = get_texts(user.language)
|
||||
|
||||
text = texts.AUTOPAY_FAILED.format(
|
||||
balance=texts.format_price(user.balance_kopeks),
|
||||
required=texts.format_price(required_amount)
|
||||
)
|
||||
|
||||
keyboard = get_autopay_notification_keyboard(subscription.id, user.language)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user.telegram_id,
|
||||
text=text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
logger.info(f"✅ Отправлено уведомление о неудачном автоплатеже пользователю {user.telegram_id}")
|
||||
return True
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
logger.warning(f"⚠️ Не удалось отправить уведомление пользователю {user.telegram_id}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка отправки уведомления о неудачном автоплатеже: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def process_autopayment(
|
||||
bot: Bot,
|
||||
db: AsyncSession,
|
||||
subscription: Subscription
|
||||
) -> bool:
|
||||
try:
|
||||
user = await get_user_by_id(db, subscription.user_id)
|
||||
if not user:
|
||||
logger.error(f"Пользователь {subscription.user_id} не найден для автоплатежа")
|
||||
return False
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
renewal_cost = await subscription_service.calculate_renewal_price(
|
||||
subscription, 30, db
|
||||
)
|
||||
|
||||
if user.balance_kopeks < renewal_cost:
|
||||
logger.warning(f"Недостаточно средств для автоплатежа у пользователя {user.telegram_id}")
|
||||
await send_autopay_failed_notification(bot, db, subscription, renewal_cost)
|
||||
return False
|
||||
|
||||
success = await subtract_user_balance(
|
||||
db, user, renewal_cost,
|
||||
f"Автопродление подписки на 30 дней"
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.error(f"Ошибка списания средств для автоплатежа у пользователя {user.telegram_id}")
|
||||
await send_autopay_failed_notification(bot, db, subscription, renewal_cost)
|
||||
return False
|
||||
|
||||
await extend_subscription(db, subscription, 30)
|
||||
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
await create_transaction(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
type=TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
amount_kopeks=renewal_cost,
|
||||
description="Автопродление подписки на 30 дней"
|
||||
)
|
||||
|
||||
texts = get_texts(user.language)
|
||||
success_text = texts.AUTOPAY_SUCCESS.format(
|
||||
days=30,
|
||||
amount=texts.format_price(renewal_cost),
|
||||
new_end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M")
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user.telegram_id,
|
||||
text=success_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
logger.info(f"✅ Автоплатеж успешно выполнен для пользователя {user.telegram_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обработки автоплатежа: {e}")
|
||||
return False
|
||||
+27
-13
@@ -172,24 +172,38 @@ def get_admin_statistics_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
])
|
||||
|
||||
|
||||
def get_user_management_keyboard(user_id: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
def get_user_management_keyboard(user_id: int, user_status: str, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="💰 Баланс", callback_data=f"admin_user_balance_{user_id}"),
|
||||
InlineKeyboardButton(text="📱 Подписка", callback_data=f"admin_user_sub_{user_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_user_stats_{user_id}"),
|
||||
InlineKeyboardButton(text="📋 Транзакции", callback_data=f"admin_user_trans_{user_id}")
|
||||
InlineKeyboardButton(text="📱 Подписка", callback_data=f"admin_user_subscription_{user_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_user_statistics_{user_id}"),
|
||||
InlineKeyboardButton(text="📋 Транзакции", callback_data=f"admin_user_transactions_{user_id}")
|
||||
]
|
||||
]
|
||||
|
||||
if user_status == "active":
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="🚫 Заблокировать", callback_data=f"admin_user_block_{user_id}"),
|
||||
InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_user_delete_{user_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users_list")
|
||||
]
|
||||
])
|
||||
elif user_status == "blocked":
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="✅ Разблокировать", callback_data=f"admin_user_unblock_{user_id}"),
|
||||
InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_user_delete_{user_id}")
|
||||
])
|
||||
elif user_status == "deleted":
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="❌ Пользователь удален", callback_data="noop")
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users_list")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def get_confirmation_keyboard(
|
||||
@@ -293,8 +307,8 @@ def get_custom_criteria_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text="⚡ Активные сегодня", callback_data="criteria_active_today")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💤 Неактивные 7+ дней", callback_data="criteria_inactive_week"),
|
||||
InlineKeyboardButton(text="💤 Неактивные 30+ дней", callback_data="criteria_inactive_month")
|
||||
InlineKeyboardButton(text="👤 Неактивные 7+ дней", callback_data="criteria_inactive_week"),
|
||||
InlineKeyboardButton(text="👤 Неактивные 30+ дней", callback_data="criteria_inactive_month")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🤝 Через рефералов", callback_data="criteria_referrals"),
|
||||
|
||||
+183
-59
@@ -97,13 +97,34 @@ def get_subscription_keyboard(
|
||||
is_trial: bool = False,
|
||||
subscription=None
|
||||
) -> InlineKeyboardMarkup:
|
||||
from app.config import settings
|
||||
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
if has_subscription:
|
||||
if subscription and subscription.subscription_url:
|
||||
connect_mode = settings.CONNECT_BUTTON_MODE
|
||||
|
||||
if connect_mode == "miniapp_subscription":
|
||||
button_text = "🚀 Открыть в мини-приложении"
|
||||
elif connect_mode == "miniapp_custom":
|
||||
button_text = "🚀 Открыть приложение"
|
||||
else:
|
||||
button_text = "🔗 Подключиться"
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
|
||||
InlineKeyboardButton(text=button_text, callback_data="subscription_connect")
|
||||
])
|
||||
|
||||
if not is_trial and subscription and subscription.days_left <= 3:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="⏰ Продлить", callback_data="subscription_extend")
|
||||
])
|
||||
|
||||
if not is_trial:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="💳 Автоплатеж", callback_data="subscription_autopay")
|
||||
])
|
||||
|
||||
if is_trial:
|
||||
@@ -111,35 +132,9 @@ def get_subscription_keyboard(
|
||||
InlineKeyboardButton(text=texts.MENU_BUY_SUBSCRIPTION, callback_data="subscription_upgrade")
|
||||
])
|
||||
else:
|
||||
row1 = []
|
||||
row2 = []
|
||||
row3 = []
|
||||
row4 = []
|
||||
|
||||
row1.append(InlineKeyboardButton(text="🌐 Добавить страны", callback_data="subscription_add_countries"))
|
||||
|
||||
if subscription and subscription.traffic_limit_gb > 0:
|
||||
row1.append(InlineKeyboardButton(text="📈 Добавить трафик", callback_data="subscription_add_traffic"))
|
||||
|
||||
row2.append(InlineKeyboardButton(text="📱 Добавить устройства", callback_data="subscription_add_devices"))
|
||||
|
||||
if subscription and subscription.days_left <= 3:
|
||||
row2.append(InlineKeyboardButton(text="⏰ Продлить", callback_data="subscription_extend"))
|
||||
|
||||
row3.append(InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="subscription_reset_traffic"))
|
||||
|
||||
row3.append(InlineKeyboardButton(text="💳 Автоплатеж", callback_data="subscription_autopay"))
|
||||
|
||||
row4.append(InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data="subscription_reset_devices"))
|
||||
|
||||
if row1:
|
||||
keyboard.append(row1)
|
||||
if row2:
|
||||
keyboard.append(row2)
|
||||
if row3:
|
||||
keyboard.append(row3)
|
||||
if row4:
|
||||
keyboard.append(row4)
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="⚙️ Настройки подписки", callback_data="subscription_settings")
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
@@ -147,6 +142,36 @@ def get_subscription_keyboard(
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_subscription_settings_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
from app.config import settings
|
||||
|
||||
texts = get_texts(language)
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="🌍 Добавить страны", callback_data="subscription_add_countries")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📱 Добавить устройства", callback_data="subscription_add_devices")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data="subscription_reset_devices")
|
||||
]
|
||||
]
|
||||
|
||||
if settings.is_traffic_selectable():
|
||||
keyboard.insert(1, [
|
||||
InlineKeyboardButton(text="📈 Добавить трафик", callback_data="subscription_add_traffic")
|
||||
])
|
||||
keyboard.insert(2, [
|
||||
InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="subscription_reset_traffic")
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def get_trial_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
@@ -184,6 +209,11 @@ def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMark
|
||||
|
||||
|
||||
def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
return get_back_keyboard(language)
|
||||
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
@@ -283,25 +313,16 @@ def get_subscription_confirm_keyboard(language: str = "ru") -> InlineKeyboardMar
|
||||
|
||||
def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BALANCE_HISTORY, callback_data="balance_history"),
|
||||
InlineKeyboardButton(text=texts.BALANCE_TOP_UP, callback_data="balance_topup")
|
||||
])
|
||||
|
||||
if settings.TRIBUTE_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="💳 Быстрое пополнение", callback_data="tribute_quick_pay")
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BALANCE_SUPPORT_REQUEST, callback_data="balance_support")
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
])
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text=texts.BALANCE_HISTORY, callback_data="balance_history"),
|
||||
InlineKeyboardButton(text=texts.BALANCE_TOP_UP, callback_data="balance_topup")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
@@ -312,16 +333,74 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> In
|
||||
|
||||
if settings.TELEGRAM_STARS_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.TOP_UP_STARS, callback_data=f"pay_stars_{amount_kopeks}")
|
||||
InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)
|
||||
])
|
||||
|
||||
if settings.TRIBUTE_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="💳 Банковская карта",
|
||||
callback_data="topup_tribute"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")
|
||||
InlineKeyboardButton(
|
||||
text="🛠️ Через поддержку",
|
||||
callback_data="topup_support"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_autopay_notification_keyboard(subscription_id: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="💳 Пополнить баланс",
|
||||
callback_data="balance_topup"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📱 Моя подписка",
|
||||
callback_data="menu_subscription"
|
||||
)
|
||||
]
|
||||
])
|
||||
|
||||
def get_subscription_expiring_keyboard(subscription_id: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="⏰ Продлить подписку",
|
||||
callback_data="subscription_extend"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="💳 Пополнить баланс",
|
||||
callback_data="balance_topup"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📱 Моя подписка",
|
||||
callback_data="menu_subscription"
|
||||
)
|
||||
]
|
||||
])
|
||||
|
||||
def get_referral_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
@@ -470,6 +549,11 @@ def get_extend_subscription_keyboard(language: str = "ru") -> InlineKeyboardMark
|
||||
|
||||
|
||||
def get_add_traffic_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
return get_back_keyboard(language)
|
||||
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
@@ -519,6 +603,11 @@ def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> Inli
|
||||
|
||||
|
||||
def get_reset_traffic_confirm_keyboard(price_kopeks: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
return get_back_keyboard(language)
|
||||
|
||||
texts = get_texts(language)
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
@@ -528,7 +617,7 @@ def get_reset_traffic_confirm_keyboard(price_kopeks: int, language: str = "ru")
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="❌ Отмена", callback_data="menu_subscription")
|
||||
InlineKeyboardButton(text="⌛ Отмена", callback_data="menu_subscription")
|
||||
]
|
||||
])
|
||||
|
||||
@@ -596,25 +685,32 @@ def get_manage_countries_keyboard(
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_device_selection_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
from app.config import settings
|
||||
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="📱 iOS (iPhone/iPad)", callback_data="device_guide_ios"),
|
||||
InlineKeyboardButton(text="🤖 Android", callback_data="device_guide_android")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💻 Windows", callback_data="device_guide_windows"),
|
||||
InlineKeyboardButton(text="🍎 macOS", callback_data="device_guide_mac")
|
||||
InlineKeyboardButton(text="🎯 macOS", callback_data="device_guide_mac")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📺 Android TV", callback_data="device_guide_tv")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📋 Показать ссылку подписки", callback_data="open_subscription_link")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
|
||||
]
|
||||
]
|
||||
|
||||
if settings.CONNECT_BUTTON_MODE == "guide":
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="📋 Показать ссылку подписки", callback_data="open_subscription_link")
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def get_connection_guide_keyboard(
|
||||
@@ -735,3 +831,31 @@ def get_specific_app_keyboard(
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_extend_subscription_keyboard_with_prices(language: str, prices: dict) -> InlineKeyboardMarkup:
|
||||
from app.localization.texts import get_texts
|
||||
texts = get_texts(language)
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"📅 30 дней - {texts.format_price(prices[30])}",
|
||||
callback_data="extend_period_30"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"📅 90 дней - {texts.format_price(prices[90])}",
|
||||
callback_data="extend_period_90"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"📅 180 дней - {texts.format_price(prices[180])}",
|
||||
callback_data="extend_period_180"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
|
||||
]
|
||||
])
|
||||
|
||||
@@ -106,11 +106,11 @@ class RussianTexts(Texts):
|
||||
REFERRAL_CODE_INVALID = "❌ Неверный реферальный код"
|
||||
REFERRAL_CODE_SKIP = "⏭️ Пропустить"
|
||||
|
||||
MAIN_MENU = """
|
||||
👤 <b>{user_name}</b>
|
||||
|
||||
MAIN_MENU = """👤 <b>{user_name}</b>
|
||||
━━━━━━━━━━━━━━━━━
|
||||
💰 <b>Баланс:</b> {balance}
|
||||
📱 <b>Подписка:</b> {subscription_status}
|
||||
━━━━━━━━━━━━━━━━━
|
||||
|
||||
Выберите действие:
|
||||
"""
|
||||
@@ -268,7 +268,7 @@ class RussianTexts(Texts):
|
||||
TRIAL_ENDING_SOON = """
|
||||
🎁 <b>Тестовая подписка скоро закончится!</b>
|
||||
|
||||
Ваша тестовая подписка истекает через 2 часа.
|
||||
Ваша тестовая подписка истекает через несколько часов.
|
||||
|
||||
💎 <b>Не хотите остаться без VPN?</b>
|
||||
Переходите на полную подписку!
|
||||
@@ -354,7 +354,7 @@ class RussianTexts(Texts):
|
||||
ADMIN_MONITORING = "🔍 Мониторинг"
|
||||
ADMIN_REFERRALS = "🤝 Рефералы"
|
||||
ADMIN_RULES = "📋 Правила"
|
||||
ADMIN_REMNAWAVE = "🖥️ RemnaWave"
|
||||
ADMIN_REMNAWAVE = "🖥️ Remnawave"
|
||||
ADMIN_STATISTICS = "📊 Статистика"
|
||||
|
||||
ACCESS_DENIED = "❌ Доступ запрещен"
|
||||
@@ -485,4 +485,4 @@ async def refresh_rules_cache(language: str = "ru"):
|
||||
def clear_rules_cache():
|
||||
global _cached_rules
|
||||
_cached_rules.clear()
|
||||
print("✅ Кеш правил очищен")
|
||||
print("✅ Кеш правил очищен")
|
||||
|
||||
+56
-6
@@ -56,7 +56,7 @@ class AuthMiddleware(BaseMiddleware):
|
||||
)
|
||||
|
||||
if is_registration_process:
|
||||
logger.info(f"🔓 Пропускаем пользователя {user.id} в процессе регистрации")
|
||||
logger.info(f"📝 Пропускаем пользователя {user.id} в процессе регистрации")
|
||||
data['db'] = db
|
||||
data['db_user'] = None
|
||||
data['is_admin'] = False
|
||||
@@ -64,24 +64,74 @@ class AuthMiddleware(BaseMiddleware):
|
||||
else:
|
||||
if isinstance(event, Message):
|
||||
await event.answer(
|
||||
"◀️ Для начала работы необходимо выполнить команду /start"
|
||||
"▶️ Для начала работы необходимо выполнить команду /start"
|
||||
)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.answer(
|
||||
"◀️ Необходимо начать с команды /start",
|
||||
"▶️ Необходимо начать с команды /start",
|
||||
show_alert=True
|
||||
)
|
||||
logger.info(f"🚫 Заблокирован незарегистрированный пользователь {user.id}")
|
||||
return
|
||||
else:
|
||||
from app.database.models import UserStatus
|
||||
|
||||
if db_user.status == UserStatus.BLOCKED.value:
|
||||
if isinstance(event, Message):
|
||||
await event.answer("🚫 Ваш аккаунт заблокирован администратором.")
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.answer("🚫 Ваш аккаунт заблокирован администратором.", show_alert=True)
|
||||
logger.info(f"🚫 Заблокированный пользователь {user.id} попытался использовать бота")
|
||||
return
|
||||
|
||||
if db_user.status == UserStatus.DELETED.value:
|
||||
state: FSMContext = data.get('state')
|
||||
current_state = None
|
||||
|
||||
if state:
|
||||
current_state = await state.get_state()
|
||||
|
||||
registration_states = [
|
||||
RegistrationStates.waiting_for_rules_accept,
|
||||
RegistrationStates.waiting_for_referral_code
|
||||
]
|
||||
|
||||
is_start_or_registration = (
|
||||
(isinstance(event, Message) and event.text and event.text.startswith('/start'))
|
||||
or (isinstance(event, CallbackQuery) and current_state and
|
||||
any(str(state) in str(current_state) for state in registration_states))
|
||||
or (isinstance(event, CallbackQuery) and event.data and
|
||||
(event.data in ['rules_accept', 'rules_decline', 'referral_skip']))
|
||||
)
|
||||
|
||||
if is_start_or_registration:
|
||||
logger.info(f"🔄 Удаленный пользователь {user.id} начинает повторную регистрацию")
|
||||
data['db'] = db
|
||||
data['db_user'] = None
|
||||
data['is_admin'] = False
|
||||
return await handler(event, data)
|
||||
else:
|
||||
if isinstance(event, Message):
|
||||
await event.answer(
|
||||
"❌ Ваш аккаунт был удален.\n"
|
||||
"🔄 Для повторной регистрации выполните команду /start"
|
||||
)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.answer(
|
||||
"❌ Ваш аккаунт был удален. Для повторной регистрации выполните /start",
|
||||
show_alert=True
|
||||
)
|
||||
logger.info(f"❌ Удаленный пользователь {user.id} попытался использовать бота без /start")
|
||||
return
|
||||
|
||||
from datetime import datetime
|
||||
db_user.last_activity = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
|
||||
data['db'] = db
|
||||
data['db_user'] = db_user
|
||||
data['is_admin'] = settings.is_admin(user.id)
|
||||
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
except Exception as e:
|
||||
@@ -90,4 +140,4 @@ class AuthMiddleware(BaseMiddleware):
|
||||
if hasattr(event, 'data'):
|
||||
logger.error(f"Callback data: {event.data}")
|
||||
await db.rollback()
|
||||
raise
|
||||
raise
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import logging
|
||||
from typing import Callable, Dict, Any, Awaitable
|
||||
from datetime import datetime
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Update, Message, CallbackQuery
|
||||
|
||||
from app.database.database import get_db
|
||||
from app.database.crud.user import get_user_by_telegram_id
|
||||
from app.database.models import SubscriptionStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SubscriptionStatusMiddleware(BaseMiddleware):
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
|
||||
telegram_id = None
|
||||
if isinstance(event, (Message, CallbackQuery)):
|
||||
telegram_id = event.from_user.id
|
||||
elif isinstance(event, Update):
|
||||
if event.message:
|
||||
telegram_id = event.message.from_user.id
|
||||
elif event.callback_query:
|
||||
telegram_id = event.callback_query.from_user.id
|
||||
|
||||
if telegram_id:
|
||||
try:
|
||||
async for db in get_db():
|
||||
user = await get_user_by_telegram_id(db, telegram_id)
|
||||
if user and user.subscription:
|
||||
current_time = datetime.utcnow()
|
||||
subscription = user.subscription
|
||||
|
||||
if (subscription.status == SubscriptionStatus.ACTIVE.value and
|
||||
subscription.end_date <= current_time):
|
||||
|
||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||
subscription.updated_at = current_time
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"⏰ Middleware: Статус подписки пользователя {user.id} изменен на 'expired' (время истекло)")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки статуса подписки для пользователя {telegram_id}: {e}")
|
||||
|
||||
return await handler(event, data)
|
||||
@@ -22,6 +22,10 @@ from app.services.subscription_service import SubscriptionService
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.localization.texts import get_texts
|
||||
|
||||
from app.external.remnawave_api import (
|
||||
RemnaWaveUser, UserStatus, TrafficLimitStrategy, RemnaWaveAPIError
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -32,7 +36,7 @@ class MonitoringService:
|
||||
self.subscription_service = SubscriptionService()
|
||||
self.payment_service = PaymentService()
|
||||
self.bot = bot
|
||||
self._notified_users: Set[str] = set() # Защита от дублирования уведомлений
|
||||
self._notified_users: Set[str] = set()
|
||||
|
||||
async def start_monitoring(self):
|
||||
if self.is_running:
|
||||
@@ -60,12 +64,11 @@ class MonitoringService:
|
||||
try:
|
||||
await self._check_expired_subscriptions(db)
|
||||
await self._check_expiring_subscriptions(db)
|
||||
await self._check_trial_expiring_soon(db) # Новый метод!
|
||||
await self._check_trial_expiring_soon(db)
|
||||
await self._process_autopayments(db)
|
||||
await self._cleanup_inactive_users(db)
|
||||
await self._sync_with_remnawave(db)
|
||||
|
||||
# Очищаем кеш уведомлений каждые 24 часа
|
||||
current_hour = datetime.utcnow().hour
|
||||
if current_hour == 0:
|
||||
self._notified_users.clear()
|
||||
@@ -88,40 +91,85 @@ class MonitoringService:
|
||||
break
|
||||
|
||||
async def _check_expired_subscriptions(self, db: AsyncSession):
|
||||
"""Проверка истекших подписок"""
|
||||
try:
|
||||
expired_subscriptions = await get_expired_subscriptions(db)
|
||||
|
||||
for subscription in expired_subscriptions:
|
||||
await deactivate_subscription(db, subscription)
|
||||
from app.database.crud.subscription import expire_subscription
|
||||
await expire_subscription(db, subscription)
|
||||
|
||||
user = await get_user_by_id(db, subscription.user_id)
|
||||
if user and user.remnawave_uuid:
|
||||
await self.subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
|
||||
# Отправляем уведомление об истечении
|
||||
if user and self.bot:
|
||||
await self._send_subscription_expired_notification(user)
|
||||
|
||||
logger.info(f"🔴 Подписка пользователя {subscription.user_id} истекла и деактивирована")
|
||||
logger.info(f"🔴 Подписка пользователя {subscription.user_id} истекла и статус изменен на 'expired'")
|
||||
|
||||
if expired_subscriptions:
|
||||
await self._log_monitoring_event(
|
||||
db, "expired_subscriptions_processed",
|
||||
f"Обработано {len(expired_subscriptions)} истекших подписок",
|
||||
f"Обработано {len(expired_subscriptions)} истёкших подписок",
|
||||
{"count": len(expired_subscriptions)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки истекших подписок: {e}")
|
||||
logger.error(f"Ошибка проверки истёкших подписок: {e}")
|
||||
|
||||
async def update_remnawave_user(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
subscription: Subscription
|
||||
) -> Optional[RemnaWaveUser]:
|
||||
|
||||
try:
|
||||
user = await get_user_by_id(db, subscription.user_id)
|
||||
if not user or not user.remnawave_uuid:
|
||||
logger.error(f"RemnaWave UUID не найден для пользователя {subscription.user_id}")
|
||||
return None
|
||||
|
||||
current_time = datetime.utcnow()
|
||||
is_active = (subscription.status == SubscriptionStatus.ACTIVE.value and
|
||||
subscription.end_date > current_time)
|
||||
|
||||
if (subscription.status == SubscriptionStatus.ACTIVE.value and
|
||||
subscription.end_date <= current_time):
|
||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||
await db.commit()
|
||||
is_active = False
|
||||
logger.info(f"📝 Статус подписки {subscription.id} обновлен на 'expired'")
|
||||
|
||||
async with self.api as api:
|
||||
updated_user = await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
status=UserStatus.ACTIVE if is_active else UserStatus.EXPIRED,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
subscription.subscription_url = updated_user.subscription_url
|
||||
await db.commit()
|
||||
|
||||
status_text = "активным" if is_active else "истёкшим"
|
||||
logger.info(f"✅ Обновлен RemnaWave пользователь {user.remnawave_uuid} со статусом {status_text}")
|
||||
return updated_user
|
||||
|
||||
except RemnaWaveAPIError as e:
|
||||
logger.error(f"Ошибка обновления RemnaWave пользователя: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления RemnaWave пользователя: {e}")
|
||||
return None
|
||||
|
||||
async def _check_expiring_subscriptions(self, db: AsyncSession):
|
||||
"""Проверка подписок, истекающих через 2-3 дня (только платные)"""
|
||||
try:
|
||||
warning_days = settings.get_autopay_warning_days()
|
||||
|
||||
for days in warning_days:
|
||||
# Получаем только платные подписки
|
||||
expiring_subscriptions = await self._get_expiring_paid_subscriptions(db, days)
|
||||
|
||||
for subscription in expiring_subscriptions:
|
||||
@@ -131,7 +179,7 @@ class MonitoringService:
|
||||
|
||||
notification_key = f"expiring_{user.telegram_id}_{days}d"
|
||||
if notification_key in self._notified_users:
|
||||
continue # Уже уведомляли сегодня
|
||||
continue
|
||||
|
||||
if self.bot:
|
||||
await self._send_subscription_expiring_notification(user, subscription, days)
|
||||
@@ -150,9 +198,7 @@ class MonitoringService:
|
||||
logger.error(f"Ошибка проверки истекающих подписок: {e}")
|
||||
|
||||
async def _check_trial_expiring_soon(self, db: AsyncSession):
|
||||
"""Проверка тестовых подписок, истекающих через 2 часа"""
|
||||
try:
|
||||
# Получаем тестовые подписки, истекающие через 2 часа
|
||||
threshold_time = datetime.utcnow() + timedelta(hours=2)
|
||||
|
||||
result = await db.execute(
|
||||
@@ -176,7 +222,7 @@ class MonitoringService:
|
||||
|
||||
notification_key = f"trial_2h_{user.telegram_id}"
|
||||
if notification_key in self._notified_users:
|
||||
continue # Уже уведомляли
|
||||
continue
|
||||
|
||||
if self.bot:
|
||||
await self._send_trial_ending_notification(user, subscription)
|
||||
@@ -195,8 +241,8 @@ class MonitoringService:
|
||||
logger.error(f"Ошибка проверки истекающих тестовых подписок: {e}")
|
||||
|
||||
async def _get_expiring_paid_subscriptions(self, db: AsyncSession, days_before: int) -> List[Subscription]:
|
||||
"""Получение платных подписок, истекающих через указанное количество дней"""
|
||||
threshold_date = datetime.utcnow() + timedelta(days=days_before)
|
||||
current_time = datetime.utcnow()
|
||||
threshold_date = current_time + timedelta(days=days_before)
|
||||
|
||||
result = await db.execute(
|
||||
select(Subscription)
|
||||
@@ -204,18 +250,24 @@ class MonitoringService:
|
||||
.where(
|
||||
and_(
|
||||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||||
Subscription.is_trial == False, # Только платные
|
||||
Subscription.end_date <= threshold_date,
|
||||
Subscription.end_date > datetime.utcnow()
|
||||
Subscription.is_trial == False,
|
||||
Subscription.end_date > current_time,
|
||||
Subscription.end_date <= threshold_date
|
||||
)
|
||||
)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
logger.info(f"🔍 Поиск платных подписок, истекающих в ближайшие {days_before} дней")
|
||||
logger.info(f"📅 Текущее время: {current_time}")
|
||||
logger.info(f"📅 Пороговая дата: {threshold_date}")
|
||||
|
||||
subscriptions = result.scalars().all()
|
||||
logger.info(f"📊 Найдено {len(subscriptions)} платных подписок для уведомлений")
|
||||
|
||||
return subscriptions
|
||||
|
||||
async def _process_autopayments(self, db: AsyncSession):
|
||||
"""Обработка автоплатежей"""
|
||||
try:
|
||||
# Исправленный запрос с использованием индивидуальных настроек
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
result = await db.execute(
|
||||
@@ -225,13 +277,12 @@ class MonitoringService:
|
||||
and_(
|
||||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||||
Subscription.autopay_enabled == True,
|
||||
Subscription.is_trial == False # Автооплата только для платных
|
||||
Subscription.is_trial == False
|
||||
)
|
||||
)
|
||||
)
|
||||
all_autopay_subscriptions = result.scalars().all()
|
||||
|
||||
# Фильтруем по времени с учетом индивидуальных настроек
|
||||
autopay_subscriptions = []
|
||||
for sub in all_autopay_subscriptions:
|
||||
days_before_expiry = (sub.end_date - current_time).days
|
||||
@@ -248,24 +299,20 @@ class MonitoringService:
|
||||
|
||||
renewal_cost = settings.PRICE_30_DAYS
|
||||
|
||||
# Проверяем, не списывали ли уже сегодня
|
||||
autopay_key = f"autopay_{user.telegram_id}_{subscription.id}"
|
||||
if autopay_key in self._notified_users:
|
||||
continue
|
||||
|
||||
if user.balance_kopeks >= renewal_cost:
|
||||
# Списываем средства
|
||||
success = await subtract_user_balance(
|
||||
db, user, renewal_cost,
|
||||
"Автопродление подписки"
|
||||
)
|
||||
|
||||
if success:
|
||||
# Продлеваем подписку
|
||||
await extend_subscription(db, subscription, 30)
|
||||
await self.subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
# Уведомляем об успешном автоплатеже
|
||||
if self.bot:
|
||||
await self._send_autopay_success_notification(user, renewal_cost, 30)
|
||||
|
||||
@@ -279,7 +326,6 @@ class MonitoringService:
|
||||
logger.warning(f"💳 Ошибка списания средств для автопродления пользователя {user.telegram_id}")
|
||||
else:
|
||||
failed_count += 1
|
||||
# Уведомляем о недостатке средств
|
||||
if self.bot:
|
||||
await self._send_autopay_failed_notification(user, user.balance_kopeks, renewal_cost)
|
||||
logger.warning(f"💳 Недостаточно средств для автопродления у пользователя {user.telegram_id}")
|
||||
@@ -294,54 +340,98 @@ class MonitoringService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки автоплатежей: {e}")
|
||||
|
||||
# Методы отправки уведомлений
|
||||
async def _send_subscription_expired_notification(self, user: User):
|
||||
"""Уведомление об истечении подписки"""
|
||||
try:
|
||||
texts = get_texts(user.language)
|
||||
message = texts.SUBSCRIPTION_EXPIRED
|
||||
await self.bot.send_message(user.telegram_id, message, parse_mode="HTML")
|
||||
message = """
|
||||
❌ <b>Подписка истекла</b>
|
||||
|
||||
Ваша подписка истекла. Для восстановления доступа продлите подписку.
|
||||
|
||||
🔧 Доступ к серверам заблокирован до продления.
|
||||
"""
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="menu_buy")],
|
||||
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
message,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
|
||||
|
||||
async def _send_subscription_expiring_notification(self, user: User, subscription: Subscription, days: int):
|
||||
"""Уведомление об истечении подписки через N дней"""
|
||||
try:
|
||||
texts = get_texts(user.language)
|
||||
message = texts.SUBSCRIPTION_EXPIRING.format(days=days)
|
||||
await self.bot.send_message(user.telegram_id, message, parse_mode="HTML")
|
||||
|
||||
if subscription.autopay_enabled:
|
||||
autopay_status = "✅ Включен - подписка продлится автоматически"
|
||||
action_text = f"💰 Убедитесь, что на балансе достаточно средств: {texts.format_price(user.balance_kopeks)}"
|
||||
else:
|
||||
autopay_status = "❌ Отключен - не забудьте продлить вручную!"
|
||||
action_text = "💡 Включите автоплатеж или продлите подписку вручную"
|
||||
|
||||
message = f"""
|
||||
⚠️ <b>Подписка истекает через {days} дней!</b>
|
||||
|
||||
Ваша платная подписка истекает {subscription.end_date.strftime("%d.%m.%Y %H:%M")}.
|
||||
|
||||
💳 <b>Автоплатеж:</b> {autopay_status}
|
||||
|
||||
{action_text}
|
||||
"""
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="⏰ Продлить подписку", callback_data="subscription_extend")],
|
||||
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")],
|
||||
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
message,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
|
||||
|
||||
async def _send_trial_ending_notification(self, user: User, subscription: Subscription):
|
||||
"""Уведомление об окончании тестовой подписки через 2 часа"""
|
||||
try:
|
||||
texts = get_texts(user.language)
|
||||
|
||||
# Создаем специальное сообщение для тестовой подписки
|
||||
message = f"""
|
||||
🎁 <b>Тестовая подписка скоро закончится!</b>
|
||||
🎁 <b>Тестовая подписка скоро закончится!</b>
|
||||
|
||||
Ваша тестовая подписка истекает через 2 часа.
|
||||
Ваша тестовая подписка истекает через 2 часа.
|
||||
|
||||
💎 <b>Не хотите остаться без VPN?</b>
|
||||
Переходите на полную подписку со скидкой!
|
||||
💎 <b>Не хотите остаться без VPN?</b>
|
||||
Переходите на полную подписку со скидкой!
|
||||
|
||||
🔥 <b>Специальное предложение:</b>
|
||||
• 30 дней всего за {settings.format_price(settings.PRICE_30_DAYS)}
|
||||
• Безлимитный трафик
|
||||
• Все серверы доступны
|
||||
• Поддержка до 3 устройств
|
||||
🔥 <b>Специальное предложение:</b>
|
||||
• 30 дней всего за {settings.format_price(settings.PRICE_30_DAYS)}
|
||||
• Безлимитный трафик
|
||||
• Все серверы доступны
|
||||
• Поддержка до 3 устройств
|
||||
|
||||
⚡️ Успейте оформить до окончания тестового периода!
|
||||
"""
|
||||
⚡️ Успейте оформить до окончания тестового периода!
|
||||
"""
|
||||
|
||||
# Добавляем inline клавиатуру с кнопкой покупки
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="buy_subscription")],
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="balance_top_up")]
|
||||
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="menu_buy")],
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="balance_topup")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
@@ -355,7 +445,6 @@ class MonitoringService:
|
||||
logger.error(f"Ошибка отправки уведомления об окончании тестовой подписки пользователю {user.telegram_id}: {e}")
|
||||
|
||||
async def _send_autopay_success_notification(self, user: User, amount: int, days: int):
|
||||
"""Уведомление об успешном автоплатеже"""
|
||||
try:
|
||||
texts = get_texts(user.language)
|
||||
message = texts.AUTOPAY_SUCCESS.format(
|
||||
@@ -367,7 +456,6 @@ class MonitoringService:
|
||||
logger.error(f"Ошибка отправки уведомления об автоплатеже пользователю {user.telegram_id}: {e}")
|
||||
|
||||
async def _send_autopay_failed_notification(self, user: User, balance: int, required: int):
|
||||
"""Уведомление о неудачном автоплатеже"""
|
||||
try:
|
||||
texts = get_texts(user.language)
|
||||
message = texts.AUTOPAY_FAILED.format(
|
||||
@@ -375,11 +463,11 @@ class MonitoringService:
|
||||
required=settings.format_price(required)
|
||||
)
|
||||
|
||||
# Добавляем кнопку пополнения баланса
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="balance_top_up")]
|
||||
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")],
|
||||
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
@@ -392,7 +480,6 @@ class MonitoringService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о неудачном автоплатеже пользователю {user.telegram_id}: {e}")
|
||||
|
||||
# Остальные методы остаются без изменений...
|
||||
async def _cleanup_inactive_users(self, db: AsyncSession):
|
||||
try:
|
||||
now = datetime.utcnow()
|
||||
@@ -523,7 +610,6 @@ class MonitoringService:
|
||||
|
||||
async def force_check_subscriptions(self, db: AsyncSession) -> Dict[str, int]:
|
||||
try:
|
||||
# Проверяем истекшие
|
||||
expired_subscriptions = await get_expired_subscriptions(db)
|
||||
expired_count = 0
|
||||
|
||||
@@ -618,4 +704,4 @@ class MonitoringService:
|
||||
return 0
|
||||
|
||||
|
||||
monitoring_service = MonitoringService()
|
||||
monitoring_service = MonitoringService()
|
||||
|
||||
@@ -86,32 +86,59 @@ class PromoCodeService:
|
||||
effects.append(f"💰 Баланс пополнен на {balance_bonus_rubles}₽")
|
||||
|
||||
if promocode.subscription_days > 0:
|
||||
from app.database.crud.subscription import create_paid_subscription
|
||||
from app.config import settings
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user.id)
|
||||
|
||||
if subscription:
|
||||
await extend_subscription(db, subscription, promocode.subscription_days)
|
||||
|
||||
await self.subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
effects.append(f"⏰ Подписка продлена на {promocode.subscription_days} дней")
|
||||
logger.info(f"✅ Подписка пользователя {user.telegram_id} продлена на {promocode.subscription_days} дней в RemnaWave с текущими сквадами")
|
||||
|
||||
else:
|
||||
await create_paid_subscription(
|
||||
from app.database.crud.subscription import create_paid_subscription
|
||||
|
||||
trial_squads = []
|
||||
if hasattr(settings, 'TRIAL_SQUAD_UUID') and settings.TRIAL_SQUAD_UUID:
|
||||
trial_squads = [settings.TRIAL_SQUAD_UUID]
|
||||
|
||||
new_subscription = await create_paid_subscription(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
duration_days=promocode.subscription_days,
|
||||
traffic_limit_gb=0,
|
||||
traffic_limit_gb=0,
|
||||
device_limit=1,
|
||||
connected_squads=[]
|
||||
connected_squads=trial_squads
|
||||
)
|
||||
|
||||
await self.subscription_service.create_remnawave_user(db, new_subscription)
|
||||
|
||||
effects.append(f"🎉 Получена подписка на {promocode.subscription_days} дней")
|
||||
logger.info(f"✅ Создана новая подписка для пользователя {user.telegram_id} на {promocode.subscription_days} дней с триал сквадом {trial_squads}")
|
||||
|
||||
if promocode.type == PromoCodeType.TRIAL_SUBSCRIPTION.value:
|
||||
from app.database.crud.subscription import create_trial_subscription
|
||||
from app.config import settings
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user.id)
|
||||
if not subscription and not user.has_had_paid_subscription:
|
||||
await create_trial_subscription(db, user.id)
|
||||
effects.append("🎁 Активирована тестовая подписка")
|
||||
|
||||
if not subscription:
|
||||
trial_days = promocode.subscription_days if promocode.subscription_days > 0 else settings.TRIAL_DURATION_DAYS
|
||||
|
||||
trial_subscription = await create_trial_subscription(
|
||||
db,
|
||||
user.id,
|
||||
duration_days=trial_days
|
||||
)
|
||||
|
||||
await self.subscription_service.create_remnawave_user(db, trial_subscription)
|
||||
|
||||
effects.append(f"🎁 Активирована тестовая подписка на {trial_days} дней")
|
||||
logger.info(f"✅ Создана триал подписка для пользователя {user.telegram_id} на {trial_days} дней")
|
||||
else:
|
||||
effects.append("ℹ️ Тестовая подписка уже недоступна")
|
||||
effects.append("ℹ️ У вас уже есть активная подписка")
|
||||
|
||||
return "\n".join(effects) if effects else "✅ Промокод активирован"
|
||||
return "\n".join(effects) if effects else "✅ Промокод активирован"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import delete
|
||||
|
||||
from app.config import settings
|
||||
from app.external.remnawave_api import (
|
||||
@@ -9,7 +10,10 @@ from app.external.remnawave_api import (
|
||||
)
|
||||
from app.database.crud.user import get_users_list, get_user_by_telegram_id, update_user
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, update_subscription_usage
|
||||
from app.database.models import User
|
||||
from app.database.models import (
|
||||
User, SubscriptionServer, Transaction, ReferralEarning,
|
||||
PromoCodeUse, SubscriptionStatus
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -391,34 +395,22 @@ class RemnaWaveService:
|
||||
return False
|
||||
|
||||
async def sync_users_from_panel(self, db: AsyncSession, sync_type: str = "all") -> Dict[str, int]:
|
||||
"""
|
||||
Синхронизация пользователей из панели RemnaWave в бота
|
||||
|
||||
sync_type:
|
||||
- "all": полная синхронизация (создание + обновление + удаление)
|
||||
- "new_only": только создание новых пользователей
|
||||
- "update_only": только обновление существующих
|
||||
"""
|
||||
try:
|
||||
stats = {"created": 0, "updated": 0, "errors": 0, "deleted": 0}
|
||||
|
||||
logger.info(f"🔄 Начинаем синхронизацию типа: {sync_type}")
|
||||
|
||||
async with self.api as api:
|
||||
# Получаем всех пользователей из панели
|
||||
panel_users_data = await api._make_request('GET', '/api/users')
|
||||
panel_users = panel_users_data['response']['users']
|
||||
|
||||
logger.info(f"👥 Найдено пользователей в панели: {len(panel_users)}")
|
||||
|
||||
# Получаем всех пользователей из бота для сравнения
|
||||
bot_users = await get_users_list(db, offset=0, limit=10000)
|
||||
bot_users_by_telegram_id = {user.telegram_id: user for user in bot_users}
|
||||
|
||||
# Множество telegram_id из панели для проверки удаленных
|
||||
panel_telegram_ids = set()
|
||||
|
||||
# Обрабатываем каждого пользователя из панели
|
||||
for i, panel_user in enumerate(panel_users):
|
||||
try:
|
||||
telegram_id = panel_user.get('telegramId')
|
||||
@@ -433,7 +425,6 @@ class RemnaWaveService:
|
||||
db_user = bot_users_by_telegram_id.get(telegram_id)
|
||||
|
||||
if not db_user:
|
||||
# Пользователя нет в боте - создаем
|
||||
if sync_type in ["new_only", "all"]:
|
||||
logger.info(f"🆕 Создание пользователя для telegram_id {telegram_id}")
|
||||
|
||||
@@ -455,7 +446,6 @@ class RemnaWaveService:
|
||||
logger.info(f"✅ Создан пользователь {telegram_id} с подпиской")
|
||||
|
||||
else:
|
||||
# Пользователь есть в боте - обновляем
|
||||
if sync_type in ["update_only", "all"]:
|
||||
logger.debug(f"🔄 Обновление пользователя {telegram_id}")
|
||||
|
||||
@@ -472,25 +462,114 @@ class RemnaWaveService:
|
||||
stats["errors"] += 1
|
||||
continue
|
||||
|
||||
# Удаляем подписки пользователей, которых нет в панели
|
||||
if sync_type == "all":
|
||||
logger.info("🗑️ Удаляем подписки пользователей, отсутствующих в панели...")
|
||||
logger.info("🗑️ ПОЛНАЯ очистка подписок пользователей, отсутствующих в панели...")
|
||||
|
||||
for telegram_id, db_user in bot_users_by_telegram_id.items():
|
||||
if telegram_id not in panel_telegram_ids and db_user.subscription:
|
||||
try:
|
||||
logger.info(f"🗑️ Удаляем подписку пользователя {telegram_id} (нет в панели)")
|
||||
logger.info(f"🗑️ ПОЛНАЯ очистка данных подписки пользователя {telegram_id} (нет в панели)")
|
||||
|
||||
# Деактивируем подписку
|
||||
from app.database.crud.subscription import deactivate_subscription
|
||||
await deactivate_subscription(db, db_user.subscription)
|
||||
subscription = db_user.subscription
|
||||
|
||||
if db_user.remnawave_uuid:
|
||||
try:
|
||||
devices_reset = await api.reset_user_devices(db_user.remnawave_uuid)
|
||||
if devices_reset:
|
||||
logger.info(f"🔧 Сброшены HWID устройства для пользователя {telegram_id}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Не удалось сбросить HWID устройства для пользователя {telegram_id}")
|
||||
except Exception as hwid_error:
|
||||
logger.error(f"❌ Ошибка сброса HWID устройств для {telegram_id}: {hwid_error}")
|
||||
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_server_ids, remove_subscription_servers
|
||||
from sqlalchemy import delete
|
||||
from app.database.models import SubscriptionServer
|
||||
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id == subscription.id
|
||||
)
|
||||
)
|
||||
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ записи SubscriptionServer для подписки {subscription.id}")
|
||||
|
||||
except Exception as servers_error:
|
||||
logger.warning(f"⚠️ Не удалось удалить серверы подписки: {servers_error}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import delete
|
||||
from app.database.models import Transaction
|
||||
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == db_user.id)
|
||||
)
|
||||
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ транзакции пользователя {telegram_id}")
|
||||
|
||||
except Exception as transactions_error:
|
||||
logger.warning(f"⚠️ Не удалось удалить транзакции: {transactions_error}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import delete
|
||||
from app.database.models import ReferralEarning
|
||||
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == db_user.id)
|
||||
)
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == db_user.id)
|
||||
)
|
||||
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ реферальные доходы пользователя {telegram_id}")
|
||||
|
||||
except Exception as referral_error:
|
||||
logger.warning(f"⚠️ Не удалось удалить реферальные доходы: {referral_error}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import delete
|
||||
from app.database.models import PromoCodeUse
|
||||
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == db_user.id)
|
||||
)
|
||||
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ использования промокодов пользователя {telegram_id}")
|
||||
|
||||
except Exception as promo_error:
|
||||
logger.warning(f"⚠️ Не удалось удалить использования промокодов: {promo_error}")
|
||||
|
||||
try:
|
||||
db_user.balance_kopeks = 0
|
||||
logger.info(f"💰 Сброшен баланс пользователя {telegram_id}")
|
||||
except Exception as balance_error:
|
||||
logger.warning(f"⚠️ Не удалось сбросить баланс: {balance_error}")
|
||||
|
||||
from app.database.models import SubscriptionStatus
|
||||
from datetime import datetime
|
||||
|
||||
subscription.status = SubscriptionStatus.DISABLED.value
|
||||
subscription.is_trial = True
|
||||
subscription.end_date = datetime.utcnow()
|
||||
subscription.traffic_limit_gb = 0
|
||||
subscription.traffic_used_gb = 0.0
|
||||
subscription.device_limit = 1
|
||||
subscription.connected_squads = []
|
||||
subscription.autopay_enabled = False
|
||||
subscription.autopay_days_before = 3
|
||||
subscription.remnawave_short_uuid = None
|
||||
subscription.subscription_url = ""
|
||||
|
||||
db_user.remnawave_uuid = None
|
||||
db_user.has_had_paid_subscription = False
|
||||
db_user.used_promocodes = 0
|
||||
|
||||
await db.commit()
|
||||
|
||||
stats["deleted"] += 1
|
||||
logger.info(f"✅ Деактивирована подписка пользователя {telegram_id}")
|
||||
logger.info(f"✅ ПОЛНОСТЬЮ очищены ВСЕ данные подписки пользователя {telegram_id}")
|
||||
|
||||
except Exception as delete_error:
|
||||
logger.error(f"❌ Ошибка удаления подписки {telegram_id}: {delete_error}")
|
||||
logger.error(f"❌ Ошибка полной очистки данных подписки {telegram_id}: {delete_error}")
|
||||
stats["errors"] += 1
|
||||
await db.rollback()
|
||||
|
||||
logger.info(f"🎯 Синхронизация завершена: создано {stats['created']}, обновлено {stats['updated']}, удалено {stats['deleted']}, ошибок {stats['errors']}")
|
||||
return stats
|
||||
@@ -499,6 +578,8 @@ class RemnaWaveService:
|
||||
logger.error(f"❌ Критическая ошибка синхронизации пользователей: {e}")
|
||||
return {"created": 0, "updated": 0, "errors": 1, "deleted": 0}
|
||||
|
||||
|
||||
|
||||
async def _create_subscription_from_panel_data(self, db: AsyncSession, user, panel_user):
|
||||
try:
|
||||
from app.database.crud.subscription import create_subscription
|
||||
@@ -599,7 +680,6 @@ class RemnaWaveService:
|
||||
await self._create_subscription_from_panel_data(db, user, panel_user)
|
||||
return
|
||||
|
||||
# Обновляем статус подписки
|
||||
panel_status = panel_user.get('status', 'ACTIVE')
|
||||
expire_at_str = panel_user.get('expireAt', '')
|
||||
|
||||
@@ -611,14 +691,12 @@ class RemnaWaveService:
|
||||
if expire_at.tzinfo is not None:
|
||||
expire_at = expire_at.replace(tzinfo=None)
|
||||
|
||||
# Обновляем дату окончания если она отличается
|
||||
if abs((subscription.end_date - expire_at).total_seconds()) > 60: # больше минуты разницы
|
||||
if abs((subscription.end_date - expire_at).total_seconds()) > 60:
|
||||
subscription.end_date = expire_at
|
||||
logger.debug(f"Обновлена дата окончания подписки до {expire_at}")
|
||||
except Exception as date_error:
|
||||
logger.warning(f"⚠️ Ошибка парсинга даты при обновлении {expire_at_str}: {date_error}")
|
||||
|
||||
# Обновляем статус
|
||||
current_time = datetime.utcnow()
|
||||
if panel_status == 'ACTIVE' and subscription.end_date > current_time:
|
||||
new_status = SubscriptionStatus.ACTIVE.value
|
||||
@@ -627,13 +705,12 @@ class RemnaWaveService:
|
||||
elif panel_status == 'DISABLED':
|
||||
new_status = SubscriptionStatus.DISABLED.value
|
||||
else:
|
||||
new_status = subscription.status # Оставляем текущий статус
|
||||
new_status = subscription.status
|
||||
|
||||
if subscription.status != new_status:
|
||||
subscription.status = new_status
|
||||
logger.debug(f"Обновлен статус подписки: {new_status}")
|
||||
|
||||
# Обновляем использованный трафик
|
||||
used_traffic_bytes = panel_user.get('usedTrafficBytes', 0)
|
||||
traffic_used_gb = used_traffic_bytes / (1024**3)
|
||||
|
||||
@@ -641,7 +718,6 @@ class RemnaWaveService:
|
||||
subscription.traffic_used_gb = traffic_used_gb
|
||||
logger.debug(f"Обновлен использованный трафик: {traffic_used_gb} GB")
|
||||
|
||||
# Обновляем лимит трафика
|
||||
traffic_limit_bytes = panel_user.get('trafficLimitBytes', 0)
|
||||
traffic_limit_gb = traffic_limit_bytes // (1024**3) if traffic_limit_bytes > 0 else 0
|
||||
|
||||
@@ -649,22 +725,18 @@ class RemnaWaveService:
|
||||
subscription.traffic_limit_gb = traffic_limit_gb
|
||||
logger.debug(f"Обновлен лимит трафика: {traffic_limit_gb} GB")
|
||||
|
||||
# Обновляем лимит устройств
|
||||
device_limit = panel_user.get('hwidDeviceLimit', 1) or 1
|
||||
if subscription.device_limit != device_limit:
|
||||
subscription.device_limit = device_limit
|
||||
logger.debug(f"Обновлен лимит устройств: {device_limit}")
|
||||
|
||||
# Обновляем RemnaWave UUID если отсутствует
|
||||
if not subscription.remnawave_short_uuid:
|
||||
subscription.remnawave_short_uuid = panel_user.get('shortUuid')
|
||||
|
||||
# Обновляем URL подписки если отсутствует или изменился
|
||||
panel_url = panel_user.get('subscriptionUrl', '')
|
||||
if not subscription.subscription_url or subscription.subscription_url != panel_url:
|
||||
subscription.subscription_url = panel_url
|
||||
|
||||
# Обновляем подключенные сквады
|
||||
active_squads = panel_user.get('activeInternalSquads', [])
|
||||
squad_uuids = []
|
||||
if isinstance(active_squads, list):
|
||||
@@ -674,7 +746,6 @@ class RemnaWaveService:
|
||||
elif isinstance(squad, str):
|
||||
squad_uuids.append(squad)
|
||||
|
||||
# Сравниваем сквады - обновляем только если есть изменения
|
||||
current_squads = set(subscription.connected_squads or [])
|
||||
new_squads = set(squad_uuids)
|
||||
|
||||
@@ -729,7 +800,6 @@ class RemnaWaveService:
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
# Обновляем UUID в нашей базе
|
||||
await update_user(db, user, remnawave_uuid=new_user.uuid)
|
||||
subscription.remnawave_short_uuid = new_user.short_uuid
|
||||
await db.commit()
|
||||
@@ -969,11 +1039,101 @@ class RemnaWaveService:
|
||||
logger.error(f"Ошибка валидации данных пользователя: {e}")
|
||||
return False
|
||||
|
||||
async def force_cleanup_user_data(self, db: AsyncSession, user: User) -> bool:
|
||||
try:
|
||||
logger.info(f"🗑️ ПРИНУДИТЕЛЬНАЯ полная очистка данных пользователя {user.telegram_id}")
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
async with self.api as api:
|
||||
devices_reset = await api.reset_user_devices(user.remnawave_uuid)
|
||||
if devices_reset:
|
||||
logger.info(f"🔧 Сброшены HWID устройства для {user.telegram_id}")
|
||||
except Exception as hwid_error:
|
||||
logger.warning(f"⚠️ Ошибка сброса HWID устройств: {hwid_error}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import delete
|
||||
from app.database.models import (
|
||||
SubscriptionServer, Transaction, ReferralEarning,
|
||||
PromoCodeUse, SubscriptionStatus
|
||||
)
|
||||
|
||||
if user.subscription:
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены серверы подписки для {user.telegram_id}")
|
||||
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены транзакции для {user.telegram_id}")
|
||||
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
|
||||
)
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены реферальные доходы для {user.telegram_id}")
|
||||
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены использования промокодов для {user.telegram_id}")
|
||||
|
||||
except Exception as records_error:
|
||||
logger.error(f"❌ Ошибка удаления связанных записей: {records_error}")
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
user.balance_kopeks = 0
|
||||
user.remnawave_uuid = None
|
||||
user.has_had_paid_subscription = False
|
||||
user.used_promocodes = 0
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
if user.subscription:
|
||||
user.subscription.status = SubscriptionStatus.DISABLED.value
|
||||
user.subscription.is_trial = True
|
||||
user.subscription.end_date = datetime.utcnow()
|
||||
user.subscription.traffic_limit_gb = 0
|
||||
user.subscription.traffic_used_gb = 0.0
|
||||
user.subscription.device_limit = 1
|
||||
user.subscription.connected_squads = []
|
||||
user.subscription.autopay_enabled = False
|
||||
user.subscription.autopay_days_before = 3
|
||||
user.subscription.remnawave_short_uuid = None
|
||||
user.subscription.subscription_url = ""
|
||||
user.subscription.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"✅ ПРИНУДИТЕЛЬНО очищены ВСЕ данные пользователя {user.telegram_id}")
|
||||
return True
|
||||
|
||||
except Exception as cleanup_error:
|
||||
logger.error(f"❌ Ошибка финальной очистки пользователя: {cleanup_error}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Критическая ошибка принудительной очистки пользователя {user.telegram_id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
async def cleanup_orphaned_subscriptions(self, db: AsyncSession) -> Dict[str, int]:
|
||||
"""
|
||||
Усиленная версия очистки с полным удалением данных
|
||||
"""
|
||||
try:
|
||||
stats = {"deactivated": 0, "errors": 0, "checked": 0}
|
||||
|
||||
logger.info("🧹 Начинаем очистку неактуальных подписок...")
|
||||
logger.info("🧹 Начинаем усиленную очистку неактуальных подписок...")
|
||||
|
||||
async with self.api as api:
|
||||
panel_users_data = await api._make_request('GET', '/api/users')
|
||||
@@ -1008,12 +1168,14 @@ class RemnaWaveService:
|
||||
continue
|
||||
|
||||
if user.telegram_id not in panel_telegram_ids:
|
||||
logger.info(f"🗑️ Деактивируем подписку пользователя {user.telegram_id} (отсутствует в панели)")
|
||||
|
||||
from app.database.crud.subscription import deactivate_subscription
|
||||
await deactivate_subscription(db, subscription)
|
||||
|
||||
stats["deactivated"] += 1
|
||||
logger.info(f"🗑️ ПОЛНАЯ деактивация подписки пользователя {user.telegram_id} (отсутствует в панели)")
|
||||
|
||||
cleanup_success = await self.force_cleanup_user_data(db, user)
|
||||
|
||||
if cleanup_success:
|
||||
stats["deactivated"] += 1
|
||||
else:
|
||||
stats["errors"] += 1
|
||||
|
||||
except Exception as sub_error:
|
||||
logger.error(f"❌ Ошибка обработки подписки {subscription.id}: {sub_error}")
|
||||
@@ -1023,11 +1185,11 @@ class RemnaWaveService:
|
||||
if len(subscriptions) < limit:
|
||||
break
|
||||
|
||||
logger.info(f"🧹 Очистка завершена: проверено {stats['checked']}, деактивировано {stats['deactivated']}, ошибок {stats['errors']}")
|
||||
logger.info(f"🧹 Усиленная очистка завершена: проверено {stats['checked']}, деактивировано {stats['deactivated']}, ошибок {stats['errors']}")
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Критическая ошибка очистки подписок: {e}")
|
||||
logger.error(f"❌ Критическая ошибка усиленной очистки подписок: {e}")
|
||||
return {"deactivated": 0, "errors": 1, "checked": 0}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional, List, Tuple
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import Subscription, User
|
||||
from app.database.models import Subscription, User, SubscriptionStatus
|
||||
from app.external.remnawave_api import (
|
||||
RemnaWaveAPI, RemnaWaveUser, UserStatus,
|
||||
TrafficLimitStrategy, RemnaWaveAPIError
|
||||
@@ -34,10 +34,23 @@ class SubscriptionService:
|
||||
logger.error(f"Пользователь {subscription.user_id} не найден")
|
||||
return None
|
||||
|
||||
validation_success = await self.validate_and_clean_subscription(db, subscription, user)
|
||||
if not validation_success:
|
||||
logger.error(f"Ошибка валидации подписки для пользователя {user.telegram_id}")
|
||||
return None
|
||||
|
||||
async with self.api as api:
|
||||
existing_users = await api.get_user_by_telegram_id(user.telegram_id)
|
||||
if existing_users:
|
||||
logger.info(f"🔄 Найден существующий пользователь в панели для {user.telegram_id}")
|
||||
remnawave_user = existing_users[0]
|
||||
|
||||
try:
|
||||
await api.reset_user_devices(remnawave_user.uuid)
|
||||
logger.info(f"🔧 Сброшены HWID устройства для пользователя {user.telegram_id}")
|
||||
except Exception as hwid_error:
|
||||
logger.warning(f"⚠️ Не удалось сбросить HWID: {hwid_error}")
|
||||
|
||||
updated_user = await api.update_user(
|
||||
uuid=remnawave_user.uuid,
|
||||
status=UserStatus.ACTIVE,
|
||||
@@ -47,7 +60,9 @@ class SubscriptionService:
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
else:
|
||||
logger.info(f"🆕 Создаем нового пользователя в панели для {user.telegram_id}")
|
||||
username = f"user_{user.telegram_id}"
|
||||
updated_user = await api.create_user(
|
||||
username=username,
|
||||
@@ -67,7 +82,7 @@ class SubscriptionService:
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"✅ Создан RemnaWave пользователь для подписки {subscription.id}")
|
||||
logger.info(f"✅ Создан/обновлен RemnaWave пользователь для подписки {subscription.id}")
|
||||
logger.info(f"🔗 Ссылка на подписку: {updated_user.subscription_url}")
|
||||
logger.info(f"📊 Стратегия сброса трафика: MONTH")
|
||||
return updated_user
|
||||
@@ -91,10 +106,23 @@ class SubscriptionService:
|
||||
logger.error(f"RemnaWave UUID не найден для пользователя {subscription.user_id}")
|
||||
return None
|
||||
|
||||
current_time = datetime.utcnow()
|
||||
is_actually_active = (subscription.status == SubscriptionStatus.ACTIVE.value and
|
||||
subscription.end_date > current_time)
|
||||
|
||||
if (subscription.status == SubscriptionStatus.ACTIVE.value and
|
||||
subscription.end_date <= current_time):
|
||||
|
||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||
subscription.updated_at = current_time
|
||||
await db.commit()
|
||||
is_actually_active = False
|
||||
logger.info(f"🔔 Статус подписки {subscription.id} автоматически изменен на 'expired'")
|
||||
|
||||
async with self.api as api:
|
||||
updated_user = await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
|
||||
status=UserStatus.ACTIVE if is_actually_active else UserStatus.EXPIRED,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
@@ -105,12 +133,13 @@ class SubscriptionService:
|
||||
subscription.subscription_url = updated_user.subscription_url
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"✅ Обновлен RemnaWave пользователь {user.remnawave_uuid}")
|
||||
status_text = "активным" if is_actually_active else "истёкшим"
|
||||
logger.info(f"✅ Обновлен RemnaWave пользователь {user.remnawave_uuid} со статусом {status_text}")
|
||||
logger.info(f"📊 Стратегия сброса трафика: MONTH")
|
||||
return updated_user
|
||||
|
||||
except RemnaWaveAPIError as e:
|
||||
logger.error(f"Ошибка обновления RemnaWave пользователя: {e}")
|
||||
logger.error(f"Ошибка RemnaWave API: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления RemnaWave пользователя: {e}")
|
||||
@@ -200,34 +229,161 @@ class SubscriptionService:
|
||||
devices: int,
|
||||
db: AsyncSession
|
||||
) -> Tuple[int, List[int]]:
|
||||
|
||||
|
||||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
|
||||
from app.database.crud.server_squad import get_server_squad_by_id
|
||||
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
traffic_price = TRAFFIC_PRICES.get(traffic_gb, 0)
|
||||
|
||||
|
||||
server_prices = []
|
||||
total_servers_price = 0
|
||||
|
||||
|
||||
for server_id in server_squad_ids:
|
||||
server = await get_server_squad_by_id(db, server_id)
|
||||
if server and server.is_available and not server.is_full:
|
||||
server_prices.append(server.price_kopeks)
|
||||
total_servers_price += server.price_kopeks
|
||||
logger.debug(f"🏷️ Сервер {server.display_name}: {server.price_kopeks/100}₽")
|
||||
else:
|
||||
server_prices.append(0)
|
||||
|
||||
logger.warning(f"⚠️ Сервер ID {server_id} недоступен")
|
||||
|
||||
devices_price = max(0, devices - 1) * settings.PRICE_PER_DEVICE
|
||||
|
||||
total_price = base_price + traffic_price + total_servers_price + devices_price
|
||||
|
||||
logger.info(f"💰 Расчет стоимости новой подписки:")
|
||||
logger.info(f" 📅 Период {period_days} дней: {base_price/100}₽")
|
||||
logger.info(f" 📊 Трафик {traffic_gb} ГБ: {traffic_price/100}₽")
|
||||
logger.info(f" 🌍 Серверы ({len(server_squad_ids)}): {total_servers_price/100}₽")
|
||||
logger.info(f" 📱 Устройства ({devices}): {devices_price/100}₽")
|
||||
logger.info(f" 💎 ИТОГО: {total_price/100}₽")
|
||||
|
||||
return total_price, server_prices
|
||||
|
||||
async def _get_countries_price(self, country_uuids: List[str]) -> int:
|
||||
# TODO: Реализовать получение цен из базы данных сквадов
|
||||
# Пока возвращаем базовую логику
|
||||
price_per_country = 1000
|
||||
return len(country_uuids) * price_per_country
|
||||
async def calculate_renewal_price(
|
||||
self,
|
||||
subscription: Subscription,
|
||||
period_days: int,
|
||||
db: AsyncSession
|
||||
) -> int:
|
||||
try:
|
||||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
servers_price, _ = await self.get_countries_price_by_uuids(
|
||||
subscription.connected_squads, db
|
||||
)
|
||||
|
||||
devices_price = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
|
||||
|
||||
traffic_price = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
|
||||
|
||||
total_price = base_price + servers_price + devices_price + traffic_price
|
||||
|
||||
logger.info(f"💰 Расчет стоимости продления для подписки {subscription.id} (по текущим ценам):")
|
||||
logger.info(f" 📅 Период {period_days} дней: {base_price/100}₽")
|
||||
logger.info(f" 🌍 Серверы ({len(subscription.connected_squads)}) по текущим ценам: {servers_price/100}₽")
|
||||
logger.info(f" 📱 Устройства ({subscription.device_limit}): {devices_price/100}₽")
|
||||
logger.info(f" 📊 Трафик ({subscription.traffic_limit_gb} ГБ): {traffic_price/100}₽")
|
||||
logger.info(f" 💎 ИТОГО: {total_price/100}₽")
|
||||
|
||||
return total_price
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка расчета стоимости продления: {e}")
|
||||
from app.config import PERIOD_PRICES
|
||||
return PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
async def validate_and_clean_subscription(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
subscription: Subscription,
|
||||
user: User
|
||||
) -> bool:
|
||||
try:
|
||||
needs_cleanup = False
|
||||
|
||||
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}, но не найден в панели")
|
||||
needs_cleanup = True
|
||||
else:
|
||||
if remnawave_user.telegram_id != user.telegram_id:
|
||||
logger.warning(f"⚠️ Несоответствие telegram_id для пользователя {user.telegram_id}")
|
||||
needs_cleanup = True
|
||||
except Exception as 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")
|
||||
needs_cleanup = True
|
||||
|
||||
if needs_cleanup:
|
||||
logger.info(f"🧹 Очищаем мусорные данные подписки для пользователя {user.telegram_id}")
|
||||
|
||||
subscription.remnawave_short_uuid = None
|
||||
subscription.subscription_url = ""
|
||||
subscription.connected_squads = []
|
||||
|
||||
user.remnawave_uuid = None
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✅ Мусорные данные очищены для пользователя {user.telegram_id}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
async def get_countries_price_by_uuids(
|
||||
self,
|
||||
country_uuids: List[str],
|
||||
db: AsyncSession
|
||||
) -> Tuple[int, List[int]]:
|
||||
try:
|
||||
from app.database.crud.server_squad import get_server_squad_by_uuid
|
||||
|
||||
total_price = 0
|
||||
prices_list = []
|
||||
|
||||
for country_uuid in country_uuids:
|
||||
server = await get_server_squad_by_uuid(db, country_uuid)
|
||||
if server and server.is_available and not server.is_full:
|
||||
price = server.price_kopeks
|
||||
total_price += price
|
||||
prices_list.append(price)
|
||||
logger.debug(f"🏷️ Страна {server.display_name}: {price/100}₽")
|
||||
else:
|
||||
default_price = 1000
|
||||
total_price += default_price
|
||||
prices_list.append(default_price)
|
||||
logger.warning(f"⚠️ Сервер {country_uuid} недоступен, используем базовую цену: {default_price/100}₽")
|
||||
|
||||
logger.info(f"💰 Общая стоимость стран: {total_price/100}₽")
|
||||
return total_price, prices_list
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения цен стран: {e}")
|
||||
default_prices = [1000] * len(country_uuids)
|
||||
return sum(default_prices), default_prices
|
||||
|
||||
async def _get_countries_price(self, country_uuids: List[str], db: AsyncSession) -> int:
|
||||
try:
|
||||
total_price, _ = await self.get_countries_price_by_uuids(country_uuids, db)
|
||||
return total_price
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения цен стран: {e}")
|
||||
return len(country_uuids) * 1000
|
||||
|
||||
def _gb_to_bytes(self, gb: int) -> int:
|
||||
if gb == 0:
|
||||
@@ -237,4 +393,4 @@ class SubscriptionService:
|
||||
def _bytes_to_gb(self, bytes_value: int) -> float:
|
||||
if bytes_value == 0:
|
||||
return 0.0
|
||||
return bytes_value / (1024 * 1024 * 1024)
|
||||
return bytes_value / (1024 * 1024 * 1024)
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import delete
|
||||
|
||||
from app.database.crud.user import (
|
||||
get_user_by_id, get_user_by_telegram_id, get_users_list,
|
||||
@@ -10,7 +11,10 @@ from app.database.crud.user import (
|
||||
)
|
||||
from app.database.crud.transaction import get_user_transactions_count
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.database.models import User, UserStatus
|
||||
from app.database.models import (
|
||||
User, UserStatus, Subscription, Transaction, PromoCodeUse,
|
||||
ReferralEarning, SubscriptionServer
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -157,6 +161,19 @@ class UserService:
|
||||
if not user:
|
||||
return False
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован при блокировке")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка деактивации RemnaWave пользователя при блокировке: {e}")
|
||||
|
||||
if user.subscription:
|
||||
from app.database.crud.subscription import deactivate_subscription
|
||||
await deactivate_subscription(db, user.subscription)
|
||||
|
||||
await update_user(db, user, status=UserStatus.BLOCKED.value)
|
||||
|
||||
logger.info(f"Админ {admin_id} заблокировал пользователя {user_id}: {reason}")
|
||||
@@ -179,6 +196,27 @@ class UserService:
|
||||
|
||||
await update_user(db, user, status=UserStatus.ACTIVE.value)
|
||||
|
||||
if user.subscription:
|
||||
from datetime import datetime
|
||||
from app.database.models import SubscriptionStatus
|
||||
|
||||
if user.subscription.end_date > datetime.utcnow():
|
||||
user.subscription.status = SubscriptionStatus.ACTIVE.value
|
||||
await db.commit()
|
||||
await db.refresh(user.subscription)
|
||||
logger.info(f"🔄 Подписка пользователя {user_id} восстановлена")
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, user.subscription)
|
||||
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} восстановлен при разблокировке")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка восстановления RemnaWave пользователя при разблокировке: {e}")
|
||||
else:
|
||||
logger.info(f"⏰ Подписка пользователя {user_id} истекла, восстановление невозможно")
|
||||
|
||||
logger.info(f"Админ {admin_id} разблокировал пользователя {user_id}")
|
||||
return True
|
||||
|
||||
@@ -195,17 +233,100 @@ class UserService:
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if not user:
|
||||
logger.warning(f"Пользователь {user_id} не найден для удаления")
|
||||
return False
|
||||
|
||||
success = await delete_user(db, user)
|
||||
logger.info(f"🗑️ Начинаем полное удаление пользователя {user_id} (Telegram ID: {user.telegram_id})")
|
||||
|
||||
if success:
|
||||
logger.info(f"Админ {admin_id} удалил пользователя {user_id}")
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка деактивации RemnaWave пользователя: {e}")
|
||||
|
||||
return success
|
||||
if user.subscription:
|
||||
try:
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
await db.delete(user.subscription)
|
||||
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления подписки: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.referred_by_id == user.id)
|
||||
.values(referred_by_id=None)
|
||||
)
|
||||
logger.info(f"🗑️ Очищены реферальные ссылки у рефералов пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены транзакции пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления транзакций: {e}")
|
||||
|
||||
try:
|
||||
user.status = UserStatus.DELETED.value
|
||||
user.balance_kopeks = 0
|
||||
user.remnawave_uuid = None
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✅ Пользователь {user_id} помечен как удаленный и обнулен")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления статуса пользователя: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
logger.info(f"✅ Пользователь {user_id} (Telegram ID: {user.telegram_id}) полностью удален админом {admin_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления пользователя: {e}")
|
||||
logger.error(f"❌ Критическая ошибка удаления пользователя {user_id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
async def get_user_statistics(self, db: AsyncSession) -> Dict[str, Any]:
|
||||
@@ -237,7 +358,7 @@ class UserService:
|
||||
deleted_count = 0
|
||||
|
||||
for user in inactive_users:
|
||||
success = await delete_user(db, user)
|
||||
success = await self.delete_user_account(db, user.id, 0)
|
||||
if success:
|
||||
deleted_count += 1
|
||||
|
||||
@@ -281,7 +402,7 @@ class UserService:
|
||||
"subscription_active": subscription.is_active if subscription else False,
|
||||
"subscription_trial": subscription.is_trial if subscription else False,
|
||||
"transactions_count": transactions_count,
|
||||
"referrer_id": user.referrer_id,
|
||||
"referrer_id": user.referred_by_id,
|
||||
"referral_code": user.referral_code
|
||||
}
|
||||
|
||||
@@ -330,4 +451,4 @@ class UserService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения пользователей по критериям: {e}")
|
||||
return []
|
||||
return []
|
||||
|
||||
+4
-1
@@ -35,6 +35,9 @@ class AdminStates(StatesGroup):
|
||||
|
||||
waiting_for_user_search = State()
|
||||
editing_user_balance = State()
|
||||
extending_subscription = State()
|
||||
adding_traffic = State()
|
||||
granting_subscription = State()
|
||||
editing_user_subscription = State()
|
||||
|
||||
creating_promocode = State()
|
||||
@@ -79,4 +82,4 @@ class SquadCreateStates(StatesGroup):
|
||||
selecting_inbounds = State()
|
||||
|
||||
class SquadRenameStates(StatesGroup):
|
||||
waiting_for_new_name = State()
|
||||
waiting_for_new_name = State()
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""add_paid_price_to_subscription
|
||||
|
||||
Revision ID: 3d9b35c6bd8f
|
||||
Revises:
|
||||
Create Date: 2025-08-23 08:17:00.563340
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3d9b35c6bd8f'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('subscriptions', sa.Column('paid_price_kopeks', sa.Integer(), nullable=False, server_default='0'))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('subscriptions', 'paid_price_kopeks')
|
||||
# ### end Alembic commands ###
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
# Основные зависимости
|
||||
aiogram==3.4.1
|
||||
aiogram==3.7.0
|
||||
aiohttp==3.9.1
|
||||
asyncpg==0.29.0
|
||||
SQLAlchemy==2.0.25
|
||||
alembic==1.13.1
|
||||
aiosqlite==0.19.0
|
||||
|
||||
# Дополнительные зависимости
|
||||
# Дополнительные зависимости
|
||||
pydantic==2.5.3
|
||||
pydantic-settings==2.1.0
|
||||
python-dotenv==1.0.0
|
||||
|
||||
Reference in New Issue
Block a user