mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
Подготовка к релизу
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user