""" Сервис генерации еженедельного дайджеста """ import logging import json from datetime import datetime, timedelta from typing import Dict, List, Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, desc from database.models import ( User, Message, Poll, Ad, Event, Schedule, VerificationRequest ) logger = logging.getLogger(__name__) class DigestService: """Сервис для генерации и управления дайджестами""" def __init__(self, session: AsyncSession): self.session = session async def generate_weekly_digest(self, year: int = None, week: int = None) -> Optional[Dict]: """ Сгенерировать дайджест за неделю Возвращает dict с данными для формирования текста """ from datetime import date # Если не указаны год/неделя - берём текущую if year is None or week is None: today = date.today() year, week, _ = today.isocalendar() # Определяем период (понедельник - воскресенье) monday = date.fromisocalendar(year, week, 1) sunday = date.fromisocalendar(year, week, 7) period_start = datetime.combine(monday, datetime.min.time()) period_end = datetime.combine(sunday, datetime.max.time()) logger.info(f"📰 Генерация дайджеста: {period_start} - {period_end}") # Собираем данные events = await self._get_events(period_start, period_end) polls = await self._get_polls(period_start, period_end) ads = await self._get_ads(period_start, period_end) top_users = await self._get_top_users(period_start, period_end) schedule = await self._get_schedule(period_end, period_end + timedelta(days=7)) verifications = await self._get_verifications(period_start, period_end) stats = await self._get_stats(period_start, period_end) content = { 'period_start': period_start.isoformat(), 'period_end': period_end.isoformat(), 'week_number': week, 'year': year, 'events': events, 'polls': polls, 'ads': ads, 'top_users': top_users, 'schedule': schedule, 'verifications': verifications, 'stats': stats, } # Формируем текст digest_text = self._format_digest_text(content) return { 'content': content, 'text': digest_text, 'period_start': period_start, 'period_end': period_end, 'week_number': week, 'year': year, } async def _get_events(self, start: datetime, end: datetime) -> List[Dict]: """Получить события за период""" stmt = ( select(Event) .where(Event.event_date >= start) .where(Event.event_date <= end) .where(Event.is_active == True) .order_by(Event.event_date) ) result = await self.session.execute(stmt) events = list(result.scalars().all()) return [ { 'id': e.id, 'title': e.title, 'event_date': e.event_date.strftime('%d.%m.%Y %H:%M'), 'location': e.location or 'Не указано', 'type': e.event_type, } for e in events ] async def _get_polls(self, start: datetime, end: datetime) -> List[Dict]: """Получить опросы за период""" stmt = ( select(Poll) .where(Poll.created_at >= start) .where(Poll.created_at <= end) .order_by(Poll.created_at.desc()) ) result = await self.session.execute(stmt) polls = list(result.scalars().all()) return [ { 'id': p.poll_id, 'question': p.question[:100], 'total_votes': p.total_votes, 'is_active': p.is_active, } for p in polls ] async def _get_ads(self, start: datetime, end: datetime) -> Dict: """Получить статистику объявлений""" new_stmt = ( select(func.count(Ad.ad_id)) .where(Ad.created_at >= start) .where(Ad.created_at <= end) ) new_count = (await self.session.execute(new_stmt)).scalar() or 0 active_stmt = select(func.count(Ad.ad_id)).where(Ad.is_active == True) active_count = (await self.session.execute(active_stmt)).scalar() or 0 return { 'new_count': new_count, 'active_count': active_count, } async def _get_top_users(self, start: datetime, end: datetime) -> List[Dict]: """Получить топ-5 активных жильцов""" # Считаем сообщения за период stmt = ( select( User.user_id, User.first_name, User.last_name, User.username, User.apartment, func.count(Message.id).label('msg_count') ) .join(Message, User.user_id == Message.user_id) .where(Message.timestamp >= start) .where(Message.timestamp <= end) .where(User.verified == True) .group_by(User.user_id) .order_by(desc('msg_count')) .limit(5) ) result = await self.session.execute(stmt) rows = result.all() return [ { 'user_id': r.user_id, 'name': r.first_name or r.username or f'User{r.user_id}', 'apartment': r.apartment or '-', 'messages': r.msg_count, } for r in rows ] async def _get_schedule(self, start: datetime, end: datetime) -> List[Dict]: """Получить отключения на следующую неделю""" stmt = ( select(Schedule) .where(Schedule.start_time >= start) .where(Schedule.start_time <= end) .where(Schedule.is_active == True) .order_by(Schedule.start_time) ) result = await self.session.execute(stmt) schedules = list(result.scalars().all()) return [ { 'id': s.schedule_id, 'title': s.title, 'type': s.schedule_type, 'start_time': s.start_time.strftime('%d.%m %H:%M'), 'end_time': s.end_time.strftime('%d.%m %H:%M'), 'emoji': s.get_type_emoji(), } for s in schedules ] async def _get_verifications(self, start: datetime, end: datetime) -> Dict: """Получить статистику верификаций""" new_stmt = ( select(func.count(VerificationRequest.id)) .where(VerificationRequest.created_at >= start) .where(VerificationRequest.created_at <= end) .where(VerificationRequest.status == 'approved') ) new_count = (await self.session.execute(new_stmt)).scalar() or 0 total_stmt = select(func.count(User.user_id)).where(User.verified == True) total_count = (await self.session.execute(total_stmt)).scalar() or 0 return { 'new_verified': new_count, 'total_verified': total_count, } async def _get_stats(self, start: datetime, end: datetime) -> Dict: """Получить общую статистику""" # Сообщения msg_stmt = ( select(func.count(Message.id)) .where(Message.timestamp >= start) .where(Message.timestamp <= end) ) msg_count = (await self.session.execute(msg_stmt)).scalar() or 0 # Активные пользователи (кто писал сообщения) active_stmt = ( select(func.count(func.distinct(Message.user_id))) .where(Message.timestamp >= start) .where(Message.timestamp <= end) ) active_users = (await self.session.execute(active_stmt)).scalar() or 0 return { 'messages_count': msg_count, 'active_users': active_users, } def _format_digest_text(self, content: Dict) -> str: """Сформировать текст дайджеста""" events = content['events'] polls = content['polls'] ads = content['ads'] top_users = content['top_users'] schedule = content['schedule'] verifications = content['verifications'] stats = content['stats'] period_start = datetime.fromisoformat(content['period_start']).strftime('%d.%m') period_end = datetime.fromisoformat(content['period_end']).strftime('%d.%m.%Y') text = f"📰 ДАЙДЖЕСТ ДОМА ЗА НЕДЕЛЮ\n" text += f"🗓 {period_start} - {period_end}\n\n" # События text += f"🔔 События:\n" if events: for e in events: text += f"• {e['title']} — {e['event_date']}\n" else: text += "• Нет событий\n" text += "\n" # Опросы text += f"📊 Опросы:\n" if polls: for p in polls: status = "✅ активен" if p['is_active'] else "🔴 завершён" text += f"• \"{p['question']}\" — {p['total_votes']} голосов ({status})\n" else: text += "• Нет опросов\n" text += "\n" # Объявления text += f"📢 Объявления: {ads['new_count']} новых\n" text += f" Всего активных: {ads['active_count']}\n\n" # Топ жильцов if top_users: text += f"🏆 Активные жильцы:\n" for i, u in enumerate(top_users, 1): medal = ['🥇', '🥈', '🥉'][i-1] if i <= 3 else ' ' text += f"{medal} {u['name']} (кв. {u['apartment']}) — {u['messages']} сообщ.\n" text += "\n" # Отключения if schedule: text += f"⏰ На следующей неделе:\n" for s in schedule: text += f"{s['emoji']} {s['title']} — {s['start_time']} до {s['end_time']}\n" text += "\n" # Верификации text += f"✅ Верификации:\n" text += f"• Новых за неделю: {verifications['new_verified']}\n" text += f"• Всего верифицировано: {verifications['total_verified']}\n\n" # Статистика text += f"📈 Статистика:\n" text += f"• Сообщений в чате: {stats['messages_count']}\n" text += f"• Активных жильцов: {stats['active_users']}\n" return text async def save_digest(self, digest_data: Dict, status: str = 'draft') -> int: """Сохранить дайджест в БД""" from database.models import Digest import json digest = Digest( period_start=digest_data['period_start'], period_end=digest_data['period_end'], week_number=digest_data['week_number'], year=digest_data['year'], content=json.dumps(digest_data['content'], ensure_ascii=False), digest_text=digest_data['text'], status=status, ) self.session.add(digest) await self.session.commit() await self.session.refresh(digest) logger.info(f"💾 Дайджест сохранён: ID={digest.id}, статус={status}") return digest.id async def update_digest_status(self, digest_id: int, status: str, **kwargs) -> bool: """Обновить статус дайджеста""" from database.models import Digest digest = await self.session.get(Digest, digest_id) if not digest: logger.error(f"❌ Дайджест ID={digest_id} не найден") return False digest.status = status if status == 'approved': digest.approved_at = datetime.utcnow() digest.approved_by = kwargs.get('approved_by') elif status == 'rejected': digest.rejected_by = kwargs.get('rejected_by') digest.rejection_reason = kwargs.get('rejection_reason', '') elif status == 'sent': digest.sent_at = datetime.utcnow() digest.message_id = kwargs.get('message_id') await self.session.commit() logger.info(f"🔄 Дайджест ID={digest_id}: статус → {status}") return True async def update_digest_text(self, digest_id: int, new_text: str) -> bool: """Редактировать текст дайджеста""" from database.models import Digest digest = await self.session.get(Digest, digest_id) if not digest: return False digest.digest_text = new_text await self.session.commit() return True async def get_pending_digests(self) -> List[Dict]: """Получить дайджесты ожидающие утверждения""" from database.models import Digest stmt = ( select(Digest) .where(Digest.status == 'pending_approval') .order_by(Digest.created_at.desc()) ) result = await self.session.execute(stmt) digests = list(result.scalars().all()) return [ { 'id': d.id, 'week': d.week_number, 'year': d.year, 'period_label': d.period_label, 'status': d.status, 'status_emoji': d.status_emoji, 'created_at': d.created_at.strftime('%d.%m.%Y %H:%M'), 'text': d.digest_text or '', } for d in digests ] async def get_digest_archive(self, limit: int = 50) -> List[Dict]: """Получить архив дайджестов""" from database.models import Digest stmt = ( select(Digest) .order_by(Digest.year.desc(), Digest.week_number.desc()) .limit(limit) ) result = await self.session.execute(stmt) digests = list(result.scalars().all()) return [ { 'id': d.id, 'week': d.week_number, 'year': d.year, 'period_label': d.period_label, 'status': d.status, 'status_emoji': d.status_emoji, 'sent_at': d.sent_at.strftime('%d.%m.%Y %H:%M') if d.sent_at else '-', 'approved_by': d.approved_by, 'rejected_reason': d.rejection_reason, } for d in digests ]