diff --git a/backend/common/external_sync_mixin.py b/backend/common/external_sync_mixin.py deleted file mode 100644 index 62cfd389..00000000 --- a/backend/common/external_sync_mixin.py +++ /dev/null @@ -1,261 +0,0 @@ -""" -Универсальный миксин для синхронизации DB операций с внешними системами (LDAP, AD, etc). - -Использование: - class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet): - external_sync_service = DirectoryService() - - def get_external_sync_method(self, action): - return { - 'create': 'create_user_ldap', - 'update': 'update_user_ldap', - 'destroy': 'delete_user_ldap', - }.get(action) - - def prepare_external_data(self, instance, action): - if action == 'create': - return {'user': instance} - return {'user_id': instance.id} -""" -import logging -from functools import wraps - -from django.db import transaction -from rest_framework import status -from rest_framework.response import Response - -logger = logging.getLogger(__name__) - - -class ExternalSystemSyncMixin: - """ - Миксин для автоматической синхронизации операций ViewSet с внешними системами. - - Паттерн: - 1. Выполняет DB операцию в транзакции - 2. При успехе - вызывает метод внешней системы - 3. При ошибке внешней системы - откатывает DB транзакцию - - Attributes: - external_sync_service: Сервис для работы с внешней системой (например, DirectoryService) - external_sync_enabled: Флаг включения синхронизации (по умолчанию True) - external_sync_async: Использовать асинхронную синхронизацию через Celery - """ - external_sync_service = None - external_sync_enabled = True - external_sync_async = False - - def get_external_sync_method(self, action): - """ - Возвращает имя метода внешнего сервиса для данного action. - - Args: - action: 'create', 'update', 'partial_update', 'destroy' - - Returns: - str: имя метода сервиса или None если синхронизация не нужна - """ - raise NotImplementedError("Переопределите get_external_sync_method()") - - def prepare_external_data(self, instance, action): - """ - Подготавливает данные для передачи во внешний сервис. - - Args: - instance: Инстанс модели Django - action: Тип операции - - Returns: - dict: данные для метода внешнего сервиса - """ - return {'instance': instance} - - def handle_external_sync_error(self, error, instance, action): - """ - Обработка ошибки синхронизации с внешней системой. - - Args: - error: Exception от внешней системы - instance: Инстанс модели - action: Тип операции - - Returns: - Response или None (если None - будет поднята ошибка) - """ - logger.error( - f"External sync failed for {action} on {instance}: {error}") - return Response( - {'detail': f'Ошибка синхронизации с внешней системой: {str(error)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def perform_create(self, serializer): - """Переопределение create с синхронизацией.""" - return self._perform_with_sync(serializer, 'create', super().perform_create) - - def perform_update(self, serializer): - """Переопределение update с синхронизацией.""" - action = 'partial_update' if self.request.method == 'PATCH' else 'update' - return self._perform_with_sync(serializer, action, super().perform_update) - - def perform_destroy(self, instance): - """Переопределение destroy с синхронизацией.""" - return self._perform_with_sync_instance(instance, 'destroy', super().perform_destroy) - - def _perform_with_sync(self, serializer, action, db_operation): - """ - Выполняет DB операцию и синхронизацию с внешней системой. - - Args: - serializer: DRF serializer с валидированными данными - action: Тип операции - db_operation: Функция для выполнения DB операции - """ - if not self.external_sync_enabled: - return db_operation(serializer) - - sync_method_name = self.get_external_sync_method(action) - if not sync_method_name or not self.external_sync_service: - return db_operation(serializer) - - # Атомарная транзакция: сохраняем в БД - with transaction.atomic(): - db_operation(serializer) - instance = serializer.instance - - # Подготавливаем данные для внешней системы - external_data = self.prepare_external_data(instance, action) - - try: - # Вызываем метод внешней системы - sync_method = getattr( - self.external_sync_service, sync_method_name) - result = sync_method(**external_data) - - # Если внешняя система вернула данные для обновления БД - if isinstance(result, dict) and result.get('update_db'): - for field, value in result['update_db'].items(): - setattr(instance, field, value) - instance.save(update_fields=list( - result['update_db'].keys())) - - # Вызываем кастомный обработчик результата если определен - if hasattr(self, 'handle_external_sync_result'): - self.handle_external_sync_result(instance, result, action) - - logger.info( - f"External sync success for {action} on {instance}") - - except Exception as e: - # При ошибке внешней системы - откатываем транзакцию - logger.error(f"External sync failed, rolling back: {e}") - transaction.set_rollback(True) - - # Вызываем обработчик ошибок - error_response = self.handle_external_sync_error( - e, instance, action) - if error_response: - raise ExternalSyncError(error_response) - raise - - def _perform_with_sync_instance(self, instance, action, db_operation): - """ - Выполняет DB операцию на инстансе с синхронизацией. - - Args: - instance: Инстанс модели - action: Тип операции - db_operation: Функция для выполнения DB операции - """ - if not self.external_sync_enabled: - return db_operation(instance) - - sync_method_name = self.get_external_sync_method(action) - if not sync_method_name or not self.external_sync_service: - return db_operation(instance) - - # Сохраняем данные перед удалением - external_data = self.prepare_external_data(instance, action) - - # Атомарная транзакция: удаляем из БД - with transaction.atomic(): - db_operation(instance) - - try: - # Вызываем метод внешней системы - sync_method = getattr( - self.external_sync_service, sync_method_name) - sync_method(**external_data) - - logger.info( - f"External sync success for {action} on {instance}") - - except Exception as e: - # При ошибке внешней системы - откатываем транзакцию - logger.error(f"External sync failed, rolling back: {e}") - transaction.set_rollback(True) - - error_response = self.handle_external_sync_error( - e, instance, action) - if error_response: - raise ExternalSyncError(error_response) - raise - - -class ExternalSyncError(Exception): - """Исключение с Response для возврата клиенту.""" - - def __init__(self, response): - self.response = response - super().__init__() - - -def with_external_sync(external_service_attr='external_sync_service', method_map=None): - """ - Декоратор для методов ViewSet, добавляющий синхронизацию с внешней системой. - - Usage: - @with_external_sync(method_map={'create': 'create_user_ldap'}) - def create(self, request, *args, **kwargs): - return super().create(request, *args, **kwargs) - - Args: - external_service_attr: Имя атрибута ViewSet с сервисом - method_map: Маппинг action -> метод внешнего сервиса - """ - def decorator(func): - @wraps(func) - def wrapper(self, request, *args, **kwargs): - # Выполняем оригинальный метод - with transaction.atomic(): - response = func(self, request, *args, **kwargs) - - # Получаем сервис и метод для синхронизации - service = getattr(self, external_service_attr, None) - if not service or not method_map: - return response - - action = self.action - sync_method_name = method_map.get(action) - if not sync_method_name: - return response - - try: - # Получаем созданный/обновленный инстанс - instance = getattr(response, 'data', {}).get('id') - if isinstance(response.data, dict): - instance = self.get_queryset().get( - id=response.data['id']) - - # Вызываем метод внешней системы - sync_method = getattr(service, sync_method_name) - sync_method(instance=instance) - - except Exception as e: - transaction.set_rollback(True) - logger.error(f"External sync failed: {e}") - raise - - return response - return wrapper - return decorator