Подготовка к релизу

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
+805
View File
@@ -0,0 +1,805 @@
"""
Сервис управления пользователями.
Отвечает за бизнес-логику работы с пользователями.
"""
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from core.exceptions import ValidationError
from core.permissions import UserRole, permission_manager
from utils.validators import InputValidator, Validator
@dataclass
class UserProfile:
"""Профиль пользователя"""
user_id: int
username: Optional[str]
first_name: str
last_name: Optional[str]
reputation: int = 0
rank: str = "Новичок"
message_count: int = 0
joined_date: Optional[datetime] = None
last_activity: Optional[datetime] = None
warnings: int = 0
achievements: Optional[List[str]] = None
def __post_init__(self):
if self.achievements is None:
self.achievements = []
if self.joined_date is None:
self.joined_date = datetime.now()
if self.last_activity is None:
self.last_activity = datetime.now()
class UserService:
"""
Сервис для управления пользователями и их данными.
Отвечает за:
- Создание и обновление профилей пользователей
- Расчет рейтингов и рангов
- Управление достижениями
- Статистику активности
"""
def __init__(self, user_repository, score_repository, role_service=None):
"""
Инициализация сервиса.
Args:
user_repository: Репозиторий пользователей
score_repository: Репозиторий очков и рейтингов
role_service: Сервис управления ролями
"""
self.user_repo = user_repository
self.score_repo = score_repository
self.role_service = role_service
self.logger = logging.getLogger(__name__)
# Настройки рангов и очков
self.rank_thresholds = {
"Новичок": 0,
"Ученик": 100,
"Активист": 500,
"Знаток": 1000,
"Эксперт": 2500,
"Мастер": 5000,
"Гуру": 10000,
"Легенда": 25000,
"Капитан": 50000,
"Генерал": 100000,
"Император": 250000
}
self.rank_names = list(self.rank_thresholds.keys())
async def get_or_create_user(self, user_id: int, username: str = None,
first_name: str = None, last_name: str = None) -> UserProfile:
"""
Получение пользователя или создание нового профиля.
Args:
user_id: ID пользователя
username: Имя пользователя
first_name: Имя
last_name: Фамилия
Returns:
Профиль пользователя
"""
try:
# Валидация входных данных
if not isinstance(user_id, int) or user_id <= 0 or user_id > 2147483647:
raise ValidationError("Неверный ID пользователя")
# Получаем данные пользователя из репозитория
user_data = await self.user_repo.get_by_id_async(user_id)
if user_data and isinstance(user_data, dict):
user_data = user_data
else:
user_data = None
self.logger.debug(f"Результат get_by_id для {user_id}: {user_data is not None}")
if user_data:
self.logger.debug(f"Найден существующий пользователь {user_id}: id={user_data.get('id')}, telegram_id={user_data.get('telegram_id')}, first_name={user_data.get('first_name')}")
# Проверяем, что все необходимые поля присутствуют и не None
if not user_data.get('id'):
self.logger.error(f"Пользователь {user_id} найден, но поле id отсутствует или None")
return None
# Пользователь существует, обновляем данные
self.logger.debug(f"Обновление данных существующего пользователя {user_id}")
updated_data = await self._update_user_data(user_data, username, first_name, last_name)
self.logger.debug(f"Результат обновления данных для {user_id}: {updated_data is not None}")
if updated_data:
profile = self._map_to_profile(updated_data)
self.logger.debug(f"Создан профиль из обновленных данных для {user_id}: {profile is not None}")
return profile
else:
self.logger.error(f"Ошибка обновления данных пользователя {user_id}")
return None
else:
# Создаем нового пользователя
self.logger.debug(f"Создание нового пользователя {user_id}")
new_user_data = await self._create_new_user(user_id, username, first_name, last_name)
self.logger.debug(f"Результат создания нового пользователя {user_id}: {new_user_data is not None}")
if new_user_data:
profile = self._map_to_profile(new_user_data)
self.logger.debug(f"Создан профиль из новых данных для {user_id}: {profile is not None}")
return profile
else:
self.logger.error(f"Ошибка создания нового пользователя {user_id}")
return None
except Exception as e:
# Handle potential sqlite3.IntegrityError or other database errors
if "FOREIGN KEY constraint failed" in str(e) or "IntegrityError" in str(type(e).__name__):
self.logger.warning(f"Нарушение внешнего ключа для пользователя {user_id}, пропускаем создание ролей")
# Попытка создания без ролей или с ролью по умолчанию
try:
# Используем роль 1 (user) по умолчанию
user_data['role_id'] = 1
new_user_data = await self._create_new_user(user_id, username, first_name, last_name)
if new_user_data:
profile = self._map_to_profile(new_user_data)
return profile
except Exception as retry_error:
self.logger.error(f"Повторная попытка создания пользователя {user_id} не удалась: {retry_error}")
return None
else:
self.logger.error(f"Ошибка базы данных при работе с пользователем {user_id}: {e}")
return None
except Exception as e:
if "FOREIGN KEY constraint failed" in str(e):
self.logger.warning(f"Нарушение внешнего ключа для пользователя {user_id}, пропускаем создание ролей")
# Попытка создания без ролей или с ролью по умолчанию
try:
# Используем роль 1 (user) по умолчанию
user_data['role_id'] = 1
new_user_data = await self._create_new_user(user_id, username, first_name, last_name)
if new_user_data:
profile = self._map_to_profile(new_user_data)
return profile
except Exception as retry_error:
self.logger.error(f"Повторная попытка создания пользователя {user_id} не удалась: {retry_error}")
return None
else:
self.logger.error(f"Ошибка базы данных при работе с пользователем {user_id}: {e}")
return None
except Exception as e:
self.logger.error(f"Ошибка в get_or_create_user для пользователя {user_id}: {e}", exc_info=True)
return None
async def update_user_activity(self, user_id: int, chat_id: int) -> Dict[str, Any]:
"""
Обновление активности пользователя.
Args:
user_id: ID пользователя
chat_id: ID чата
Returns:
Результат обновления (включая повышение ранга)
"""
try:
# Обновляем статистику активности
await self.user_repo.update_activity(user_id, chat_id)
# Обновляем очки
print(f"[DEBUG] UserService.update_user_activity: calling score_repo.update_score({user_id}, 1)")
await self.score_repo.update_score(user_id, 1)
# Проверяем повышение ранга
rank_update = await self._check_rank_promotion(user_id)
return {
'activity_updated': True,
'rank_promoted': rank_update is not None,
'rank_update': rank_update
}
except Exception as e:
self.logger.error(f"Ошибка в update_user_activity для пользователя {user_id}: {e}", exc_info=True)
# Возвращаем результат с ошибкой, но не прерываем выполнение
return {
'activity_updated': False,
'rank_promoted': False,
'rank_update': None,
'error': str(e)
}
async def add_warning(self, user_id: int, reason: str, admin_id: int) -> bool:
"""
Добавление предупреждения пользователю.
Args:
user_id: ID пользователя
reason: Причина предупреждения
admin_id: ID администратора
Returns:
True если предупреждение добавлено
"""
if not InputValidator.validate_text_content(reason, max_length=500):
raise ValidationError("Неверная причина предупреждения")
print(f"[DEBUG] UserService.add_warning called: user_id={user_id}, reason='{reason}', admin_id={admin_id}")
result = await self.user_repo.add_warning(user_id, reason, admin_id)
print(f"[DEBUG] UserService.add_warning result: {result}")
# Проверяем обновление счетчика предупреждений
if result:
warnings_count = await self.user_repo.get_warnings_count(user_id)
print(f"[DEBUG] Warnings count after adding warning: {warnings_count}")
return result
async def get_user_achievements(self, user_id: int) -> List[Tuple[str, datetime]]:
"""
Получение достижений пользователя.
Args:
user_id: ID пользователя
Returns:
Список достижений с датами разблокировки
"""
return await self.user_repo.get_user_achievements(user_id)
async def check_and_unlock_achievements(self, user_id: int) -> List[str]:
"""
Проверка и разблокировка новых достижений.
Args:
user_id: ID пользователя
Returns:
Список новых достижений
"""
self.logger.debug(f"Starting achievement check for user {user_id}")
user_profile = await self.get_or_create_user(user_id)
user_stats = await self._get_user_statistics(user_id)
self.logger.debug(f"User stats for {user_id}: {user_stats}")
new_achievements = []
all_achievements = await self.user_repo.get_all_achievements()
self.logger.debug(f"Total achievements to check: {len(all_achievements)}")
for achievement in all_achievements:
achievement_id, name, description, condition_type, condition_value, badge = achievement
self.logger.debug(f"Checking achievement: {name}, type: {condition_type}, value: {condition_value}")
if await self._check_achievement_condition(user_profile, user_stats, condition_type, condition_value):
self.logger.debug(f"Achievement condition met for: {name}")
if not await self.user_repo.has_achievement(user_id, achievement_id):
self.logger.debug(f"Unlocking achievement: {name}")
await self.user_repo.unlock_achievement(user_id, achievement_id)
new_achievements.append(name)
else:
self.logger.debug(f"Achievement already unlocked: {name}")
else:
self.logger.debug(f"Achievement condition NOT met for: {name}")
self.logger.debug(f"New achievements unlocked: {new_achievements}")
return new_achievements
async def get_top_users(self, limit: int = 10) -> List[Tuple[int, str, str, int]]:
"""
Получение топ пользователей по очкам.
Args:
limit: Количество пользователей
Returns:
Список пользователей (id, username, first_name, score)
"""
return await self.user_repo.get_top_users_async(limit)
async def search_users(self, query: str, limit: int = 10) -> List[Dict[str, Any]]:
"""
Поиск пользователей по имени или username.
Args:
query: Поисковый запрос
limit: Максимальное количество результатов
Returns:
Список найденных пользователей
"""
if not InputValidator.validate_text_content(query, max_length=50):
raise ValidationError("Неверный поисковый запрос")
return await self.user_repo.search_users(query, limit)
def calculate_rank(self, score: int) -> str:
"""
Расчет ранга пользователя по количеству очков.
Args:
score: Количество очков
Returns:
Название ранга
"""
for rank, threshold in reversed(self.rank_thresholds.items()):
if score >= threshold:
return rank
return "Новичок"
def get_user_role(self, user_id: int) -> str:
"""
Получение роли пользователя.
Args:
user_id: ID пользователя
Returns:
Название роли
"""
if self.role_service:
return self.role_service.get_user_role_name(user_id)
return UserRole.USER.value # fallback
def is_admin(self, user_id: int) -> bool:
"""
Проверка, является ли пользователь администратором.
Args:
user_id: ID пользователя
Returns:
True если администратор
"""
if self.role_service:
return self.role_service.is_admin(user_id)
# Используем permission_manager как fallback
user_role = self.get_user_role(user_id)
try:
role_enum = UserRole(user_role)
return role_enum in [UserRole.ADMIN, UserRole.SUPER_ADMIN]
except ValueError:
return False
def is_moderator(self, user_id: int) -> bool:
"""
Проверка, является ли пользователь модератором или администратором.
Args:
user_id: ID пользователя
Returns:
True если модератор или администратор
"""
if self.role_service:
return self.role_service.is_moderator(user_id)
# Используем permission_manager как fallback
user_role = self.get_user_role(user_id)
try:
role_enum = UserRole(user_role)
return role_enum in [UserRole.MODERATOR, UserRole.ADMIN, UserRole.SUPER_ADMIN]
except ValueError:
return False
async def get_user_role_enum_async(self, user_id: int) -> UserRole:
"""
Получение роли пользователя как enum UserRole (асинхронная версия).
Args:
user_id: ID пользователя
Returns:
Роль пользователя как enum
"""
try:
# Используем permission_manager для получения роли
from core.permissions import permission_manager
# Получаем config из Application если возможно, иначе создаем минимальный
try:
from core.application import app_instance
if app_instance and hasattr(app_instance, 'config'):
config = app_instance.config
else:
config = self._get_minimal_config_for_permissions()
except ImportError:
config = self._get_minimal_config_for_permissions()
role = await permission_manager.get_effective_role(None, user_id, config)
print(f"[DEBUG] get_user_role_enum_async: permission_manager returned role: {role.value if hasattr(role, 'value') else role}") # ОТЛАДКА
return role
except Exception as e:
# Fallback к старому методу
print(f"[DEBUG] get_user_role_enum_async: permission_manager failed ({e}), using fallback") # ОТЛАДКА
role_str = self.get_user_role(user_id)
try:
role_enum = UserRole(role_str)
print(f"[DEBUG] get_user_role_enum_async: fallback returned role: {role_enum.value}") # ОТЛАДКА
return role_enum
except ValueError:
print(f"[DEBUG] get_user_role_enum_async: fallback failed, returning USER") # ОТЛАДКА
return UserRole.USER # fallback
def get_user_role_enum(self, user_id: int) -> UserRole:
"""
Получение роли пользователя как enum UserRole (синхронная версия).
Args:
user_id: ID пользователя
Returns:
Роль пользователя как enum
"""
# Создаем event loop для вызова асинхронного метода
import asyncio
try:
loop = asyncio.get_event_loop()
if loop.is_running():
# Если loop уже запущен, создаем новую задачу
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.get_user_role_enum_async(user_id))
return future.result()
else:
return loop.run_until_complete(self.get_user_role_enum_async(user_id))
except Exception as e:
print(f"[ERROR] get_user_role_enum: failed to get async role ({e}), using fallback") # ОТЛАДКА
role_str = self.get_user_role(user_id)
try:
return UserRole(role_str)
except ValueError:
return UserRole.USER # fallback
def _get_minimal_config_for_permissions(self):
"""Создание минимального конфига для permission_manager"""
class MinimalConfig:
def __init__(self):
self.bot_config = MinimalBotConfig()
class MinimalBotConfig:
def __init__(self):
self.super_admin_ids = []
self.admin_ids = []
self.moderator_ids = []
config = MinimalConfig()
return config
def get_rank_progress(self, current_score: int) -> Dict[str, Any]:
"""
Получение прогресса до следующего ранга.
Args:
current_score: Текущие очки пользователя
Returns:
Информация о прогрессе
"""
current_rank = self.calculate_rank(current_score)
# Находим следующий ранг
current_index = self.rank_names.index(current_rank)
next_index = min(current_index + 1, len(self.rank_names) - 1)
current_threshold = self.rank_thresholds[current_rank]
next_threshold = self.rank_thresholds[self.rank_names[next_index]]
progress = current_score - current_threshold
needed = next_threshold - current_threshold
return {
'current_rank': current_rank,
'next_rank': self.rank_names[next_index] if current_index < len(self.rank_names) - 1 else current_rank,
'current_score': current_score,
'progress': progress,
'needed': needed,
'percentage': (progress / needed * 100) if needed > 0 else 100
}
async def _update_user_data(self, user_data: Dict, username: str, first_name: str, last_name: str) -> Dict:
"""Обновление данных существующего пользователя"""
try:
update_data = {}
if username and user_data.get('username') != username:
update_data['username'] = username
if first_name and user_data.get('first_name') != first_name:
update_data['first_name'] = first_name
if last_name and user_data.get('last_name') != last_name:
update_data['last_name'] = last_name
if update_data:
self.logger.debug(f"Обновление данных пользователя {user_data['id']}: {update_data}")
await self.user_repo.update_user(user_data['id'], update_data)
self.logger.debug(f"Данные пользователя {user_data['id']} успешно обновлены")
else:
self.logger.debug(f"Для пользователя {user_data['id']} нет данных для обновления")
# Возвращаем актуальные данные пользователя - используем telegram_id вместо id
telegram_id = user_data.get('telegram_id')
if telegram_id:
current_data = await self.user_repo.get_by_id_async(telegram_id)
if current_data:
return current_data
else:
self.logger.error(f"Не удалось получить обновленные данные пользователя telegram_id={telegram_id}")
return None
else:
self.logger.error(f"telegram_id отсутствует в данных пользователя: {user_data}")
return None
except Exception as e:
self.logger.error(f"Ошибка при обновлении пользователя {user_data.get('id', 'unknown')}: {e}", exc_info=True)
return None
async def _create_new_user(self, user_id: int, username: str, first_name: str, last_name: str) -> Dict:
"""Создание нового пользователя"""
try:
user_data = {
'telegram_id': user_id,
'username': username,
'first_name': first_name or '',
'last_name': last_name,
'joined_date': datetime.now(),
'last_activity': datetime.now()
}
result = await self.user_repo._execute_query_async(
"INSERT INTO users (telegram_id, username, first_name, last_name, joined_date, last_activity) VALUES (?, ?, ?, ?, ?, ?)",
(user_data['telegram_id'], user_data.get('username'), user_data.get('first_name'), user_data.get('last_name'), user_data.get('joined_date'), user_data.get('last_activity'))
)
result = True
# Проверяем результат создания пользователя
if result:
# Получаем созданного пользователя для возврата полных данных
return await self.user_repo.get_by_id(user_id)
else:
print(f"Ошибка: репозиторий вернул None при создании пользователя {user_id}")
return None
except Exception as e:
print(f"Ошибка при создании пользователя {user_id}: {e}")
return None
async def _check_rank_promotion(self, user_id: int) -> Optional[Dict[str, Any]]:
"""Проверка повышения ранга пользователя"""
user_data = await self.user_repo.get_by_id_async(user_id)
if not user_data:
return None
current_rank = user_data.get('rank', 'Новичок')
new_rank = self.calculate_rank(user_data.get('reputation', 0))
if new_rank != current_rank:
# Обновляем ранг в базе данных
await self.user_repo.update_rank(user_id, new_rank)
return {
'old_rank': current_rank,
'new_rank': new_rank,
'user_name': user_data.get('first_name', 'Пользователь'),
'promoted': True
}
return None
async def _get_user_statistics(self, user_id: int) -> Dict[str, Any]:
"""Получение статистики пользователя"""
message_count = await self.score_repo.get_message_count(user_id)
total_score = await self.score_repo.get_total_score(user_id)
days_active = await self.user_repo.get_days_active(user_id)
warnings_count = await self.user_repo.get_warnings_count(user_id)
return {
'message_count': message_count,
'total_score': total_score,
'days_active': days_active,
'warnings_count': warnings_count
}
async def _check_achievement_condition(self, profile: UserProfile, stats: Dict[str, Any],
condition_type: str, condition_value: Any) -> bool:
"""Проверка условия достижения"""
if condition_type == 'score' and stats.get('total_score', 0) >= condition_value:
return True
elif condition_type == 'messages' and stats.get('message_count', 0) >= condition_value:
return True
elif condition_type == 'days_active' and stats.get('days_active', 0) >= condition_value:
return True
elif condition_type == 'warnings' and stats.get('warnings_count', 0) <= condition_value:
return True
elif condition_type == 'donations':
# Для достижений за донаты проверяем общую сумму донатов
total_donations = await self.get_total_donations(profile.user_id)
self.logger.debug(f"Checking donation achievement: total_donations={total_donations}, condition_value={condition_value}")
result = total_donations >= float(condition_value)
self.logger.debug(f"Donation achievement condition result: {result}")
return result
return False
def _map_to_profile(self, user_data: Dict) -> UserProfile:
"""Преобразование данных из БД в профиль пользователя"""
if not user_data:
raise ValueError("Данные пользователя не могут быть None")
# Дополнительная проверка на наличие обязательных полей
if 'id' not in user_data or user_data['id'] is None:
raise ValueError(f"Обязательное поле 'id' отсутствует в данных пользователя: {user_data}")
# Преобразование строковых дат в datetime объекты
joined_date = user_data.get('joined_date')
last_activity = user_data.get('last_activity')
if isinstance(joined_date, str):
try:
joined_date = datetime.fromisoformat(joined_date.replace('Z', '+00:00'))
except (ValueError, AttributeError):
joined_date = None
if isinstance(last_activity, str):
try:
last_activity = datetime.fromisoformat(last_activity.replace('Z', '+00:00'))
except (ValueError, AttributeError):
last_activity = None
return UserProfile(
user_id=user_data['id'],
username=user_data.get('username'),
first_name=user_data.get('first_name', ''),
last_name=user_data.get('last_name'),
reputation=user_data.get('reputation', 0),
rank=user_data.get('rank', 'Новичок'),
message_count=user_data.get('message_count', 0),
joined_date=joined_date,
last_activity=last_activity,
warnings=user_data.get('warnings', 0)
)
async def add_donation(self, user_id: int, amount: float) -> bool:
"""
Добавление доната пользователя.
Args:
user_id: ID пользователя
amount: Сумма доната
Returns:
True если донат добавлен успешно
"""
self.logger.info(f"Начало добавления доната: user_id={user_id}, amount={amount}")
try:
# Валидация данных
self.logger.debug(f"Валидация входных данных для пользователя {user_id}")
if not Validator.validate_user_id(user_id):
self.logger.error(f"Неверный ID пользователя: {user_id}")
raise ValidationError("Неверный ID пользователя")
if amount <= 0:
self.logger.error(f"Неверная сумма доната: {amount} (должна быть положительной)")
raise ValidationError("Сумма доната должна быть положительной")
self.logger.debug(f"Валидация прошла успешно для пользователя {user_id}")
# Создаем пользователя, если не существует
self.logger.debug(f"Получение или создание профиля пользователя {user_id}")
try:
user_profile = await self.get_or_create_user(user_id)
self.logger.debug(f"Результат get_or_create_user для {user_id}: {user_profile is not None}")
if user_profile:
self.logger.debug(f"Профиль пользователя {user_id}: user_id={user_profile.user_id}, first_name={user_profile.first_name}")
else:
self.logger.error(f"get_or_create_user вернул None для пользователя {user_id}")
except Exception as profile_error:
self.logger.error(f"Исключение в get_or_create_user для пользователя {user_id}: {profile_error}", exc_info=True)
return False
if not user_profile:
self.logger.error(f"Не удалось создать или получить профиль пользователя {user_id}")
return False
self.logger.debug(f"Профиль пользователя {user_id} получен: {user_profile.user_id if user_profile else None}")
# Получаем текущий год для статистики
current_year = datetime.now().year
self.logger.debug(f"Текущий год для статистики: {current_year}")
# Начинаем транзакцию
self.logger.debug("Начало транзакции для добавления доната")
self.logger.debug("Проверка подключения к репозиториям перед транзакцией")
if self.user_repo is None:
self.logger.error("user_repo is None - невозможно начать транзакцию")
return False
if self.score_repo is None:
self.logger.error("score_repo is None - невозможно начать транзакцию")
return False
try:
await self.user_repo.begin_transaction()
await self.score_repo.begin_transaction()
except Exception as e:
self.logger.error(f"Не удалось начать транзакцию: {e}")
return False
try:
# Добавляем донат в базу данных
self.logger.debug(f"Добавление доната в базу данных: user_id={user_id}, amount={amount}, year={current_year}")
self.logger.debug(f"Вызов user_repo.add_donation с параметрами: user_id={user_id}, amount={amount}, year={current_year}")
success = await self.user_repo.add_donation(user_id, amount, current_year)
self.logger.debug(f"Результат user_repo.add_donation: {success}")
if success:
self.logger.info(f"Донат успешно добавлен в базу данных для пользователя {user_id}")
# Начисляем очки за донат (1 очко за каждые 100 рублей)
points = int(amount // 100)
self.logger.debug(f"Расчет очков за донат: amount={amount}, points={points}")
if points > 0:
self.logger.debug(f"Начисление {points} очков пользователю {user_id}")
print(f"[DEBUG] UserService.add_donation: calling score_repo.update_score({user_id}, {points})")
score_success = await self.score_repo.update_score(user_id, points)
self.logger.debug(f"Результат update_score: {score_success}")
if score_success:
self.logger.info(f"Начислено {points} очков пользователю {user_id}")
else:
self.logger.warning(f"Не удалось начислить очки пользователю {user_id}")
# Проверяем достижения
self.logger.debug(f"Проверка достижений для пользователя {user_id}")
await self.check_and_unlock_achievements(user_id)
self.logger.debug(f"Проверка достижений завершена для пользователя {user_id}")
# Подтверждаем транзакцию
await self.user_repo.commit_transaction()
await self.score_repo.commit_transaction()
self.logger.info(f"Донат успешно обработан для пользователя {user_id}")
else:
self.logger.error(f"Не удалось добавить донат в базу данных для пользователя {user_id}")
# Откатываем транзакцию
await self.user_repo.rollback_transaction()
await self.score_repo.rollback_transaction()
return False
return success
except Exception as inner_e:
# Откатываем транзакцию при ошибке
self.logger.error(f"Ошибка во время транзакции, выполняем откат: {inner_e}")
try:
await self.user_repo.rollback_transaction()
await self.score_repo.rollback_transaction()
except Exception as rollback_e:
self.logger.error(f"Ошибка при откате транзакции: {rollback_e}")
raise inner_e
except ValidationError as e:
self.logger.warning(f"Ошибка валидации при добавлении доната для пользователя {user_id}: {e}")
raise
except Exception as e:
self.logger.error(f"Неожиданная ошибка при добавлении доната для пользователя {user_id}: {e}", exc_info=True)
return False
async def get_total_donations(self, user_id: int, year: int = None) -> float:
"""
Получение общей суммы донатов пользователя.
Args:
user_id: ID пользователя
year: Год для фильтрации (если None, то все года)
Returns:
Общая сумма донатов
"""
result = await self.user_repo._fetch_one_async(
"SELECT COALESCE(SUM(amount), 0) as total FROM donations WHERE user_id = (SELECT id FROM users WHERE telegram_id = ?)" + (" AND year = ?" if year else ""),
(user_id, year) if year else (user_id,)
)
return result['total'] if result else 0.0