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, func from sqlalchemy.orm import selectinload from aiogram import Bot, types 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, get_users_spending_stats, get_referrals ) from app.database.crud.promo_group import get_promo_group_by_id from app.database.crud.transaction import get_user_transactions_count from app.database.crud.subscription import ( get_subscription_by_user_id, decrement_subscription_server_counts, ) from app.database.models import ( User, UserStatus, Subscription, Transaction, PromoCode, PromoCodeUse, ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory, CryptoBotPayment, PlategaPayment, SubscriptionConversion, UserMessage, WelcomeText, SentNotification, PromoGroup, MulenPayPayment, Pal24Payment, HeleketPayment, FreekassaPayment, AdvertisingCampaign, AdvertisingCampaignRegistration, PaymentMethod, TransactionType ) from app.config import settings from app.localization.texts import get_texts logger = logging.getLogger(__name__) class UserService: async def send_topup_success_to_user( self, bot: Bot, user: User, amount_kopeks: int, subscription: Optional[Subscription] = None, ) -> bool: """ Отправляет пользователю уведомление об успешном пополнении баланса. Если подписки нет - показывает БОЛЬШОЕ предупреждение что нужно активировать. """ try: texts = get_texts(user.language) has_active_subscription = ( subscription is not None and subscription.status in {"active", "trial"} ) if has_active_subscription: # У пользователя есть активная подписка - обычное сообщение message = ( f"✅ Баланс пополнен на {settings.format_price(amount_kopeks)}!\n\n" f"💳 Текущий баланс: {settings.format_price(user.balance_kopeks)}\n\n" f"Спасибо за использование нашего сервиса! 🎉" ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton( text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), callback_data="subscription_extend" )] ]) else: # НЕТ активной подписки - БОЛЬШОЕ ПРЕДУПРЕЖДЕНИЕ message = ( f"✅ Баланс пополнен на {settings.format_price(amount_kopeks)}!\n\n" f"💳 Текущий баланс: {settings.format_price(user.balance_kopeks)}\n\n" f"{'─' * 25}\n\n" f"⚠️ ВАЖНО! ⚠️\n\n" f"🔴 ПОДПИСКА НЕ АКТИВНА!\n\n" f"Пополнение баланса НЕ активирует подписку автоматически!\n\n" f"👇 Выберите действие:" ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton( text="🚀 АКТИВИРОВАТЬ ПОДПИСКУ", callback_data="subscription_buy" )], [types.InlineKeyboardButton( text="💎 ПРОДЛИТЬ ПОДПИСКУ", callback_data="subscription_extend" )], [types.InlineKeyboardButton( text="📱 ДОБАВИТЬ УСТРОЙСТВА", callback_data="subscription_add_devices" )] ]) await bot.send_message( chat_id=user.telegram_id, text=message, parse_mode="HTML", reply_markup=keyboard ) logger.info(f"✅ Уведомление о пополнении отправлено пользователю {user.telegram_id}") return True except TelegramForbiddenError: logger.warning(f"⚠️ Пользователь {user.telegram_id} заблокировал бота") return False except TelegramBadRequest as e: logger.error(f"❌ Ошибка Telegram API: {e}") return False except Exception as e: logger.error(f"❌ Ошибка отправки уведомления: {e}") return False async def _send_balance_notification( self, bot: Bot, user: User, amount_kopeks: int, admin_name: str ) -> bool: """Отправляет уведомление пользователю о пополнении/списании баланса""" try: if amount_kopeks > 0: # Пополнение emoji = "💰" action = "пополнен" amount_text = f"+{settings.format_price(amount_kopeks)}" message = ( f"{emoji} Баланс пополнен!\n\n" f"💵 Сумма: {amount_text}\n" f"👤 Администратор: {admin_name}\n" f"💳 Текущий баланс: {settings.format_price(user.balance_kopeks)}\n\n" f"Спасибо за использование нашего сервиса! 🎉" ) else: # Списание emoji = "💸" action = "списан" amount_text = f"-{settings.format_price(abs(amount_kopeks))}" message = ( f"{emoji} Средства списаны с баланса\n\n" f"💵 Сумма: {amount_text}\n" f"👤 Администратор: {admin_name}\n" f"💳 Текущий баланс: {settings.format_price(user.balance_kopeks)}\n\n" f"Если у вас есть вопросы, обратитесь в поддержку." ) keyboard_rows = [] if getattr(user, "subscription", None) and user.subscription.status in { "active", "expired", "trial", }: keyboard_rows.append([ types.InlineKeyboardButton( text=get_texts(user.language).t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), callback_data="subscription_extend", ) ]) reply_markup = ( types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) if keyboard_rows else None ) await bot.send_message( chat_id=user.telegram_id, text=message, parse_mode="HTML", reply_markup=reply_markup, ) logger.info(f"✅ Уведомление о изменении баланса отправлено пользователю {user.telegram_id}") return True except TelegramForbiddenError: logger.warning(f"⚠️ Пользователь {user.telegram_id} заблокировал бота") return False except TelegramBadRequest as e: logger.error(f"❌ Ошибка Telegram API при отправке уведомления пользователю {user.telegram_id}: {e}") return False except Exception as e: logger.error(f"❌ Неожиданная ошибка при отправке уведомления пользователю {user.telegram_id}: {e}") return False async def get_user_profile( self, db: AsyncSession, user_id: int ) -> Optional[Dict[str, Any]]: try: user = await get_user_by_id(db, user_id) if not user: return None subscription = await get_subscription_by_user_id(db, user_id) transactions_count = await get_user_transactions_count(db, user_id) return { "user": user, "subscription": subscription, "transactions_count": transactions_count, "is_admin": settings.is_admin(user.telegram_id), "registration_days": (datetime.utcnow() - user.created_at).days } except Exception as e: logger.error(f"Ошибка получения профиля пользователя {user_id}: {e}") return None async def search_users( self, db: AsyncSession, query: str, page: int = 1, limit: int = 20 ) -> Dict[str, Any]: try: offset = (page - 1) * limit users = await get_users_list( db, offset=offset, limit=limit, search=query ) total_count = await get_users_count(db, search=query) total_pages = (total_count + limit - 1) // limit return { "users": users, "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": [], "current_page": 1, "total_pages": 1, "total_count": 0, "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_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, 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) total_pages = (total_count + limit - 1) // limit return { "users": users, "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": [], "current_page": 1, "total_pages": 1, "total_count": 0, "has_next": False, "has_prev": False } async def get_users_ready_to_renew( self, db: AsyncSession, min_balance_kopeks: int, page: int = 1, limit: int = 10, ) -> Dict[str, Any]: """Возвращает пользователей с истекшей подпиской и достаточным балансом.""" try: offset = (page - 1) * limit now = datetime.utcnow() base_filters = [ User.balance_kopeks >= min_balance_kopeks, Subscription.end_date.isnot(None), Subscription.end_date <= now, ] query = ( select(User) .options(selectinload(User.subscription)) .join(Subscription, Subscription.user_id == User.id) .where(*base_filters) .order_by(User.balance_kopeks.desc(), Subscription.end_date.asc()) .offset(offset) .limit(limit) ) result = await db.execute(query) users = result.scalars().unique().all() count_query = ( select(func.count(User.id)) .join(Subscription, Subscription.user_id == User.id) .where(*base_filters) ) total_count = (await db.execute(count_query)).scalar() or 0 total_pages = (total_count + limit - 1) // limit if total_count else 0 return { "users": users, "current_page": page, "total_pages": total_pages, "total_count": total_count, } except Exception as e: logger.error(f"Ошибка получения пользователей для продления: {e}") return { "users": [], "current_page": 1, "total_pages": 1, "total_count": 0, } 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, user_id: int, amount_kopeks: int, description: str, admin_id: int, bot: Optional[Bot] = None, admin_name: Optional[str] = None ) -> bool: try: user = await get_user_by_id(db, user_id) if not user: return False # Сохраняем старый баланс для уведомления old_balance = user.balance_kopeks if amount_kopeks > 0: await add_user_balance( db, user, amount_kopeks, description=description, payment_method=PaymentMethod.MANUAL ) logger.info(f"Админ {admin_id} пополнил баланс пользователя {user_id} на {amount_kopeks/100}₽") success = True else: success = await subtract_user_balance( db, user, abs(amount_kopeks), description, create_transaction=True, payment_method=PaymentMethod.MANUAL, ) if success: logger.info(f"Админ {admin_id} списал с баланса пользователя {user_id} {abs(amount_kopeks)/100}₽") # Отправляем уведомление пользователю, если операция прошла успешно if success and bot: # Обновляем пользователя для получения нового баланса await db.refresh(user) # Получаем имя администратора if not admin_name: admin_user = await get_user_by_id(db, admin_id) admin_name = admin_user.full_name if admin_user else f"Админ #{admin_id}" # Отправляем уведомление (не блокируем операцию если не удалось отправить) await self._send_balance_notification(bot, user, amount_kopeks, admin_name) return success except Exception as e: logger.error(f"Ошибка изменения баланса пользователя: {e}") return False async def update_user_promo_group( self, db: AsyncSession, user_id: int, promo_group_id: int ) -> Tuple[bool, Optional[User], Optional[PromoGroup], Optional[PromoGroup]]: try: user = await get_user_by_id(db, user_id) if not user: return False, None, None, None old_group = user.promo_group promo_group = await get_promo_group_by_id(db, promo_group_id) if not promo_group: return False, None, None, old_group user.promo_group_id = promo_group.id user.promo_group = promo_group user.updated_at = datetime.utcnow() await db.commit() await db.refresh(user) logger.info( "👥 Промогруппа пользователя %s обновлена на '%s'", user.telegram_id, promo_group.name, ) return True, user, promo_group, old_group except Exception as e: await db.rollback() logger.error(f"Ошибка обновления промогруппы пользователя {user_id}: {e}") return False, None, None, None async def update_user_referrals( self, db: AsyncSession, user_id: int, referral_user_ids: List[int], admin_id: int, ) -> Tuple[bool, Dict[str, int]]: try: user = await get_user_by_id(db, user_id) if not user: return False, {"error": "user_not_found"} unique_ids: List[int] = [] for referral_id in referral_user_ids: if referral_id == user_id: continue if referral_id not in unique_ids: unique_ids.append(referral_id) current_referrals = await get_referrals(db, user_id) current_ids = {ref.id for ref in current_referrals} to_assign = unique_ids to_remove = [rid for rid in current_ids if rid not in unique_ids] to_add = [rid for rid in unique_ids if rid not in current_ids] if to_assign: await db.execute( update(User) .where(User.id.in_(to_assign)) .values(referred_by_id=user_id) ) if to_remove: await db.execute( update(User) .where(User.id.in_(to_remove)) .values(referred_by_id=None) ) await db.commit() logger.info( "Админ %s обновил рефералов пользователя %s: добавлено %s, удалено %s, всего %s", admin_id, user_id, len(to_add), len(to_remove), len(unique_ids), ) return True, { "added": len(to_add), "removed": len(to_remove), "total": len(unique_ids), } except Exception as e: await db.rollback() logger.error( "Ошибка обновления рефералов пользователя %s: %s", user_id, e, ) return False, {"error": "update_failed"} async def block_user( self, db: AsyncSession, user_id: int, admin_id: int, reason: str = "Заблокирован администратором" ) -> bool: try: user = await get_user_by_id(db, user_id) if not user: return False if user.remnawave_uuid: try: from app.services.subscription_service import SubscriptionService subscription_service = SubscriptionService() await subscription_service.disable_remnawave_user(user.remnawave_uuid) logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован при блокировке") except Exception as e: logger.error(f"❌ Ошибка деактивации RemnaWave пользователя при блокировке: {e}") if user.subscription: from app.database.crud.subscription import deactivate_subscription await deactivate_subscription(db, user.subscription) await update_user(db, user, status=UserStatus.BLOCKED.value) logger.info(f"Админ {admin_id} заблокировал пользователя {user_id}: {reason}") return True except Exception as e: logger.error(f"Ошибка блокировки пользователя: {e}") return False async def unblock_user( self, db: AsyncSession, user_id: int, admin_id: int ) -> bool: try: user = await get_user_by_id(db, user_id) if not user: return False await update_user(db, user, status=UserStatus.ACTIVE.value) if user.subscription: from datetime import datetime from app.database.models import SubscriptionStatus if user.subscription.end_date > datetime.utcnow(): user.subscription.status = SubscriptionStatus.ACTIVE.value await db.commit() await db.refresh(user.subscription) logger.info(f"🔄 Подписка пользователя {user_id} восстановлена") if user.remnawave_uuid: try: from app.services.subscription_service import SubscriptionService subscription_service = SubscriptionService() await subscription_service.update_remnawave_user(db, user.subscription) logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} восстановлен при разблокировке") except Exception as e: logger.error(f"❌ Ошибка восстановления RemnaWave пользователя при разблокировке: {e}") else: logger.info(f"⏰ Подписка пользователя {user_id} истекла, восстановление невозможно") logger.info(f"Админ {admin_id} разблокировал пользователя {user_id}") return True except Exception as e: logger.error(f"Ошибка разблокировки пользователя: {e}") return False async def delete_user_account( self, db: AsyncSession, user_id: int, admin_id: int ) -> bool: try: user = await get_user_by_id(db, user_id) if not user: logger.warning(f"Пользователь {user_id} не найден для удаления") return False logger.info(f"🗑️ Начинаем полное удаление пользователя {user_id} (Telegram ID: {user.telegram_id})") if user.remnawave_uuid: from app.config import settings delete_mode = settings.get_remnawave_user_delete_mode() try: from app.services.remnawave_service import RemnaWaveService remnawave_service = RemnaWaveService() if delete_mode == "delete": # Удаляем пользователя из панели Remnawave async with remnawave_service.get_api_client() as api: delete_success = await api.delete_user(user.remnawave_uuid) if delete_success: logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} удален из панели") else: logger.warning(f"⚠️ Не удалось удалить пользователя {user.remnawave_uuid} из панели Remnawave") else: # Деактивируем пользователя в панели Remnawave from app.services.subscription_service import SubscriptionService subscription_service = SubscriptionService() await subscription_service.disable_remnawave_user(user.remnawave_uuid) logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован (режим: {delete_mode})") except Exception as e: logger.warning(f"⚠️ Ошибка обработки пользователя в Remnawave (режим: {delete_mode}): {e}") # Если основное действие не удалось, попытаемся хотя бы деактивировать if delete_mode == "delete": try: from app.services.subscription_service import SubscriptionService subscription_service = SubscriptionService() await subscription_service.disable_remnawave_user(user.remnawave_uuid) logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован как fallback") except Exception as fallback_e: logger.error(f"❌ Ошибка деактивации RemnaWave как fallback: {fallback_e}") try: sent_notifications_result = await db.execute( select(SentNotification).where(SentNotification.user_id == user_id) ) sent_notifications = sent_notifications_result.scalars().all() if sent_notifications: logger.info(f"🔄 Удаляем {len(sent_notifications)} уведомлений") await db.execute( delete(SentNotification).where(SentNotification.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления уведомлений: {e}") try: if user.subscription: subscription_servers_result = await db.execute( select(SubscriptionServer).where( SubscriptionServer.subscription_id == user.subscription.id ) ) subscription_servers = subscription_servers_result.scalars().all() await decrement_subscription_server_counts( db, user.subscription, subscription_servers=subscription_servers, ) if subscription_servers: logger.info(f"🔄 Удаляем {len(subscription_servers)} связей подписка-сервер") await db.execute( delete(SubscriptionServer).where( SubscriptionServer.subscription_id == user.subscription.id ) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления связей подписка-сервер: {e}") try: user_messages_result = await db.execute( update(UserMessage) .where(UserMessage.created_by == user_id) .values(created_by=None) ) if user_messages_result.rowcount > 0: logger.info(f"🔄 Обновлено {user_messages_result.rowcount} пользовательских сообщений") await db.flush() except Exception as e: logger.error(f"❌ Ошибка обновления пользовательских сообщений: {e}") try: promocodes_result = await db.execute( update(PromoCode) .where(PromoCode.created_by == user_id) .values(created_by=None) ) if promocodes_result.rowcount > 0: logger.info(f"🔄 Обновлено {promocodes_result.rowcount} промокодов") await db.flush() except Exception as e: logger.error(f"❌ Ошибка обновления промокодов: {e}") try: welcome_texts_result = await db.execute( update(WelcomeText) .where(WelcomeText.created_by == user_id) .values(created_by=None) ) if welcome_texts_result.rowcount > 0: logger.info(f"🔄 Обновлено {welcome_texts_result.rowcount} приветственных текстов") await db.flush() except Exception as e: logger.error(f"❌ Ошибка обновления приветственных текстов: {e}") try: referrals_result = await db.execute( update(User) .where(User.referred_by_id == user_id) .values(referred_by_id=None) ) if referrals_result.rowcount > 0: logger.info(f"🔗 Очищены реферальные ссылки у {referrals_result.rowcount} рефералов") await db.flush() except Exception as e: logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}") try: yookassa_result = await db.execute( select(YooKassaPayment).where(YooKassaPayment.user_id == user_id) ) yookassa_payments = yookassa_result.scalars().all() if yookassa_payments: logger.info(f"🔄 Удаляем {len(yookassa_payments)} YooKassa платежей") await db.execute( update(YooKassaPayment) .where(YooKassaPayment.user_id == user_id) .values(transaction_id=None) ) await db.flush() await db.execute( delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления YooKassa платежей: {e}") try: cryptobot_result = await db.execute( select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id) ) cryptobot_payments = cryptobot_result.scalars().all() if cryptobot_payments: logger.info(f"🔄 Удаляем {len(cryptobot_payments)} CryptoBot платежей") await db.execute( update(CryptoBotPayment) .where(CryptoBotPayment.user_id == user_id) .values(transaction_id=None) ) await db.flush() await db.execute( delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления CryptoBot платежей: {e}") try: platega_result = await db.execute( select(PlategaPayment).where(PlategaPayment.user_id == user_id) ) platega_payments = platega_result.scalars().all() if platega_payments: logger.info(f"🔄 Удаляем {len(platega_payments)} Platega платежей") await db.execute( update(PlategaPayment) .where(PlategaPayment.user_id == user_id) .values(transaction_id=None) ) await db.flush() await db.execute( delete(PlategaPayment).where(PlategaPayment.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления Platega платежей: {e}") try: mulenpay_result = await db.execute( select(MulenPayPayment).where(MulenPayPayment.user_id == user_id) ) mulenpay_payments = mulenpay_result.scalars().all() if mulenpay_payments: mulenpay_name = settings.get_mulenpay_display_name() logger.info( f"🔄 Удаляем {len(mulenpay_payments)} {mulenpay_name} платежей" ) await db.execute( update(MulenPayPayment) .where(MulenPayPayment.user_id == user_id) .values(transaction_id=None) ) await db.flush() await db.execute( delete(MulenPayPayment).where(MulenPayPayment.user_id == user_id) ) await db.flush() except Exception as e: logger.error( f"❌ Ошибка удаления {settings.get_mulenpay_display_name()} платежей: {e}" ) try: pal24_result = await db.execute( select(Pal24Payment).where(Pal24Payment.user_id == user_id) ) pal24_payments = pal24_result.scalars().all() if pal24_payments: logger.info(f"🔄 Удаляем {len(pal24_payments)} Pal24 платежей") await db.execute( update(Pal24Payment) .where(Pal24Payment.user_id == user_id) .values(transaction_id=None) ) await db.flush() await db.execute( delete(Pal24Payment).where(Pal24Payment.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления Pal24 платежей: {e}") try: heleket_result = await db.execute( select(HeleketPayment).where(HeleketPayment.user_id == user_id) ) heleket_payments = heleket_result.scalars().all() if heleket_payments: logger.info( f"🔄 Удаляем {len(heleket_payments)} Heleket платежей" ) await db.execute( update(HeleketPayment) .where(HeleketPayment.user_id == user_id) .values(transaction_id=None) ) await db.flush() await db.execute( delete(HeleketPayment).where(HeleketPayment.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления Heleket платежей: {e}") # Удаляем Freekassa платежи try: freekassa_payments_result = await db.execute( select(FreekassaPayment).where(FreekassaPayment.user_id == user_id) ) freekassa_payments = freekassa_payments_result.scalars().all() if freekassa_payments: logger.info(f"🔄 Удаляем {len(freekassa_payments)} Freekassa платежей") await db.execute( update(FreekassaPayment) .where(FreekassaPayment.user_id == user_id) .values(transaction_id=None) ) await db.flush() await db.execute( delete(FreekassaPayment).where(FreekassaPayment.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления Freekassa платежей: {e}") try: transactions_result = await db.execute( select(Transaction).where(Transaction.user_id == user_id) ) transactions = transactions_result.scalars().all() if transactions: logger.info(f"🔄 Удаляем {len(transactions)} транзакций") await db.execute( delete(Transaction).where(Transaction.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления транзакций: {e}") try: promocode_uses_result = await db.execute( select(PromoCodeUse).where(PromoCodeUse.user_id == user_id) ) promocode_uses = promocode_uses_result.scalars().all() if promocode_uses: logger.info(f"🔄 Удаляем {len(promocode_uses)} использований промокодов") await db.execute( delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления использований промокодов: {e}") try: referral_earnings_result = await db.execute( select(ReferralEarning).where(ReferralEarning.user_id == user_id) ) referral_earnings = referral_earnings_result.scalars().all() if referral_earnings: logger.info(f"🔄 Удаляем {len(referral_earnings)} реферальных доходов") await db.execute( delete(ReferralEarning).where(ReferralEarning.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления реферальных доходов: {e}") try: referral_records_result = await db.execute( select(ReferralEarning).where(ReferralEarning.referral_id == user_id) ) referral_records = referral_records_result.scalars().all() if referral_records: logger.info(f"🔄 Удаляем {len(referral_records)} записей о рефералах") await db.execute( delete(ReferralEarning).where(ReferralEarning.referral_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления записей о рефералах: {e}") try: conversions_result = await db.execute( select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id) ) conversions = conversions_result.scalars().all() if conversions: logger.info(f"🔄 Удаляем {len(conversions)} записей конверсий") await db.execute( delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления записей конверсий: {e}") try: broadcast_history_result = await db.execute( select(BroadcastHistory).where(BroadcastHistory.admin_id == user_id) ) broadcast_history = broadcast_history_result.scalars().all() if broadcast_history: logger.info(f"🔄 Удаляем {len(broadcast_history)} записей истории рассылок") await db.execute( delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления истории рассылок: {e}") try: campaigns_result = await db.execute( select(AdvertisingCampaign).where(AdvertisingCampaign.created_by == user_id) ) campaigns = campaigns_result.scalars().all() if campaigns: logger.info(f"🔄 Очищаем создателя у {len(campaigns)} рекламных кампаний") await db.execute( update(AdvertisingCampaign) .where(AdvertisingCampaign.created_by == user_id) .values(created_by=None) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка обновления рекламных кампаний: {e}") try: if user.subscription: logger.info(f"🔄 Удаляем подписку {user.subscription.id}") await db.execute( delete(Subscription).where(Subscription.user_id == user_id) ) await db.flush() except Exception as e: logger.error(f"❌ Ошибка удаления подписки: {e}") try: await db.execute( delete(User).where(User.id == user_id) ) await db.commit() logger.info(f"✅ Пользователь {user_id} окончательно удален из базы") except Exception as e: logger.error(f"❌ Ошибка финального удаления пользователя: {e}") await db.rollback() return False logger.info(f"✅ Пользователь {user.telegram_id} (ID: {user_id}) полностью удален администратором {admin_id}") return True except Exception as e: logger.error(f"❌ Критическая ошибка удаления пользователя {user_id}: {e}") await db.rollback() return False async def get_user_statistics(self, db: AsyncSession) -> Dict[str, Any]: try: stats = await get_users_statistics(db) return stats except Exception as e: logger.error(f"Ошибка получения статистики пользователей: {e}") return { "total_users": 0, "active_users": 0, "blocked_users": 0, "new_today": 0, "new_week": 0, "new_month": 0 } async def cleanup_inactive_users( self, db: AsyncSession, months: int = None ) -> int: try: if months is None: months = settings.INACTIVE_USER_DELETE_MONTHS inactive_users = await get_inactive_users(db, months) deleted_count = 0 for user in inactive_users: success = await self.delete_user_account(db, user.id, 0) if success: deleted_count += 1 logger.info(f"Удалено {deleted_count} неактивных пользователей") return deleted_count except Exception as e: logger.error(f"Ошибка очистки неактивных пользователей: {e}") return 0 async def get_user_activity_summary( self, db: AsyncSession, user_id: int ) -> Dict[str, Any]: try: user = await get_user_by_id(db, user_id) if not user: return {} subscription = await get_subscription_by_user_id(db, user_id) transactions_count = await get_user_transactions_count(db, user_id) days_since_registration = (datetime.utcnow() - user.created_at).days days_since_activity = (datetime.utcnow() - user.last_activity).days if user.last_activity else None return { "user_id": user.id, "telegram_id": user.telegram_id, "username": user.username, "full_name": user.full_name, "status": user.status, "language": user.language, "balance_kopeks": user.balance_kopeks, "registration_date": user.created_at, "last_activity": user.last_activity, "days_since_registration": days_since_registration, "days_since_activity": days_since_activity, "has_subscription": subscription is not None, "subscription_active": subscription.is_active if subscription else False, "subscription_trial": subscription.is_trial if subscription else False, "transactions_count": transactions_count, "referrer_id": user.referred_by_id, "referral_code": user.referral_code } except Exception as e: logger.error(f"Ошибка получения сводки активности пользователя {user_id}: {e}") return {} async def get_users_by_criteria( self, db: AsyncSession, criteria: Dict[str, Any] ) -> List[User]: try: status = criteria.get('status') has_subscription = criteria.get('has_subscription') is_trial = criteria.get('is_trial') min_balance = criteria.get('min_balance', 0) max_balance = criteria.get('max_balance') days_inactive = criteria.get('days_inactive') registered_after = criteria.get('registered_after') registered_before = criteria.get('registered_before') users = await get_users_list(db, offset=0, limit=10000, status=status) filtered_users = [] for user in users: if user.balance_kopeks < min_balance: continue if max_balance and user.balance_kopeks > max_balance: continue if registered_after and user.created_at < registered_after: continue if registered_before and user.created_at > registered_before: continue if days_inactive and user.last_activity: inactive_threshold = datetime.utcnow() - timedelta(days=days_inactive) if user.last_activity > inactive_threshold: continue filtered_users.append(user) return filtered_users except Exception as e: logger.error(f"Ошибка получения пользователей по критериям: {e}") return []