✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
190 lines
7.3 KiB
Python
190 lines
7.3 KiB
Python
"""
|
||
Хендлеры для работы с квартирами
|
||
Показывает кто живёт в квартире, статистику и т.д.
|
||
"""
|
||
import logging
|
||
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, func
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import User
|
||
from utils.decorators import admin_only
|
||
from datetime import datetime
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
|
||
@router.message(Command('apt'))
|
||
@admin_only
|
||
async def cmd_apartment_info(message: Message):
|
||
"""
|
||
Показать всех жильцов квартиры
|
||
Использование: /apt 88
|
||
"""
|
||
args = message.text.split()
|
||
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'Использование: <code>/apt <номер квартиры></code>\n\n'
|
||
'Пример: <code>/apt 88</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
apartment = args[1].strip().upper()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Находим всех жильцов этой квартиры
|
||
stmt = (
|
||
select(User)
|
||
.where(User.apartment == apartment)
|
||
.order_by(User.verification_date.asc())
|
||
)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await message.answer(f'🏠 <b>Квартира {apartment}</b>\n\n❌ Нет жильцов')
|
||
return
|
||
|
||
# Считаем статистику
|
||
verified_count = sum(1 for u in users if u.verified)
|
||
unverified_count = len(users) - verified_count
|
||
|
||
text = f'🏠 <b>Квартира {apartment}</b> — {len(users)} чел.\n\n'
|
||
text += f'✅ Верифицированы: {verified_count}\n'
|
||
if unverified_count > 0:
|
||
text += f'⏳ Не верифицированы: {unverified_count}\n'
|
||
text += '\n'
|
||
|
||
for i, user in enumerate(users, 1):
|
||
status = '✅' if user.verified else '⏳'
|
||
name = user.full_name or f'user{user.user_id}'
|
||
username = f'@{user.username}' if user.username else ''
|
||
|
||
# Кто первый верифицировался — основной жилец
|
||
main_marker = '👤 <b>(основной)</b>' if i == 1 else ''
|
||
|
||
text += f'{i}. {status} <code>{user.user_id}</code> — {name} {username} {main_marker}\n'
|
||
|
||
if user.verification_date:
|
||
days_ago = (datetime.utcnow() - user.verification_date).days
|
||
text += f' └─ Верифицирован: {days_ago} дн. назад\n'
|
||
else:
|
||
text += f' └─ Не верифицирован\n'
|
||
|
||
if user.message_count > 0:
|
||
text += f' └─ Сообщений: {user.message_count}\n'
|
||
|
||
text += '\n'
|
||
|
||
# Если больше одного — предупреждение для админа
|
||
if len(users) > 1:
|
||
text += '⚠️ <b>Внимание: несколько человек в одной квартире!</b>\n\n'
|
||
text += 'Это может быть:\n'
|
||
text += '✅ Члены семьи (нормально)\n'
|
||
text += '❌ Шпион (нужно проверить)\n\n'
|
||
text += '💡 Спросите в чате: "Кто ещё живёт в кв. {apartment}?"'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('apt_stats'))
|
||
@admin_only
|
||
async def cmd_apartment_stats(message: Message):
|
||
"""
|
||
Статистика по квартирам с несколькими жильцами
|
||
"""
|
||
async with AsyncSessionLocal() as session:
|
||
# Группируем по квартирам и считаем количество жильцов
|
||
stmt = (
|
||
select(User.apartment, func.count(User.user_id).label('count'))
|
||
.where(User.apartment.isnot(None))
|
||
.group_by(User.apartment)
|
||
.having(func.count(User.user_id) > 1)
|
||
.order_by(func.count(User.user_id).desc())
|
||
)
|
||
result = await session.execute(stmt)
|
||
multi_apt = list(result.all())
|
||
|
||
# Общая статистика
|
||
stmt = select(func.count(User.user_id))
|
||
total_users = (await session.execute(stmt)).scalar() or 0
|
||
|
||
stmt = select(func.count(User.apartment)).where(User.apartment.isnot(None))
|
||
total_apartments = (await session.execute(stmt)).scalar() or 0
|
||
|
||
if not multi_apt:
|
||
await message.answer(
|
||
f'📊 <b>Статистика по квартирам</b>\n\n'
|
||
f'Всего жильцов: {total_users}\n'
|
||
f'Всего квартир: {total_apartments}\n\n'
|
||
f'✅ Нет квартир с несколькими жильцами'
|
||
)
|
||
return
|
||
|
||
text = f'📊 <b>Статистика по квартирам</b>\n\n'
|
||
text += f'Всего жильцов: {total_users}\n'
|
||
text += f'Всего квартир: {total_apartments}\n'
|
||
text += f'🏠 Квартир с несколькими жильцами: {len(multi_apt)}\n\n'
|
||
|
||
text += f'<b>Квартиры с несколькими жильцами:</b>\n\n'
|
||
|
||
for apt, count in multi_apt[:20]: # Показываем первые 20
|
||
text += f'🏠 <b>Кв. {apt}</b> — {count} чел.\n'
|
||
|
||
if len(multi_apt) > 20:
|
||
text += f'\n... и ещё {len(multi_apt) - 20} квартир'
|
||
|
||
text += '\n\n💡 Используйте <code>/apt <номер></code> для деталей'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('find_apt'))
|
||
@admin_only
|
||
async def cmd_find_apartment(message: Message):
|
||
"""
|
||
Поиск квартиры по номеру (частичное совпадение)
|
||
Использование: /find 8
|
||
Найдёт: 8, 88, 8А, 108, 808 и т.д.
|
||
"""
|
||
args = message.text.split()
|
||
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'Использование: <code>/find <часть номера></code>\n\n'
|
||
'Пример: <code>/find 8</code> найдёт 8, 88, 8А, 108...',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
search_term = args[1].strip().upper()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Ищем квартиры где есть такой номер
|
||
stmt = (
|
||
select(User.apartment, func.count(User.user_id).label('count'))
|
||
.where(User.apartment.like(f'%{search_term}%'))
|
||
.group_by(User.apartment)
|
||
.order_by(func.count(User.user_id).desc())
|
||
)
|
||
result = await session.execute(stmt)
|
||
apartments = list(result.all())
|
||
|
||
if not apartments:
|
||
await message.answer(f'🔍 Квартиры с "{search_term}" не найдены')
|
||
return
|
||
|
||
text = f'🔍 <b>Квартиры с "{search_term}"</b> ({len(apartments)}):\n\n'
|
||
|
||
for apt, count in apartments:
|
||
text += f'🏠 <b>Кв. {apt}</b> — {count} чел.\n'
|
||
|
||
text += '\n💡 Используйте <code>/apt <номер></code> для деталей'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|