From 8798529b4d0a2ef9117b4c9cb40cc59f29d34085 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 28 Jul 2026 14:41:01 +1000 Subject: [PATCH] =?UTF-8?q?fix(api,core):=20=D0=98=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=D1=8B=D0=B4=D0=B0=D1=87=D0=B8=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=20=D1=80=D0=BE=D0=B1=D0=BE=D1=82=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Contracts/V1/ApiRoutes.cs | 10 +++ .../StatRobotStatusDetailsController.cs | 40 +++++++++++ .../StatRobotTaskDetailsController.cs | 46 +++++++++++++ .../StatTemplateAutoControlController.cs | 5 +- PARR.Core/DependencyInjection.cs | 3 + .../Shared/JobGroupResultMappingProfile.cs | 17 +++++ PARR.Core/PARR.Core.csproj | 4 ++ .../Implementations/RobotTaskService.cs | 55 +++++++++------ .../RobotTaskDetailsService.cs | 68 +++++++++++++++++++ .../Interfaces/IRobotTaskDetailsService.cs | 16 +++++ .../RobotTaskDetailsResult.cs | 18 +++++ PARR.Domain/DTOs/Shared/JobGroupResult.cs | 15 ++++ 12 files changed, 276 insertions(+), 21 deletions(-) create mode 100644 PARR.API/Controllers/V1/Statistics/StatRobotStatusDetailsController.cs create mode 100644 PARR.API/Controllers/V1/Statistics/StatRobotTaskDetailsController.cs create mode 100644 PARR.Core/Infrastructure/Mapping/Shared/JobGroupResultMappingProfile.cs create mode 100644 PARR.Core/Services/RobotTaskDetailsServices/Implementations/RobotTaskDetailsService.cs create mode 100644 PARR.Core/Services/RobotTaskDetailsServices/Interfaces/IRobotTaskDetailsService.cs create mode 100644 PARR.Domain/DTOs/RobotTaskDetails/RobotTaskDetailsResult.cs create mode 100644 PARR.Domain/DTOs/Shared/JobGroupResult.cs diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index b52b7878..a84f37ec 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -212,11 +212,21 @@ namespace PARR.API.Contracts.V1 public const string GetPeriodStatistics = BaseStat + "/robot-tasks/{robot}/period/"; } + public static class StatRobotTaskDetails + { + public const string Details = BaseStat + "/robot-tasks/details/{robot}/{task}"; + } + public static class StatRobotStatus { public const string Get = BaseStat + "/robot-statuses/"; } + public static class StatRobotStatusDetails + { + public const string Details = BaseStat + "/robot-statuses/details/{robot}/{status}"; + } + public static class StatRobotHistory { public const string Get = BaseStat + "/robot-histories/"; diff --git a/PARR.API/Controllers/V1/Statistics/StatRobotStatusDetailsController.cs b/PARR.API/Controllers/V1/Statistics/StatRobotStatusDetailsController.cs new file mode 100644 index 00000000..caf87b10 --- /dev/null +++ b/PARR.API/Controllers/V1/Statistics/StatRobotStatusDetailsController.cs @@ -0,0 +1,40 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PARR.API.Contracts.V1; +using PARR.API.Controllers.V1.Base; +using PARR.Domain.Common.Roles; +using PARR.Domain.Enums; + +namespace PARR.API.Controllers.V1.Statistics +{ + /// + /// Детальная статистика по статусам заданий роботам + /// + [Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)] + public class StatRobotStatusDetailsController : BaseApiController + { + private readonly IMapper _mapper; + + public StatRobotStatusDetailsController( + IMapper mapper + ) + { + _mapper = mapper; + } + + + /// + /// Список групп работ по статусам заданий роботам + /// + /// + /// + /// + [HttpGet(ApiRoutes.StatRobotStatusDetails.Details)] + public async Task Details([FromRoute] RobotsEnum robot, [FromRoute] RobotStatusEnum status) + { + return Ok(); + } + + } +} diff --git a/PARR.API/Controllers/V1/Statistics/StatRobotTaskDetailsController.cs b/PARR.API/Controllers/V1/Statistics/StatRobotTaskDetailsController.cs new file mode 100644 index 00000000..7edd3c4c --- /dev/null +++ b/PARR.API/Controllers/V1/Statistics/StatRobotTaskDetailsController.cs @@ -0,0 +1,46 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PARR.API.Contracts.V1; +using PARR.API.Controllers.V1.Base; +using PARR.Core.Services.RobotTaskDetailsServices.Interfaces; +using PARR.Domain.Common.Roles; +using PARR.Domain.Enums; + +namespace PARR.API.Controllers.V1.Statistics +{ + /// + /// Детальная статистика по заданиям роботам + /// + [Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)] + public class StatRobotTaskDetailsController : BaseApiController + { + private readonly IMapper _mapper; + private readonly IRobotTaskDetailsService _robotTaskDetailsService; + + public StatRobotTaskDetailsController( + IMapper mapper, + IRobotTaskDetailsService robotTaskDetailsService + ) + { + _mapper = mapper; + _robotTaskDetailsService = robotTaskDetailsService; + } + + + /// + /// Список групп работ по заданиям роботам + /// + /// + /// + /// + [HttpGet(ApiRoutes.StatRobotTaskDetails.Details)] + public async Task Details([FromRoute] RobotsEnum robot, [FromRoute] TaskStatusEnum task) + { + var details = await _robotTaskDetailsService.GetDetailsAsync(robot, task); + + return Ok(); + } + + } +} diff --git a/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs b/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs index 49205754..8e82fca4 100644 --- a/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests.BaseRequests; @@ -8,10 +9,12 @@ using PARR.API.Controllers.V1.Base; using PARR.API.Helpers; using PARR.Core.Repositories.Interfaces; using PARR.Core.Services.NextRunServices; +using PARR.Domain.Common.Roles; using PARR.Domain.Enums; namespace PARR.API.Controllers.V1.Statistics { + [Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)] public class StatTemplateAutoControlController : BaseApiController { private readonly ITemplateRepository templateService; diff --git a/PARR.Core/DependencyInjection.cs b/PARR.Core/DependencyInjection.cs index e611a188..daf4c62e 100644 --- a/PARR.Core/DependencyInjection.cs +++ b/PARR.Core/DependencyInjection.cs @@ -12,6 +12,8 @@ using PARR.Core.Services.RobotMetrics; using PARR.Core.Services.RobotSnapshotServices; using PARR.Core.Services.RobotTask.Implementations; using PARR.Core.Services.RobotTask.Interfaces; +using PARR.Core.Services.RobotTaskDetailsServices.Implementations; +using PARR.Core.Services.RobotTaskDetailsServices.Interfaces; using PARR.Core.Services.RobotTaskRobotStatus.Implemetations; using PARR.Core.Services.RobotTaskRobotStatus.Interfaces; using PARR.Core.Services.Shortcodes; @@ -114,6 +116,7 @@ namespace PARR.Core services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/PARR.Core/Infrastructure/Mapping/Shared/JobGroupResultMappingProfile.cs b/PARR.Core/Infrastructure/Mapping/Shared/JobGroupResultMappingProfile.cs new file mode 100644 index 00000000..8993090d --- /dev/null +++ b/PARR.Core/Infrastructure/Mapping/Shared/JobGroupResultMappingProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using PARR.Domain.DTOs.Shared; +using PARR.Domain.Entities.JobGroupEntities; + +namespace PARR.Core.Infrastructure.Mapping.Shared +{ + public class JobGroupResultMappingProfile: Profile + { + public JobGroupResultMappingProfile() + { + CreateMap() + .Include(); + + CreateMap(); + } + } +} diff --git a/PARR.Core/PARR.Core.csproj b/PARR.Core/PARR.Core.csproj index a13275dd..86e44fa7 100644 --- a/PARR.Core/PARR.Core.csproj +++ b/PARR.Core/PARR.Core.csproj @@ -25,4 +25,8 @@ + + + + diff --git a/PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs b/PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs index 0ff18d5b..d058a5c3 100644 --- a/PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs +++ b/PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs @@ -273,6 +273,9 @@ namespace PARR.Core.Services.RobotTask.Implementations if (templatesToRename.Count == 0) return tasks; + // Создаем словарь маппинга TemplateId -> OldName. + var templateIdToOldName = templatesToRename.ToDictionary(t => t.TemplateId, t => t.OldName); + // Ищем конфигурации роботов для СТАРЫХ шаблонов (которые переименовываются) по ИД, смотрим, можем ли взять их в работу var renameTemplateIds = templatesToRename.Select(t => t.TemplateId).ToList(); var renameTasks = await _robotConfigurationRepository.Get() @@ -299,10 +302,16 @@ namespace PARR.Core.Services.RobotTask.Implementations { _logger.LogDebug("Найдено старых заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям.", errorTasks.Count); - var errorTemplateNames = errorTasks.Select(t => t.Template!.Name).ToHashSet(); + //var errorTemplateNames = errorTasks.Select(t => t.Template!.Name).ToHashSet(); + // Берем OldName из словаря + var errorOldNames = errorTasks + .Where(t => templateIdToOldName.ContainsKey(t.TemplateId)) + .Select(t => templateIdToOldName[t.TemplateId]) + .ToHashSet(); + // Берем целевые таски, находим в них задания которым надо поставить ошибку tasksToSetErrorStatus = tasks - .Where(t => errorTemplateNames.Contains(t.TemplateName)) + .Where(t => errorOldNames.Contains(t.TemplateName)) .Select(t => t.TaskId) .ToList(); @@ -319,30 +328,36 @@ namespace PARR.Core.Services.RobotTask.Implementations var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime); // Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InpRogress но которые еще не просрочены) - var allowedRenameTasks = renameTasks.Where(t => + var allowedTasks = renameTasks.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.Wait || (t.RobotStatusCode == (int)RobotStatusEnum.InProgress && t.AttemptsNumber < _settingsFromDb.RobotAttemptsNumber && t.LastRobotStatusUpdated < endDate) ).ToList(); - // Словарь для поиска подменной задачи по имени шаблона. - // GroupBy + First на случай, если в бд есть дубликаты, но такого быть не может - var renameTasksToDictionary = allowedRenameTasks - .GroupBy(t => t.Template!.Name) - .ToDictionary( - t => t.Key, - t => new RobotTaskDetails(t.First().Id, t.First().Template!.Name, t.First().Template!.NextRun) - ); + // Формируем список заданий + var originalCount = tasks.Count; // Запоминаем сколько было изначально var errorTaskIdsSet = tasksToSetErrorStatus.ToHashSet(); - // Создаем итоговый список + // Из исходных тасков удалить те которым установлен статус ошибки + var filteredOriginalTasks = tasks.Where(t => !errorTaskIdsSet.Contains(t.TaskId)).ToList(); + var errorCount = errorTaskIdsSet.Count; + + // Создаем словарь, где КЛЮЧОМ является OldName (который совпадает с task.TemplateName), + // а не текущее имя шаблона (которое уже новое). + var renameTasksToDictionary = allowedTasks + .Where(t => templateIdToOldName.ContainsKey(t.TemplateId)) + .GroupBy(t => templateIdToOldName[t.TemplateId]) // Группируем по OldName! + .ToDictionary( + g => g.Key, // Ключ = OldName (например, "СтароеИмя") + g => new RobotTaskDetails(g.First().Id, g.First().Template!.Name, g.First().Template!.NextRun) + ); + + // Проходим по ИСХОДНОМУ списку, чтобы сохранить порядок сортировки и корректно подменить задачи var finalTasks = new List(tasks.Count); int replacedCount = 0; - int errorCount = errorTaskIdsSet.Count; - // Проходим по ИСХОДНОМУ списку, чтобы сохранить его порядок сортировки foreach (var task in tasks) { // Если задаче нужно поставить ошибку, просто пропускаем ее (она не попадет в итоговый список) @@ -351,7 +366,7 @@ namespace PARR.Core.Services.RobotTask.Implementations continue; } - // Если для этого имени шаблона есть разрешенная задача на переименование - вставляем ее на место текущей + // ключ в словаре - это OldName, который равен task.TemplateName if (renameTasksToDictionary.TryGetValue(task.TemplateName, out var renameTask)) { finalTasks.Add(renameTask); @@ -364,13 +379,13 @@ namespace PARR.Core.Services.RobotTask.Implementations } } + // Логируем итоговую статистику трансформации пула задач _logger.LogInformation( - "Трансформация пула задач (Rename). Исходных: {OriginalCount}. Отклонено (Error): {ErrorCount}. " + - "Заменено на старые: {ReplacedCount}. Итого к выдаче: {FinalCount}", - tasks.Count, errorCount, replacedCount, finalTasks.Count - ); + "Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " + + "Отклонено (ошибка): {ErrorCount} шт. Заменено на старые: {ReplacedCount} шт. " + + "Итого к выдаче: {FinalCount} шт.", + originalCount, errorCount, replacedCount, finalTasks.Count); - // Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка return finalTasks; } diff --git a/PARR.Core/Services/RobotTaskDetailsServices/Implementations/RobotTaskDetailsService.cs b/PARR.Core/Services/RobotTaskDetailsServices/Implementations/RobotTaskDetailsService.cs new file mode 100644 index 00000000..62f37e18 --- /dev/null +++ b/PARR.Core/Services/RobotTaskDetailsServices/Implementations/RobotTaskDetailsService.cs @@ -0,0 +1,68 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.Core.Repositories.Interfaces; +using PARR.Core.Services.RobotTaskDetailsServices.Interfaces; +using PARR.Domain.DTOs.RobotTaskDetails; +using PARR.Domain.DTOs.Shared; +using PARR.Domain.Enums; + +namespace PARR.Core.Services.RobotTaskDetailsServices.Implementations +{ + internal class RobotTaskDetailsService : IRobotTaskDetailsService + { + private readonly ILogger _logger; + private readonly IRobotConfigurationRepository _robotConfigurationRepository; + private readonly IMapper _mapper; + private readonly IRobotRepository _robotRepository; + private readonly ITaskStatusRepository _taskStatusRepository; + + public RobotTaskDetailsService( + ILogger logger, + IRobotConfigurationRepository robotConfigurationRepository, + IMapper mapper, + IRobotRepository robotRepository, + ITaskStatusRepository taskStatusRepository + ) + { + _logger = logger; + _robotConfigurationRepository = robotConfigurationRepository; + _mapper = mapper; + _robotRepository = robotRepository; + _taskStatusRepository = taskStatusRepository; + } + + public async Task GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task) + { + var details = await _robotConfigurationRepository.Get() + .Where(t => t.RobotCode == (int)robot && t.TaskStatusCode == (int)task) + .GroupBy(t => t.Template!.Job!.Group) + .Select(t => new + { + JobGroup = t.Key, + TemplatesCount = t.Count() + }).ToListAsync(); + + var robotObj = await _robotRepository.Get() + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Code == (int)robot); + + var taskObj = await _taskStatusRepository.Get() + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Code == (int)task); + + var result = new RobotTaskDetailsResult + { + Robot = _mapper.Map(robotObj), + Task = _mapper.Map(taskObj), + Details = details.Select(t => new RobotTaskGroupDetailsResult + { + JobGroup = _mapper.Map(t.JobGroup), + TemplatesCount = t.TemplatesCount + }).OrderBy(t => t.JobGroup.GroupName).ToList() + }; + + return result; + } + } +} diff --git a/PARR.Core/Services/RobotTaskDetailsServices/Interfaces/IRobotTaskDetailsService.cs b/PARR.Core/Services/RobotTaskDetailsServices/Interfaces/IRobotTaskDetailsService.cs new file mode 100644 index 00000000..814c7ba1 --- /dev/null +++ b/PARR.Core/Services/RobotTaskDetailsServices/Interfaces/IRobotTaskDetailsService.cs @@ -0,0 +1,16 @@ +using PARR.Domain.DTOs.RobotTaskDetails; +using PARR.Domain.Enums; + +namespace PARR.Core.Services.RobotTaskDetailsServices.Interfaces +{ + public interface IRobotTaskDetailsService + { + /// + /// Список групп работ по заданиям робота + /// + /// + /// + /// + Task GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task); + } +} diff --git a/PARR.Domain/DTOs/RobotTaskDetails/RobotTaskDetailsResult.cs b/PARR.Domain/DTOs/RobotTaskDetails/RobotTaskDetailsResult.cs new file mode 100644 index 00000000..1765cab9 --- /dev/null +++ b/PARR.Domain/DTOs/RobotTaskDetails/RobotTaskDetailsResult.cs @@ -0,0 +1,18 @@ +using PARR.Domain.DTOs.Shared; + +namespace PARR.Domain.DTOs.RobotTaskDetails +{ + public record RobotTaskDetailsResult + { + public RobotResult Robot { get; init; } + public RobotTaskStatusResult Task { get; init; } + + public List Details { get; init; } + } + + public record RobotTaskGroupDetailsResult + { + public JobGroupShortResult JobGroup { get; init; } + public int TemplatesCount { get; init; } + } +} diff --git a/PARR.Domain/DTOs/Shared/JobGroupResult.cs b/PARR.Domain/DTOs/Shared/JobGroupResult.cs new file mode 100644 index 00000000..1a12c0ee --- /dev/null +++ b/PARR.Domain/DTOs/Shared/JobGroupResult.cs @@ -0,0 +1,15 @@ +namespace PARR.Domain.DTOs.Shared +{ + public record JobGroupShortResult + { + public Guid Id { get; set; } + public required string GroupName { get; set; } + } + + + + public record JobGroupResult : JobGroupShortResult + { + + } +}