import logging from datetime import datetime, timedelta from aiogram import Dispatcher, types, F from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import User from app.keyboards.admin import get_admin_statistics_keyboard, get_period_selection_keyboard from app.localization.texts import get_texts from app.services.user_service import UserService from app.database.crud.subscription import get_subscriptions_statistics from app.database.crud.transaction import get_transactions_statistics, get_revenue_by_period from app.database.crud.referral import get_referral_statistics from app.utils.decorators import admin_required, error_handler from app.utils.formatters import format_datetime, format_percentage logger = logging.getLogger(__name__) @admin_required @error_handler async def show_statistics_menu( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): text = """ 📊 Статистика системы Выберите раздел для просмотра статистики: """ await callback.message.edit_text( text, reply_markup=get_admin_statistics_keyboard(db_user.language) ) await callback.answer() @admin_required @error_handler async def show_users_statistics( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): user_service = UserService() stats = await user_service.get_user_statistics(db) total_users = stats['total_users'] active_rate = format_percentage(stats['active_users'] / total_users * 100 if total_users > 0 else 0) current_time = format_datetime(datetime.utcnow()) text = f""" 👥 Статистика пользователей Общие показатели: - Всего зарегистрировано: {stats['total_users']} - Активных: {stats['active_users']} ({active_rate}) - Заблокированных: {stats['blocked_users']} Новые регистрации: - Сегодня: {stats['new_today']} - За неделю: {stats['new_week']} - За месяц: {stats['new_month']} Активность: - Коэффициент активности: {active_rate} - Рост за месяц: +{stats['new_month']} ({format_percentage(stats['new_month'] / total_users * 100 if total_users > 0 else 0)}) Обновлено: {current_time} """ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats_users")], [types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_statistics")] ]) try: await callback.message.edit_text(text, reply_markup=keyboard) except Exception as e: if "message is not modified" in str(e): await callback.answer("📊 Данные актуальны", show_alert=False) else: logger.error(f"Ошибка обновления статистики пользователей: {e}") await callback.answer("❌ Ошибка обновления данных", show_alert=True) return await callback.answer("✅ Статистика обновлена") @admin_required @error_handler async def show_subscriptions_statistics( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): stats = await get_subscriptions_statistics(db) total_subs = stats['total_subscriptions'] conversion_rate = format_percentage(stats['paid_subscriptions'] / total_subs * 100 if total_subs > 0 else 0) current_time = format_datetime(datetime.utcnow()) text = f""" 📱 Статистика подписок Общие показатели: - Всего подписок: {stats['total_subscriptions']} - Активных: {stats['active_subscriptions']} - Платных: {stats['paid_subscriptions']} - Триальных: {stats['trial_subscriptions']} Конверсия: - Из триала в платную: {conversion_rate} - Активных платных: {stats['paid_subscriptions']} Продажи: - Сегодня: {stats['purchased_today']} - За неделю: {stats['purchased_week']} - За месяц: {stats['purchased_month']} Обновлено: {current_time} """ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats_subs")], [types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_statistics")] ]) try: await callback.message.edit_text(text, reply_markup=keyboard) await callback.answer("✅ Статистика обновлена") except Exception as e: if "message is not modified" in str(e): await callback.answer("📊 Данные актуальны", show_alert=False) else: logger.error(f"Ошибка обновления статистики подписок: {e}") await callback.answer("❌ Ошибка обновления данных", show_alert=True) @admin_required @error_handler async def show_revenue_statistics( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): now = datetime.utcnow() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_stats = await get_transactions_statistics(db, month_start, now) all_time_stats = await get_transactions_statistics(db) current_time = format_datetime(datetime.utcnow()) text = f""" 💰 Статистика доходов За текущий месяц: - Доходы: {settings.format_price(month_stats['totals']['income_kopeks'])} - Расходы: {settings.format_price(month_stats['totals']['expenses_kopeks'])} - Прибыль: {settings.format_price(month_stats['totals']['profit_kopeks'])} - От подписок: {settings.format_price(month_stats['totals']['subscription_income_kopeks'])} Сегодня: - Транзакций: {month_stats['today']['transactions_count']} - Доходы: {settings.format_price(month_stats['today']['income_kopeks'])} За все время: - Общий доход: {settings.format_price(all_time_stats['totals']['income_kopeks'])} - Общая прибыль: {settings.format_price(all_time_stats['totals']['profit_kopeks'])} Способы оплаты: """ for method, data in month_stats['by_payment_method'].items(): if method and data['count'] > 0: text += f"• {method}: {data['count']} ({settings.format_price(data['amount'])})\n" text += f"\nОбновлено: {current_time}" keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ # [types.InlineKeyboardButton(text="📈 Период", callback_data="admin_revenue_period")], [types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats_revenue")], [types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_statistics")] ]) try: await callback.message.edit_text(text, reply_markup=keyboard) await callback.answer("✅ Статистика обновлена") except Exception as e: if "message is not modified" in str(e): await callback.answer("📊 Данные актуальны", show_alert=False) else: logger.error(f"Ошибка обновления статистики доходов: {e}") await callback.answer("❌ Ошибка обновления данных", show_alert=True) @admin_required @error_handler async def show_referral_statistics( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): stats = await get_referral_statistics(db) current_time = format_datetime(datetime.utcnow()) avg_per_referrer = 0 if stats['active_referrers'] > 0: avg_per_referrer = stats['total_paid_kopeks'] / stats['active_referrers'] text = f""" 🤝 Реферальная статистика Общие показатели: - Пользователей с рефералами: {stats['users_with_referrals']} - Активных рефереров: {stats['active_referrers']} - Выплачено всего: {settings.format_price(stats['total_paid_kopeks'])} За период: - Сегодня: {settings.format_price(stats['today_earnings_kopeks'])} - За неделю: {settings.format_price(stats['week_earnings_kopeks'])} - За месяц: {settings.format_price(stats['month_earnings_kopeks'])} Средние показатели: - На одного рефререра: {settings.format_price(int(avg_per_referrer))} Топ рефереры: """ if stats['top_referrers']: for i, referrer in enumerate(stats['top_referrers'][:5], 1): name = referrer['display_name'] earned = settings.format_price(referrer['total_earned_kopeks']) count = referrer['referrals_count'] text += f"{i}. {name}: {earned} ({count} реф.)\n" else: text += "Пока нет активных рефереров" text += f"\nОбновлено: {current_time}" keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats_referrals")], [types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_statistics")] ]) try: await callback.message.edit_text(text, reply_markup=keyboard) await callback.answer("✅ Статистика обновлена") except Exception as e: if "message is not modified" in str(e): await callback.answer("📊 Данные актуальны", show_alert=False) else: logger.error(f"Ошибка обновления реферальной статистики: {e}") await callback.answer("❌ Ошибка обновления данных", show_alert=True) @admin_required @error_handler async def show_summary_statistics( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): user_service = UserService() user_stats = await user_service.get_user_statistics(db) sub_stats = await get_subscriptions_statistics(db) now = datetime.utcnow() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) revenue_stats = await get_transactions_statistics(db, month_start, now) current_time = format_datetime(datetime.utcnow()) conversion_rate = 0 if user_stats['total_users'] > 0: conversion_rate = sub_stats['paid_subscriptions'] / user_stats['total_users'] * 100 arpu = 0 if user_stats['active_users'] > 0: arpu = revenue_stats['totals']['income_kopeks'] / user_stats['active_users'] text = f""" 📊 Общая сводка системы Пользователи: - Всего: {user_stats['total_users']} - Активных: {user_stats['active_users']} - Новых за месяц: {user_stats['new_month']} Подписки: - Активных: {sub_stats['active_subscriptions']} - Платных: {sub_stats['paid_subscriptions']} - Конверсия: {format_percentage(conversion_rate)} Финансы (месяц): - Доходы: {settings.format_price(revenue_stats['totals']['income_kopeks'])} - ARPU: {settings.format_price(int(arpu))} - Транзакций: {sum(data['count'] for data in revenue_stats['by_type'].values())} Рост: - Пользователи: +{user_stats['new_month']} за месяц - Продажи: +{sub_stats['purchased_month']} за месяц Обновлено: {current_time} """ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats_summary")], [types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_statistics")] ]) try: await callback.message.edit_text(text, reply_markup=keyboard) await callback.answer("✅ Статистика обновлена") except Exception as e: if "message is not modified" in str(e): await callback.answer("📊 Данные актуальны", show_alert=False) else: logger.error(f"Ошибка обновления общей статистики: {e}") await callback.answer("❌ Ошибка обновления данных", show_alert=True) @admin_required @error_handler async def show_revenue_by_period( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): period = callback.data.split('_')[-1] period_map = { "today": 1, "yesterday": 1, "week": 7, "month": 30, "all": 365 } days = period_map.get(period, 30) revenue_data = await get_revenue_by_period(db, days) if period == "yesterday": yesterday = datetime.utcnow().date() - timedelta(days=1) revenue_data = [r for r in revenue_data if r['date'] == yesterday] elif period == "today": today = datetime.utcnow().date() revenue_data = [r for r in revenue_data if r['date'] == today] total_revenue = sum(r['amount_kopeks'] for r in revenue_data) avg_daily = total_revenue / len(revenue_data) if revenue_data else 0 text = f""" 📈 Доходы за период: {period} Сводка: - Общий доход: {settings.format_price(total_revenue)} - Дней с данными: {len(revenue_data)} - Средний доход в день: {settings.format_price(int(avg_daily))} По дням: """ for revenue in revenue_data[-10:]: text += f"• {revenue['date'].strftime('%d.%m')}: {settings.format_price(revenue['amount_kopeks'])}\n" if len(revenue_data) > 10: text += f"... и еще {len(revenue_data) - 10} дней" await callback.message.edit_text( text, reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="📊 Другой период", callback_data="admin_revenue_period")], [types.InlineKeyboardButton(text="⬅️ К доходам", callback_data="admin_stats_revenue")] ]) ) await callback.answer() def register_handlers(dp: Dispatcher): dp.callback_query.register(show_statistics_menu, F.data == "admin_statistics") dp.callback_query.register(show_users_statistics, F.data == "admin_stats_users") dp.callback_query.register(show_subscriptions_statistics, F.data == "admin_stats_subs") dp.callback_query.register(show_revenue_statistics, F.data == "admin_stats_revenue") dp.callback_query.register(show_referral_statistics, F.data == "admin_stats_referrals") dp.callback_query.register(show_summary_statistics, F.data == "admin_stats_summary") dp.callback_query.register(show_revenue_by_period, F.data.startswith("period_")) periods = ["today", "yesterday", "week", "month", "all"] for period in periods: dp.callback_query.register( show_revenue_by_period, F.data == f"period_{period}" )