mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-01 15:52:30 +00:00
Merge pull request #273 from Fr1ngg/revert-272-4r6xny-bedolaga/add-daily-summary-report-feature
Revert "Add automated admin reports"
This commit is contained in:
@@ -14,8 +14,6 @@ ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА! ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # Опционально: ID топика
|
||||
ADMIN_NOTIFICATIONS_TICKET_TOPIC_ID=126 # Опционально: ID топика для тикетов
|
||||
ADMIN_REPORTS_TOPIC_ID=130 # Опционально: отдельный ID топика для отчетов
|
||||
ADMIN_REPORTS_TIME_MOSCOW=09:00 # Время ежедневного отчета (МСК)
|
||||
# Обязательная подписка на канал
|
||||
CHANNEL_SUB_ID= # Опционально ID твоего канала (-100)
|
||||
CHANNEL_IS_REQUIRED_SUB=false # Обязательна ли подписка на канал
|
||||
|
||||
@@ -254,8 +254,6 @@ ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА! ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # Опционально: ID топика
|
||||
ADMIN_NOTIFICATIONS_TICKET_TOPIC_ID=126 # Опционально: ID топика для тикетов
|
||||
ADMIN_REPORTS_TOPIC_ID=130 # Опционально: отдельный ID топика для отчетов
|
||||
ADMIN_REPORTS_TIME_MOSCOW=09:00 # Время ежедневного отчета (МСК)
|
||||
# Обязательная подписка на канал
|
||||
CHANNEL_SUB_ID= # Опционально ID твоего канала (-100)
|
||||
CHANNEL_IS_REQUIRED_SUB=false # Обязательна ли подписка на канал
|
||||
@@ -971,12 +969,8 @@ docker compose down -v --remove-orphans
|
||||
ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # ID канала/группы
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # ID топика (опционально)
|
||||
ADMIN_REPORTS_TOPIC_ID=130 # ID топика для отчетов (опционально)
|
||||
ADMIN_REPORTS_TIME_MOSCOW=09:00 # Время ежедневного отчета (МСК)
|
||||
```
|
||||
|
||||
> ⚙️ Бот автоматически отправит ежедневный отчет за предыдущие сутки в указанное время (по МСК). Если `ADMIN_REPORTS_TOPIC_ID` не задан, отчеты будут приходить в основной топик уведомлений. В админ-панели доступен раздел «Отчеты» для ручной отправки ежедневных, недельных и месячных сводок.
|
||||
|
||||
#### 2. Создание канала
|
||||
|
||||
1. **Создайте приватный канал** или группу для уведомлений
|
||||
|
||||
@@ -38,7 +38,6 @@ from app.handlers.admin import (
|
||||
backup as admin_backup,
|
||||
welcome_text as admin_welcome_text,
|
||||
tickets as admin_tickets,
|
||||
reports as admin_reports,
|
||||
)
|
||||
from app.handlers.stars_payments import register_stars_handlers
|
||||
|
||||
@@ -140,7 +139,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_backup.register_handlers(dp)
|
||||
admin_welcome_text.register_welcome_text_handlers(dp)
|
||||
admin_tickets.register_handlers(dp)
|
||||
admin_reports.register_handlers(dp)
|
||||
common.register_handlers(dp)
|
||||
register_stars_handlers(dp)
|
||||
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")
|
||||
|
||||
@@ -2,7 +2,6 @@ import os
|
||||
import re
|
||||
import html
|
||||
from collections import defaultdict
|
||||
from datetime import time as dt_time
|
||||
from typing import List, Optional, Union, Dict
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import field_validator, Field
|
||||
@@ -27,8 +26,6 @@ class Settings(BaseSettings):
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID: Optional[str] = None
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID: Optional[int] = None
|
||||
ADMIN_NOTIFICATIONS_TICKET_TOPIC_ID: Optional[int] = None
|
||||
ADMIN_REPORTS_TOPIC_ID: Optional[int] = None
|
||||
ADMIN_REPORTS_TIME_MOSCOW: str = "09:00"
|
||||
|
||||
CHANNEL_SUB_ID: Optional[str] = None
|
||||
CHANNEL_LINK: Optional[str] = None
|
||||
@@ -640,26 +637,6 @@ class Settings(BaseSettings):
|
||||
return (self.ADMIN_NOTIFICATIONS_ENABLED and
|
||||
self.get_admin_notifications_chat_id() is not None)
|
||||
|
||||
def get_admin_reports_topic_id(self) -> Optional[int]:
|
||||
if not self.ADMIN_REPORTS_TOPIC_ID:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(self.ADMIN_REPORTS_TOPIC_ID)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def get_admin_reports_time(self) -> dt_time:
|
||||
raw_value = (self.ADMIN_REPORTS_TIME_MOSCOW or "09:00").strip()
|
||||
|
||||
try:
|
||||
hours_str, minutes_str = raw_value.split(":", maxsplit=1)
|
||||
hours = max(0, min(23, int(hours_str)))
|
||||
minutes = max(0, min(59, int(minutes_str)))
|
||||
return dt_time(hour=hours, minute=minutes)
|
||||
except (ValueError, AttributeError):
|
||||
return dt_time(hour=9, minute=0)
|
||||
|
||||
def get_backup_send_chat_id(self) -> Optional[int]:
|
||||
if not self.BACKUP_SEND_CHAT_ID:
|
||||
return None
|
||||
|
||||
@@ -12,8 +12,7 @@ from app.keyboards.admin import (
|
||||
get_admin_communications_submenu_keyboard,
|
||||
get_admin_support_submenu_keyboard,
|
||||
get_admin_settings_submenu_keyboard,
|
||||
get_admin_system_submenu_keyboard,
|
||||
get_admin_reports_keyboard,
|
||||
get_admin_system_submenu_keyboard
|
||||
)
|
||||
from app.localization.texts import get_texts
|
||||
from app.handlers.admin import support_settings as support_settings_handlers
|
||||
@@ -145,23 +144,6 @@ async def show_support_submenu(
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_reports_submenu(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"📈 **{texts.ADMIN_REPORTS}**\n\n" + texts.ADMIN_REPORTS_MENU_HINT,
|
||||
reply_markup=get_admin_reports_keyboard(db_user.language),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# Moderator panel entry (from main menu quick button)
|
||||
async def show_moderator_panel(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -424,11 +406,6 @@ def register_handlers(dp: Dispatcher):
|
||||
show_support_audit,
|
||||
F.data.in_(["admin_support_audit"]) | F.data.startswith("admin_support_audit_page_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_reports_submenu,
|
||||
F.data == "admin_submenu_reports"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_settings_submenu,
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import logging
|
||||
|
||||
from aiogram import Dispatcher, types, F
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import User
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.report_service import report_service, ReportPeriod
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _send_report(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
period: ReportPeriod,
|
||||
success_message: str,
|
||||
error_message: str,
|
||||
):
|
||||
success, _ = await report_service.send_report(period)
|
||||
|
||||
if success:
|
||||
logger.info("Админ %s отправил отчет %s", db_user.id, period.value)
|
||||
await callback.answer(success_message)
|
||||
else:
|
||||
logger.error("Не удалось отправить отчет %s по запросу админа %s", period.value, db_user.id)
|
||||
await callback.answer(error_message, show_alert=True)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def send_daily_report(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
await _send_report(
|
||||
callback,
|
||||
db_user,
|
||||
ReportPeriod.DAILY,
|
||||
texts.ADMIN_REPORTS_SENT,
|
||||
texts.ADMIN_REPORTS_ERROR,
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def send_weekly_report(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
await _send_report(
|
||||
callback,
|
||||
db_user,
|
||||
ReportPeriod.WEEKLY,
|
||||
texts.ADMIN_REPORTS_SENT,
|
||||
texts.ADMIN_REPORTS_ERROR,
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def send_monthly_report(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
await _send_report(
|
||||
callback,
|
||||
db_user,
|
||||
ReportPeriod.MONTHLY,
|
||||
texts.ADMIN_REPORTS_SENT,
|
||||
texts.ADMIN_REPORTS_ERROR,
|
||||
)
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher) -> None:
|
||||
dp.callback_query.register(
|
||||
send_daily_report,
|
||||
F.data == "admin_report_daily",
|
||||
)
|
||||
dp.callback_query.register(
|
||||
send_weekly_report,
|
||||
F.data == "admin_report_weekly",
|
||||
)
|
||||
dp.callback_query.register(
|
||||
send_monthly_report,
|
||||
F.data == "admin_report_monthly",
|
||||
)
|
||||
@@ -10,7 +10,6 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👥 Юзеры/Подписки", callback_data="admin_submenu_users")],
|
||||
[InlineKeyboardButton(text="💰 Промокоды/Статистика", callback_data="admin_submenu_promo")],
|
||||
[InlineKeyboardButton(text=texts.ADMIN_REPORTS, callback_data="admin_submenu_reports")],
|
||||
[InlineKeyboardButton(text="🛟 Поддержка", callback_data="admin_submenu_support")],
|
||||
[InlineKeyboardButton(text="📨 Сообщения", callback_data="admin_submenu_communications")],
|
||||
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_submenu_settings")],
|
||||
@@ -92,17 +91,6 @@ def get_admin_support_submenu_keyboard(language: str = "ru") -> InlineKeyboardMa
|
||||
])
|
||||
|
||||
|
||||
def get_admin_reports_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=texts.ADMIN_REPORTS_DAILY, callback_data="admin_report_daily")],
|
||||
[InlineKeyboardButton(text=texts.ADMIN_REPORTS_WEEKLY, callback_data="admin_report_weekly")],
|
||||
[InlineKeyboardButton(text=texts.ADMIN_REPORTS_MONTHLY, callback_data="admin_report_monthly")],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")]
|
||||
])
|
||||
|
||||
|
||||
def get_admin_settings_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ class AdminNotificationService:
|
||||
self.chat_id = getattr(settings, 'ADMIN_NOTIFICATIONS_CHAT_ID', None)
|
||||
self.topic_id = getattr(settings, 'ADMIN_NOTIFICATIONS_TOPIC_ID', None)
|
||||
self.ticket_topic_id = getattr(settings, 'ADMIN_NOTIFICATIONS_TICKET_TOPIC_ID', None)
|
||||
self.reports_topic_id = settings.get_admin_reports_topic_id()
|
||||
self.enabled = getattr(settings, 'ADMIN_NOTIFICATIONS_ENABLED', False)
|
||||
|
||||
async def _get_referrer_info(self, db: AsyncSession, referred_by_id: Optional[int]) -> str:
|
||||
@@ -307,18 +306,11 @@ class AdminNotificationService:
|
||||
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
|
||||
return False
|
||||
|
||||
async def _send_message(
|
||||
self,
|
||||
text: str,
|
||||
reply_markup: types.InlineKeyboardMarkup | None = None,
|
||||
*,
|
||||
ticket_event: bool = False,
|
||||
topic_id: Optional[int] = None
|
||||
) -> bool:
|
||||
async def _send_message(self, text: str, reply_markup: types.InlineKeyboardMarkup | None = None, *, ticket_event: bool = False) -> bool:
|
||||
if not self.chat_id:
|
||||
logger.warning("ADMIN_NOTIFICATIONS_CHAT_ID не настроен")
|
||||
return False
|
||||
|
||||
|
||||
try:
|
||||
message_kwargs = {
|
||||
'chat_id': self.chat_id,
|
||||
@@ -329,9 +321,7 @@ class AdminNotificationService:
|
||||
|
||||
# route to ticket-specific topic if provided
|
||||
thread_id = None
|
||||
if topic_id:
|
||||
thread_id = topic_id
|
||||
elif ticket_event and self.ticket_topic_id:
|
||||
if ticket_event and self.ticket_topic_id:
|
||||
thread_id = self.ticket_topic_id
|
||||
elif self.topic_id:
|
||||
thread_id = self.topic_id
|
||||
@@ -339,7 +329,7 @@ class AdminNotificationService:
|
||||
message_kwargs['message_thread_id'] = thread_id
|
||||
if reply_markup is not None:
|
||||
message_kwargs['reply_markup'] = reply_markup
|
||||
|
||||
|
||||
await self.bot.send_message(**message_kwargs)
|
||||
logger.info(f"Уведомление отправлено в чат {self.chat_id}")
|
||||
return True
|
||||
@@ -356,16 +346,6 @@ class AdminNotificationService:
|
||||
|
||||
def _is_enabled(self) -> bool:
|
||||
return self.enabled and bool(self.chat_id)
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return self._is_enabled()
|
||||
|
||||
async def send_report_message(self, text: str, *, topic_id: Optional[int] = None) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
effective_topic = topic_id or self.reports_topic_id or self.topic_id
|
||||
return await self._send_message(text, topic_id=effective_topic)
|
||||
|
||||
def _get_payment_method_display(self, payment_method: Optional[str]) -> str:
|
||||
method_names = {
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Optional, Tuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from app.database.models import (
|
||||
Subscription,
|
||||
SubscriptionStatus,
|
||||
Transaction,
|
||||
TransactionType,
|
||||
)
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReportPeriod(Enum):
|
||||
DAILY = "daily"
|
||||
WEEKLY = "weekly"
|
||||
MONTHLY = "monthly"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReportPeriodInfo:
|
||||
start_msk: datetime
|
||||
end_msk: datetime
|
||||
title: str
|
||||
caption: str
|
||||
range_caption: str
|
||||
emoji: str
|
||||
|
||||
|
||||
class ReportService:
|
||||
def __init__(self) -> None:
|
||||
self.notification_service: Optional[AdminNotificationService] = None
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._stop_event = asyncio.Event()
|
||||
self._moscow_tz = ZoneInfo("Europe/Moscow")
|
||||
self._utc_tz = ZoneInfo("UTC")
|
||||
|
||||
def set_notification_service(self, service: AdminNotificationService) -> None:
|
||||
self.notification_service = service
|
||||
|
||||
async def start(self) -> Optional[asyncio.Task]:
|
||||
if self._task and not self._task.done():
|
||||
return self._task
|
||||
|
||||
if not self.notification_service or not self.notification_service.is_enabled():
|
||||
logger.info("Сервис отчетов не запущен: админ-уведомления отключены или не настроен чат")
|
||||
return None
|
||||
|
||||
self._stop_event.clear()
|
||||
self._task = asyncio.create_task(self._scheduler_loop())
|
||||
logger.info("Сервис отчетов запущен")
|
||||
return self._task
|
||||
|
||||
async def stop(self) -> None:
|
||||
if not self._task:
|
||||
return
|
||||
|
||||
self._stop_event.set()
|
||||
try:
|
||||
await self._task
|
||||
finally:
|
||||
self._task = None
|
||||
self._stop_event.clear()
|
||||
logger.info("Сервис отчетов остановлен")
|
||||
|
||||
async def send_report(self, period: ReportPeriod) -> Tuple[bool, str]:
|
||||
text, _ = await self.generate_report(period)
|
||||
|
||||
if not text:
|
||||
return False, text
|
||||
|
||||
if not self.notification_service or not self.notification_service.is_enabled():
|
||||
logger.warning("Отчет не отправлен: сервис админ-уведомлений недоступен")
|
||||
return False, text
|
||||
|
||||
success = await self.notification_service.send_report_message(text)
|
||||
if success:
|
||||
logger.info("Отчет %s отправлен", period.value)
|
||||
else:
|
||||
logger.error("Не удалось отправить отчет %s", period.value)
|
||||
return success, text
|
||||
|
||||
async def generate_report(self, period: ReportPeriod) -> Tuple[str, dict]:
|
||||
info = self._get_period_info(period)
|
||||
if not info:
|
||||
return "", {}
|
||||
|
||||
start_utc = info.start_msk.astimezone(self._utc_tz).replace(tzinfo=None)
|
||||
end_utc = info.end_msk.astimezone(self._utc_tz).replace(tzinfo=None)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
stats = await self._collect_stats(session, start_utc, end_utc)
|
||||
|
||||
text = self._format_report(info, stats)
|
||||
return text, stats
|
||||
|
||||
async def _scheduler_loop(self) -> None:
|
||||
while not self._stop_event.is_set():
|
||||
next_run = self._get_next_run_datetime()
|
||||
now_utc = datetime.now(self._utc_tz)
|
||||
wait_seconds = max(0, (next_run - now_utc).total_seconds())
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(self._stop_event.wait(), timeout=wait_seconds)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
|
||||
try:
|
||||
await self.send_report(ReportPeriod.DAILY)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error("Ошибка отправки ежедневного отчета: %s", error, exc_info=True)
|
||||
|
||||
async def _collect_stats(self, session: AsyncSession, start: datetime, end: datetime) -> dict:
|
||||
now_utc = datetime.utcnow()
|
||||
|
||||
total_trials_query = select(func.count()).select_from(Subscription).where(
|
||||
Subscription.is_trial.is_(True),
|
||||
Subscription.end_date > now_utc,
|
||||
Subscription.status.in_([
|
||||
SubscriptionStatus.ACTIVE.value,
|
||||
SubscriptionStatus.TRIAL.value,
|
||||
]),
|
||||
)
|
||||
total_trials = (await session.scalar(total_trials_query)) or 0
|
||||
|
||||
total_paid_query = select(func.count()).select_from(Subscription).where(
|
||||
Subscription.is_trial.is_(False),
|
||||
Subscription.end_date > now_utc,
|
||||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||||
)
|
||||
total_paid = (await session.scalar(total_paid_query)) or 0
|
||||
|
||||
new_trials_query = select(func.count()).select_from(Subscription).where(
|
||||
Subscription.is_trial.is_(True),
|
||||
Subscription.start_date >= start,
|
||||
Subscription.start_date < end,
|
||||
)
|
||||
new_trials = (await session.scalar(new_trials_query)) or 0
|
||||
|
||||
new_paid_query = select(func.count()).select_from(Subscription).where(
|
||||
Subscription.is_trial.is_(False),
|
||||
Subscription.start_date >= start,
|
||||
Subscription.start_date < end,
|
||||
)
|
||||
new_paid = (await session.scalar(new_paid_query)) or 0
|
||||
|
||||
payments_query = select(
|
||||
func.count(Transaction.id),
|
||||
func.coalesce(func.sum(Transaction.amount_kopeks), 0),
|
||||
).where(
|
||||
Transaction.type == TransactionType.DEPOSIT.value,
|
||||
Transaction.is_completed.is_(True),
|
||||
Transaction.created_at >= start,
|
||||
Transaction.created_at < end,
|
||||
)
|
||||
payments_count, payments_sum = (await session.execute(payments_query)).one()
|
||||
|
||||
return {
|
||||
"total_trials": int(total_trials),
|
||||
"total_paid": int(total_paid),
|
||||
"new_trials": int(new_trials),
|
||||
"new_paid": int(new_paid),
|
||||
"payments_count": int(payments_count or 0),
|
||||
"payments_sum": int(payments_sum or 0),
|
||||
"period_start": start,
|
||||
"period_end": end,
|
||||
}
|
||||
|
||||
def _format_report(self, info: ReportPeriodInfo, stats: dict) -> str:
|
||||
now_msk = datetime.now(self._moscow_tz)
|
||||
end_display = info.end_msk - timedelta(seconds=1)
|
||||
period_range = (
|
||||
f"{info.start_msk.strftime('%d.%m.%Y %H:%M')} — "
|
||||
f"{end_display.strftime('%d.%m.%Y %H:%M')}"
|
||||
)
|
||||
|
||||
lines = [
|
||||
f"{info.emoji} <b>{info.title}</b> ({info.caption})",
|
||||
"",
|
||||
"🎯 <b>Триалы</b>",
|
||||
f"• Активных сейчас: {stats['total_trials']}",
|
||||
f"• Новых за период: {stats['new_trials']}",
|
||||
"",
|
||||
"💎 <b>Платные подписки</b>",
|
||||
f"• Активных сейчас: {stats['total_paid']}",
|
||||
f"• Новых за период: {stats['new_paid']}",
|
||||
"",
|
||||
"💳 <b>Пополнения</b>",
|
||||
f"• Количество платежей: {stats['payments_count']}",
|
||||
f"• Сумма: {settings.format_price(stats['payments_sum'])}",
|
||||
"",
|
||||
f"🕒 Период (МСК): {period_range}",
|
||||
f"📅 Сформировано: {now_msk.strftime('%d.%m.%Y %H:%M')}",
|
||||
]
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _get_period_info(self, period: ReportPeriod) -> Optional[ReportPeriodInfo]:
|
||||
now_msk = datetime.now(self._moscow_tz)
|
||||
|
||||
if period is ReportPeriod.DAILY:
|
||||
target_date = now_msk.date() - timedelta(days=1)
|
||||
start_msk = datetime.combine(target_date, datetime.min.time(), tzinfo=self._moscow_tz)
|
||||
end_msk = start_msk + timedelta(days=1)
|
||||
caption = start_msk.strftime('%d.%m.%Y')
|
||||
return ReportPeriodInfo(
|
||||
start_msk=start_msk,
|
||||
end_msk=end_msk,
|
||||
title="Ежедневный отчет",
|
||||
caption=caption,
|
||||
range_caption=caption,
|
||||
emoji="🗓️",
|
||||
)
|
||||
|
||||
if period is ReportPeriod.WEEKLY:
|
||||
end_msk = datetime.combine(now_msk.date(), datetime.min.time(), tzinfo=self._moscow_tz)
|
||||
start_msk = end_msk - timedelta(days=7)
|
||||
caption = (
|
||||
f"{start_msk.strftime('%d.%m.%Y')} — "
|
||||
f"{(end_msk - timedelta(days=1)).strftime('%d.%m.%Y')}"
|
||||
)
|
||||
return ReportPeriodInfo(
|
||||
start_msk=start_msk,
|
||||
end_msk=end_msk,
|
||||
title="Еженедельный отчет",
|
||||
caption=caption,
|
||||
range_caption=caption,
|
||||
emoji="🗓️",
|
||||
)
|
||||
|
||||
if period is ReportPeriod.MONTHLY:
|
||||
current_month_start = datetime(now_msk.year, now_msk.month, 1, tzinfo=self._moscow_tz)
|
||||
end_msk = current_month_start
|
||||
previous_month_last_day = current_month_start - timedelta(days=1)
|
||||
start_msk = datetime(
|
||||
previous_month_last_day.year,
|
||||
previous_month_last_day.month,
|
||||
1,
|
||||
tzinfo=self._moscow_tz,
|
||||
)
|
||||
caption = (
|
||||
f"{start_msk.strftime('%d.%m.%Y')} — "
|
||||
f"{previous_month_last_day.strftime('%d.%m.%Y')}"
|
||||
)
|
||||
return ReportPeriodInfo(
|
||||
start_msk=start_msk,
|
||||
end_msk=end_msk,
|
||||
title="Ежемесячный отчет",
|
||||
caption=caption,
|
||||
range_caption=caption,
|
||||
emoji="📆",
|
||||
)
|
||||
|
||||
logger.warning("Неизвестный период отчета: %s", period)
|
||||
return None
|
||||
|
||||
def _get_next_run_datetime(self) -> datetime:
|
||||
dispatch_time = settings.get_admin_reports_time()
|
||||
now_msk = datetime.now(self._moscow_tz)
|
||||
|
||||
run_msk = datetime.combine(now_msk.date(), dispatch_time, tzinfo=self._moscow_tz)
|
||||
if run_msk <= now_msk:
|
||||
run_msk += timedelta(days=1)
|
||||
|
||||
return run_msk.astimezone(self._utc_tz)
|
||||
|
||||
|
||||
report_service = ReportService()
|
||||
@@ -130,7 +130,6 @@
|
||||
"ADMIN_MONITORING": "🔍 Monitoring",
|
||||
"ADMIN_PANEL": "\n⚙️ <b>Administration panel</b>\n\nSelect a section to manage:\n",
|
||||
"ADMIN_PROMOCODES": "🎫 Promo codes",
|
||||
"ADMIN_REPORTS": "📈 Reports",
|
||||
"ADMIN_REFERRALS": "🤝 Referral program",
|
||||
"ADMIN_REMNAWAVE": "🖥️ Remnawave",
|
||||
"ADMIN_RULES": "📋 Rules",
|
||||
@@ -258,12 +257,6 @@
|
||||
"ADMIN_PROMO_GROUP_DELETED": "Promo group “{name}” deleted.",
|
||||
"ADMIN_SUBSCRIPTIONS": "📱 Subscriptions",
|
||||
"ADMIN_USERS": "👥 Users",
|
||||
"ADMIN_REPORTS_MENU_HINT": "Choose which report to send to the admin topic.",
|
||||
"ADMIN_REPORTS_DAILY": "📅 Daily report (yesterday)",
|
||||
"ADMIN_REPORTS_WEEKLY": "🗓️ Weekly report",
|
||||
"ADMIN_REPORTS_MONTHLY": "📆 Monthly report",
|
||||
"ADMIN_REPORTS_SENT": "✅ Report sent to the admin topic.",
|
||||
"ADMIN_REPORTS_ERROR": "❌ Failed to send the report. Check notification settings.",
|
||||
"AUTOPAY_DISABLED_TEXT": "Disabled — don't forget to renew manually!",
|
||||
"AUTOPAY_ENABLED_TEXT": "Enabled — the subscription will renew automatically",
|
||||
"AUTOPAY_FAILED": "\n❌ <b>Autopay failed</b>\n\nWe couldn't charge the renewal payment.\nBalance available: {balance}\nRequired: {required}\n\nPlease top up your balance and renew manually.\n",
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"ADMIN_MONITORING": "🔍 Мониторинг",
|
||||
"ADMIN_PANEL": "\n⚙️ <b>Административная панель</b>\n\nВыберите раздел для управления:\n",
|
||||
"ADMIN_PROMOCODES": "🎫 Промокоды",
|
||||
"ADMIN_REPORTS": "📈 Отчеты",
|
||||
"ADMIN_REFERRALS": "🤝 Партнерка",
|
||||
"ADMIN_REMNAWAVE": "🖥️ Remnawave",
|
||||
"ADMIN_RULES": "📋 Правила",
|
||||
@@ -135,12 +134,6 @@
|
||||
"ADMIN_PROMO_GROUP_DELETED": "Промогруппа «{name}» удалена.",
|
||||
"ADMIN_SUBSCRIPTIONS": "📱 Подписки",
|
||||
"ADMIN_USERS": "👥 Пользователи",
|
||||
"ADMIN_REPORTS_MENU_HINT": "Выберите период отчета для отправки в админ-топик.",
|
||||
"ADMIN_REPORTS_DAILY": "📅 Отчет за вчера",
|
||||
"ADMIN_REPORTS_WEEKLY": "🗓️ Отчет за неделю",
|
||||
"ADMIN_REPORTS_MONTHLY": "📆 Отчет за месяц",
|
||||
"ADMIN_REPORTS_SENT": "✅ Отчет отправлен в админ-топик.",
|
||||
"ADMIN_REPORTS_ERROR": "❌ Не удалось отправить отчет. Проверьте настройки уведомлений.",
|
||||
"AUTOPAY_BUTTON": "💳 Автоплатёж",
|
||||
"AUTOPAY_DISABLED_TEXT": "Отключен - не забудьте продлить вручную!",
|
||||
"AUTOPAY_ENABLED_TEXT": "Включен - подписка продлится автоматически",
|
||||
|
||||
18
main.py
18
main.py
@@ -20,7 +20,6 @@ from app.external.pal24_webhook import start_pal24_webhook_server, Pal24WebhookS
|
||||
from app.database.universal_migration import run_universal_migration
|
||||
from app.services.backup_service import backup_service
|
||||
from app.localization.loader import ensure_locale_templates
|
||||
from app.services.report_service import report_service
|
||||
|
||||
|
||||
class GracefulExit:
|
||||
@@ -61,7 +60,6 @@ async def main():
|
||||
monitoring_task = None
|
||||
maintenance_task = None
|
||||
version_check_task = None
|
||||
reports_task = None
|
||||
polling_task = None
|
||||
|
||||
try:
|
||||
@@ -98,9 +96,6 @@ async def main():
|
||||
version_service.set_notification_service(admin_notification_service)
|
||||
logger.info(f"📄 Сервис версий настроен для репозитория: {version_service.repo}")
|
||||
logger.info(f"📦 Текущая версия: {version_service.current_version}")
|
||||
|
||||
report_service.set_notification_service(admin_notification_service)
|
||||
reports_task = await report_service.start()
|
||||
|
||||
logger.info("🔗 Бот подключен к сервисам мониторинга и техработ")
|
||||
|
||||
@@ -227,13 +222,6 @@ async def main():
|
||||
if settings.is_version_check_enabled():
|
||||
logger.info("🔄 Перезапуск сервиса проверки версий...")
|
||||
version_check_task = asyncio.create_task(version_service.start_periodic_check())
|
||||
|
||||
if reports_task and reports_task.done():
|
||||
exception = reports_task.exception()
|
||||
if exception:
|
||||
logger.error(f"Сервис отчетов завершился с ошибкой: {exception}")
|
||||
new_task = await report_service.start()
|
||||
reports_task = new_task if new_task else None
|
||||
|
||||
if polling_task.done():
|
||||
exception = polling_task.exception()
|
||||
@@ -289,12 +277,6 @@ async def main():
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
logger.info("ℹ️ Остановка сервиса отчетов...")
|
||||
try:
|
||||
await report_service.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки сервиса отчетов: {e}")
|
||||
|
||||
logger.info("ℹ️ Остановка сервиса бекапов...")
|
||||
try:
|
||||
await backup_service.stop_auto_backup()
|
||||
|
||||
Reference in New Issue
Block a user