Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.
This commit is contained in:
10
services/business/__init__.py
Normal file
10
services/business/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Бизнес-логика сервисов
|
||||
"""
|
||||
|
||||
from .user_service import UserService
|
||||
from .question_service import QuestionService
|
||||
from .message_service import MessageService
|
||||
from .pagination_service import PaginationService
|
||||
|
||||
__all__ = ['UserService', 'QuestionService', 'MessageService', 'PaginationService']
|
||||
237
services/business/message_service.py
Normal file
237
services/business/message_service.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
Сервис для отправки сообщений
|
||||
"""
|
||||
from typing import Optional, Union
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, ReplyKeyboardMarkup
|
||||
from aiogram import Bot
|
||||
|
||||
from services.infrastructure.logger import get_logger
|
||||
from services.rate_limiting.rate_limit_service import RateLimitService
|
||||
from services.infrastructure.logging_decorators import log_function_call, log_business_event
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MessageService:
|
||||
"""Сервис для отправки сообщений"""
|
||||
|
||||
def __init__(self, rate_limit_service: Optional[RateLimitService] = None):
|
||||
self.rate_limit_service = rate_limit_service
|
||||
|
||||
@log_business_event("send_message", log_params=True, log_result=True)
|
||||
async def send_message(
|
||||
self,
|
||||
target: Union[Message, CallbackQuery, Bot],
|
||||
text: str,
|
||||
reply_markup: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
|
||||
parse_mode: str = "HTML"
|
||||
) -> bool:
|
||||
"""
|
||||
Отправка сообщения
|
||||
|
||||
Args:
|
||||
target: Целевой объект (Message, CallbackQuery или Bot)
|
||||
text: Текст сообщения
|
||||
reply_markup: Клавиатура
|
||||
parse_mode: Режим парсинга
|
||||
|
||||
Returns:
|
||||
True если успешно отправлено
|
||||
"""
|
||||
try:
|
||||
logger.info(f"📤 Отправка сообщения с клавиатурой: {type(reply_markup).__name__ if reply_markup else 'None'}")
|
||||
|
||||
if isinstance(target, Message):
|
||||
await target.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
|
||||
elif isinstance(target, CallbackQuery):
|
||||
await target.message.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
|
||||
elif isinstance(target, Bot):
|
||||
# Для Bot нужен chat_id, который должен быть передан отдельно
|
||||
logger.error("Для Bot нужен chat_id")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке сообщения: {e}")
|
||||
return False
|
||||
|
||||
@log_business_event("edit_message", log_params=True, log_result=True)
|
||||
async def edit_message(
|
||||
self,
|
||||
target: Union[Message, CallbackQuery],
|
||||
text: str,
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||||
parse_mode: str = "HTML"
|
||||
) -> bool:
|
||||
"""
|
||||
Редактирование сообщения
|
||||
|
||||
Args:
|
||||
target: Целевой объект (Message или CallbackQuery)
|
||||
text: Новый текст сообщения
|
||||
reply_markup: Новая клавиатура
|
||||
parse_mode: Режим парсинга
|
||||
|
||||
Returns:
|
||||
True если успешно отредактировано
|
||||
"""
|
||||
try:
|
||||
if isinstance(target, Message):
|
||||
await target.edit_text(text, reply_markup=reply_markup, parse_mode=parse_mode)
|
||||
elif isinstance(target, CallbackQuery):
|
||||
await target.message.edit_text(text, reply_markup=reply_markup, parse_mode=parse_mode)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при редактировании сообщения: {e}")
|
||||
return False
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def send_callback_answer(
|
||||
self,
|
||||
callback: CallbackQuery,
|
||||
text: Optional[str] = None,
|
||||
show_alert: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Отправка ответа на callback query
|
||||
|
||||
Args:
|
||||
callback: CallbackQuery объект
|
||||
text: Текст ответа
|
||||
show_alert: Показывать ли alert
|
||||
|
||||
Returns:
|
||||
True если успешно отправлено
|
||||
"""
|
||||
try:
|
||||
await callback.answer(text, show_alert=show_alert)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке callback answer: {e}")
|
||||
return False
|
||||
|
||||
@log_business_event("send_bot_message", log_params=True, log_result=True)
|
||||
async def send_bot_message(
|
||||
self,
|
||||
bot: Bot,
|
||||
chat_id: int,
|
||||
text: str,
|
||||
reply_markup: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
|
||||
parse_mode: str = "HTML"
|
||||
) -> bool:
|
||||
"""
|
||||
Отправка сообщения через бота с rate limiting
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
chat_id: ID чата
|
||||
text: Текст сообщения
|
||||
reply_markup: Клавиатура
|
||||
parse_mode: Режим парсинга
|
||||
|
||||
Returns:
|
||||
True если успешно отправлено
|
||||
"""
|
||||
try:
|
||||
if self.rate_limit_service:
|
||||
# Используем rate limiting
|
||||
await self.rate_limit_service.send_with_rate_limit(
|
||||
bot.send_message,
|
||||
chat_id,
|
||||
text=text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode=parse_mode
|
||||
)
|
||||
else:
|
||||
# Отправляем без rate limiting
|
||||
await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode=parse_mode
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке сообщения через бота в чат {chat_id}: {e}")
|
||||
return False
|
||||
|
||||
@log_business_event("send_notification", log_params=True, log_result=True)
|
||||
async def send_notification(
|
||||
self,
|
||||
bot: Bot,
|
||||
user_id: int,
|
||||
title: str,
|
||||
message: str,
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Отправка уведомления пользователю с rate limiting
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
user_id: ID пользователя
|
||||
title: Заголовок уведомления
|
||||
message: Текст уведомления
|
||||
reply_markup: Клавиатура
|
||||
|
||||
Returns:
|
||||
True если успешно отправлено
|
||||
"""
|
||||
try:
|
||||
notification_text = f"🔔 <b>{title}</b>\n\n{message}"
|
||||
|
||||
if self.rate_limit_service:
|
||||
# Используем rate limiting
|
||||
await self.rate_limit_service.send_with_rate_limit(
|
||||
bot.send_message,
|
||||
user_id,
|
||||
text=notification_text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
# Отправляем без rate limiting
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=notification_text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
logger.info(f"Уведомление отправлено пользователю {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
|
||||
return False
|
||||
|
||||
@log_business_event("send_error_message", log_params=True, log_result=True)
|
||||
async def send_error_message(
|
||||
self,
|
||||
target: Union[Message, CallbackQuery],
|
||||
error_text: str = "❌ Произошла ошибка. Попробуйте позже."
|
||||
) -> bool:
|
||||
"""
|
||||
Отправка сообщения об ошибке
|
||||
|
||||
Args:
|
||||
target: Целевой объект
|
||||
error_text: Текст ошибки
|
||||
|
||||
Returns:
|
||||
True если успешно отправлено
|
||||
"""
|
||||
try:
|
||||
if isinstance(target, CallbackQuery):
|
||||
await target.answer(error_text, show_alert=True)
|
||||
else:
|
||||
await self.send_message(target, error_text)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке сообщения об ошибке: {e}")
|
||||
return False
|
||||
185
services/business/pagination_service.py
Normal file
185
services/business/pagination_service.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Сервис для работы с пагинацией
|
||||
"""
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
|
||||
from config.constants import DEFAULT_PAGE_SIZE, MIN_PAGE_NUMBER
|
||||
from services.infrastructure.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PaginationService:
|
||||
"""Сервис для работы с пагинацией"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def calculate_pagination_from_db(
|
||||
self,
|
||||
total_count: int,
|
||||
page: int,
|
||||
per_page: int = DEFAULT_PAGE_SIZE
|
||||
) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
Расчет пагинации на основе общего количества записей в БД
|
||||
|
||||
Args:
|
||||
total_count: Общее количество записей в БД
|
||||
page: Номер страницы (начиная с 0)
|
||||
per_page: Количество элементов на странице
|
||||
|
||||
Returns:
|
||||
Кортеж (общее_количество, текущая_страница, общее_количество_страниц, offset)
|
||||
"""
|
||||
try:
|
||||
total_pages = (total_count + per_page - 1) // per_page # Округление вверх
|
||||
|
||||
# Проверяем корректность номера страницы
|
||||
if page < MIN_PAGE_NUMBER:
|
||||
page = MIN_PAGE_NUMBER
|
||||
elif page >= total_pages and total_pages > 0:
|
||||
page = total_pages - 1
|
||||
|
||||
# Вычисляем offset для БД
|
||||
offset = page * per_page
|
||||
|
||||
return total_count, page, total_pages, offset
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при расчете пагинации из БД: {e}")
|
||||
return MIN_PAGE_NUMBER, MIN_PAGE_NUMBER, MIN_PAGE_NUMBER, MIN_PAGE_NUMBER
|
||||
|
||||
def calculate_pagination(
|
||||
self,
|
||||
items: List[Any],
|
||||
page: int,
|
||||
per_page: int = DEFAULT_PAGE_SIZE
|
||||
) -> Tuple[List[Any], int, int, int]:
|
||||
"""
|
||||
Расчет пагинации для списка элементов
|
||||
|
||||
Args:
|
||||
items: Список элементов
|
||||
page: Номер страницы (начиная с 0)
|
||||
per_page: Количество элементов на странице
|
||||
|
||||
Returns:
|
||||
Кортеж (элементы_страницы, общее_количество, текущая_страница, общее_количество_страниц)
|
||||
"""
|
||||
try:
|
||||
total_items = len(items)
|
||||
total_pages = (total_items + per_page - 1) // per_page # Округление вверх
|
||||
|
||||
# Проверяем корректность номера страницы
|
||||
if page < MIN_PAGE_NUMBER:
|
||||
page = MIN_PAGE_NUMBER
|
||||
elif page >= total_pages and total_pages > 0:
|
||||
page = total_pages - 1
|
||||
|
||||
# Вычисляем диапазон элементов для текущей страницы
|
||||
start_idx = page * per_page
|
||||
end_idx = min(start_idx + per_page, total_items)
|
||||
page_items = items[start_idx:end_idx]
|
||||
|
||||
return page_items, total_items, page, total_pages
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при расчете пагинации: {e}")
|
||||
return [], MIN_PAGE_NUMBER, MIN_PAGE_NUMBER, MIN_PAGE_NUMBER
|
||||
|
||||
def format_pagination_info(
|
||||
self,
|
||||
current_page: int,
|
||||
total_pages: int,
|
||||
start_idx: int,
|
||||
end_idx: int,
|
||||
total_items: int
|
||||
) -> str:
|
||||
"""
|
||||
Форматирование информации о пагинации
|
||||
|
||||
Args:
|
||||
current_page: Текущая страница
|
||||
total_pages: Общее количество страниц
|
||||
start_idx: Начальный индекс
|
||||
end_idx: Конечный индекс
|
||||
total_items: Общее количество элементов
|
||||
|
||||
Returns:
|
||||
Отформатированная строка с информацией о пагинации
|
||||
"""
|
||||
try:
|
||||
info = f"📊 Показано {start_idx + 1}-{end_idx} из {total_items}\n"
|
||||
info += f"📄 Страница {current_page + 1} из {total_pages}\n\n"
|
||||
|
||||
return info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при форматировании информации о пагинации: {e}")
|
||||
return ""
|
||||
|
||||
def get_pagination_buttons(
|
||||
self,
|
||||
current_page: int,
|
||||
total_pages: int,
|
||||
callback_prefix: str,
|
||||
additional_buttons: Optional[List[Tuple[str, str]]] = None
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Получение кнопок пагинации
|
||||
|
||||
Args:
|
||||
current_page: Текущая страница
|
||||
total_pages: Общее количество страниц
|
||||
callback_prefix: Префикс для callback_data
|
||||
additional_buttons: Дополнительные кнопки
|
||||
|
||||
Returns:
|
||||
Список кортежей (текст_кнопки, callback_data)
|
||||
"""
|
||||
try:
|
||||
buttons = []
|
||||
|
||||
# Кнопка "Предыдущая"
|
||||
if current_page > MIN_PAGE_NUMBER:
|
||||
buttons.append(("⬅️", f"{callback_prefix}_page_{current_page - 1}"))
|
||||
|
||||
# Кнопка "Следующая"
|
||||
if current_page < total_pages - 1:
|
||||
buttons.append(("➡️", f"{callback_prefix}_page_{current_page + 1}"))
|
||||
|
||||
# Дополнительные кнопки
|
||||
if additional_buttons:
|
||||
buttons.extend(additional_buttons)
|
||||
|
||||
return buttons
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании кнопок пагинации: {e}")
|
||||
return []
|
||||
|
||||
def validate_page_number(self, page: int, total_pages: int) -> int:
|
||||
"""
|
||||
Валидация номера страницы
|
||||
|
||||
Args:
|
||||
page: Номер страницы
|
||||
total_pages: Общее количество страниц
|
||||
|
||||
Returns:
|
||||
Валидный номер страницы
|
||||
"""
|
||||
try:
|
||||
if page < 0:
|
||||
return 0
|
||||
elif page >= total_pages and total_pages > 0:
|
||||
return total_pages - 1
|
||||
else:
|
||||
return page
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при валидации номера страницы: {e}")
|
||||
return MIN_PAGE_NUMBER
|
||||
280
services/business/question_service.py
Normal file
280
services/business/question_service.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Сервис для управления вопросами
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple
|
||||
from aiogram import Bot
|
||||
|
||||
from models.question import Question, QuestionStatus
|
||||
from services.infrastructure.database import DatabaseService
|
||||
from services.utils import UtilsService
|
||||
from services.infrastructure.logger import get_logger
|
||||
from services.infrastructure.metrics import get_metrics_service
|
||||
from services.infrastructure.logging_decorators import log_function_call, log_business_event
|
||||
from services.infrastructure.logging_utils import log_question_created, log_question_answered
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class QuestionService:
|
||||
"""Сервис для управления вопросами"""
|
||||
|
||||
def __init__(self, database: DatabaseService, utils: UtilsService):
|
||||
self.database = database
|
||||
self.utils = utils
|
||||
self.metrics = get_metrics_service()
|
||||
|
||||
@log_business_event("create_question", log_params=True, log_result=True)
|
||||
async def create_question(self, from_user_id: int, to_user_id: int, message_text: str) -> Question:
|
||||
"""
|
||||
Создание нового вопроса
|
||||
|
||||
Args:
|
||||
from_user_id: ID автора вопроса
|
||||
to_user_id: ID получателя вопроса
|
||||
message_text: Текст вопроса
|
||||
|
||||
Returns:
|
||||
Созданный объект вопроса
|
||||
"""
|
||||
try:
|
||||
question = Question(
|
||||
from_user_id=from_user_id,
|
||||
to_user_id=to_user_id,
|
||||
message_text=message_text.strip(),
|
||||
status=QuestionStatus.PENDING,
|
||||
created_at=datetime.now(),
|
||||
is_anonymous=True
|
||||
)
|
||||
|
||||
question = await self.database.create_question(question)
|
||||
self.metrics.increment_questions("created")
|
||||
return question
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании вопроса от {from_user_id} к {to_user_id}: {e}")
|
||||
raise
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def get_question(self, question_id: int) -> Optional[Question]:
|
||||
"""
|
||||
Получение вопроса по ID
|
||||
|
||||
Args:
|
||||
question_id: ID вопроса
|
||||
|
||||
Returns:
|
||||
Объект вопроса или None
|
||||
"""
|
||||
try:
|
||||
return await self.database.get_question(question_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении вопроса {question_id}: {e}")
|
||||
return None
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def get_user_questions(self, user_id: int, limit: int = 50, offset: int = 0) -> List[Question]:
|
||||
"""
|
||||
Получение вопросов пользователя
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
limit: Лимит вопросов
|
||||
offset: Смещение
|
||||
|
||||
Returns:
|
||||
Список вопросов
|
||||
"""
|
||||
try:
|
||||
return await self.database.get_user_questions(user_id, limit=limit, offset=offset)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении вопросов пользователя {user_id}: {e}")
|
||||
return []
|
||||
|
||||
@log_business_event("answer_question", log_params=True, log_result=True)
|
||||
async def answer_question(self, question_id: int, answer_text: str) -> Optional[Question]:
|
||||
"""
|
||||
Ответ на вопрос
|
||||
|
||||
Args:
|
||||
question_id: ID вопроса
|
||||
answer_text: Текст ответа
|
||||
|
||||
Returns:
|
||||
Обновленный объект вопроса или None
|
||||
"""
|
||||
try:
|
||||
question = await self.database.get_question(question_id)
|
||||
if not question:
|
||||
return None
|
||||
|
||||
question.mark_as_answered(answer_text.strip())
|
||||
question.answered_at = datetime.now()
|
||||
|
||||
question = await self.database.update_question(question)
|
||||
self.metrics.increment_answers("sent")
|
||||
return question
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при ответе на вопрос {question_id}: {e}")
|
||||
return None
|
||||
|
||||
@log_business_event("reject_question", log_params=True, log_result=True)
|
||||
async def reject_question(self, question_id: int) -> Optional[Question]:
|
||||
"""
|
||||
Отклонение вопроса
|
||||
|
||||
Args:
|
||||
question_id: ID вопроса
|
||||
|
||||
Returns:
|
||||
Обновленный объект вопроса или None
|
||||
"""
|
||||
try:
|
||||
question = await self.database.get_question(question_id)
|
||||
if not question:
|
||||
return None
|
||||
|
||||
question.mark_as_rejected()
|
||||
question.answered_at = datetime.now()
|
||||
|
||||
question = await self.database.update_question(question)
|
||||
self.metrics.increment_questions("rejected")
|
||||
return question
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отклонении вопроса {question_id}: {e}")
|
||||
return None
|
||||
|
||||
@log_business_event("delete_question", log_params=True, log_result=True)
|
||||
async def delete_question(self, question_id: int) -> Optional[Question]:
|
||||
"""
|
||||
Удаление вопроса
|
||||
|
||||
Args:
|
||||
question_id: ID вопроса
|
||||
|
||||
Returns:
|
||||
Обновленный объект вопроса или None
|
||||
"""
|
||||
try:
|
||||
question = await self.database.get_question(question_id)
|
||||
if not question:
|
||||
return None
|
||||
|
||||
question.mark_as_deleted()
|
||||
question.answered_at = datetime.now()
|
||||
|
||||
question = await self.database.update_question(question)
|
||||
self.metrics.increment_questions("deleted")
|
||||
return question
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении вопроса {question_id}: {e}")
|
||||
return None
|
||||
|
||||
@log_business_event("edit_answer", log_params=True, log_result=True)
|
||||
async def edit_answer(self, question_id: int, new_answer_text: str) -> Optional[Question]:
|
||||
"""
|
||||
Редактирование ответа на вопрос
|
||||
|
||||
Args:
|
||||
question_id: ID вопроса
|
||||
new_answer_text: Новый текст ответа
|
||||
|
||||
Returns:
|
||||
Обновленный объект вопроса или None
|
||||
"""
|
||||
try:
|
||||
question = await self.database.get_question(question_id)
|
||||
if not question:
|
||||
return None
|
||||
|
||||
question.answer_text = new_answer_text.strip()
|
||||
question.answered_at = datetime.now()
|
||||
|
||||
question = await self.database.update_question(question)
|
||||
self.metrics.increment_answers("edited")
|
||||
return question
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при редактировании ответа на вопрос {question_id}: {e}")
|
||||
return None
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
def validate_question_text(self, text: str, max_length: int = 1000) -> Tuple[bool, str]:
|
||||
"""
|
||||
Валидация текста вопроса
|
||||
|
||||
Args:
|
||||
text: Текст вопроса
|
||||
max_length: Максимальная длина
|
||||
|
||||
Returns:
|
||||
Кортеж (валидность, сообщение об ошибке)
|
||||
"""
|
||||
return self.utils.is_valid_question_text(text, max_length)
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
def validate_answer_text(self, text: str, max_length: int = 2000) -> Tuple[bool, str]:
|
||||
"""
|
||||
Валидация текста ответа
|
||||
|
||||
Args:
|
||||
text: Текст ответа
|
||||
max_length: Максимальная длина
|
||||
|
||||
Returns:
|
||||
Кортеж (валидность, сообщение об ошибке)
|
||||
"""
|
||||
return self.utils.is_valid_answer_text(text, max_length)
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def send_answer_to_author(self, bot: Bot, question: Question, answer_text: str) -> bool:
|
||||
"""
|
||||
Отправка ответа автору вопроса
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
question: Объект вопроса
|
||||
answer_text: Текст ответа
|
||||
|
||||
Returns:
|
||||
True если успешно отправлено
|
||||
"""
|
||||
try:
|
||||
await self.utils.send_answer_to_author(bot, question, answer_text)
|
||||
self.metrics.increment_answers("delivered")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке ответа автору вопроса {question.id}: {e}")
|
||||
self.metrics.increment_answers("delivery_failed")
|
||||
return False
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
def format_question_info(self, question: Question, show_answer: bool = False) -> str:
|
||||
"""
|
||||
Форматирование информации о вопросе
|
||||
|
||||
Args:
|
||||
question: Объект вопроса
|
||||
show_answer: Показывать ли ответ
|
||||
|
||||
Returns:
|
||||
Отформатированная строка
|
||||
"""
|
||||
return self.utils.format_question_info(question, show_answer)
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
def get_question_preview(self, question: Question, max_length: int = 50) -> str:
|
||||
"""
|
||||
Получение превью вопроса
|
||||
|
||||
Args:
|
||||
question: Объект вопроса
|
||||
max_length: Максимальная длина превью
|
||||
|
||||
Returns:
|
||||
Превью вопроса
|
||||
"""
|
||||
return question.get_question_preview(max_length)
|
||||
208
services/business/user_service.py
Normal file
208
services/business/user_service.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Сервис для управления пользователями
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
from aiogram.types import User as TelegramUser
|
||||
|
||||
from models.user import User
|
||||
from services.infrastructure.database import DatabaseService
|
||||
from services.utils import UtilsService
|
||||
from services.infrastructure.logger import get_logger
|
||||
from services.infrastructure.metrics import get_metrics_service
|
||||
from services.infrastructure.logging_decorators import log_function_call, log_business_event
|
||||
from services.infrastructure.logging_utils import log_user_created, log_user_blocked
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UserService:
|
||||
"""Сервис для управления пользователями"""
|
||||
|
||||
def __init__(self, database: DatabaseService, utils: UtilsService):
|
||||
self.database = database
|
||||
self.utils = utils
|
||||
self.metrics = get_metrics_service()
|
||||
|
||||
@log_business_event("create_or_update_user", log_params=True, log_result=True)
|
||||
async def create_or_update_user(self, telegram_user: TelegramUser, chat_id: int) -> User:
|
||||
"""
|
||||
Создание или обновление пользователя
|
||||
|
||||
Args:
|
||||
telegram_user: Объект пользователя из Telegram
|
||||
chat_id: ID чата
|
||||
|
||||
Returns:
|
||||
Объект пользователя
|
||||
"""
|
||||
try:
|
||||
# Проверяем, существует ли пользователь
|
||||
existing_user = await self.database.get_user(telegram_user.id)
|
||||
|
||||
if existing_user:
|
||||
# Обновляем существующего пользователя
|
||||
logger.info(f"👤 Обновление существующего пользователя {telegram_user.id}")
|
||||
self.metrics.increment_users("updated")
|
||||
return await self._update_existing_user(existing_user, telegram_user, chat_id)
|
||||
else:
|
||||
# Создаем нового пользователя
|
||||
logger.info(f"👤 Создание нового пользователя {telegram_user.id}")
|
||||
self.metrics.increment_users("created")
|
||||
return await self._create_new_user(telegram_user, chat_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании/обновлении пользователя {telegram_user.id}: {e}")
|
||||
raise
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def _update_existing_user(self, existing_user: User, telegram_user: TelegramUser, chat_id: int) -> User:
|
||||
"""Обновление существующего пользователя"""
|
||||
existing_user.username = telegram_user.username
|
||||
existing_user.first_name = telegram_user.first_name or "Пользователь"
|
||||
existing_user.last_name = telegram_user.last_name
|
||||
existing_user.chat_id = chat_id
|
||||
existing_user.update_timestamp()
|
||||
|
||||
return await self.database.update_user(existing_user)
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def _create_new_user(self, telegram_user: TelegramUser, chat_id: int) -> User:
|
||||
"""Создание нового пользователя"""
|
||||
user = User(
|
||||
telegram_id=telegram_user.id,
|
||||
username=telegram_user.username,
|
||||
first_name=telegram_user.first_name or "Пользователь",
|
||||
last_name=telegram_user.last_name,
|
||||
chat_id=chat_id,
|
||||
profile_link=self.utils.generate_anonymous_id(),
|
||||
is_active=True,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
|
||||
return await self.database.create_user(user)
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def get_user_by_profile_link(self, profile_link: str) -> Optional[User]:
|
||||
"""
|
||||
Получение пользователя по ссылке профиля
|
||||
|
||||
Args:
|
||||
profile_link: Ссылка профиля
|
||||
|
||||
Returns:
|
||||
Объект пользователя или None
|
||||
"""
|
||||
try:
|
||||
return await self.database.get_user_by_profile_link(profile_link)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении пользователя по ссылке {profile_link}: {e}")
|
||||
return None
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]:
|
||||
"""
|
||||
Получение пользователя по Telegram ID
|
||||
|
||||
Args:
|
||||
telegram_id: ID пользователя в Telegram
|
||||
|
||||
Returns:
|
||||
Объект пользователя или None
|
||||
"""
|
||||
try:
|
||||
return await self.database.get_user(telegram_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
def generate_referral_link(self, bot_username: str, user: User) -> str:
|
||||
"""
|
||||
Генерация реферальной ссылки для пользователя
|
||||
|
||||
Args:
|
||||
bot_username: Имя бота
|
||||
user: Объект пользователя
|
||||
|
||||
Returns:
|
||||
Реферальная ссылка
|
||||
"""
|
||||
return self.utils.generate_referral_link(bot_username, user.profile_link)
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def is_user_blocked(self, blocker_id: int, blocked_id: int) -> bool:
|
||||
"""
|
||||
Проверка, заблокирован ли пользователь
|
||||
|
||||
Args:
|
||||
blocker_id: ID пользователя, который блокирует
|
||||
blocked_id: ID пользователя, которого блокируют
|
||||
|
||||
Returns:
|
||||
True если заблокирован, False иначе
|
||||
"""
|
||||
try:
|
||||
return await self.database.is_user_blocked(blocker_id, blocked_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке блокировки {blocker_id} -> {blocked_id}: {e}")
|
||||
return False
|
||||
|
||||
@log_business_event("block_user", log_params=True, log_result=True)
|
||||
async def block_user(self, blocker_id: int, blocked_id: int) -> bool:
|
||||
"""
|
||||
Блокировка пользователя
|
||||
|
||||
Args:
|
||||
blocker_id: ID пользователя, который блокирует
|
||||
blocked_id: ID пользователя, которого блокируют
|
||||
|
||||
Returns:
|
||||
True если успешно заблокирован
|
||||
"""
|
||||
try:
|
||||
await self.database.block_user(blocker_id, blocked_id)
|
||||
logger.info(f"Пользователь {blocked_id} заблокирован пользователем {blocker_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при блокировке пользователя {blocked_id}: {e}")
|
||||
return False
|
||||
|
||||
@log_business_event("unblock_user", log_params=True, log_result=True)
|
||||
async def unblock_user(self, blocker_id: int, blocked_id: int) -> bool:
|
||||
"""
|
||||
Разблокировка пользователя
|
||||
|
||||
Args:
|
||||
blocker_id: ID пользователя, который разблокирует
|
||||
blocked_id: ID пользователя, которого разблокируют
|
||||
|
||||
Returns:
|
||||
True если успешно разблокирован
|
||||
"""
|
||||
try:
|
||||
result = await self.database.unblock_user(blocker_id, blocked_id)
|
||||
if result:
|
||||
logger.info(f"Пользователь {blocked_id} разблокирован пользователем {blocker_id}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при разблокировке пользователя {blocked_id}: {e}")
|
||||
return False
|
||||
|
||||
@log_function_call(log_params=True, log_result=True)
|
||||
async def get_blocked_users(self, user_id: int) -> list:
|
||||
"""
|
||||
Получение списка заблокированных пользователей
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
|
||||
Returns:
|
||||
Список ID заблокированных пользователей
|
||||
"""
|
||||
try:
|
||||
return await self.database.user_blocks.get_blocked_users(user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении заблокированных пользователей для {user_id}: {e}")
|
||||
return []
|
||||
Reference in New Issue
Block a user