""" Profile History — история изменений профиля пользователя Только для администраторов """ import logging from datetime import datetime from aiogram import Router, F from aiogram.types import Message from aiogram.filters import Command from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from database.db import AsyncSessionLocal from database.models import User, ProfileHistory from utils.decorators import admin_only logger = logging.getLogger(__name__) router = Router() # ============================================================================= # ИСТОРИЯ ПОЛЬЗОВАТЕЛЯ # ============================================================================= @router.message(Command('history')) @router.message(Command('история')) @admin_only async def cmd_history(message: Message): """ Показать историю изменений профиля пользователя Использование: /history [лимит] """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/history <user_id> [лимит]\n\n' 'Пример: /history 123456789 20', parse_mode='HTML' ) return try: user_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID пользователя') return limit = int(args[2]) if len(args) > 2 and args[2].isdigit() else 20 async with AsyncSessionLocal() as session: # Получаем пользователя user = await session.get(User, user_id) if not user: await message.answer(f'❌ Пользователь {user_id} не найден') return # Получаем историю stmt = ( select(ProfileHistory) .where(ProfileHistory.user_id == user_id) .order_by(desc(ProfileHistory.changed_at)) .limit(limit) ) result = await session.execute(stmt) history = list(result.scalars().all()) name = user.full_name or user.username or f'user{user_id}' apt = user.apartment or '?' if not history: await message.answer(f'ℹ️ У пользователя {name} нет истории изменений') return text = f'📋 История профиля: {name} (кв. {apt})\n\n' text += f'Показано последних: {len(history)} из {limit}\n\n' field_names = { 'username': 'Никнейм', 'first_name': 'Имя', 'last_name': 'Фамилия', 'apartment': 'Квартира', 'phone': 'Телефон', } for i, record in enumerate(history, 1): field_name = field_names.get(record.field_name, record.field_name) old_val = record.old_value or 'пусто' new_val = record.new_value or 'пусто' changed_at = record.changed_at.strftime('%d.%m.%Y %H:%M') text += f'{i}. {field_name} ({changed_at})\n' text += f' ├─ Было: {old_val}\n' text += f' └─ Стало: {new_val}\n\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # ВСЕ ИЗМЕНЕНИЯ ЗА ПЕРИОД # ============================================================================= @router.message(Command('all_history')) @router.message(Command('вся_история')) @admin_only async def cmd_all_history(message: Message): """ Показать все изменения профилей за период Использование: /all_history [дни] [лимит] """ args = message.text.split() days = int(args[1]) if len(args) > 1 and args[1].isdigit() else 7 limit = int(args[2]) if len(args) > 2 and args[2].isdigit() else 50 from datetime import timedelta time_window = datetime.utcnow() - timedelta(days=days) async with AsyncSessionLocal() as session: stmt = ( select(ProfileHistory) .where(ProfileHistory.changed_at >= time_window) .order_by(desc(ProfileHistory.changed_at)) .limit(limit) ) result = await session.execute(stmt) history = list(result.scalars().all()) if not history: await message.answer(f'ℹ️ Нет изменений за последние {days} дней') return text = f'📋 Все изменения за {days} дн.\n\n' text += f'Показано: {len(history)} из {limit}\n\n' field_names = { 'username': 'Никнейм', 'first_name': 'Имя', 'last_name': 'Фамилия', 'apartment': 'Квартира', 'phone': 'Телефон', } # Группируем по пользователям user_changes = {} for record in history: if record.user_id not in user_changes: user_changes[record.user_id] = [] user_changes[record.user_id].append(record) for user_id, changes in user_changes.items(): user = await session.get(User, user_id) name = user.full_name or user.username or f'user{user_id}' apt = user.apartment or '?' text += f'👤 {name} (кв. {apt}) — {len(changes)} изм.\n' for record in changes[:5]: # Показываем первые 5 field_name = field_names.get(record.field_name, record.field_name) old_val = record.old_value or 'пусто' new_val = record.new_value or 'пусто' text += f' └─ {field_name}: {old_val} → {new_val}\n' if len(changes) > 5: text += f' └─ ... и ещё {len(changes) - 5}\n' text += '\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # ИЗМЕНЕНИЯ КОНКРЕТНОГО ПОЛЯ # ============================================================================= @router.message(Command('field_history')) @router.message(Command('история_поля')) @admin_only async def cmd_field_history(message: Message): """ Показать изменения конкретного поля Использование: /field_history <поле> [дни] Поля: username, first_name, last_name, apartment, phone """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/field_history <поле> [дни]\n\n' 'Поля: username, first_name, last_name, apartment, phone\n' 'Пример: /field_history apartment 7', parse_mode='HTML' ) return field = args[1].lower() days = int(args[2]) if len(args) > 2 and args[2].isdigit() else 7 valid_fields = ['username', 'first_name', 'last_name', 'apartment', 'phone'] if field not in valid_fields: await message.answer(f'❌ Неверное поле. Доступные: {", ".join(valid_fields)}') return from datetime import timedelta time_window = datetime.utcnow() - timedelta(days=days) async with AsyncSessionLocal() as session: stmt = ( select(ProfileHistory) .where(ProfileHistory.field_name == field) .where(ProfileHistory.changed_at >= time_window) .order_by(desc(ProfileHistory.changed_at)) .limit(20) ) result = await session.execute(stmt) history = list(result.scalars().all()) if not history: await message.answer(f'ℹ️ Нет изменений поля "{field}" за {days} дней') return field_names = { 'username': 'Никнейм', 'first_name': 'Имя', 'last_name': 'Фамилия', 'apartment': 'Квартира', 'phone': 'Телефон', } text = f'📋 История поля: {field_names.get(field, field)}\n\n' text += f'За {days} дн. ({len(history)} изм.)\n\n' for i, record in enumerate(history, 1): user = await session.get(User, record.user_id) name = user.full_name or user.username or f'user{record.user_id}' apt = user.apartment or '?' old_val = record.old_value or 'пусто' new_val = record.new_value or 'пусто' changed_at = record.changed_at.strftime('%d.%m.%Y %H:%M') text += f'{i}. {name} (кв. {apt}) — {changed_at}\n' text += f' └─ {old_val} → {new_val}\n\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # СТАТИСТИКА ИЗМЕНЕНИЙ # ============================================================================= @router.message(Command('history_stats')) @router.message(Command('статистика_истории')) @admin_only async def cmd_history_stats(message: Message): """Статистика изменений профилей""" from sqlalchemy import func async with AsyncSessionLocal() as session: # Общее количество записей stmt = select(func.count(ProfileHistory.id)) result = await session.execute(stmt) total = result.scalar() # По полям stmt = ( select(ProfileHistory.field_name, func.count(ProfileHistory.id)) .group_by(ProfileHistory.field_name) ) result = await session.execute(stmt) by_field = list(result.all()) # За последние 7 дней from datetime import timedelta time_window = datetime.utcnow() - timedelta(days=7) stmt = select(func.count(ProfileHistory.id)).where( ProfileHistory.changed_at >= time_window ) result = await session.execute(stmt) last_7_days = result.scalar() text = '📊 Статистика истории профиля\n\n' text += f'Всего записей: {total}\n' text += f'За последние 7 дней: {last_7_days}\n\n' if by_field: text += '📁 По полям:\n\n' field_names = { 'username': 'Никнейм', 'first_name': 'Имя', 'last_name': 'Фамилия', 'apartment': 'Квартира', 'phone': 'Телефон', } for field, count in sorted(by_field, key=lambda x: x[1], reverse=True): text += f'• {field_names.get(field, field)}: {count}\n' await message.answer(text, parse_mode='HTML')