This commit is contained in:
Mikhail Kuznetsov
2026-07-28 16:34:13 +10:00
12 changed files with 304 additions and 22 deletions

View File

@@ -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<IRobotTaskService, RobotTaskService>();
services.AddScoped<IRobotSnapshotService, RobotSnapshotService>();
services.AddScoped<IRobotTaskRobotStatusService, RobotTaskRobotStatusService>();
services.AddScoped<IRobotTaskDetailsService, RobotTaskDetailsService>();
services.AddScoped<IUnitService, UnitService>();
services.AddScoped<UnitCacheService>();

View File

@@ -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<JobGroup, JobGroupShortResult>()
.Include<JobGroup, JobGroupResult>();
CreateMap<JobGroup, JobGroupResult>();
}
}
}

View File

@@ -25,4 +25,8 @@
<!-- Разрешаем Castle DynamicProxy (Moq / NSubstitute) видеть internal классы PARR.Core -->
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\RobotStatusDetails\" />
</ItemGroup>
</Project>

View File

@@ -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<RobotTaskDetails>(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);
}

View File

@@ -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<RobotTaskDetailsService> _logger;
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
private readonly IMapper _mapper;
private readonly IRobotRepository _robotRepository;
private readonly ITaskStatusRepository _taskStatusRepository;
public RobotTaskDetailsService(
ILogger<RobotTaskDetailsService> logger,
IRobotConfigurationRepository robotConfigurationRepository,
IMapper mapper,
IRobotRepository robotRepository,
ITaskStatusRepository taskStatusRepository
)
{
_logger = logger;
_robotConfigurationRepository = robotConfigurationRepository;
_mapper = mapper;
_robotRepository = robotRepository;
_taskStatusRepository = taskStatusRepository;
}
public async Task<RobotTaskDetailsResult> 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<RobotResult>(robotObj),
Task = _mapper.Map<RobotTaskStatusResult>(taskObj),
Details = details.Select(t => new RobotTaskGroupDetailsResult
{
JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup),
TemplatesCount = t.TemplatesCount
}).OrderBy(t => t.JobGroup.GroupName).ToList()
};
return result;
}
}
}

View File

@@ -0,0 +1,16 @@
using PARR.Domain.DTOs.RobotTaskDetails;
using PARR.Domain.Enums;
namespace PARR.Core.Services.RobotTaskDetailsServices.Interfaces
{
public interface IRobotTaskDetailsService
{
/// <summary>
/// Список групп работ по заданиям робота
/// </summary>
/// <param name="robot"></param>
/// <param name="task"></param>
/// <returns></returns>
Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task);
}
}