diff --git a/app/database/crud/user.py b/app/database/crud/user.py index a7fd93d4..98ee1f75 100644 --- a/app/database/crud/user.py +++ b/app/database/crud/user.py @@ -2,8 +2,8 @@ import logging import secrets import string from datetime import datetime, timedelta -from typing import Optional, List -from sqlalchemy import select, and_, or_, func +from typing import Optional, List, Dict +from sqlalchemy import select, and_, or_, func, case, nullslast from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -273,7 +273,11 @@ async def get_users_list( limit: int = 50, search: Optional[str] = None, status: Optional[UserStatus] = None, - order_by_balance: bool = False + order_by_balance: bool = False, + order_by_traffic: bool = False, + order_by_last_activity: bool = False, + order_by_total_spent: bool = False, + order_by_purchase_count: bool = False ) -> List[User]: query = select(User).options(selectinload(User.subscription)) @@ -293,10 +297,71 @@ async def get_users_list( conditions.append(User.telegram_id == int(search)) query = query.where(or_(*conditions)) - - # Сортировка по балансу в порядке убывания, если order_by_balance=True - if order_by_balance: - query = query.order_by(User.balance_kopeks.desc()) + + sort_flags = [ + order_by_balance, + order_by_traffic, + order_by_last_activity, + order_by_total_spent, + order_by_purchase_count, + ] + if sum(int(flag) for flag in sort_flags) > 1: + logger.debug( + "Выбрано несколько сортировок пользователей — применяется приоритет: трафик > траты > покупки > баланс > активность" + ) + + transactions_stats = None + if order_by_total_spent or order_by_purchase_count: + from app.database.models import Transaction + + transactions_stats = ( + select( + Transaction.user_id.label("user_id"), + func.coalesce( + func.sum( + case( + ( + Transaction.type == TransactionType.SUBSCRIPTION_PAYMENT.value, + Transaction.amount_kopeks, + ), + else_=0, + ) + ), + 0, + ).label("total_spent"), + func.coalesce( + func.sum( + case( + ( + Transaction.type == TransactionType.SUBSCRIPTION_PAYMENT.value, + 1, + ), + else_=0, + ) + ), + 0, + ).label("purchase_count"), + ) + .where(Transaction.is_completed.is_(True)) + .group_by(Transaction.user_id) + .subquery() + ) + query = query.outerjoin(transactions_stats, transactions_stats.c.user_id == User.id) + + if order_by_traffic: + traffic_sort = func.coalesce(Subscription.traffic_used_gb, 0.0) + query = query.outerjoin(Subscription, Subscription.user_id == User.id) + query = query.order_by(traffic_sort.desc(), User.created_at.desc()) + elif order_by_total_spent: + order_column = func.coalesce(transactions_stats.c.total_spent, 0) + query = query.order_by(order_column.desc(), User.created_at.desc()) + elif order_by_purchase_count: + order_column = func.coalesce(transactions_stats.c.purchase_count, 0) + query = query.order_by(order_column.desc(), User.created_at.desc()) + elif order_by_balance: + query = query.order_by(User.balance_kopeks.desc(), User.created_at.desc()) + elif order_by_last_activity: + query = query.order_by(nullslast(User.last_activity.desc()), User.created_at.desc()) else: query = query.order_by(User.created_at.desc()) @@ -334,6 +399,62 @@ async def get_users_count( return result.scalar() +async def get_users_spending_stats( + db: AsyncSession, + user_ids: List[int] +) -> Dict[int, Dict[str, int]]: + if not user_ids: + return {} + + from app.database.models import Transaction + + stats_query = ( + select( + Transaction.user_id, + func.coalesce( + func.sum( + case( + ( + Transaction.type == TransactionType.SUBSCRIPTION_PAYMENT.value, + Transaction.amount_kopeks, + ), + else_=0, + ) + ), + 0, + ).label("total_spent"), + func.coalesce( + func.sum( + case( + ( + Transaction.type == TransactionType.SUBSCRIPTION_PAYMENT.value, + 1, + ), + else_=0, + ) + ), + 0, + ).label("purchase_count"), + ) + .where( + Transaction.user_id.in_(user_ids), + Transaction.is_completed.is_(True), + ) + .group_by(Transaction.user_id) + ) + + result = await db.execute(stats_query) + rows = result.all() + + return { + row.user_id: { + "total_spent": int(row.total_spent or 0), + "purchase_count": int(row.purchase_count or 0), + } + for row in rows + } + + async def get_referrals(db: AsyncSession, user_id: int) -> List[User]: result = await db.execute( select(User) diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py index 64d20f98..c786c1cc 100644 --- a/app/handlers/admin/users.py +++ b/app/handlers/admin/users.py @@ -81,7 +81,7 @@ async def show_users_filters( state: FSMContext ): - text = "⚙️ Фильтры пользователей\n\nВыберите фильтр для отображения пользователей:" + text = ("⚙️ Фильтры пользователей\n\nВыберите фильтр для отображения пользователей:\n") await callback.message.edit_text( text, @@ -289,6 +289,464 @@ async def show_users_list_by_balance( await callback.answer() +@admin_required +@error_handler +async def show_users_list_by_traffic( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, + page: int = 1 +): + + await state.set_state(AdminStates.viewing_user_from_traffic_list) + + user_service = UserService() + users_data = await user_service.get_users_page( + db, page=page, limit=10, order_by_traffic=True + ) + + if not users_data["users"]: + await callback.message.edit_text( + "📶 Пользователи с трафиком не найдены", + reply_markup=get_admin_users_keyboard(db_user.language) + ) + await callback.answer() + return + + text = f"👥 Список пользователей по использованному трафику (стр. {page}/{users_data['total_pages']})\n\n" + text += "Нажмите на пользователя для управления:" + + keyboard = [] + + for user in users_data["users"]: + if user.status == UserStatus.ACTIVE.value: + status_emoji = "✅" + elif user.status == UserStatus.BLOCKED.value: + status_emoji = "🚫" + else: + status_emoji = "🗑️" + + if user.subscription: + sub = user.subscription + if sub.is_trial: + subscription_emoji = "🎁" + elif sub.is_active: + subscription_emoji = "💎" + else: + subscription_emoji = "⏰" + used = sub.traffic_used_gb or 0.0 + if sub.traffic_limit_gb and sub.traffic_limit_gb > 0: + limit_display = f"{sub.traffic_limit_gb}" + else: + limit_display = "♾️" + traffic_display = f"{used:.1f}/{limit_display} ГБ" + else: + subscription_emoji = "❌" + traffic_display = "нет подписки" + + button_text = f"{status_emoji} {subscription_emoji} {user.full_name}" + button_text += f" | 📶 {traffic_display}" + + if user.balance_kopeks > 0: + button_text += f" | 💰 {settings.format_price(user.balance_kopeks)}" + + if len(button_text) > 60: + short_name = user.full_name + if len(short_name) > 20: + short_name = short_name[:17] + "..." + button_text = f"{status_emoji} {subscription_emoji} {short_name}" + button_text += f" | 📶 {traffic_display}" + + keyboard.append([ + types.InlineKeyboardButton( + text=button_text, + callback_data=f"admin_user_manage_{user.id}" + ) + ]) + + if users_data["total_pages"] > 1: + pagination_row = get_admin_pagination_keyboard( + users_data["current_page"], + users_data["total_pages"], + "admin_users_traffic_list", + "admin_users", + db_user.language + ).inline_keyboard[0] + keyboard.append(pagination_row) + + keyboard.extend([ + [ + types.InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_users_search"), + types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_users_stats") + ], + [ + types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users") + ] + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) + ) + await callback.answer() + + +@admin_required +@error_handler +async def show_users_list_by_last_activity( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, + page: int = 1 +): + + await state.set_state(AdminStates.viewing_user_from_last_activity_list) + + user_service = UserService() + users_data = await user_service.get_users_page( + db, + page=page, + limit=10, + order_by_last_activity=True, + ) + + if not users_data["users"]: + await callback.message.edit_text( + "🕒 Пользователи с активностью не найдены", + reply_markup=get_admin_users_keyboard(db_user.language) + ) + await callback.answer() + return + + text = f"👥 Пользователи по активности (стр. {page}/{users_data['total_pages']})\n\n" + text += "Нажмите на пользователя для управления:" + + keyboard = [] + + for user in users_data["users"]: + if user.status == UserStatus.ACTIVE.value: + status_emoji = "✅" + elif user.status == UserStatus.BLOCKED.value: + status_emoji = "🚫" + else: + status_emoji = "🗑️" + + activity_display = ( + format_time_ago(user.last_activity) + if user.last_activity + else "неизвестно" + ) + + subscription_emoji = "❌" + if user.subscription: + if user.subscription.is_trial: + subscription_emoji = "🎁" + elif user.subscription.is_active: + subscription_emoji = "💎" + else: + subscription_emoji = "⏰" + + button_text = f"{status_emoji} {subscription_emoji} {user.full_name}" + button_text += f" | 🕒 {activity_display}" + + keyboard.append([ + types.InlineKeyboardButton( + text=button_text, + callback_data=f"admin_user_manage_{user.id}" + ) + ]) + + if users_data["total_pages"] > 1: + pagination_row = get_admin_pagination_keyboard( + users_data["current_page"], + users_data["total_pages"], + "admin_users_activity_list", + "admin_users", + db_user.language + ).inline_keyboard[0] + keyboard.append(pagination_row) + + keyboard.extend([ + [ + types.InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_users_search"), + types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_users_stats") + ], + [ + types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users") + ] + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) + ) + await callback.answer() + + +@admin_required +@error_handler +async def show_users_list_by_spending( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, + page: int = 1 +): + + await state.set_state(AdminStates.viewing_user_from_spending_list) + + user_service = UserService() + users_data = await user_service.get_users_page( + db, + page=page, + limit=10, + order_by_total_spent=True, + ) + + users = users_data["users"] + if not users: + await callback.message.edit_text( + "💳 Пользователи с тратами не найдены", + reply_markup=get_admin_users_keyboard(db_user.language) + ) + await callback.answer() + return + + spending_map = await user_service.get_user_spending_stats_map( + db, + [user.id for user in users], + ) + + text = f"👥 Пользователи по сумме трат (стр. {page}/{users_data['total_pages']})\n\n" + text += "Нажмите на пользователя для управления:" + + keyboard = [] + + for user in users: + stats = spending_map.get( + user.id, + {"total_spent": 0, "purchase_count": 0}, + ) + total_spent = stats.get("total_spent", 0) + purchases = stats.get("purchase_count", 0) + + status_emoji = "✅" if user.status == UserStatus.ACTIVE.value else "🚫" if user.status == UserStatus.BLOCKED.value else "🗑️" + + button_text = ( + f"{status_emoji} {user.full_name}" + f" | 💳 {settings.format_price(total_spent)}" + f" | 🛒 {purchases}" + ) + + keyboard.append([ + types.InlineKeyboardButton( + text=button_text, + callback_data=f"admin_user_manage_{user.id}" + ) + ]) + + if users_data["total_pages"] > 1: + pagination_row = get_admin_pagination_keyboard( + users_data["current_page"], + users_data["total_pages"], + "admin_users_spending_list", + "admin_users", + db_user.language + ).inline_keyboard[0] + keyboard.append(pagination_row) + + keyboard.extend([ + [ + types.InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_users_search"), + types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_users_stats") + ], + [ + types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users") + ] + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) + ) + await callback.answer() + + +@admin_required +@error_handler +async def show_users_list_by_purchases( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, + page: int = 1 +): + + await state.set_state(AdminStates.viewing_user_from_purchases_list) + + user_service = UserService() + users_data = await user_service.get_users_page( + db, + page=page, + limit=10, + order_by_purchase_count=True, + ) + + users = users_data["users"] + if not users: + await callback.message.edit_text( + "🛒 Пользователи с покупками не найдены", + reply_markup=get_admin_users_keyboard(db_user.language) + ) + await callback.answer() + return + + spending_map = await user_service.get_user_spending_stats_map( + db, + [user.id for user in users], + ) + + text = f"👥 Пользователи по количеству покупок (стр. {page}/{users_data['total_pages']})\n\n" + text += "Нажмите на пользователя для управления:" + + keyboard = [] + + for user in users: + stats = spending_map.get( + user.id, + {"total_spent": 0, "purchase_count": 0}, + ) + total_spent = stats.get("total_spent", 0) + purchases = stats.get("purchase_count", 0) + + status_emoji = "✅" if user.status == UserStatus.ACTIVE.value else "🚫" if user.status == UserStatus.BLOCKED.value else "🗑️" + + button_text = ( + f"{status_emoji} {user.full_name}" + f" | 🛒 {purchases}" + f" | 💳 {settings.format_price(total_spent)}" + ) + + keyboard.append([ + types.InlineKeyboardButton( + text=button_text, + callback_data=f"admin_user_manage_{user.id}" + ) + ]) + + if users_data["total_pages"] > 1: + pagination_row = get_admin_pagination_keyboard( + users_data["current_page"], + users_data["total_pages"], + "admin_users_purchases_list", + "admin_users", + db_user.language + ).inline_keyboard[0] + keyboard.append(pagination_row) + + keyboard.extend([ + [ + types.InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_users_search"), + types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_users_stats") + ], + [ + types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users") + ] + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) + ) + await callback.answer() + + +@admin_required +@error_handler +async def show_users_list_by_campaign( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, + page: int = 1 +): + + await state.set_state(AdminStates.viewing_user_from_campaign_list) + + user_service = UserService() + users_data = await user_service.get_users_by_campaign_page( + db, + page=page, + limit=10, + ) + + users = users_data.get("users", []) + campaign_map = users_data.get("campaigns", {}) + + if not users: + await callback.message.edit_text( + "📢 Пользователи с кампанией не найдены", + reply_markup=get_admin_users_keyboard(db_user.language) + ) + await callback.answer() + return + + text = f"👥 Пользователи по кампании регистрации (стр. {page}/{users_data['total_pages']})\n\n" + text += "Нажмите на пользователя для управления:" + + keyboard = [] + + for user in users: + info = campaign_map.get(user.id, {}) + campaign_name = info.get("campaign_name") or "Без кампании" + registered_at = info.get("registered_at") + registered_display = format_datetime(registered_at) if registered_at else "неизвестно" + + status_emoji = "✅" if user.status == UserStatus.ACTIVE.value else "🚫" if user.status == UserStatus.BLOCKED.value else "🗑️" + + button_text = ( + f"{status_emoji} {user.full_name}" + f" | 📢 {campaign_name}" + f" | 📅 {registered_display}" + ) + + keyboard.append([ + types.InlineKeyboardButton( + text=button_text, + callback_data=f"admin_user_manage_{user.id}" + ) + ]) + + if users_data["total_pages"] > 1: + pagination_row = get_admin_pagination_keyboard( + users_data["current_page"], + users_data["total_pages"], + "admin_users_campaign_list", + "admin_users", + db_user.language + ).inline_keyboard[0] + keyboard.append(pagination_row) + + keyboard.extend([ + [ + types.InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_users_search"), + types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_users_stats") + ], + [ + types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users") + ] + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) + ) + await callback.answer() + + + @admin_required @error_handler async def handle_users_list_pagination_fixed( @@ -323,6 +781,91 @@ async def handle_users_balance_list_pagination( await show_users_list_by_balance(callback, db_user, db, state, 1) +@admin_required +@error_handler +async def handle_users_traffic_list_pagination( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext +): + try: + callback_parts = callback.data.split('_') + page = int(callback_parts[-1]) + await show_users_list_by_traffic(callback, db_user, db, state, page) + except (ValueError, IndexError) as e: + logger.error(f"Ошибка парсинга номера страницы: {e}") + await show_users_list_by_traffic(callback, db_user, db, state, 1) + + +@admin_required +@error_handler +async def handle_users_activity_list_pagination( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext +): + try: + callback_parts = callback.data.split('_') + page = int(callback_parts[-1]) + await show_users_list_by_last_activity(callback, db_user, db, state, page) + except (ValueError, IndexError) as e: + logger.error(f"Ошибка парсинга номера страницы: {e}") + await show_users_list_by_last_activity(callback, db_user, db, state, 1) + + +@admin_required +@error_handler +async def handle_users_spending_list_pagination( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext +): + try: + callback_parts = callback.data.split('_') + page = int(callback_parts[-1]) + await show_users_list_by_spending(callback, db_user, db, state, page) + except (ValueError, IndexError) as e: + logger.error(f"Ошибка парсинга номера страницы: {e}") + await show_users_list_by_spending(callback, db_user, db, state, 1) + + +@admin_required +@error_handler +async def handle_users_purchases_list_pagination( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext +): + try: + callback_parts = callback.data.split('_') + page = int(callback_parts[-1]) + await show_users_list_by_purchases(callback, db_user, db, state, page) + except (ValueError, IndexError) as e: + logger.error(f"Ошибка парсинга номера страницы: {e}") + await show_users_list_by_purchases(callback, db_user, db, state, 1) + + +@admin_required +@error_handler +async def handle_users_campaign_list_pagination( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext +): + try: + callback_parts = callback.data.split('_') + page = int(callback_parts[-1]) + await show_users_list_by_campaign(callback, db_user, db, state, page) + except (ValueError, IndexError) as e: + logger.error(f"Ошибка парсинга номера страницы: {e}") + await show_users_list_by_campaign(callback, db_user, db, state, 1) + + @admin_required @error_handler async def start_user_search( @@ -867,6 +1410,16 @@ async def show_user_management( current_state = await state.get_state() if current_state == AdminStates.viewing_user_from_balance_list: back_callback = "admin_users_balance_filter" + elif current_state == AdminStates.viewing_user_from_traffic_list: + back_callback = "admin_users_traffic_filter" + elif current_state == AdminStates.viewing_user_from_last_activity_list: + back_callback = "admin_users_activity_filter" + elif current_state == AdminStates.viewing_user_from_spending_list: + back_callback = "admin_users_spending_filter" + elif current_state == AdminStates.viewing_user_from_purchases_list: + back_callback = "admin_users_purchases_filter" + elif current_state == AdminStates.viewing_user_from_campaign_list: + back_callback = "admin_users_campaign_filter" # Базовая клавиатура профиля kb = get_user_management_keyboard(user.id, user.status, db_user.language, back_callback) @@ -3317,6 +3870,31 @@ def register_handlers(dp: Dispatcher): F.data.startswith("admin_users_balance_list_page_") ) + dp.callback_query.register( + handle_users_traffic_list_pagination, + F.data.startswith("admin_users_traffic_list_page_") + ) + + dp.callback_query.register( + handle_users_activity_list_pagination, + F.data.startswith("admin_users_activity_list_page_") + ) + + dp.callback_query.register( + handle_users_spending_list_pagination, + F.data.startswith("admin_users_spending_list_page_") + ) + + dp.callback_query.register( + handle_users_purchases_list_pagination, + F.data.startswith("admin_users_purchases_list_page_") + ) + + dp.callback_query.register( + handle_users_campaign_list_pagination, + F.data.startswith("admin_users_campaign_list_page_") + ) + dp.callback_query.register( start_user_search, F.data == "admin_users_search" @@ -3522,9 +4100,27 @@ def register_handlers(dp: Dispatcher): ) dp.callback_query.register( - show_users_list_by_balance, - F.data.startswith("admin_users_balance_list_page_") + show_users_list_by_traffic, + F.data == "admin_users_traffic_filter" ) + dp.callback_query.register( + show_users_list_by_last_activity, + F.data == "admin_users_activity_filter" + ) + dp.callback_query.register( + show_users_list_by_spending, + F.data == "admin_users_spending_filter" + ) + dp.callback_query.register( + show_users_list_by_purchases, + F.data == "admin_users_purchases_filter" + ) + + dp.callback_query.register( + show_users_list_by_campaign, + F.data == "admin_users_campaign_filter" + ) + diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index 809481d9..42bb14d3 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -175,6 +175,21 @@ def get_admin_users_filters_keyboard(language: str = "ru") -> InlineKeyboardMark [ InlineKeyboardButton(text="💰 По балансу", callback_data="admin_users_balance_filter") ], + [ + InlineKeyboardButton(text="📶 По трафику", callback_data="admin_users_traffic_filter") + ], + [ + InlineKeyboardButton(text="🕒 По активности", callback_data="admin_users_activity_filter") + ], + [ + InlineKeyboardButton(text="💳 По сумме трат", callback_data="admin_users_spending_filter") + ], + [ + InlineKeyboardButton(text="🛒 По количеству покупок", callback_data="admin_users_purchases_filter") + ], + [ + InlineKeyboardButton(text="📢 По кампании", callback_data="admin_users_campaign_filter") + ], [ InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users") ] diff --git a/app/services/user_service.py b/app/services/user_service.py index 3a20b857..89a6139a 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -2,13 +2,14 @@ import logging from datetime import datetime, timedelta from typing import Optional, List, Dict, Any, Tuple from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import delete, select, update +from sqlalchemy import delete, select, update, func from aiogram import Bot from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError from app.database.crud.user import ( get_user_by_id, get_user_by_telegram_id, get_users_list, get_users_count, get_users_statistics, get_inactive_users, - add_user_balance, subtract_user_balance, update_user, delete_user + add_user_balance, subtract_user_balance, update_user, delete_user, + get_users_spending_stats ) from app.database.crud.promo_group import get_promo_group_by_id from app.database.crud.transaction import get_user_transactions_count @@ -18,7 +19,8 @@ from app.database.models import ( ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory, CryptoBotPayment, SubscriptionConversion, UserMessage, WelcomeText, SentNotification, PromoGroup, MulenPayPayment, Pal24Payment, - AdvertisingCampaign, PaymentMethod + AdvertisingCampaign, AdvertisingCampaignRegistration, PaymentMethod, + TransactionType ) from app.config import settings @@ -141,20 +143,32 @@ class UserService: "has_next": False, "has_prev": False } - + async def get_users_page( self, db: AsyncSession, page: int = 1, limit: int = 20, status: Optional[UserStatus] = None, - order_by_balance: bool = False + order_by_balance: bool = False, + order_by_traffic: bool = False, + order_by_last_activity: bool = False, + order_by_total_spent: bool = False, + order_by_purchase_count: bool = False ) -> Dict[str, Any]: try: offset = (page - 1) * limit users = await get_users_list( - db, offset=offset, limit=limit, status=status, order_by_balance=order_by_balance + db, + offset=offset, + limit=limit, + status=status, + order_by_balance=order_by_balance, + order_by_traffic=order_by_traffic, + order_by_last_activity=order_by_last_activity, + order_by_total_spent=order_by_total_spent, + order_by_purchase_count=order_by_purchase_count, ) total_count = await get_users_count(db, status=status) @@ -179,7 +193,110 @@ class UserService: "has_next": False, "has_prev": False } - + + async def get_user_spending_stats_map( + self, + db: AsyncSession, + user_ids: List[int] + ) -> Dict[int, Dict[str, int]]: + try: + return await get_users_spending_stats(db, user_ids) + except Exception as e: + logger.error(f"Ошибка получения статистики трат пользователей: {e}") + return {} + + async def get_users_by_campaign_page( + self, + db: AsyncSession, + page: int = 1, + limit: int = 20 + ) -> Dict[str, Any]: + try: + offset = (page - 1) * limit + + campaign_ranked = ( + select( + AdvertisingCampaignRegistration.user_id.label("user_id"), + AdvertisingCampaignRegistration.campaign_id.label("campaign_id"), + AdvertisingCampaignRegistration.created_at.label("created_at"), + func.row_number() + .over( + partition_by=AdvertisingCampaignRegistration.user_id, + order_by=AdvertisingCampaignRegistration.created_at.desc(), + ) + .label("rn"), + ) + .cte("campaign_ranked") + ) + + latest_campaign = ( + select( + campaign_ranked.c.user_id, + campaign_ranked.c.campaign_id, + campaign_ranked.c.created_at, + ) + .where(campaign_ranked.c.rn == 1) + .subquery() + ) + + query = ( + select( + User, + AdvertisingCampaign.name.label("campaign_name"), + latest_campaign.c.created_at, + ) + .join(latest_campaign, latest_campaign.c.user_id == User.id) + .join( + AdvertisingCampaign, + AdvertisingCampaign.id == latest_campaign.c.campaign_id, + ) + .order_by( + AdvertisingCampaign.name.asc(), + latest_campaign.c.created_at.desc(), + ) + .offset(offset) + .limit(limit) + ) + + result = await db.execute(query) + rows = result.all() + + users = [row[0] for row in rows] + campaign_map = { + row[0].id: { + "campaign_name": row[1], + "registered_at": row[2], + } + for row in rows + } + + total_stmt = select(func.count()).select_from(latest_campaign) + total_result = await db.execute(total_stmt) + total_count = total_result.scalar() or 0 + total_pages = (total_count + limit - 1) // limit if total_count else 1 + + return { + "users": users, + "campaigns": campaign_map, + "current_page": page, + "total_pages": total_pages, + "total_count": total_count, + "has_next": page < total_pages, + "has_prev": page > 1, + } + + except Exception as e: + logger.error(f"Ошибка получения пользователей по кампаниям: {e}") + return { + "users": [], + "campaigns": {}, + "current_page": 1, + "total_pages": 1, + "total_count": 0, + "has_next": False, + "has_prev": False, + } + async def update_user_balance( self, db: AsyncSession, diff --git a/app/states.py b/app/states.py index 7dbc74e6..43655b35 100644 --- a/app/states.py +++ b/app/states.py @@ -108,6 +108,11 @@ class AdminStates(StatesGroup): # Состояния для отслеживания источника перехода viewing_user_from_balance_list = State() + viewing_user_from_traffic_list = State() + viewing_user_from_last_activity_list = State() + viewing_user_from_spending_list = State() + viewing_user_from_purchases_list = State() + viewing_user_from_campaign_list = State() class SupportStates(StatesGroup): waiting_for_message = State()