✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
298 lines
11 KiB
Python
298 lines
11 KiB
Python
"""
|
||
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 <user_id> [лимит]</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 <поле> [дни]</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')
|