domovoy_bot/services/digest_service.py
Admin 9a8dafba27 🕐 fix: Локальное время UTC+4 (Ульяновск) во всех страницах панели
- Добавлены функции to_local() и fmt_local()
- Время рассылок теперь 17:55 вместо 13:55
- Время прочтений в локальном поясе
- Время запланированных постов исправлено
- Время дайджестов исправлено

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-12 18:12:52 +04:00

414 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

"""
Сервис генерации еженедельного дайджеста
"""
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"📰 <b>ДАЙДЖЕСТ ДОМА ЗА НЕДЕЛЮ</b>\n"
text += f"🗓 {period_start} - {period_end}\n\n"
# События
text += f"🔔 <b>События:</b>\n"
if events:
for e in events:
text += f"{e['title']}{e['event_date']}\n"
else:
text += "• Нет событий\n"
text += "\n"
# Опросы
text += f"📊 <b>Опросы:</b>\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"📢 <b>Объявления:</b> {ads['new_count']} новых\n"
text += f" Всего активных: {ads['active_count']}\n\n"
# Топ жильцов
if top_users:
text += f"🏆 <b>Активные жильцы:</b>\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"⏰ <b>На следующей неделе:</b>\n"
for s in schedule:
text += f"{s['emoji']} {s['title']}{s['start_time']} до {s['end_time']}\n"
text += "\n"
# Верификации
text += f"✅ <b>Верификации:</b>\n"
text += f"• Новых за неделю: {verifications['new_verified']}\n"
text += f"Всего верифицировано: {verifications['total_verified']}\n\n"
# Статистика
text += f"📈 <b>Статистика:</b>\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
]