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..0cb55db5 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,39 +328,71 @@ 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) - ); + // Проверим StatusTypeId у старых шаблонов в процессе переименования + // 1. Находим задачи переименования, у которых StatusTypeId шаблона != Used + var invalidRenameTasks = allowedTasks + .Where(t => t.Template != null && t.Template.StatusTypeId != TemplateStatusTypeEnum.Used) + .ToList(); + // 2. Создаем словарь для быстрого поиска и логирования: OldName -> StatusTypeId + var invalidOldNamesWithStatus = invalidRenameTasks + .Where(t => templateIdToOldName.ContainsKey(t.TemplateId)) + .Select(t => new { OldName = templateIdToOldName[t.TemplateId], StatusTypeId = t.Template!.StatusTypeId }) + .ToDictionary(x => x.OldName, x => x.StatusTypeId); + + // 3. Оставляем для подмены только те задачи, у которых StatusTypeId == 0 + var validAllowedTasks = allowedTasks + .Where(t => t.Template != null && t.Template.StatusTypeId == TemplateStatusTypeEnum.Used) + .ToList(); + + // Формируем список заданий + var originalCount = tasks.Count; var errorTaskIdsSet = tasksToSetErrorStatus.ToHashSet(); + var errorCount = errorTaskIdsSet.Count; - // Создаем итоговый список + // Создаем словарь подмены ТОЛЬКО из валидных задач (где StatusTypeId == Used) + var renameTasksToDictionary = validAllowedTasks + .Where(t => templateIdToOldName.ContainsKey(t.TemplateId)) + .GroupBy(t => templateIdToOldName[t.TemplateId]) + .ToDictionary( + g => g.Key, + 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; + int excludedByStatusCount = 0; // Счетчик для логов - // Проходим по ИСХОДНОМУ списку, чтобы сохранить его порядок сортировки foreach (var task in tasks) { - // Если задаче нужно поставить ошибку, просто пропускаем ее (она не попадет в итоговый список) + // 1. Если задаче нужно поставить ошибку, пропускаем ее if (errorTaskIdsSet.Contains(task.TaskId)) { continue; } - // Если для этого имени шаблона есть разрешенная задача на переименование - вставляем ее на место текущей + // 2. Если этот шаблон связан с переименованием, но у старого шаблона StatusTypeId != Used + if (invalidOldNamesWithStatus.TryGetValue(task.TemplateName, out var badStatusId)) + { + _logger.LogWarning( + "Задача для шаблона '{TemplateName}' (TaskId: {TaskId}) ИСКЛЮЧЕНА из выдачи. " + + "Связанный шаблон в процессе переименования имеет недопустимый StatusTypeId = {StatusTypeId} (ожидалось Used). " + + "Исходная задача также не выполняется.", + task.TemplateName, task.TaskId, badStatusId); + + excludedByStatusCount++; + continue; // Не добавляем ни старую, ни новую задачу в итоговый список + } + + // 3. Если для этого имени шаблона есть разрешенная задача на переименование (и она валидна) - вставляем ее if (renameTasksToDictionary.TryGetValue(task.TemplateName, out var renameTask)) { finalTasks.Add(renameTask); @@ -364,11 +405,12 @@ namespace PARR.Core.Services.RobotTask.Implementations } } + // Логируем итоговую статистику трансформации пула задач _logger.LogInformation( - "Трансформация пула задач (Rename). Исходных: {OriginalCount}. Отклонено (Error): {ErrorCount}. " + - "Заменено на старые: {ReplacedCount}. Итого к выдаче: {FinalCount}", - tasks.Count, errorCount, replacedCount, finalTasks.Count - ); + "Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " + + "Отклонено (ошибка): {ErrorCount} шт. Исключено (невалидный StatusTypeId): {ExcludedCount} шт. " + + "Заменено на старые: {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт.", + originalCount, errorCount, excludedByStatusCount, replacedCount, finalTasks.Count); // Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка return finalTasks; @@ -414,7 +456,7 @@ namespace PARR.Core.Services.RobotTask.Implementations await _robotHistoryRepository.CreateAsync(history); _logger.LogInformation("Для целевого задания {TaskId} (шаблон '{TemplateName}') установлен статус ошибки, " + - "так как связанное задание со старым шаблоном не было успешно выполнено.", + "так как связанное задание со старым шаблоном не было успешно выполнено.", task.Id, task.Template!.Name); } 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 + { + + } +}