import logging from aiogram import Dispatcher, F, types from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.database.crud.subscription import ( get_all_subscriptions, get_expired_subscriptions, get_expiring_subscriptions, get_subscriptions_statistics, ) from app.database.models import User from app.utils.decorators import admin_required, error_handler from app.utils.formatters import format_datetime 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""" 📱 Управление подписками 📊 Статистика: - Всего: {stats['total_subscriptions']} - Активных: {stats['active_subscriptions']} - Платных: {stats['paid_subscriptions']} - Триальных: {stats['trial_subscriptions']} 📈 Продажи: - Сегодня: {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_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 = '📱 Список подписок\n\n❌ Подписки не найдены.' else: text = '📱 Список подписок\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.telegram_id else sub.user.email or f'#{sub.user.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""" ⏰ Истекающие подписки 📊 Статистика: - Истекают через 3 дня: {len(expiring_3d)} - Истекают завтра: {len(expiring_1d)} - Уже истекли: {len(expired)} Истекают через 3 дня: """ for sub in expiring_3d[:5]: user_info = ( (f'ID{sub.user.telegram_id}' if sub.user.telegram_id else sub.user.email or f'#{sub.user.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 += '\nИстекают завтра:\n' for sub in expiring_1d[:5]: user_info = ( (f'ID{sub.user.telegram_id}' if sub.user.telegram_id else sub.user.email or f'#{sub.user.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""" 📊 Детальная статистика подписок 📱 Общая информация: • Всего подписок: {stats['total_subscriptions']} • Активных: {stats['active_subscriptions']} • Неактивных: {stats['total_subscriptions'] - stats['active_subscriptions']} 💎 По типам: • Платных: {stats['paid_subscriptions']} • Триальных: {stats['trial_subscriptions']} 📈 Продажи: • Сегодня: {stats['purchased_today']} • За неделю: {stats['purchased_week']} • За месяц: {stats['purchased_month']} ⏰ Истечение: • Истекают через 3 дня: {len(expiring_3d)} • Истекают через 7 дней: {len(expiring_7d)} • Уже истекли: {len(expired)} 💰 Конверсия: • Из триала в платную: {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_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 = '🌍 Управление странами\n\n' if nodes_data: text += 'Доступные серверы:\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Всего сквадов: {len(squads_data)}\n' total_members = sum(squad.get('members_count', 0) for squad in squads_data) text += f'Участников в сквадах: {total_members}\n' text += '\nСквады:\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Пользователи по регионам:\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""" 🌍 Управление странамиОшибка загрузки данных Не удалось получить информацию о серверах. Проверьте подключение к RemnaWave API. Детали ошибки: {e!s} """ keyboard = [ [types.InlineKeyboardButton(text='🔄 Обновить', callback_data='admin_subs_countries')], [ 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 # Skip email-only users (no telegram_id) if not user.telegram_id: logger.debug(f'Пропуск email-пользователя {user.id} при отправке напоминания') continue days_left = max(1, subscription.days_left) reminder_text = f""" ⚠️ Подписка истекает! Ваша подписка истекает через {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_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_'))