domovoy_bot/handlers/profile_history.py
Admin ba4676b0af v1.0 MVP - Инициализация проекта 🏠
 Регистрация и верификация жильцов
 Рейтинг активности (уровни)
 Детект шпионов (Score 0-100)
 Анти-мат система (3 предупреждения → бан)
 Админ-панель с рассылкой
 Учёт квартир (несколько жильцов)
 Телефоны экстренных служб и мастеров
 Еженедельный экспорт JSON
 Прокси (socks5) для обхода РКН

Бекап: backups/versions/v1.0_mvp_2026-02-26/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 10:54:16 +00:00

298 lines
11 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 <user_id> [лимит]
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ <b>Использование:</b>\n'
'<code>/history &lt;user_id&gt; [лимит]</code>\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'📋 <b>История профиля: {name}</b> (кв. {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}. <b>{field_name}</b> ({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'📋 <b>Все изменения за {days} дн.</b>\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'👤 <b>{name}</b> (кв. {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(
'❌ <b>Использование:</b>\n'
'<code>/field_history &lt;поле&gt; [дни]</code>\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'📋 <b>История поля: {field_names.get(field, field)}</b>\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 = '📊 <b>Статистика истории профиля</b>\n\n'
text += f'Всего записей: {total}\n'
text += f'За последние 7 дней: {last_7_days}\n\n'
if by_field:
text += '📁 <b>По полям:</b>\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')