diff --git a/PARR.AIHITLoaderWorker/Dockerfile b/PARR.AIHITLoaderWorker/Dockerfile index 37399825..8ca61df3 100644 --- a/PARR.AIHITLoaderWorker/Dockerfile +++ b/PARR.AIHITLoaderWorker/Dockerfile @@ -4,7 +4,6 @@ FROM 10.99.253.167:8090/dotnet/runtime:9.0 AS base WORKDIR /app FROM 10.99.253.167:8090/dotnet/sdk:9.0 AS build -USER root ARG BUILD_CONFIGURATION=Release USER root WORKDIR /src diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs index ef330478..f7d4082f 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs @@ -84,27 +84,11 @@ namespace PARR.API.Controllers.V1.Statistics InitiatorComment = "Отправлен запрос из API на формирование отчета о загруженности" }; - //todo: избавиться от try/catch + var queueSettings = taskMqSettingsProvider.GetSettings(TaskTypeEnum.Workload); - try - { - //mqSettings.Tasks.TryGetValue(TaskTypeEnum.Workload, out var queueSettings); + var taskId = await taskManagementService.CreateTaskAsync(TaskTypeEnum.Workload, default, initiator, queueSettings); - //if (queueSettings == null) - //{ - // logger.LogError("Не удалось получить настройки очереди для TaskType: {TaskType}", TaskTypeEnum.Workload); - // throw new InvalidOperationException($"Не удалось получить настройки очереди для TaskTypeEnum.Workload"); - //} - var queueSettings = taskMqSettingsProvider.GetSettings(TaskTypeEnum.Workload); - - var taskId = await taskManagementService.CreateTaskAsync(TaskTypeEnum.Workload, default, initiator, queueSettings); - - return Ok(new Response(null, true, null!, $"Создана задача {taskId} на формирование отчета")); - } - catch (Exception ex) - { - return BadRequest(new Response(false, new List { new ErrorModel { Message = ex.Message } })); - } + return Ok(new Response(null, true, null!, $"Создана задача {taskId} на формирование отчета")); } @@ -123,7 +107,6 @@ namespace PARR.API.Controllers.V1.Statistics } - /// /// Универсальный метод получения отчетности /// diff --git a/PARR.API/Infrastructure/ConfigureValidator.cs b/PARR.API/Infrastructure/ConfigureValidator.cs new file mode 100644 index 00000000..c50bab4a --- /dev/null +++ b/PARR.API/Infrastructure/ConfigureValidator.cs @@ -0,0 +1,51 @@ +using FluentValidation; +using FluentValidation.AspNetCore; +using Microsoft.AspNetCore.Mvc; +using PARR.API.Contracts.V1.Responses.Base; +using PARR.API.Infrastructure.Middleware; + +namespace PARR.API.Infrastructure +{ + /// + /// Настройка валидаторов + /// + public static class ConfigureValidator + { + /// + /// Подключить конфигурацию валидаторов + /// + /// + /// + public static IServiceCollection AddValidatorServices(this IServiceCollection services) + { + services.AddExceptionHandler(); // регим GlobalExceptionHandler + services.AddProblemDetails(); // Генерирует стандартный формат ошибки RFC 7807 + + // Настройка контроллеров и кастомного формата ошибок валидации + services.AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.InvalidModelStateResponseFactory = context => + { + // наша стандартная модель ошибок + var errors = context.ModelState.Values + .SelectMany(t => t.Errors) + .Select(t => new ErrorModel { Message= t.ErrorMessage }) + .ToList(); + + return new BadRequestObjectResult( new Response( false, errors)); + }; + }); + + // Регистрируем валидаторы API. + // Просканирует всю сборку API (где лежит Program.cs) и найдет там валидаторы + services.AddValidatorsFromAssembly(typeof(Program).Assembly); + //services.AddValidatorsFromAssemblies(Assembly.GetExecutingAssembly()); + + // Включаем авто-валидацию FluentValidation для контроллеров + services.AddFluentValidationAutoValidation(); + + return services; + } + } +} diff --git a/PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs b/PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs index 50009b57..da6ce47e 100644 --- a/PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs +++ b/PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs @@ -1,9 +1,73 @@ using Microsoft.AspNetCore.Diagnostics; +using PARR.API.Contracts.V1.Responses.Base; +using PARR.Domain.Exceptions; namespace PARR.API.Infrastructure.Middleware { - public class GlobalExceptionHandler//: IExceptionHandler + /// + /// Глобальная обработка ошибок согласно моих кастомных Exception + /// + public class GlobalExceptionHandler : IExceptionHandler { - //todo: + private readonly ILogger logger; + + public GlobalExceptionHandler(ILogger logger) + { + this.logger = logger; + } + + public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) + { + logger.LogError(exception, "Ошибка во время запроса {TraceId}: {Message}", httpContext.TraceIdentifier, exception.Message); + + // определяем статус код, в зависимости от типа исключения + + var statusCode = exception switch + { + NotFoundException => StatusCodes.Status404NotFound, + DbErrorException => StatusCodes.Status502BadGateway, + ExternalServiceException => StatusCodes.Status502BadGateway, + AppValidationException => StatusCodes.Status400BadRequest, + FluentValidation.ValidationException => StatusCodes.Status400BadRequest, + AlreadyExistsException => StatusCodes.Status409Conflict, + UnauthorizedException => StatusCodes.Status401Unauthorized, + ForbiddenException => StatusCodes.Status403Forbidden, + //todo: ---------- другие ошибки ---------- + _ => StatusCodes.Status500InternalServerError + }; + + // Для критических серверных ошибок (5xx) пишем детальный лог + if (statusCode >= 500) + { + logger.LogError(exception, "Критическая ошибка сервера: {Message}", exception.Message); + } + + //var errorMessage = exception.Message; + var errors = new List(); + + // Если это ошибка FluentValidatioun, вытаскиваем детали по каждому полю + if (exception is FluentValidation.ValidationException fluentEx) + { + // ошибка валидации, вернем по каждому полю + foreach (var error in fluentEx.Errors) + { + errors.Add(new ErrorModel { FieldName = error.PropertyName, Message = error.ErrorMessage }); + } + } + else + { + // это не ошибка валидации, вернем сообщение + errors.Add(new ErrorModel { Message = exception.Message }); + } + + httpContext.Response.StatusCode = statusCode; + + var response = new Response(null, false, errors, exception.Message); + + await httpContext.Response.WriteAsJsonAsync(response, cancellationToken); + + // ошибка обработана + return true; + } } } diff --git a/PARR.API/PARR.API.csproj b/PARR.API/PARR.API.csproj index 32d87361..c37fa14b 100644 --- a/PARR.API/PARR.API.csproj +++ b/PARR.API/PARR.API.csproj @@ -20,7 +20,7 @@ - + all diff --git a/PARR.API/Program.cs b/PARR.API/Program.cs index 756206de..0157159a 100644 --- a/PARR.API/Program.cs +++ b/PARR.API/Program.cs @@ -2,6 +2,7 @@ using Elastic.CommonSchema.Serilog; using FluentValidation; using Microsoft.AspNetCore.HttpOverrides; using PARR.API.Authentication; +using PARR.API.Infrastructure; using PARR.API.Installers; using PARR.API.Settings; using PARR.Core; @@ -68,10 +69,11 @@ builder.Services.Configure(options => }); -builder.Services.AddHttpContextAccessor(); +builder.Services.AddValidatorServices(); +//builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); +//builder.Services.AddControllers(); -builder.Services.AddControllers(); -builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); +builder.Services.AddHttpContextAccessor(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -82,6 +84,9 @@ builder.Services.InstallCorsServices(); var app = builder.Build(); +// подключаем обработку моих кастомных Exceptions (GlobalExceptionHandler) +app.UseExceptionHandler(); + // При использовании балансировщика (haproxy, перенаправлять заголовки) app.UseForwardedHeaders(); diff --git a/PARR.Core/PARR.Core.csproj b/PARR.Core/PARR.Core.csproj index a0e51996..f222fc12 100644 --- a/PARR.Core/PARR.Core.csproj +++ b/PARR.Core/PARR.Core.csproj @@ -8,7 +8,8 @@ - + + diff --git a/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs b/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs index 5db6eb93..6194cd68 100644 --- a/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs +++ b/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs @@ -8,6 +8,7 @@ using PARR.Domain.Common.Rabbit.Messages; using PARR.Domain.DTOs.TaskDTO; using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; +using PARR.Domain.Exceptions; using PARR.Domain.Settings; using System.Text.Encodings.Web; using System.Text.Json; @@ -50,15 +51,19 @@ namespace PARR.Core.Services.TaskServices.Implementations throw new InvalidOperationException($"Тип задачи typeCode не найден в БД"); } - // Проверка IsSingleton: если задача уже активна — возвращаем её + // Проверка IsSingleton: если задача уже есть и она активна, генерим ошибку, что нельзя создать новую if (taskType.IsSingleton) { var existingTask = await GetActiveSingletonTaskAsync(typeCode); if (existingTask != null) { - logger.LogInformation("Задача типа {TypeCode} уже активна (id: {ExistingId}). Возвращаем существующую.", typeCode, existingTask.Id); - return existingTask.Id; + //logger.LogInformation("Задача типа {TypeCode} уже активна (id: {ExistingId}). Возвращаем существующую.", typeCode, existingTask.Id); + //return existingTask.Id; + + logger.LogInformation("Задача типа {TypeCode} уже активна (id: {ExistingId}).", typeCode, existingTask.Id); + + throw new AlreadyExistsException($"Уже существует активная задача (id: {existingTask.Id}). Нельзя создать новую пока не выполнится существующая."); } } @@ -78,7 +83,7 @@ namespace PARR.Core.Services.TaskServices.Implementations if (!await taskRepository.CreateAsync(task) || !await taskRepository.CommitAsync(initiator)) { logger.LogError("Ошибка при сохранении задачи в БД"); - throw new DbUpdateException("Ошибка при сохранении задачи в БД"); + throw new DbErrorException("Ошибка при сохранении задачи в БД"); } logger.LogInformation("Задача {TaskId} типа {TypeCode} сохранена в БД со статусом Pending", task.Id, typeCode); diff --git a/PARR.Domain/Exceptions/CustomExceptions.cs b/PARR.Domain/Exceptions/CustomExceptions.cs new file mode 100644 index 00000000..0cc6e079 --- /dev/null +++ b/PARR.Domain/Exceptions/CustomExceptions.cs @@ -0,0 +1,96 @@ +namespace PARR.Domain.Exceptions +{ + /// + /// Базовый класс для всех ошибок + /// + public abstract class BaseException : Exception + { + protected BaseException(string message) : base(message) { } + } + + /// + /// Ошибка БД. 502 + /// + public class DbErrorException : BaseException + { + /// + /// Ошибка БД. + /// + /// + public DbErrorException(string message) : base(message) { } + } + + /// + /// Объект не найден. 404 + /// + public class NotFoundException : BaseException + { + /// + /// Объект не найден. + /// + /// + public NotFoundException(string message) : base(message) { } + } + + /// + /// Ошибка валидации. 400. + /// Использовать эту ошибку, если не подходит валидация от Fluent (ValidationException) + /// + public class AppValidationException : BaseException + { + /// + /// Ошибка валидации. + /// + /// + public AppValidationException(string message) : base(message) { } + } + + /// + /// Объект уже существует. 409 + /// + public class AlreadyExistsException : BaseException + { + /// + /// Объект уже существует. + /// + /// + public AlreadyExistsException(string message) : base(message) { } + } + + /// + /// Ошибка при работе с внешним сервисом. 502 + /// + public class ExternalServiceException : BaseException + { + /// + /// Ошибка при работе с внешним сервисом. + /// + /// + public ExternalServiceException(string message) : base(message) { } + } + + /// + /// Пользователь не аутентифицирован. 401 + /// + public class UnauthorizedException : BaseException + { + /// + /// Пользователь не аутентифицирован. + /// + /// + public UnauthorizedException(string message) : base(message) { } + } + + /// + /// Доступ запрещен. 403. + /// Пользователь вошел в систему (мы знаем, кто он), но у него нет прав на конкретное действие или объект. + /// + public class ForbiddenException : BaseException + { + /// + /// Доступ запрещен. + /// + /// + public ForbiddenException(string message) : base(message) { } + } +} diff --git a/PARR.Domain/PARR.Domain.csproj b/PARR.Domain/PARR.Domain.csproj index e405ce10..757fe82b 100644 --- a/PARR.Domain/PARR.Domain.csproj +++ b/PARR.Domain/PARR.Domain.csproj @@ -8,7 +8,6 @@ -