Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5aad1e025b | |||
| e811e7c877 | |||
| 932e9f292b | |||
| 03749574d6 | |||
| 1059a51688 | |||
| a1963d5b17 | |||
| 426e736cd5 | |||
| fb03cff403 | |||
| 40780f1184 | |||
| c76290b51e | |||
| 5b6dd3058e | |||
| c8d91a1ec1 | |||
| 21952f84eb | |||
| 7bdfaa312b | |||
| 1829a469e7 | |||
| ddd37a4e98 | |||
| 2804c701c0 | |||
| b3c85b2db5 | |||
| 987b8170c6 | |||
| ab49f3cd1e | |||
| 732e8f7b8b | |||
| 9273a4e15b | |||
| d89293f76a | |||
| 174462183d | |||
| 783927be37 | |||
| 98c68dd8dc | |||
| 13585eab54 | |||
| 169336da43 | |||
| 9ef875f6bf | |||
| 3f54706a72 | |||
| 835e5e6f7f | |||
| b292442e8d | |||
| 9faaf5c963 | |||
| 117e88fe4a | |||
| 96bf08a07c | |||
| 4523403de5 | |||
| 02682e88c1 | |||
| 323b9a7e18 | |||
| 100dc3730f | |||
| 919047317a | |||
| 6ae5b59ed1 | |||
| b3123e2e85 | |||
| fbe0ca790e | |||
| 785648ba7a | |||
| 06f8728efa | |||
| 99f102a06e | |||
| d759dd3a66 | |||
| 73dc22b0b8 | |||
| 5a082f0eb8 | |||
| 073d5a119b | |||
| 2dab106bef | |||
| ec8c7b7242 | |||
| a4b96a089f | |||
| 269f8f6220 | |||
| f1beda2b89 | |||
| 6a00db32ae | |||
| 276bc5eb40 | |||
| 8678afd055 | |||
| 31340ad4a5 | |||
| 98ce47ce71 | |||
| 3462f5b3b1 |
+6
-10
@@ -37,9 +37,11 @@ TRIAL_SQUAD_UUID=
|
||||
# Сколько устройств доступно по дефолту при покупке платной подписки
|
||||
DEFAULT_DEVICE_LIMIT=3
|
||||
|
||||
# Максимум устройств достопных к покупке (0 = Нет лимита)
|
||||
MAX_DEVICES_LIMIT=15
|
||||
|
||||
# Дефолт параметры для подписок выданных через админку
|
||||
DEFAULT_TRAFFIC_LIMIT_GB=100
|
||||
DEFAULT_DEVICE_LIMIT=1
|
||||
|
||||
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
|
||||
@@ -71,16 +73,10 @@ PRICE_90_DAYS=36900
|
||||
PRICE_180_DAYS=69900
|
||||
PRICE_360_DAYS=109900
|
||||
|
||||
# Цены за трафик
|
||||
PRICE_TRAFFIC_5GB=1000
|
||||
PRICE_TRAFFIC_10GB=2000
|
||||
PRICE_TRAFFIC_25GB=3000
|
||||
PRICE_TRAFFIC_50GB=4000
|
||||
PRICE_TRAFFIC_100GB=6000
|
||||
PRICE_TRAFFIC_250GB=10000
|
||||
PRICE_TRAFFIC_UNLIMITED=15000
|
||||
# Выводимые пакеты трафика и их цены в копейках
|
||||
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,250:17000:false,500:19000:false,1000:19500:true,0:20000:true"
|
||||
|
||||
# Цена за дополнительное устройство
|
||||
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
|
||||
PRICE_PER_DEVICE=5000
|
||||
|
||||
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
|
||||
|
||||
@@ -130,9 +130,11 @@ TRIAL_SQUAD_UUID=
|
||||
# Сколько устройств доступно по дефолту при покупке платной подписки
|
||||
DEFAULT_DEVICE_LIMIT=3
|
||||
|
||||
# Максимум устройств достопных к покупке (0 = Нет лимита)
|
||||
MAX_DEVICES_LIMIT=15
|
||||
|
||||
# Дефолт параметры для подписок выданных через админку
|
||||
DEFAULT_TRAFFIC_LIMIT_GB=100
|
||||
DEFAULT_DEVICE_LIMIT=1
|
||||
|
||||
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
|
||||
@@ -164,16 +166,10 @@ PRICE_90_DAYS=36900
|
||||
PRICE_180_DAYS=69900
|
||||
PRICE_360_DAYS=109900
|
||||
|
||||
# Цены за трафик
|
||||
PRICE_TRAFFIC_5GB=1000
|
||||
PRICE_TRAFFIC_10GB=2000
|
||||
PRICE_TRAFFIC_25GB=3000
|
||||
PRICE_TRAFFIC_50GB=4000
|
||||
PRICE_TRAFFIC_100GB=6000
|
||||
PRICE_TRAFFIC_250GB=10000
|
||||
PRICE_TRAFFIC_UNLIMITED=15000
|
||||
# Выводимые пакеты трафика и их цены в копейках
|
||||
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,250:17000:false,500:19000:false,1000:19500:true,0:20000:true"
|
||||
|
||||
# Цена за дополнительное устройство
|
||||
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
|
||||
PRICE_PER_DEVICE=5000
|
||||
|
||||
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
|
||||
|
||||
+146
-17
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from typing import List, Optional, Union
|
||||
from typing import List, Optional, Union, Dict
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import field_validator, Field
|
||||
from pathlib import Path
|
||||
@@ -25,6 +25,7 @@ class Settings(BaseSettings):
|
||||
DEFAULT_DEVICE_LIMIT: int = 1
|
||||
TRIAL_SQUAD_UUID: str
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY: str = "MONTH"
|
||||
MAX_DEVICES_LIMIT: int = 20
|
||||
|
||||
TRIAL_WARNING_HOURS: int = 2
|
||||
ENABLE_NOTIFICATIONS: bool = True
|
||||
@@ -44,13 +45,17 @@ class Settings(BaseSettings):
|
||||
PRICE_180_DAYS: int = 499000
|
||||
PRICE_360_DAYS: int = 899000
|
||||
|
||||
PRICE_TRAFFIC_5GB: int = 10000
|
||||
PRICE_TRAFFIC_10GB: int = 19000
|
||||
PRICE_TRAFFIC_25GB: int = 45000
|
||||
PRICE_TRAFFIC_50GB: int = 85000
|
||||
PRICE_TRAFFIC_100GB: int = 159000
|
||||
PRICE_TRAFFIC_250GB: int = 369000
|
||||
PRICE_TRAFFIC_UNLIMITED: int = 0
|
||||
PRICE_TRAFFIC_5GB: int = 2000
|
||||
PRICE_TRAFFIC_10GB: int = 3500
|
||||
PRICE_TRAFFIC_25GB: int = 7000
|
||||
PRICE_TRAFFIC_50GB: int = 11000
|
||||
PRICE_TRAFFIC_100GB: int = 15000
|
||||
PRICE_TRAFFIC_250GB: int = 17000
|
||||
PRICE_TRAFFIC_500GB: int = 19000
|
||||
PRICE_TRAFFIC_1000GB: int = 19500
|
||||
PRICE_TRAFFIC_UNLIMITED: int = 20000
|
||||
|
||||
TRAFFIC_PACKAGES_CONFIG: str = ""
|
||||
|
||||
PRICE_PER_DEVICE: int = 5000
|
||||
|
||||
@@ -292,6 +297,128 @@ class Settings(BaseSettings):
|
||||
def rubles_to_stars(self, rubles: float) -> int:
|
||||
return max(1, int(rubles / self.get_stars_rate()))
|
||||
|
||||
def get_traffic_packages(self) -> List[Dict]:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
packages = []
|
||||
config_str = self.TRAFFIC_PACKAGES_CONFIG.strip()
|
||||
|
||||
logger.info(f"CONFIG STRING: '{config_str}'")
|
||||
|
||||
if not config_str:
|
||||
logger.info("CONFIG EMPTY, USING FALLBACK")
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
logger.info("PARSING CONFIG...")
|
||||
|
||||
for package_config in config_str.split(','):
|
||||
package_config = package_config.strip()
|
||||
if not package_config:
|
||||
continue
|
||||
|
||||
parts = package_config.split(':')
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
gb = int(parts[0])
|
||||
price = int(parts[1])
|
||||
enabled = parts[2].lower() == 'true'
|
||||
|
||||
packages.append({
|
||||
"gb": gb,
|
||||
"price": price,
|
||||
"enabled": enabled
|
||||
})
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
logger.info(f"PARSED {len(packages)} packages from config")
|
||||
return packages if packages else self._get_fallback_traffic_packages()
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"ERROR PARSING CONFIG: {e}")
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
def _get_fallback_traffic_packages(self) -> List[Dict]:
|
||||
try:
|
||||
if self.TRAFFIC_PACKAGES_CONFIG.strip():
|
||||
packages = []
|
||||
for package_config in self.TRAFFIC_PACKAGES_CONFIG.split(','):
|
||||
package_config = package_config.strip()
|
||||
if not package_config:
|
||||
continue
|
||||
|
||||
parts = package_config.split(':')
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
gb = int(parts[0])
|
||||
price = int(parts[1])
|
||||
enabled = parts[2].lower() == 'true'
|
||||
|
||||
packages.append({
|
||||
"gb": gb,
|
||||
"price": price,
|
||||
"enabled": enabled
|
||||
})
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if packages:
|
||||
return packages
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return [
|
||||
{"gb": 5, "price": self.PRICE_TRAFFIC_5GB, "enabled": True},
|
||||
{"gb": 10, "price": self.PRICE_TRAFFIC_10GB, "enabled": True},
|
||||
{"gb": 25, "price": self.PRICE_TRAFFIC_25GB, "enabled": True},
|
||||
{"gb": 50, "price": self.PRICE_TRAFFIC_50GB, "enabled": True},
|
||||
{"gb": 100, "price": self.PRICE_TRAFFIC_100GB, "enabled": True},
|
||||
{"gb": 250, "price": self.PRICE_TRAFFIC_250GB, "enabled": True},
|
||||
{"gb": 500, "price": self.PRICE_TRAFFIC_500GB, "enabled": True},
|
||||
{"gb": 1000, "price": self.PRICE_TRAFFIC_1000GB, "enabled": True},
|
||||
{"gb": 0, "price": self.PRICE_TRAFFIC_UNLIMITED, "enabled": True},
|
||||
]
|
||||
|
||||
def get_traffic_price(self, gb: int) -> int:
|
||||
packages = self.get_traffic_packages()
|
||||
|
||||
for package in packages:
|
||||
if package["gb"] == gb and package["enabled"]:
|
||||
return package["price"]
|
||||
|
||||
|
||||
enabled_packages = [pkg for pkg in packages if pkg["enabled"]]
|
||||
if not enabled_packages:
|
||||
return 0
|
||||
|
||||
unlimited_package = next((pkg for pkg in enabled_packages if pkg["gb"] == 0), None)
|
||||
|
||||
finite_packages = [pkg for pkg in enabled_packages if pkg["gb"] > 0]
|
||||
if finite_packages:
|
||||
max_package = max(finite_packages, key=lambda x: x["gb"])
|
||||
|
||||
if gb > max_package["gb"]:
|
||||
if unlimited_package:
|
||||
return unlimited_package["price"]
|
||||
else:
|
||||
return max_package["price"]
|
||||
|
||||
suitable_packages = [pkg for pkg in finite_packages if pkg["gb"] >= gb]
|
||||
if suitable_packages:
|
||||
nearest_package = min(suitable_packages, key=lambda x: x["gb"])
|
||||
return nearest_package["price"]
|
||||
|
||||
if unlimited_package:
|
||||
return unlimited_package["price"]
|
||||
|
||||
return 0
|
||||
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
"env_file_encoding": "utf-8"
|
||||
@@ -310,12 +437,14 @@ PERIOD_PRICES = {
|
||||
360: settings.PRICE_360_DAYS,
|
||||
}
|
||||
|
||||
TRAFFIC_PRICES = {
|
||||
5: settings.PRICE_TRAFFIC_5GB,
|
||||
10: settings.PRICE_TRAFFIC_10GB,
|
||||
25: settings.PRICE_TRAFFIC_25GB,
|
||||
50: settings.PRICE_TRAFFIC_50GB,
|
||||
100: settings.PRICE_TRAFFIC_100GB,
|
||||
250: settings.PRICE_TRAFFIC_250GB,
|
||||
0: settings.PRICE_TRAFFIC_UNLIMITED,
|
||||
}
|
||||
def get_traffic_prices() -> Dict[int, int]:
|
||||
packages = settings.get_traffic_packages()
|
||||
return {package["gb"]: package["price"] for package in packages}
|
||||
|
||||
TRAFFIC_PRICES = get_traffic_prices()
|
||||
|
||||
def refresh_traffic_prices():
|
||||
global TRAFFIC_PRICES
|
||||
TRAFFIC_PRICES = get_traffic_prices()
|
||||
|
||||
refresh_traffic_prices()
|
||||
|
||||
@@ -16,6 +16,8 @@ from app.localization.texts import get_texts
|
||||
from app.services.user_service import UserService
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
from app.utils.formatters import format_datetime, format_time_ago
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
from app.database.crud.server_squad import get_all_server_squads, get_server_squad_by_uuid, get_server_squad_by_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1300,6 +1302,642 @@ async def process_subscription_grant_text(
|
||||
|
||||
await state.clear()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_user_servers_management(
|
||||
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"
|
||||
|
||||
if subscription:
|
||||
current_squads = subscription.connected_squads or []
|
||||
|
||||
if current_squads:
|
||||
text += f"<b>Текущие серверы ({len(current_squads)}):</b>\n"
|
||||
|
||||
for squad_uuid in current_squads:
|
||||
try:
|
||||
server = await get_server_squad_by_uuid(db, squad_uuid)
|
||||
if server:
|
||||
text += f"• {server.display_name}\n"
|
||||
else:
|
||||
text += f"• {squad_uuid[:8]}... (неизвестный)\n"
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения сервера {squad_uuid}: {e}")
|
||||
text += f"• {squad_uuid[:8]}... (ошибка загрузки)\n"
|
||||
else:
|
||||
text += "<b>Серверы:</b> Не подключены\n"
|
||||
|
||||
text += f"\n<b>Устройства:</b> {subscription.device_limit}\n"
|
||||
traffic_display = f"{subscription.traffic_used_gb:.1f}/"
|
||||
if subscription.traffic_limit_gb == 0:
|
||||
traffic_display += "∞ ГБ"
|
||||
else:
|
||||
traffic_display += f"{subscription.traffic_limit_gb} ГБ"
|
||||
text += f"<b>Трафик:</b> {traffic_display}\n"
|
||||
else:
|
||||
text += "❌ <b>Подписка отсутствует</b>"
|
||||
|
||||
keyboard = [
|
||||
[
|
||||
types.InlineKeyboardButton(text="🌍 Сменить сервер", callback_data=f"admin_user_change_server_{user_id}"),
|
||||
types.InlineKeyboardButton(text="📱 Устройства", callback_data=f"admin_user_devices_{user_id}")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="📊 Трафик", callback_data=f"admin_user_traffic_{user_id}"),
|
||||
types.InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data=f"admin_user_reset_devices_{user_id}")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="⬅️ К пользователю", callback_data=f"admin_user_manage_{user_id}")
|
||||
]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_server_selection(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
await _show_servers_for_user(callback, user_id, db)
|
||||
await callback.answer()
|
||||
|
||||
async def _show_servers_for_user(
|
||||
callback: types.CallbackQuery,
|
||||
user_id: int,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
current_squads = []
|
||||
if user and user.subscription:
|
||||
current_squads = user.subscription.connected_squads or []
|
||||
|
||||
all_servers, _ = await get_all_server_squads(db, available_only=False)
|
||||
|
||||
servers_to_show = []
|
||||
for server in all_servers:
|
||||
if server.is_available or server.squad_uuid in current_squads:
|
||||
servers_to_show.append(server)
|
||||
|
||||
if not servers_to_show:
|
||||
await callback.message.edit_text(
|
||||
"❌ Доступные серверы не найдены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
text = f"🌍 <b>Управление серверами</b>\n\n"
|
||||
text += f"Нажмите на сервер чтобы добавить/убрать:\n"
|
||||
text += f"✅ - выбранный сервер\n"
|
||||
text += f"⚪ - доступный сервер\n"
|
||||
text += f"🔒 - неактивный (только для уже назначенных)\n\n"
|
||||
|
||||
keyboard = []
|
||||
selected_servers = [s for s in servers_to_show if s.squad_uuid in current_squads]
|
||||
available_servers = [s for s in servers_to_show if s.squad_uuid not in current_squads and s.is_available]
|
||||
inactive_servers = [s for s in servers_to_show if s.squad_uuid not in current_squads and not s.is_available]
|
||||
|
||||
sorted_servers = selected_servers + available_servers + inactive_servers
|
||||
|
||||
for server in sorted_servers[:20]:
|
||||
is_selected = server.squad_uuid in current_squads
|
||||
|
||||
if is_selected:
|
||||
emoji = "✅"
|
||||
elif server.is_available:
|
||||
emoji = "⚪"
|
||||
else:
|
||||
emoji = "🔒"
|
||||
|
||||
display_name = server.display_name
|
||||
if not server.is_available and not is_selected:
|
||||
display_name += " (неактивный)"
|
||||
|
||||
keyboard.append([
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{emoji} {display_name}",
|
||||
callback_data=f"admin_user_toggle_server_{user_id}_{server.id}"
|
||||
)
|
||||
])
|
||||
|
||||
if len(servers_to_show) > 20:
|
||||
text += f"\n📝 Показано первых 20 из {len(servers_to_show)} серверов"
|
||||
|
||||
keyboard.append([
|
||||
types.InlineKeyboardButton(text="✅ Готово", callback_data=f"admin_user_servers_{user_id}"),
|
||||
types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка показа серверов: {e}")
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def toggle_user_server(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[4])
|
||||
server_id = int(parts[5])
|
||||
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if not user or not user.subscription:
|
||||
await callback.answer("❌ Пользователь или подписка не найдены", show_alert=True)
|
||||
return
|
||||
|
||||
server = await get_server_squad_by_id(db, server_id)
|
||||
if not server:
|
||||
await callback.answer("❌ Сервер не найден", show_alert=True)
|
||||
return
|
||||
|
||||
subscription = user.subscription
|
||||
current_squads = list(subscription.connected_squads or [])
|
||||
|
||||
if server.squad_uuid in current_squads:
|
||||
current_squads.remove(server.squad_uuid)
|
||||
action_text = "удален"
|
||||
else:
|
||||
current_squads.append(server.squad_uuid)
|
||||
action_text = "добавлен"
|
||||
|
||||
subscription.connected_squads = current_squads
|
||||
subscription.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
remnawave_service = RemnaWaveService()
|
||||
async with remnawave_service.api as api:
|
||||
await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
active_internal_squads=current_squads
|
||||
)
|
||||
logger.info(f"✅ Обновлены серверы в RemnaWave для пользователя {user.telegram_id}")
|
||||
except Exception as rw_error:
|
||||
logger.error(f"❌ Ошибка обновления RemnaWave: {rw_error}")
|
||||
|
||||
logger.info(f"Админ {db_user.id}: сервер {server.display_name} {action_text} для пользователя {user_id}")
|
||||
|
||||
await refresh_server_selection_screen(callback, user_id, db_user, db)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка переключения сервера: {e}")
|
||||
await callback.answer("❌ Ошибка изменения сервера", show_alert=True)
|
||||
|
||||
async def refresh_server_selection_screen(
|
||||
callback: types.CallbackQuery,
|
||||
user_id: int,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
current_squads = []
|
||||
if user and user.subscription:
|
||||
current_squads = user.subscription.connected_squads or []
|
||||
|
||||
servers, _ = await get_all_server_squads(db, available_only=True)
|
||||
|
||||
if not servers:
|
||||
await callback.message.edit_text(
|
||||
"❌ Доступные серверы не найдены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
text = f"🌍 <b>Управление серверами</b>\n\n"
|
||||
text += f"Нажмите на сервер чтобы добавить/убрать:\n\n"
|
||||
|
||||
keyboard = []
|
||||
for server in servers[:15]:
|
||||
is_selected = server.squad_uuid in current_squads
|
||||
emoji = "✅" if is_selected else "⚪"
|
||||
|
||||
keyboard.append([
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{emoji} {server.display_name}",
|
||||
callback_data=f"admin_user_toggle_server_{user_id}_{server.id}"
|
||||
)
|
||||
])
|
||||
|
||||
if len(servers) > 15:
|
||||
text += f"\n📝 Показано первых 15 из {len(servers)} серверов"
|
||||
|
||||
keyboard.append([
|
||||
types.InlineKeyboardButton(text="✅ Готово", callback_data=f"admin_user_servers_{user_id}"),
|
||||
types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления экрана серверов: {e}")
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_devices_edit(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(editing_devices_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📱 <b>Изменение количества устройств</b>\n\n"
|
||||
"Введите новое количество устройств (от 1 до 10):\n"
|
||||
"• Текущее значение будет заменено\n"
|
||||
"• Примеры: 1, 2, 5, 10\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="1", callback_data=f"admin_user_devices_set_{user_id}_1"),
|
||||
types.InlineKeyboardButton(text="2", callback_data=f"admin_user_devices_set_{user_id}_2"),
|
||||
types.InlineKeyboardButton(text="3", callback_data=f"admin_user_devices_set_{user_id}_3")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="5", callback_data=f"admin_user_devices_set_{user_id}_5"),
|
||||
types.InlineKeyboardButton(text="10", callback_data=f"admin_user_devices_set_{user_id}_10")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_servers_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.editing_user_devices)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def set_user_devices_button(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
devices = int(parts[-1])
|
||||
|
||||
success = await _update_user_devices(db, user_id, devices, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
f"✅ Количество устройств изменено на: {devices}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка изменения количества устройств",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_devices_edit_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("editing_devices_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
devices = int(message.text.strip())
|
||||
|
||||
if devices <= 0 or devices > 10:
|
||||
await message.answer("❌ Количество устройств должно быть от 1 до 10")
|
||||
return
|
||||
|
||||
success = await _update_user_devices(db, user_id, devices, db_user.id)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Количество устройств изменено на: {devices}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка изменения количества устройств")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число устройств")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_traffic_edit(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(editing_traffic_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📊 <b>Изменение лимита трафика</b>\n\n"
|
||||
"Введите новый лимит трафика в ГБ:\n"
|
||||
"• 0 - безлимитный трафик\n"
|
||||
"• Примеры: 50, 100, 500, 1000\n"
|
||||
"• Максимум: 10000 ГБ\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="50 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_50"),
|
||||
types.InlineKeyboardButton(text="100 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_100")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="500 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_500"),
|
||||
types.InlineKeyboardButton(text="1000 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_1000")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="♾️ Безлимит", callback_data=f"admin_user_traffic_set_{user_id}_0")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_servers_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.editing_user_traffic)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def set_user_traffic_button(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
traffic_gb = int(parts[-1])
|
||||
|
||||
success = await _update_user_traffic(db, user_id, traffic_gb, db_user.id)
|
||||
|
||||
if success:
|
||||
traffic_text = "♾️ безлимитный" if traffic_gb == 0 else f"{traffic_gb} ГБ"
|
||||
await callback.message.edit_text(
|
||||
f"✅ Лимит трафика изменен на: {traffic_text}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка изменения лимита трафика",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_traffic_edit_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("editing_traffic_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
traffic_gb = int(message.text.strip())
|
||||
|
||||
if traffic_gb < 0 or traffic_gb > 10000:
|
||||
await message.answer("❌ Лимит трафика должен быть от 0 до 10000 ГБ (0 = безлимит)")
|
||||
return
|
||||
|
||||
success = await _update_user_traffic(db, user_id, traffic_gb, db_user.id)
|
||||
|
||||
if success:
|
||||
traffic_text = "♾️ безлимитный" if traffic_gb == 0 else f"{traffic_gb} ГБ"
|
||||
await message.answer(
|
||||
f"✅ Лимит трафика изменен на: {traffic_text}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка изменения лимита трафика")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число ГБ")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def confirm_reset_devices(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔄 <b>Сброс устройств пользователя</b>\n\n"
|
||||
"⚠️ <b>ВНИМАНИЕ!</b>\n"
|
||||
"Вы уверены, что хотите сбросить все HWID устройства этого пользователя?\n\n"
|
||||
"Это действие:\n"
|
||||
"• Удалит все привязанные устройства\n"
|
||||
"• Пользователь сможет заново подключить устройства\n"
|
||||
"• Действие необратимо!\n\n"
|
||||
"Продолжить?",
|
||||
reply_markup=get_confirmation_keyboard(
|
||||
f"admin_user_reset_devices_confirm_{user_id}",
|
||||
f"admin_user_servers_{user_id}",
|
||||
db_user.language
|
||||
)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def reset_user_devices(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if not user or not user.remnawave_uuid:
|
||||
await callback.answer("❌ Пользователь не найден или не связан с RemnaWave", show_alert=True)
|
||||
return
|
||||
|
||||
remnawave_service = RemnaWaveService()
|
||||
async with remnawave_service.api as api:
|
||||
success = await api.reset_user_devices(user.remnawave_uuid)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Устройства пользователя успешно сброшены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
logger.info(f"Админ {db_user.id} сбросил устройства пользователя {user_id}")
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка сброса устройств",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сброса устройств: {e}")
|
||||
await callback.answer("❌ Ошибка сброса устройств", show_alert=True)
|
||||
|
||||
async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, admin_id: int) -> bool:
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if not user or not user.subscription:
|
||||
logger.error(f"Пользователь {user_id} или подписка не найдены")
|
||||
return False
|
||||
|
||||
subscription = user.subscription
|
||||
old_devices = subscription.device_limit
|
||||
subscription.device_limit = devices
|
||||
subscription.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
remnawave_service = RemnaWaveService()
|
||||
async with remnawave_service.api as api:
|
||||
await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
hwid_device_limit=devices
|
||||
)
|
||||
logger.info(f"✅ Обновлен лимит устройств в RemnaWave для пользователя {user.telegram_id}")
|
||||
except Exception as rw_error:
|
||||
logger.error(f"❌ Ошибка обновления лимита устройств в RemnaWave: {rw_error}")
|
||||
|
||||
logger.info(f"Админ {admin_id} изменил лимит устройств пользователя {user_id}: {old_devices} -> {devices}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления лимита устройств: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int, admin_id: int) -> bool:
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if not user or not user.subscription:
|
||||
logger.error(f"Пользователь {user_id} или подписка не найдены")
|
||||
return False
|
||||
|
||||
subscription = user.subscription
|
||||
old_traffic = subscription.traffic_limit_gb
|
||||
subscription.traffic_limit_gb = traffic_gb
|
||||
subscription.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
from app.external.remnawave_api import TrafficLimitStrategy
|
||||
|
||||
remnawave_service = RemnaWaveService()
|
||||
async with remnawave_service.api as api:
|
||||
await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
traffic_limit_bytes=traffic_gb * (1024**3) if traffic_gb > 0 else 0,
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH
|
||||
)
|
||||
logger.info(f"✅ Обновлен лимит трафика в RemnaWave для пользователя {user.telegram_id}")
|
||||
except Exception as rw_error:
|
||||
logger.error(f"❌ Ошибка обновления лимита трафика в RemnaWave: {rw_error}")
|
||||
|
||||
traffic_text_old = "безлимитный" if old_traffic == 0 else f"{old_traffic} ГБ"
|
||||
traffic_text_new = "безлимитный" if traffic_gb == 0 else f"{traffic_gb} ГБ"
|
||||
logger.info(f"Админ {admin_id} изменил лимит трафика пользователя {user_id}: {traffic_text_old} -> {traffic_text_new}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления лимита трафика: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
async def _extend_subscription_by_days(db: AsyncSession, user_id: int, days: int, admin_id: int) -> bool:
|
||||
try:
|
||||
@@ -1648,3 +2286,58 @@ def register_handlers(dp: Dispatcher):
|
||||
process_subscription_grant_text,
|
||||
AdminStates.granting_subscription
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_user_servers_management,
|
||||
F.data.startswith("admin_user_servers_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_server_selection,
|
||||
F.data.startswith("admin_user_change_server_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
toggle_user_server,
|
||||
F.data.startswith("admin_user_toggle_server_") & ~F.data.endswith("_add") & ~F.data.endswith("_remove")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
start_devices_edit,
|
||||
F.data.startswith("admin_user_devices_") & ~F.data.contains("set")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
set_user_devices_button,
|
||||
F.data.startswith("admin_user_devices_set_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_devices_edit_text,
|
||||
AdminStates.editing_user_devices
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
start_traffic_edit,
|
||||
F.data.startswith("admin_user_traffic_") & ~F.data.contains("set")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
set_user_traffic_button,
|
||||
F.data.startswith("admin_user_traffic_set_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_traffic_edit_text,
|
||||
AdminStates.editing_user_traffic
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_reset_devices,
|
||||
F.data.startswith("admin_user_reset_devices_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
reset_user_devices,
|
||||
F.data.startswith("admin_user_reset_devices_confirm_")
|
||||
)
|
||||
|
||||
+314
-33
@@ -8,7 +8,7 @@ import json
|
||||
import os
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
|
||||
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
|
||||
from app.config import settings, PERIOD_PRICES, get_traffic_prices
|
||||
from app.states import SubscriptionStates
|
||||
from app.database.crud.subscription import (
|
||||
get_subscription_by_user_id, create_trial_subscription,
|
||||
@@ -43,6 +43,7 @@ from app.services.referral_service import process_referral_purchase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TRAFFIC_PRICES = get_traffic_prices()
|
||||
|
||||
async def show_subscription_info(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -80,7 +81,7 @@ async def show_subscription_info(
|
||||
if subscription.is_trial:
|
||||
actual_status = "trial_active"
|
||||
status_display = "Тестовая"
|
||||
status_emoji = "🎁"
|
||||
status_emoji = "🎯"
|
||||
else:
|
||||
actual_status = "paid_active"
|
||||
status_display = "Активна"
|
||||
@@ -116,26 +117,61 @@ async def show_subscription_info(
|
||||
subscription_type = "Триал" if subscription.is_trial else "Платная"
|
||||
|
||||
if subscription.traffic_limit_gb == 0:
|
||||
traffic_used_display = f"∞ (безлимит) / {subscription.traffic_used_gb:.1f} ГБ"
|
||||
traffic_used_display = f"∞ (безлимит) | Использовано: {subscription.traffic_used_gb:.1f} ГБ"
|
||||
else:
|
||||
traffic_used_display = f"{subscription.traffic_used_gb:.1f} / {subscription.traffic_limit_gb} ГБ"
|
||||
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
|
||||
devices_used_str = "—"
|
||||
devices_list = []
|
||||
devices_count = 0
|
||||
|
||||
try:
|
||||
if db_user.remnawave_uuid:
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
service = RemnaWaveService()
|
||||
|
||||
async with service.api as api:
|
||||
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
|
||||
|
||||
if response and 'response' in response:
|
||||
devices_info = response['response']
|
||||
devices_count = devices_info.get('total', 0)
|
||||
devices_list = devices_info.get('devices', [])
|
||||
devices_used_str = str(devices_count)
|
||||
logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}")
|
||||
else:
|
||||
logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения устройств для отображения: {e}")
|
||||
devices_used_str = await get_current_devices_count(db_user)
|
||||
|
||||
servers_names = await get_servers_display_names(subscription.connected_squads)
|
||||
servers_display = servers_names if servers_names else "Нет серверов"
|
||||
|
||||
message = f"""👤 {db_user.full_name}
|
||||
━━━━━━━━━━━━━━━━━
|
||||
💰 Баланс: {settings.format_price(db_user.balance_kopeks)}
|
||||
📱 Подписка: {status_emoji} {status_display}{warning_text}
|
||||
━━━━━━━━━━━━━━━━━
|
||||
|
||||
📱 Информация о подписке
|
||||
🎭 Тип: {subscription_type}
|
||||
📅 Действует до: {subscription.end_date.strftime("%d.%m.%Y %H:%M")}
|
||||
⏰ Осталось: {time_left_text}
|
||||
📈 Трафик: {traffic_used_display}
|
||||
🌍 Серверы: {len(subscription.connected_squads)} стран
|
||||
📱 Устройства: {devices_used} / {subscription.device_limit}"""
|
||||
🌍 Серверы: {servers_display}
|
||||
📱 Устройства: {devices_used_str} / {subscription.device_limit}"""
|
||||
|
||||
if devices_list and len(devices_list) > 0:
|
||||
message += f"\n\n<blockquote>📱 <b>Подключенные устройства:</b>\n"
|
||||
for device in devices_list[:5]:
|
||||
platform = device.get('platform', 'Unknown')
|
||||
device_model = device.get('deviceModel', 'Unknown')
|
||||
device_info = f"{platform} - {device_model}"
|
||||
|
||||
if len(device_info) > 35:
|
||||
device_info = device_info[:32] + "..."
|
||||
message += f"• {device_info}\n"
|
||||
message += "</blockquote>"
|
||||
|
||||
if hasattr(subscription, 'subscription_url') and subscription.subscription_url:
|
||||
if actual_status in ['trial_active', 'paid_active']:
|
||||
@@ -154,6 +190,64 @@ async def show_subscription_info(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
async def get_current_devices_detailed(db_user: User) -> dict:
|
||||
try:
|
||||
if not db_user.remnawave_uuid:
|
||||
return {"count": 0, "devices": []}
|
||||
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
service = RemnaWaveService()
|
||||
|
||||
async with service.api as api:
|
||||
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
|
||||
|
||||
if response and 'response' in response:
|
||||
devices_info = response['response']
|
||||
total_devices = devices_info.get('total', 0)
|
||||
devices_list = devices_info.get('devices', [])
|
||||
|
||||
return {
|
||||
"count": total_devices,
|
||||
"devices": devices_list[:5]
|
||||
}
|
||||
else:
|
||||
return {"count": 0, "devices": []}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения детальной информации об устройствах: {e}")
|
||||
return {"count": 0, "devices": []}
|
||||
|
||||
async def get_servers_display_names(squad_uuids: List[str]) -> str:
|
||||
"""
|
||||
Получает отображаемые названия серверов по их UUID
|
||||
"""
|
||||
if not squad_uuids:
|
||||
return "Нет серверов"
|
||||
|
||||
try:
|
||||
countries = await _get_available_countries()
|
||||
|
||||
server_names = []
|
||||
for uuid in squad_uuids:
|
||||
for country in countries:
|
||||
if country['uuid'] == uuid:
|
||||
server_names.append(country['name'])
|
||||
break
|
||||
|
||||
if not server_names:
|
||||
return f"{len(squad_uuids)} стран"
|
||||
|
||||
if len(server_names) > 6:
|
||||
displayed = ", ".join(server_names[:6])
|
||||
remaining = len(server_names) - 6
|
||||
return f"{displayed} и ещё {remaining}"
|
||||
else:
|
||||
return ", ".join(server_names)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения названий серверов: {e}")
|
||||
return f"{len(squad_uuids)} стран"
|
||||
|
||||
async def get_current_devices_count(db_user: User) -> str:
|
||||
try:
|
||||
if not db_user.remnawave_uuid:
|
||||
@@ -181,7 +275,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
if subscription.is_trial:
|
||||
return 0
|
||||
|
||||
from app.config import TRAFFIC_PRICES, PERIOD_PRICES, settings
|
||||
from app.config import settings
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
@@ -197,7 +291,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
subscription.connected_squads, db
|
||||
)
|
||||
|
||||
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
|
||||
traffic_cost = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
|
||||
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
|
||||
@@ -212,7 +306,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
return total_cost
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"⚠ Ошибка расчета стоимости подписки: {e}")
|
||||
logger.error(f"⚠️ Ошибка расчета стоимости подписки: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -785,10 +879,14 @@ async def confirm_add_traffic(
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
price = TRAFFIC_PRICES[traffic_gb]
|
||||
price = settings.get_traffic_price(traffic_gb)
|
||||
|
||||
if price == 0 and traffic_gb != 0:
|
||||
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
|
||||
return
|
||||
|
||||
if db_user.balance_kopeks < price:
|
||||
await callback.answer("⌛ Недостаточно средств на балансе", show_alert=True)
|
||||
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -798,7 +896,7 @@ async def confirm_add_traffic(
|
||||
)
|
||||
|
||||
if not success:
|
||||
await callback.answer("⌛ Ошибка списания средств", show_alert=True)
|
||||
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
|
||||
return
|
||||
|
||||
if traffic_gb == 0:
|
||||
@@ -853,6 +951,11 @@ async def confirm_add_traffic(
|
||||
|
||||
await callback.answer()
|
||||
|
||||
def update_traffic_prices():
|
||||
from app.config import refresh_traffic_prices
|
||||
refresh_traffic_prices()
|
||||
logger.info("🔄 TRAFFIC_PRICES обновлены из конфигурации")
|
||||
|
||||
|
||||
async def confirm_add_devices(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -864,10 +967,20 @@ async def confirm_add_devices(
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
new_total_devices = subscription.device_limit + devices_count
|
||||
|
||||
if settings.MAX_DEVICES_LIMIT > 0 and new_total_devices > settings.MAX_DEVICES_LIMIT:
|
||||
await callback.answer(
|
||||
f"⚠️ Превышен максимальный лимит устройств ({settings.MAX_DEVICES_LIMIT}). "
|
||||
f"У вас: {subscription.device_limit}, добавляете: {devices_count}",
|
||||
show_alert=True
|
||||
)
|
||||
return
|
||||
|
||||
price = devices_count * settings.PRICE_PER_DEVICE
|
||||
|
||||
if db_user.balance_kopeks < price:
|
||||
await callback.answer("❌ Недостаточно средств на балансе", show_alert=True)
|
||||
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -877,7 +990,7 @@ async def confirm_add_devices(
|
||||
)
|
||||
|
||||
if not success:
|
||||
await callback.answer("❌ Ошибка списания средств", show_alert=True)
|
||||
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
|
||||
return
|
||||
|
||||
await add_subscription_devices(db, subscription, devices_count)
|
||||
@@ -1146,12 +1259,19 @@ async def select_period(
|
||||
data['total_price'] = PERIOD_PRICES[period_days]
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
fixed_traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
|
||||
fixed_traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
|
||||
data['total_price'] += fixed_traffic_price
|
||||
data['traffic_gb'] = settings.get_fixed_traffic_limit()
|
||||
|
||||
await state.set_data(data)
|
||||
|
||||
if settings.is_traffic_selectable():
|
||||
available_packages = [pkg for pkg in settings.get_traffic_packages() if pkg['enabled']]
|
||||
|
||||
if not available_packages:
|
||||
await callback.answer("⚠️ Пакеты трафика не настроены", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_TRAFFIC,
|
||||
reply_markup=get_traffic_packages_keyboard(db_user.language)
|
||||
@@ -1179,6 +1299,56 @@ async def select_period(
|
||||
|
||||
await callback.answer()
|
||||
|
||||
async def refresh_traffic_config():
|
||||
try:
|
||||
from app.config import refresh_traffic_prices
|
||||
refresh_traffic_prices()
|
||||
|
||||
packages = settings.get_traffic_packages()
|
||||
enabled_count = sum(1 for pkg in packages if pkg['enabled'])
|
||||
|
||||
logger.info(f"🔄 Конфигурация трафика обновлена: {enabled_count} активных пакетов")
|
||||
for pkg in packages:
|
||||
if pkg['enabled']:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
logger.info(f" 📦 {gb_text}: {pkg['price']/100}₽")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Ошибка обновления конфигурации трафика: {e}")
|
||||
return False
|
||||
|
||||
async def get_traffic_packages_info() -> str:
|
||||
try:
|
||||
packages = settings.get_traffic_packages()
|
||||
|
||||
info_lines = ["📦 Настроенные пакеты трафика:"]
|
||||
|
||||
enabled_packages = [pkg for pkg in packages if pkg['enabled']]
|
||||
disabled_packages = [pkg for pkg in packages if not pkg['enabled']]
|
||||
|
||||
if enabled_packages:
|
||||
info_lines.append("\n✅ Активные:")
|
||||
for pkg in enabled_packages:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']/100}₽")
|
||||
|
||||
if disabled_packages:
|
||||
info_lines.append("\n❌ Отключенные:")
|
||||
for pkg in disabled_packages:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']/100}₽")
|
||||
|
||||
info_lines.append(f"\n📊 Всего пакетов: {len(packages)}")
|
||||
info_lines.append(f"🟢 Активных: {len(enabled_packages)}")
|
||||
info_lines.append(f"🔴 Отключенных: {len(disabled_packages)}")
|
||||
|
||||
return "\n".join(info_lines)
|
||||
|
||||
except Exception as e:
|
||||
return f"⚠️ Ошибка получения информации: {e}"
|
||||
|
||||
async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSession):
|
||||
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
@@ -1257,7 +1427,10 @@ async def select_traffic(
|
||||
|
||||
data = await state.get_data()
|
||||
data['traffic_gb'] = traffic_gb
|
||||
data['total_price'] += TRAFFIC_PRICES[traffic_gb]
|
||||
|
||||
traffic_price = settings.get_traffic_price(traffic_gb)
|
||||
data['total_price'] += traffic_price
|
||||
|
||||
await state.set_data(data)
|
||||
|
||||
if await _should_show_countries_management():
|
||||
@@ -1288,7 +1461,6 @@ async def select_country(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
country_uuid = callback.data.split('_')[1]
|
||||
data = await state.get_data()
|
||||
|
||||
@@ -1300,7 +1472,7 @@ async def select_country(
|
||||
|
||||
countries = await _get_available_countries()
|
||||
|
||||
base_price = PERIOD_PRICES[data['period_days']] + TRAFFIC_PRICES[data['traffic_gb']]
|
||||
base_price = PERIOD_PRICES[data['period_days']] + settings.get_traffic_price(data['traffic_gb'])
|
||||
|
||||
try:
|
||||
subscription_service = SubscriptionService()
|
||||
@@ -1360,7 +1532,7 @@ async def select_devices(
|
||||
|
||||
base_price = (
|
||||
PERIOD_PRICES[data['period_days']] +
|
||||
TRAFFIC_PRICES[data['traffic_gb']]
|
||||
settings.get_traffic_price(data['traffic_gb'])
|
||||
)
|
||||
|
||||
countries = await _get_available_countries()
|
||||
@@ -1387,7 +1559,7 @@ async def devices_continue(
|
||||
db: AsyncSession
|
||||
):
|
||||
if not callback.data == "devices_continue":
|
||||
await callback.answer("⌛ Некорректный запрос", show_alert=True)
|
||||
await callback.answer("⚠️ Некорректный запрос", show_alert=True)
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
@@ -1410,9 +1582,11 @@ async def devices_continue(
|
||||
base_price = PERIOD_PRICES[data['period_days']]
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
|
||||
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
|
||||
final_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
|
||||
traffic_price = settings.get_traffic_price(data['traffic_gb'])
|
||||
final_traffic_gb = data['traffic_gb']
|
||||
|
||||
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
total_price = base_price + traffic_price + countries_price + devices_price
|
||||
@@ -1421,10 +1595,10 @@ async def devices_continue(
|
||||
await state.set_data(data)
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
if data['traffic_gb'] == 0:
|
||||
if final_traffic_gb == 0:
|
||||
traffic_display = "Безлимитный"
|
||||
else:
|
||||
traffic_display = f"{data['traffic_gb']} ГБ"
|
||||
traffic_display = f"{final_traffic_gb} ГБ"
|
||||
else:
|
||||
if data['traffic_gb'] == 0:
|
||||
traffic_display = "Безлимитный"
|
||||
@@ -1477,10 +1651,10 @@ async def confirm_purchase(
|
||||
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
|
||||
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
|
||||
final_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
|
||||
traffic_price = settings.get_traffic_price(data['traffic_gb'])
|
||||
final_traffic_gb = data['traffic_gb']
|
||||
|
||||
final_price = base_price + traffic_price + countries_price + devices_price
|
||||
@@ -1568,7 +1742,7 @@ async def confirm_purchase(
|
||||
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
if not remnawave_user:
|
||||
logger.error(f"❌ Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
|
||||
logger.error(f"⚠️ Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
|
||||
logger.info(f"🔄 Fallback: принудительное создание нового RemnaWave пользователя")
|
||||
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
@@ -1634,6 +1808,91 @@ async def confirm_purchase(
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
|
||||
async def add_traffic(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
if settings.is_traffic_fixed():
|
||||
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
|
||||
return
|
||||
|
||||
traffic_gb = int(callback.data.split('_')[2])
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
price = settings.get_traffic_price(traffic_gb)
|
||||
|
||||
if price == 0 and traffic_gb != 0:
|
||||
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
|
||||
return
|
||||
|
||||
if db_user.balance_kopeks < price:
|
||||
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
success = await subtract_user_balance(
|
||||
db, db_user, price,
|
||||
f"Добавление {traffic_gb} ГБ трафика"
|
||||
)
|
||||
|
||||
if not success:
|
||||
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
|
||||
return
|
||||
|
||||
if traffic_gb == 0:
|
||||
subscription.traffic_limit_gb = 0
|
||||
else:
|
||||
await add_subscription_traffic(db, subscription, traffic_gb)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
await create_transaction(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
type=TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
amount_kopeks=price,
|
||||
description=f"Добавление {traffic_gb} ГБ трафика"
|
||||
)
|
||||
|
||||
try:
|
||||
await process_referral_purchase(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
purchase_amount_kopeks=price,
|
||||
transaction_id=None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки реферальной покупки: {e}")
|
||||
|
||||
await db.refresh(db_user)
|
||||
await db.refresh(subscription)
|
||||
|
||||
success_text = f"✅ Трафик успешно добавлен!\n\n"
|
||||
if traffic_gb == 0:
|
||||
success_text += "🎉 Теперь у вас безлимитный трафик!"
|
||||
else:
|
||||
success_text += f"📈 Добавлено: {traffic_gb} ГБ\n"
|
||||
success_text += f"Новый лимит: {texts.format_traffic(subscription.traffic_limit_gb)}"
|
||||
|
||||
await callback.message.edit_text(
|
||||
success_text,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
logger.info(f"✅ Пользователь {db_user.telegram_id} добавил {traffic_gb} ГБ трафика")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления трафика: {e}")
|
||||
await callback.message.edit_text(
|
||||
texts.ERROR,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
async def create_paid_subscription_with_traffic_mode(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
@@ -1666,6 +1925,15 @@ async def create_paid_subscription_with_traffic_mode(
|
||||
|
||||
return subscription
|
||||
|
||||
def validate_traffic_price(gb: int) -> bool:
|
||||
from app.config import settings
|
||||
|
||||
price = settings.get_traffic_price(gb)
|
||||
if gb == 0:
|
||||
return True
|
||||
|
||||
return price > 0
|
||||
|
||||
|
||||
async def handle_subscription_settings(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -2455,6 +2723,16 @@ async def handle_specific_app_guide(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
async def handle_no_traffic_packages(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User
|
||||
):
|
||||
await callback.answer(
|
||||
"⚠️ В данный момент нет доступных пакетов трафика. "
|
||||
"Обратитесь в техподдержку для получения информации.",
|
||||
show_alert=True
|
||||
)
|
||||
|
||||
|
||||
async def handle_open_subscription_link(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -2566,6 +2844,7 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
update_traffic_prices()
|
||||
|
||||
dp.callback_query.register(
|
||||
show_subscription_info,
|
||||
@@ -2587,7 +2866,6 @@ def register_handlers(dp: Dispatcher):
|
||||
F.data.in_(["menu_buy", "subscription_upgrade"])
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_add_countries,
|
||||
F.data == "subscription_add_countries"
|
||||
@@ -2643,7 +2921,6 @@ def register_handlers(dp: Dispatcher):
|
||||
F.data == "confirm_reset_devices"
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
select_period,
|
||||
F.data.startswith("period_"),
|
||||
@@ -2674,7 +2951,6 @@ def register_handlers(dp: Dispatcher):
|
||||
SubscriptionStates.confirming_purchase
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_autopay_menu,
|
||||
F.data == "subscription_autopay"
|
||||
@@ -2756,3 +3032,8 @@ def register_handlers(dp: Dispatcher):
|
||||
handle_subscription_settings,
|
||||
F.data == "subscription_settings"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_no_traffic_packages,
|
||||
F.data == "no_traffic_packages"
|
||||
)
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
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
|
||||
@@ -180,7 +180,10 @@ def get_user_management_keyboard(user_id: int, user_status: str, language: str =
|
||||
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_servers_{user_id}"),
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_user_statistics_{user_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📋 Транзакции", callback_data=f"admin_user_transactions_{user_id}")
|
||||
]
|
||||
]
|
||||
@@ -578,7 +581,6 @@ def get_admin_pagination_keyboard(
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_maintenance_keyboard(language: str = "ru", is_active: bool = False, monitoring_active: bool = False) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура для управления техработами"""
|
||||
|
||||
if language == "en":
|
||||
toggle_text = "🔴 Disable maintenance" if is_active else "🔧 Enable maintenance"
|
||||
|
||||
+88
-29
@@ -242,28 +242,57 @@ def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMark
|
||||
|
||||
|
||||
def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
return get_back_keyboard(language)
|
||||
|
||||
logger.info(f"🔍 RAW CONFIG: '{settings.TRAFFIC_PACKAGES_CONFIG}'")
|
||||
|
||||
all_packages = settings.get_traffic_packages()
|
||||
logger.info(f"🔍 ALL PACKAGES: {all_packages}")
|
||||
|
||||
enabled_packages = [pkg for pkg in all_packages if pkg['enabled']]
|
||||
disabled_packages = [pkg for pkg in all_packages if not pkg['enabled']]
|
||||
|
||||
logger.info(f"🔍 ENABLED: {len(enabled_packages)} packages")
|
||||
logger.info(f"🔍 DISABLED: {len(disabled_packages)} packages")
|
||||
|
||||
for pkg in disabled_packages:
|
||||
logger.info(f"🔍 DISABLED PACKAGE: {pkg['gb']}GB = {pkg['price']} kopeks, enabled={pkg['enabled']}")
|
||||
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
packages = [
|
||||
(5, texts.TRAFFIC_5GB),
|
||||
(10, texts.TRAFFIC_10GB),
|
||||
(25, texts.TRAFFIC_25GB),
|
||||
(50, texts.TRAFFIC_50GB),
|
||||
(100, texts.TRAFFIC_100GB),
|
||||
(250, texts.TRAFFIC_250GB),
|
||||
(0, texts.TRAFFIC_UNLIMITED)
|
||||
]
|
||||
traffic_packages = settings.get_traffic_packages()
|
||||
|
||||
for gb, text in packages:
|
||||
for package in traffic_packages:
|
||||
gb = package["gb"]
|
||||
price = package["price"]
|
||||
enabled = package["enabled"]
|
||||
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
if gb == 0:
|
||||
text = f"♾️ Безлимит - {settings.format_price(package['price'])}"
|
||||
else:
|
||||
text = f"📊 {gb} ГБ - {settings.format_price(package['price'])}"
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=text, callback_data=f"traffic_{gb}")
|
||||
])
|
||||
|
||||
if not keyboard:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Пакеты трафика не настроены",
|
||||
callback_data="no_traffic_packages"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="subscription_config_back")
|
||||
@@ -271,7 +300,6 @@ def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def get_countries_keyboard(countries: List[dict], selected: List[str], language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
@@ -315,21 +343,30 @@ def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMa
|
||||
keyboard = []
|
||||
|
||||
start_devices = settings.DEFAULT_DEVICE_LIMIT
|
||||
end_devices = max(start_devices + 3, 6)
|
||||
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 50
|
||||
end_devices = min(max_devices + 1, start_devices + 10)
|
||||
|
||||
buttons = []
|
||||
|
||||
for devices in range(start_devices, end_devices):
|
||||
price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
price_text = f" (+{texts.format_price(price)})" if price > 0 else " (включено)"
|
||||
price_text = f" (+{texts.format_price(price)})" if price > 0 else " (вкл.)"
|
||||
emoji = "✅" if devices == current else "⚪"
|
||||
|
||||
device_word = _get_device_declension(devices)
|
||||
button_text = f"{emoji} {devices}{price_text}"
|
||||
|
||||
keyboard.append([
|
||||
buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{emoji} {devices} {device_word}{price_text}",
|
||||
text=button_text,
|
||||
callback_data=f"devices_{devices}"
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
for i in range(0, len(buttons), 2):
|
||||
if i + 1 < len(buttons):
|
||||
keyboard.append([buttons[i], buttons[i + 1]])
|
||||
else:
|
||||
keyboard.append([buttons[i]])
|
||||
|
||||
keyboard.extend([
|
||||
[InlineKeyboardButton(text="✅ Продолжить", callback_data="devices_continue")],
|
||||
@@ -628,34 +665,48 @@ def get_add_traffic_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
packages = [
|
||||
(5, f"📊 +5 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_5GB)}"),
|
||||
(10, f"📊 +10 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_10GB)}"),
|
||||
(25, f"📊 +25 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_25GB)}"),
|
||||
(50, f"📊 +50 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_50GB)}"),
|
||||
(100, f"📊 +100 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}"),
|
||||
(0, f"📊 Безлимит - {settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)}")
|
||||
]
|
||||
traffic_packages = settings.get_traffic_packages()
|
||||
|
||||
for gb, text in packages:
|
||||
for package in traffic_packages:
|
||||
gb = package["gb"]
|
||||
price = package["price"]
|
||||
enabled = package["enabled"]
|
||||
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
if gb == 0:
|
||||
text = f"📊 Безлимит - {settings.format_price(package['price'])}"
|
||||
else:
|
||||
text = f"📊 +{gb} ГБ - {settings.format_price(package['price'])}"
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=text, callback_data=f"add_traffic_{gb}")
|
||||
])
|
||||
|
||||
if not keyboard:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Пакеты трафика не настроены",
|
||||
callback_data="no_traffic_packages"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
max_devices = 10
|
||||
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 100
|
||||
|
||||
for add_count in range(1, min(6, max_devices - current_devices + 1)):
|
||||
max_add = min(5, max_devices - current_devices)
|
||||
|
||||
for add_count in range(1, max_add + 1):
|
||||
price = add_count * settings.PRICE_PER_DEVICE
|
||||
total_devices = current_devices + add_count
|
||||
|
||||
@@ -668,6 +719,14 @@ def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> Inli
|
||||
)
|
||||
])
|
||||
|
||||
if max_add == 0:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Достигнут максимум устройств",
|
||||
callback_data="max_devices_reached"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
|
||||
])
|
||||
|
||||
@@ -107,9 +107,8 @@ class RussianTexts(Texts):
|
||||
REFERRAL_CODE_SKIP = "⏭️ Пропустить"
|
||||
|
||||
MAIN_MENU = """👤 <b>{user_name}</b>
|
||||
━━━━━━━━━━━━━━━━━
|
||||
|
||||
📱 <b>Подписка:</b> {subscription_status}
|
||||
━━━━━━━━━━━━━━━━━
|
||||
|
||||
Выберите действие:
|
||||
"""
|
||||
|
||||
@@ -37,6 +37,7 @@ class MonitoringService:
|
||||
self.payment_service = PaymentService()
|
||||
self.bot = bot
|
||||
self._notified_users: Set[str] = set()
|
||||
self._last_cleanup = datetime.utcnow()
|
||||
|
||||
async def start_monitoring(self):
|
||||
if self.is_running:
|
||||
@@ -62,6 +63,8 @@ class MonitoringService:
|
||||
async def _monitoring_cycle(self):
|
||||
async for db in get_db():
|
||||
try:
|
||||
await self._cleanup_notification_cache()
|
||||
|
||||
await self._check_expired_subscriptions(db)
|
||||
await self._check_expiring_subscriptions(db)
|
||||
await self._check_trial_expiring_soon(db)
|
||||
@@ -69,10 +72,6 @@ class MonitoringService:
|
||||
await self._cleanup_inactive_users(db)
|
||||
await self._sync_with_remnawave(db)
|
||||
|
||||
current_hour = datetime.utcnow().hour
|
||||
if current_hour == 0:
|
||||
self._notified_users.clear()
|
||||
|
||||
await self._log_monitoring_event(
|
||||
db, "monitoring_cycle_completed",
|
||||
"Цикл мониторинга успешно завершен",
|
||||
@@ -90,6 +89,15 @@ class MonitoringService:
|
||||
finally:
|
||||
break
|
||||
|
||||
async def _cleanup_notification_cache(self):
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
if (current_time - self._last_cleanup).total_seconds() >= 3600:
|
||||
old_count = len(self._notified_users)
|
||||
self._notified_users.clear()
|
||||
self._last_cleanup = current_time
|
||||
logger.info(f"🧹 Очищен кеш уведомлений ({old_count} записей)")
|
||||
|
||||
async def _check_expired_subscriptions(self, db: AsyncSession):
|
||||
try:
|
||||
expired_subscriptions = await get_expired_subscriptions(db)
|
||||
@@ -168,30 +176,52 @@ class MonitoringService:
|
||||
async def _check_expiring_subscriptions(self, db: AsyncSession):
|
||||
try:
|
||||
warning_days = settings.get_autopay_warning_days()
|
||||
all_processed_users = set()
|
||||
|
||||
for days in warning_days:
|
||||
expiring_subscriptions = await self._get_expiring_paid_subscriptions(db, days)
|
||||
sent_count = 0
|
||||
|
||||
for subscription in expiring_subscriptions:
|
||||
user = await get_user_by_id(db, subscription.user_id)
|
||||
if not user:
|
||||
continue
|
||||
|
||||
notification_key = f"expiring_{user.telegram_id}_{days}d"
|
||||
if notification_key in self._notified_users:
|
||||
continue
|
||||
notification_key = f"expiring_{user.telegram_id}_{days}d_{subscription.id}"
|
||||
user_key = f"user_{user.telegram_id}_today"
|
||||
|
||||
if (notification_key in self._notified_users or
|
||||
user_key in all_processed_users):
|
||||
logger.debug(f"🔄 Пропускаем дублирование для пользователя {user.telegram_id} на {days} дней")
|
||||
continue
|
||||
|
||||
should_send = True
|
||||
for other_days in warning_days:
|
||||
if other_days < days:
|
||||
other_subs = await self._get_expiring_paid_subscriptions(db, other_days)
|
||||
if any(s.user_id == user.id for s in other_subs):
|
||||
should_send = False
|
||||
logger.debug(f"🎯 Пропускаем уведомление на {days} дней для пользователя {user.telegram_id}, есть более срочное на {other_days} дней")
|
||||
break
|
||||
|
||||
if not should_send:
|
||||
continue
|
||||
|
||||
if self.bot:
|
||||
await self._send_subscription_expiring_notification(user, subscription, days)
|
||||
self._notified_users.add(notification_key)
|
||||
|
||||
logger.info(f"⚠️ Пользователю {user.telegram_id} отправлено уведомление об истечении подписки через {days} дней")
|
||||
success = await self._send_subscription_expiring_notification(user, subscription, days)
|
||||
if success:
|
||||
self._notified_users.add(notification_key)
|
||||
all_processed_users.add(user_key)
|
||||
sent_count += 1
|
||||
logger.info(f"✅ Пользователю {user.telegram_id} отправлено уведомление об истечении подписки через {days} дней")
|
||||
else:
|
||||
logger.warning(f"❌ Не удалось отправить уведомление пользователю {user.telegram_id}")
|
||||
|
||||
if expiring_subscriptions:
|
||||
if sent_count > 0:
|
||||
await self._log_monitoring_event(
|
||||
db, "expiring_notifications_sent",
|
||||
f"Отправлено {len(expiring_subscriptions)} уведомлений об истечении через {days} дней",
|
||||
{"days": days, "count": len(expiring_subscriptions)}
|
||||
f"Отправлено {sent_count} уведомлений об истечении через {days} дней",
|
||||
{"days": days, "count": sent_count}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -220,15 +250,15 @@ class MonitoringService:
|
||||
if not user:
|
||||
continue
|
||||
|
||||
notification_key = f"trial_2h_{user.telegram_id}"
|
||||
notification_key = f"trial_2h_{user.telegram_id}_{subscription.id}"
|
||||
if notification_key in self._notified_users:
|
||||
continue
|
||||
|
||||
if self.bot:
|
||||
await self._send_trial_ending_notification(user, subscription)
|
||||
self._notified_users.add(notification_key)
|
||||
|
||||
logger.info(f"🎁 Пользователю {user.telegram_id} отправлено уведомление об окончании тестовой подписки через 2 часа")
|
||||
success = await self._send_trial_ending_notification(user, subscription)
|
||||
if success:
|
||||
self._notified_users.add(notification_key)
|
||||
logger.info(f"🎁 Пользователю {user.telegram_id} отправлено уведомление об окончании тестовой подписки через 2 часа")
|
||||
|
||||
if trial_expiring:
|
||||
await self._log_monitoring_event(
|
||||
@@ -257,9 +287,9 @@ class MonitoringService:
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"🔍 Поиск платных подписок, истекающих в ближайшие {days_before} дней")
|
||||
logger.info(f"📅 Текущее время: {current_time}")
|
||||
logger.info(f"📅 Пороговая дата: {threshold_date}")
|
||||
logger.debug(f"🔍 Поиск платных подписок, истекающих в ближайшие {days_before} дней")
|
||||
logger.debug(f"📅 Текущее время: {current_time}")
|
||||
logger.debug(f"📅 Пороговая дата: {threshold_date}")
|
||||
|
||||
subscriptions = result.scalars().all()
|
||||
logger.info(f"📊 Найдено {len(subscriptions)} платных подписок для уведомлений")
|
||||
@@ -340,15 +370,15 @@ class MonitoringService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки автоплатежей: {e}")
|
||||
|
||||
async def _send_subscription_expired_notification(self, user: User):
|
||||
async def _send_subscription_expired_notification(self, user: User) -> bool:
|
||||
try:
|
||||
message = """
|
||||
❌ <b>Подписка истекла</b>
|
||||
⛔ <b>Подписка истекла</b>
|
||||
|
||||
Ваша подписка истекла. Для восстановления доступа продлите подписку.
|
||||
Ваша подписка истекла. Для восстановления доступа продлите подписку.
|
||||
|
||||
🔧 Доступ к серверам заблокирован до продления.
|
||||
"""
|
||||
🔧 Доступ к серверам заблокирован до продления.
|
||||
"""
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
@@ -363,11 +393,13 @@ class MonitoringService:
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
async def _send_subscription_expiring_notification(self, user: User, subscription: Subscription, days: int):
|
||||
async def _send_subscription_expiring_notification(self, user: User, subscription: Subscription, days: int) -> bool:
|
||||
try:
|
||||
from app.utils.formatters import format_days_declension
|
||||
|
||||
@@ -382,14 +414,14 @@ class MonitoringService:
|
||||
action_text = "💡 Включите автоплатеж или продлите подписку вручную"
|
||||
|
||||
message = f"""
|
||||
⚠️ <b>Подписка истекает через {days_text}!</b>
|
||||
⚠️ <b>Подписка истекает через {days_text}!</b>
|
||||
|
||||
Ваша платная подписка истекает {subscription.end_date.strftime("%d.%m.%Y %H:%M")}.
|
||||
Ваша платная подписка истекает {subscription.end_date.strftime("%d.%m.%Y %H:%M")}.
|
||||
|
||||
💳 <b>Автоплатеж:</b> {autopay_status}
|
||||
💳 <b>Автоплатеж:</b> {autopay_status}
|
||||
|
||||
{action_text}
|
||||
"""
|
||||
{action_text}
|
||||
"""
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
@@ -405,30 +437,32 @@ class MonitoringService:
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
async def _send_trial_ending_notification(self, user: User, subscription: Subscription):
|
||||
async def _send_trial_ending_notification(self, user: User, subscription: Subscription) -> bool:
|
||||
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 устройств
|
||||
|
||||
⚡️ Успейте оформить до окончания тестового периода!
|
||||
"""
|
||||
⚡️ Успейте оформить до окончания тестового периода!
|
||||
"""
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
@@ -443,9 +477,11 @@ class MonitoringService:
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об окончании тестовой подписки пользователю {user.telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
async def _send_autopay_success_notification(self, user: User, amount: int, days: int):
|
||||
try:
|
||||
@@ -554,7 +590,7 @@ class MonitoringService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка логирования события мониторинга: {e}")
|
||||
|
||||
|
||||
async def get_monitoring_status(self, db: AsyncSession) -> Dict[str, Any]:
|
||||
try:
|
||||
from sqlalchemy import select, desc
|
||||
|
||||
@@ -229,36 +229,40 @@ class SubscriptionService:
|
||||
devices: int,
|
||||
db: AsyncSession
|
||||
) -> Tuple[int, List[int]]:
|
||||
|
||||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
|
||||
|
||||
from app.config import PERIOD_PRICES
|
||||
from app.database.crud.server_squad import get_server_squad_by_id
|
||||
|
||||
|
||||
if settings.MAX_DEVICES_LIMIT > 0 and devices > settings.MAX_DEVICES_LIMIT:
|
||||
raise ValueError(f"Превышен максимальный лимит устройств: {settings.MAX_DEVICES_LIMIT}")
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
traffic_price = TRAFFIC_PRICES.get(traffic_gb, 0)
|
||||
|
||||
|
||||
traffic_price = settings.get_traffic_price(traffic_gb)
|
||||
|
||||
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}₽")
|
||||
logger.debug(f"Сервер {server.display_name}: {server.price_kopeks/100}₽")
|
||||
else:
|
||||
server_prices.append(0)
|
||||
logger.warning(f"⚠️ Сервер ID {server_id} недоступен")
|
||||
|
||||
logger.warning(f"Сервер ID {server_id} недоступен")
|
||||
|
||||
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * 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}₽")
|
||||
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
|
||||
|
||||
@@ -269,7 +273,7 @@ class SubscriptionService:
|
||||
db: AsyncSession
|
||||
) -> int:
|
||||
try:
|
||||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
|
||||
from app.config import PERIOD_PRICES
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
@@ -279,7 +283,7 @@ class SubscriptionService:
|
||||
|
||||
devices_price = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
|
||||
traffic_price = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
|
||||
traffic_price = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
|
||||
total_price = base_price + servers_price + devices_price + traffic_price
|
||||
|
||||
|
||||
@@ -2,7 +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 sqlalchemy import delete, select, update
|
||||
|
||||
from app.database.crud.user import (
|
||||
get_user_by_id, get_user_by_telegram_id, get_users_list,
|
||||
@@ -13,7 +13,7 @@ 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, Subscription, Transaction, PromoCodeUse,
|
||||
ReferralEarning, SubscriptionServer
|
||||
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
@@ -242,9 +242,81 @@ class UserService:
|
||||
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} деактивирован")
|
||||
logger.info(f" ✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка деактивации RemnaWave пользователя: {e}")
|
||||
logger.warning(f" ⚠️ Ошибка деактивации RemnaWave: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import YooKassaPayment
|
||||
from sqlalchemy import select
|
||||
|
||||
yookassa_result = await db.execute(
|
||||
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
yookassa_payments = yookassa_result.scalars().all()
|
||||
|
||||
if yookassa_payments:
|
||||
logger.info(f" 🔄 Удаляем {len(yookassa_payments)} YooKassa платежей")
|
||||
await db.execute(
|
||||
delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" ✅ YooKassa платежи удалены")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления YooKassa платежей: {e}")
|
||||
|
||||
try:
|
||||
transactions_result = await db.execute(
|
||||
select(Transaction).where(Transaction.user_id == user_id)
|
||||
)
|
||||
transactions = transactions_result.scalars().all()
|
||||
|
||||
if transactions:
|
||||
logger.info(f" 🔄 Удаляем {len(transactions)} транзакций")
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" ✅ Транзакции удалены")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления транзакций: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
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)
|
||||
)
|
||||
await db.flush()
|
||||
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)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалены реферальные записи о пользователе {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления реферальных записей: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import BroadcastHistory
|
||||
await db.execute(
|
||||
delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалена история рассылок админа {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления истории рассылок: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
@@ -253,74 +325,47 @@ class UserService:
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
logger.error(f" ❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
await db.delete(user.subscription)
|
||||
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
|
||||
from app.database.models import Subscription
|
||||
await db.execute(
|
||||
delete(Subscription).where(Subscription.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
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}")
|
||||
logger.error(f" ❌ Ошибка удаления подписки: {e}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
await db.execute(
|
||||
referrals_result = await db.execute(
|
||||
update(User)
|
||||
.where(User.referred_by_id == user.id)
|
||||
.where(User.referred_by_id == user_id)
|
||||
.values(referred_by_id=None)
|
||||
)
|
||||
logger.info(f"🗑️ Очищены реферальные ссылки у рефералов пользователя {user_id}")
|
||||
if referrals_result.rowcount > 0:
|
||||
logger.info(f" 🔗 Очищены реферальные ссылки у {referrals_result.rowcount} рефералов")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
logger.error(f" ❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user.id)
|
||||
delete(User).where(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} помечен как удаленный и обнулен")
|
||||
logger.info(f" ✅ Пользователь {user_id} окончательно удален из базы")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления статуса пользователя: {e}")
|
||||
logger.error(f" ❌ Ошибка финального удаления пользователя: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
logger.info(f"✅ Пользователь {user_id} (Telegram ID: {user.telegram_id}) полностью удален админом {admin_id}")
|
||||
logger.info(f"✅ Пользователь {user.telegram_id} (ID: {user_id}) полностью удален администратором {admin_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -52,6 +52,8 @@ class AdminStates(StatesGroup):
|
||||
editing_squad_price = State()
|
||||
editing_traffic_price = State()
|
||||
editing_device_price = State()
|
||||
editing_user_devices = State()
|
||||
editing_user_traffic = State()
|
||||
|
||||
editing_rules_page = State()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user