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}"
)