Files
remnawave-bedolaga-telegram…/app/handlers/admin/subscriptions.py
Egor 736e4c6cae NEW VERSION
NEW VERSION
2025-08-20 23:57:04 +03:00

498 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.config import settings
from app.states import AdminStates
from app.database.models import User
from app.keyboards.admin import get_admin_subscriptions_keyboard
from app.localization.texts import get_texts
from app.database.crud.subscription import (
get_expiring_subscriptions, get_subscriptions_statistics, get_expired_subscriptions,
get_all_subscriptions
)
from app.services.subscription_service import SubscriptionService
from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_time_ago
def get_country_flag(country_name: str) -> str:
flags = {
'USA': '🇺🇸', 'United States': '🇺🇸', 'US': '🇺🇸',
'Germany': '🇩🇪', 'DE': '🇩🇪', 'Deutschland': '🇩🇪',
'Netherlands': '🇳🇱', 'NL': '🇳🇱', 'Holland': '🇳🇱',
'United Kingdom': '🇬🇧', 'UK': '🇬🇧', 'GB': '🇬🇧',
'Japan': '🇯🇵', 'JP': '🇯🇵',
'France': '🇫🇷', 'FR': '🇫🇷',
'Canada': '🇨🇦', 'CA': '🇨🇦',
'Russia': '🇷🇺', 'RU': '🇷🇺',
'Singapore': '🇸🇬', 'SG': '🇸🇬',
}
return flags.get(country_name, '🌍')
async def get_users_by_countries(db: AsyncSession) -> dict:
try:
result = await db.execute(
select(User.preferred_location, func.count(User.id))
.where(User.preferred_location.isnot(None))
.group_by(User.preferred_location)
)
stats = {}
for location, count in result.fetchall():
if location:
stats[location] = count
return stats
except Exception as e:
logger.error(f"Ошибка получения статистики по странам: {e}")
return {}
logger = logging.getLogger(__name__)
@admin_required
@error_handler
async def show_subscriptions_menu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
stats = await get_subscriptions_statistics(db)
text = f"""
📱 <b>Управление подписками</b>
📊 <b>Статистика:</b>
- Всего: {stats['total_subscriptions']}
- Активных: {stats['active_subscriptions']}
- Платных: {stats['paid_subscriptions']}
- Триальных: {stats['trial_subscriptions']}
📈 <b>Продажи:</b>
- Сегодня: {stats['purchased_today']}
- За неделю: {stats['purchased_week']}
- За месяц: {stats['purchased_month']}
Выберите действие:
"""
keyboard = [
[
types.InlineKeyboardButton(text="📋 Список подписок", callback_data="admin_subs_list"),
types.InlineKeyboardButton(text="⏰ Истекающие", callback_data="admin_subs_expiring")
],
[
types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_subs_stats"),
types.InlineKeyboardButton(text="💰 Настройки цен", callback_data="admin_subs_pricing")
],
[
types.InlineKeyboardButton(text="🌐 Управление серверами", callback_data="admin_servers"),
types.InlineKeyboardButton(text="🌍 География", callback_data="admin_subs_countries")
],
[
types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_subscriptions_list(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
page: int = 1
):
subscriptions, total_count = await get_all_subscriptions(db, page=page, limit=10)
total_pages = (total_count + 9) // 10
if not subscriptions:
text = "📱 <b>Список подписок</b>\n\n❌ Подписки не найдены."
else:
text = f"📱 <b>Список подписок</b>\n\n"
text += f"📊 Всего: {total_count} | Страница: {page}/{total_pages}\n\n"
for i, sub in enumerate(subscriptions, 1 + (page - 1) * 10):
user_info = f"ID{sub.user.telegram_id}" if sub.user else "Неизвестно"
sub_type = "🎁" if sub.is_trial else "💎"
status = "✅ Активна" if sub.is_active else "❌ Неактивна"
text += f"{i}. {sub_type} {user_info}\n"
text += f" {status} | До: {format_datetime(sub.end_date)}\n"
if sub.device_limit > 0:
text += f" 📱 Устройств: {sub.device_limit}\n"
text += "\n"
keyboard = []
if total_pages > 1:
nav_row = []
if page > 1:
nav_row.append(types.InlineKeyboardButton(
text="⬅️", callback_data=f"admin_subs_list_page_{page-1}"
))
nav_row.append(types.InlineKeyboardButton(
text=f"{page}/{total_pages}", callback_data="current_page"
))
if page < total_pages:
nav_row.append(types.InlineKeyboardButton(
text="➡️", callback_data=f"admin_subs_list_page_{page+1}"
))
keyboard.append(nav_row)
keyboard.extend([
[types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_list")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_expiring_subscriptions(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
expiring_3d = await get_expiring_subscriptions(db, 3)
expiring_1d = await get_expiring_subscriptions(db, 1)
expired = await get_expired_subscriptions(db)
text = f"""
⏰ <b>Истекающие подписки</b>
📊 <b>Статистика:</b>
- Истекают через 3 дня: {len(expiring_3d)}
- Истекают завтра: {len(expiring_1d)}
- Уже истекли: {len(expired)}
<b>Истекают через 3 дня:</b>
"""
for sub in expiring_3d[:5]:
user_info = f"ID{sub.user.telegram_id}" if sub.user else "Неизвестно"
sub_type = "🎁" if sub.is_trial else "💎"
text += f"{sub_type} {user_info} - {format_datetime(sub.end_date)}\n"
if len(expiring_3d) > 5:
text += f"... и еще {len(expiring_3d) - 5}\n"
text += f"\n<b>Истекают завтра:</b>\n"
for sub in expiring_1d[:5]:
user_info = f"ID{sub.user.telegram_id}" if sub.user else "Неизвестно"
sub_type = "🎁" if sub.is_trial else "💎"
text += f"{sub_type} {user_info} - {format_datetime(sub.end_date)}\n"
if len(expiring_1d) > 5:
text += f"... и еще {len(expiring_1d) - 5}\n"
keyboard = [
[types.InlineKeyboardButton(text="📨 Отправить напоминания", callback_data="admin_send_expiry_reminders")],
[types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_expiring")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_subscriptions_stats(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
stats = await get_subscriptions_statistics(db)
expiring_3d = await get_expiring_subscriptions(db, 3)
expiring_7d = await get_expiring_subscriptions(db, 7)
expired = await get_expired_subscriptions(db)
text = f"""
📊 <b>Детальная статистика подписок</b>
<b>📱 Общая информация:</b>
Всего подписок: {stats['total_subscriptions']}
• Активных: {stats['active_subscriptions']}
• Неактивных: {stats['total_subscriptions'] - stats['active_subscriptions']}
<b>💎 По типам:</b>
• Платных: {stats['paid_subscriptions']}
• Триальных: {stats['trial_subscriptions']}
<b>📈 Продажи:</b>
• Сегодня: {stats['purchased_today']}
За неделю: {stats['purchased_week']}
За месяц: {stats['purchased_month']}
<b>⏰ Истечение:</b>
• Истекают через 3 дня: {len(expiring_3d)}
• Истекают через 7 дней: {len(expiring_7d)}
• Уже истекли: {len(expired)}
<b>💰 Конверсия:</b>
• Из триала в платную: {stats.get('trial_to_paid_conversion', 0)}%
• Продлений: {stats.get('renewals_count', 0)}
"""
keyboard = [
[
types.InlineKeyboardButton(text="📊 Экспорт данных", callback_data="admin_subs_export"),
types.InlineKeyboardButton(text="📈 Графики", callback_data="admin_subs_charts")
],
[types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_stats")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_pricing_settings(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
text = f"""
⚙️ <b>Настройки цен</b>
<b>Периоды подписки:</b>
- 14 дней: {settings.format_price(settings.PRICE_14_DAYS)}
- 30 дней: {settings.format_price(settings.PRICE_30_DAYS)}
- 60 дней: {settings.format_price(settings.PRICE_60_DAYS)}
- 90 дней: {settings.format_price(settings.PRICE_90_DAYS)}
- 180 дней: {settings.format_price(settings.PRICE_180_DAYS)}
- 360 дней: {settings.format_price(settings.PRICE_360_DAYS)}
<b>Трафик-пакеты:</b>
- 5 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_5GB)}
- 10 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_10GB)}
- 25 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_25GB)}
- 50 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_50GB)}
- 100 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_100GB)}
- 250 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_250GB)}
<b>Дополнительно:</b>
- За устройство: {settings.format_price(settings.PRICE_PER_DEVICE)}
"""
keyboard = [
[
types.InlineKeyboardButton(text="📅 Периоды", callback_data="admin_edit_period_prices"),
types.InlineKeyboardButton(text="📈 Трафик", callback_data="admin_edit_traffic_prices")
],
[
types.InlineKeyboardButton(text="📱 Устройства", callback_data="admin_edit_device_price")
],
[
types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")
]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_countries_management(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
try:
from app.services.remnawave_service import RemnaWaveService
remnawave_service = RemnaWaveService()
nodes_data = await remnawave_service.get_all_nodes()
squads_data = await remnawave_service.get_all_squads()
text = "🌍 <b>Управление странами</b>\n\n"
if nodes_data:
text += "<b>Доступные серверы:</b>\n"
countries = {}
for node in nodes_data:
country_code = node.get('country_code', 'XX')
country_name = country_code
if country_name not in countries:
countries[country_name] = []
countries[country_name].append(node)
for country, nodes in countries.items():
active_nodes = len([n for n in nodes if n.get('is_connected') and n.get('is_node_online')])
total_nodes = len(nodes)
country_flag = get_country_flag(country)
text += f"{country_flag} {country}: {active_nodes}/{total_nodes} серверов\n"
total_users_online = sum(n.get('users_online', 0) or 0 for n in nodes)
if total_users_online > 0:
text += f" 👥 Пользователей онлайн: {total_users_online}\n"
else:
text += "Не удалось загрузить данные о серверах\n"
if squads_data:
text += f"\n<b>Всего сквадов:</b> {len(squads_data)}\n"
total_members = sum(squad.get('members_count', 0) for squad in squads_data)
text += f"<b>Участников в сквадах:</b> {total_members}\n"
text += "\n<b>Сквады:</b>\n"
for squad in squads_data[:5]:
name = squad.get('name', 'Неизвестно')
members = squad.get('members_count', 0)
inbounds = squad.get('inbounds_count', 0)
text += f"{name}: {members} участников, {inbounds} inbound(s)\n"
if len(squads_data) > 5:
text += f"... и еще {len(squads_data) - 5} сквадов\n"
user_stats = await get_users_by_countries(db)
if user_stats:
text += "\n<b>Пользователи по регионам:</b>\n"
for country, count in user_stats.items():
country_flag = get_country_flag(country)
text += f"{country_flag} {country}: {count} пользователей\n"
except Exception as e:
logger.error(f"Ошибка получения данных о странах: {e}")
text = f"""
🌍 <b>Управление странами</b>
❌ <b>Ошибка загрузки данных</b>
Не удалось получить информацию о серверах.
Проверьте подключение к RemnaWave API.
<b>Детали ошибки:</b> {str(e)}
"""
keyboard = [
[
types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_countries"),
types.InlineKeyboardButton(text="⚙️ API настройки", callback_data="admin_rw_api")
],
[
types.InlineKeyboardButton(text="📊 Статистика нод", callback_data="admin_rw_nodes"),
types.InlineKeyboardButton(text="🔧 Сквады", callback_data="admin_rw_squads")
],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def send_expiry_reminders(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
await callback.message.edit_text(
"📨 Отправка напоминаний...\n\nПодождите, это может занять время.",
reply_markup=None
)
expiring_subs = await get_expiring_subscriptions(db, 1)
sent_count = 0
for subscription in expiring_subs:
if subscription.user:
try:
user = subscription.user
days_left = max(1, subscription.days_left)
reminder_text = f"""
⚠️ <b>Подписка истекает!</b>
Ваша подписка истекает через {days_left} день(а).
Не забудьте продлить подписку, чтобы не потерять доступ к серверам.
💎 Продлить подписку можно в главном меню.
"""
await callback.bot.send_message(
chat_id=user.telegram_id,
text=reminder_text
)
sent_count += 1
except Exception as e:
logger.error(f"Ошибка отправки напоминания пользователю {subscription.user_id}: {e}")
await callback.message.edit_text(
f"✅ Напоминания отправлены: {sent_count} из {len(expiring_subs)}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subs_expiring")]
])
)
await callback.answer()
@admin_required
@error_handler
async def handle_subscriptions_pagination(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
page = int(callback.data.split('_')[-1])
await show_subscriptions_list(callback, db_user, db, page)
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_subscriptions_menu, F.data == "admin_subscriptions")
dp.callback_query.register(show_subscriptions_list, F.data == "admin_subs_list")
dp.callback_query.register(show_expiring_subscriptions, F.data == "admin_subs_expiring")
dp.callback_query.register(show_subscriptions_stats, F.data == "admin_subs_stats")
dp.callback_query.register(show_pricing_settings, F.data == "admin_subs_pricing")
dp.callback_query.register(show_countries_management, F.data == "admin_subs_countries")
dp.callback_query.register(send_expiry_reminders, F.data == "admin_send_expiry_reminders")
dp.callback_query.register(
handle_subscriptions_pagination,
F.data.startswith("admin_subs_list_page_")
)