This commit is contained in:
Mikhail Kuznetsov
2026-07-30 14:18:14 +10:00
23 changed files with 8756 additions and 97 deletions

View File

@@ -0,0 +1,88 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.RobotStatusDetails.Interfaces;
using PARR.Domain.DTOs.RobotStatusDetails;
using PARR.Domain.DTOs.Shared;
using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotStatusDetails.Implementations
{
internal class RobotStatusDetailsService : IRobotStatusDetailsService
{
private readonly ILogger<RobotStatusDetailsService> _logger;
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
private readonly IMapper _mapper;
private readonly IRobotRepository _robotRepository;
private readonly IRobotStatusRepository _robotStatusRepository;
public RobotStatusDetailsService(
ILogger<RobotStatusDetailsService> logger,
IRobotConfigurationRepository robotConfigurationRepository,
IMapper mapper,
IRobotRepository robotRepository,
IRobotStatusRepository robotStatusRepository
)
{
_logger = logger;
_robotConfigurationRepository = robotConfigurationRepository;
_mapper = mapper;
_robotRepository = robotRepository;
_robotStatusRepository = robotStatusRepository;
}
public async Task<RobotStatusDetailsResult> GetDetailsAsync(RobotsEnum robot, RobotStatusEnum status, CancellationToken cancellationToken = default)
{
var groupedDetails = await _robotConfigurationRepository.Get()
.AsNoTracking()
.Where(config => config.RobotCode == (int)robot && config.RobotStatusCode == (int)status)
.GroupBy(config => config.Template!.Job!.Group)
.Select(t => new
{
JobGroup = t.Key,
TemplatesCount = t.Count()
})
.ToListAsync(cancellationToken);
var robotEntity = await _robotRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(r => r.Code == (int)robot, cancellationToken);
var robotStatusEntity = await _robotStatusRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Code == (int)status, cancellationToken);
if (robotEntity == null)
{
_logger.LogWarning("Робот с кодом {RobotCode} не найден в БД", robot);
throw new AppValidationException($"Робот с кодом {(int)robot} не найден");
}
if (robotStatusEntity == null)
{
_logger.LogWarning("Статус робота с кодом {StatusCode} не найден в БД", status);
throw new AppValidationException($"Статус робота с кодом {(int)status} не найден");
}
var result = new RobotStatusDetailsResult
{
Robot = _mapper.Map<RobotResult>(robotEntity),
Status = _mapper.Map<RobotStatusResult>(robotStatusEntity),
Details = groupedDetails
.Select(t => new RobotStatusGroupDetailsResult
{
JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup),
TemplatesCount = t.TemplatesCount
}).OrderBy(t => t.JobGroup.GroupName)
.ToList()
};
return result;
}
}
}

View File

@@ -0,0 +1,10 @@
using PARR.Domain.DTOs.RobotStatusDetails;
using PARR.Domain.Enums;
namespace PARR.Core.Services.RobotStatusDetails.Interfaces
{
public interface IRobotStatusDetailsService
{
Task<RobotStatusDetailsResult> GetDetailsAsync(RobotsEnum robot, RobotStatusEnum status, CancellationToken cancellationToken = default);
}
}

View File

@@ -260,15 +260,18 @@ namespace PARR.Core.Services.RobotTask.Implementations
if (tasks.Count == 0 || robotCode != RobotsEnum.TemplateOrder)
return tasks;
_logger.LogDebug("Исходный пул задач для проверки переименования: {Tasks}",
string.Join(" | ", tasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
// Ищем есть ли связанные шаблоны с таким имененм на переименование
var taskTemplateNames = tasks.Select(t => t.TemplateName).Distinct().ToList();
// Ищем записи в таблице переименований, где OldName совпадает с именами наших новых задач
var templatesToRename = await _templateRenamePendingRepository.Get().AsNoTracking()
var templatesToRename = await _templateRenamePendingRepository.Get()
.AsNoTracking()
.Where(t => taskTemplateNames.Contains(t.OldName))
.ToListAsync();
_logger.LogDebug("Найдено шаблонов в процессе переименования для текущих задач: {Count} шт.", templatesToRename.Count);
_logger.LogDebug("Найдено записей в TemplateRenamePending для текущих задач: {Count} шт.", templatesToRename.Count);
if (templatesToRename.Count == 0)
return tasks;
@@ -288,7 +291,9 @@ namespace PARR.Core.Services.RobotTask.Implementations
&& t.TaskStatusCode == (int)TaskStatusEnum.Updating
).ToListAsync();
// --- Блок обработки ошибок ---
// =========================================================================
// БЛОК 1: ОБРАБОТКА ОШИБОК (Правило: если ХОТЯ БЫ ОДНА упала в ошибку -> оригинал в ошибку)
// =========================================================================
// Если старый шаблон в ошибке и лимит попыток исчерпан, ставим ошибку и новому шаблону
var errorTasks = renameTasks
@@ -300,16 +305,16 @@ namespace PARR.Core.Services.RobotTask.Implementations
var tasksToSetErrorStatus = new List<Guid>();
if (errorTasks.Count > 0)
{
_logger.LogDebug("Найдено старых заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям.", errorTasks.Count);
_logger.LogDebug("Найдено связанных заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям.", errorTasks.Count);
//var errorTemplateNames = errorTasks.Select(t => t.Template!.Name).ToHashSet();
// Берем OldName из словаря
// Собираем ВСЕ OldName, для которых есть хотя бы одна упавшая в ошибку задача.
// Использование ToHashSet() гарантирует, что если 1 или 10 задач в ошибке, OldName попадет в набор один раз.
var errorOldNames = errorTasks
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
.Select(t => templateIdToOldName[t.TemplateId])
.ToHashSet();
// Берем целевые таски, находим в них задания которым надо поставить ошибку
// Находим оригинальные задачи, чье имя совпадает с любым из "ошибочных" OldName
tasksToSetErrorStatus = tasks
.Where(t => errorOldNames.Contains(t.TemplateName))
.Select(t => t.TaskId)
@@ -318,16 +323,17 @@ namespace PARR.Core.Services.RobotTask.Implementations
if (tasksToSetErrorStatus.Count > 0)
{
// Устанавливаем ошибку целевым + пишем комментарий от робота + нажимаем комит
var logMessage = "[RobotTaskService] Установлен статус ошибки, так как не переименован связанный шаблон";
var logMessage = "[RobotTaskService] Установлен статус ошибки, так как хотя бы одна из связанных задач переименования не была успешно выполнена.";
await SetErrorStatusAsync(tasksToSetErrorStatus, logMessage);
}
}
// --- Блок подмены задач ---
// =========================================================================
// БЛОК 2: ПОДМЕНА ЗАДАЧ (Правило: берем ПЕРВУЮ валидную задачу для подмены)
// =========================================================================
var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime);
// Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InpRogress но которые еще не просрочены)
// Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InProgress но которые еще не просрочены)
var allowedTasks = renameTasks.Where(t =>
t.RobotStatusCode == (int)RobotStatusEnum.Wait
|| (t.RobotStatusCode == (int)RobotStatusEnum.InProgress
@@ -336,20 +342,28 @@ namespace PARR.Core.Services.RobotTask.Implementations
).ToList();
// Проверим StatusTypeId у старых шаблонов в процессе переименования
// 1. Находим задачи переименования, у которых StatusTypeId шаблона != Used
// 1. Находим задачи переименования, у которых StatusTypeId шаблона НЕ является допустимым (!= Used и != Unused)
var invalidRenameTasks = allowedTasks
.Where(t => t.Template != null && t.Template.StatusTypeId != TemplateStatusTypeEnum.Used)
.Where(t => t.Template != null && t.Template.StatusTypeId != TemplateStatusTypeEnum.Used && t.Template.StatusTypeId != TemplateStatusTypeEnum.Unused)
.ToList();
// 2. Создаем словарь для быстрого поиска и логирования: OldName -> StatusTypeId
// Так как OldName не уникален, используем GroupBy, чтобы избежать ArgumentException, при наличии нескольких невалидных задач с одинаковым OldName.
//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);
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);
.GroupBy(t => templateIdToOldName[t.TemplateId]) // Группируем по OldName
.ToDictionary(
g => g.Key, // Ключ = OldName
g => g.First().Template!.StatusTypeId // Значение = StatusTypeId первой задачи в группе (для лога)
);
// 3. Оставляем для подмены только те задачи, у которых StatusTypeId == 0
// 3. Оставляем для подмены только те задачи, у которых StatusTypeId является допустимым (== Used или == Unused)
var validAllowedTasks = allowedTasks
.Where(t => t.Template != null && t.Template.StatusTypeId == TemplateStatusTypeEnum.Used)
.Where(t => t.Template != null && (t.Template.StatusTypeId == TemplateStatusTypeEnum.Used || t.Template.StatusTypeId == TemplateStatusTypeEnum.Unused))
.ToList();
// Формируем список заданий
@@ -357,14 +371,16 @@ namespace PARR.Core.Services.RobotTask.Implementations
var errorTaskIdsSet = tasksToSetErrorStatus.ToHashSet();
var errorCount = errorTaskIdsSet.Count;
// Создаем словарь подмены ТОЛЬКО из валидных задач (где StatusTypeId == Used)
// Создаем словарь подмены ТОЛЬКО из валидных задач (где StatusTypeId == Used или Unused)
// ГРУППИРУЕМ по OldName и берем .First()!
// Это реализует правило: "если записей несколько, берем из них первую и подменяем ей оригинальное задание".
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)
);
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
.GroupBy(t => templateIdToOldName[t.TemplateId])
.ToDictionary(
g => g.Key, // Ключ = OldName
g => new RobotTaskDetails(g.First().Id, g.First().Template!.Name, g.First().Template!.NextRun)
);
// Проходим по ИСХОДНОМУ списку, чтобы сохранить порядок сортировки
var finalTasks = new List<RobotTaskDetails>(tasks.Count);
@@ -384,7 +400,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
{
_logger.LogWarning(
"Задача для шаблона '{TemplateName}' (TaskId: {TaskId}) ИСКЛЮЧЕНА из выдачи. " +
"Связанный шаблон в процессе переименования имеет недопустимый StatusTypeId = {StatusTypeId} (ожидалось Used). " +
"Связанный шаблон в процессе переименования имеет недопустимый StatusTypeId = {StatusTypeId} (ожидалось Used или Unused). " +
"Исходная задача также не выполняется.",
task.TemplateName, task.TaskId, badStatusId);
@@ -393,8 +409,13 @@ namespace PARR.Core.Services.RobotTask.Implementations
}
// 3. Если для этого имени шаблона есть разрешенная задача на переименование (и она валидна) - вставляем ее
// Подменяем оригинальную задачу на ПЕРВУЮ валидную задачу переименования
if (renameTasksToDictionary.TryGetValue(task.TemplateName, out var renameTask))
{
_logger.LogDebug("ПОДМЕНА ЗАДАЧИ: Исходная [Id: {OriginalId}, Name: '{OriginalName}'] " +
"-> Заменена на [Id: {NewId}, Name: '{NewName}']",
task.TaskId, task.TemplateName, renameTask.TaskId, renameTask.TemplateName);
finalTasks.Add(renameTask);
replacedCount++;
}
@@ -405,11 +426,13 @@ namespace PARR.Core.Services.RobotTask.Implementations
}
}
// Логируем итоговую статистику трансформации пула задач
_logger.LogDebug("Итоговый пул задач после трансформации: {Tasks}",
string.Join(" | ", finalTasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
_logger.LogInformation(
"Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " +
"Отклонено (ошибка): {ErrorCount} шт. Исключено (невалидный StatusTypeId): {ExcludedCount} шт. " +
"Заменено на старые: {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт.",
"Заменено на старые (взята первая из группы): {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт.",
originalCount, errorCount, excludedByStatusCount, replacedCount, finalTasks.Count);
// Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка

View File

@@ -6,6 +6,7 @@ using PARR.Core.Services.RobotTaskDetailsServices.Interfaces;
using PARR.Domain.DTOs.RobotTaskDetails;
using PARR.Domain.DTOs.Shared;
using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotTaskDetailsServices.Implementations
{
@@ -32,34 +33,55 @@ namespace PARR.Core.Services.RobotTaskDetailsServices.Implementations
_taskStatusRepository = taskStatusRepository;
}
public async Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task)
public async Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task, CancellationToken cancellationToken = default)
{
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();
// 1. Получаем группировку конфигураций
var groupedDetails = await _robotConfigurationRepository.Get()
.AsNoTracking()
.Where(config => config.RobotCode == (int)robot && config.TaskStatusCode == (int)task)
.GroupBy(config => config.Template!.Job!.Group)
.Select(t => new
{
JobGroup = t.Key,
TemplatesCount = t.Count()
})
.ToListAsync(cancellationToken);
var robotObj = await _robotRepository.Get()
// 2. Получаем сущности робота и статуса задачи)
var robotEntity = await _robotRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Code == (int)robot);
.FirstOrDefaultAsync(r => r.Code == (int)robot, cancellationToken);
var taskObj = await _taskStatusRepository.Get()
var taskStatusEntity = await _taskStatusRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Code == (int)task);
.FirstOrDefaultAsync(t => t.Code == (int)task, cancellationToken);
if (robotEntity == null)
{
_logger.LogWarning("Робот с кодом {RobotCode} не найден в БД", robot);
throw new AppValidationException($"Робот с кодом {(int)robot} не найден");
}
if (taskStatusEntity == null)
{
_logger.LogWarning("Статус задачи с кодом {TaskCode} не найден в БД", task);
throw new AppValidationException($"Статус задачи с кодом {(int)task} не найден");
}
// 3. Маппинг и сборка результирующего DTO
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()
Robot = _mapper.Map<RobotResult>(robotEntity),
Task = _mapper.Map<RobotTaskStatusResult>(taskStatusEntity),
Details = groupedDetails
.Select(t => new RobotTaskGroupDetailsResult
{
JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup),
TemplatesCount = t.TemplatesCount
})
.OrderBy(d => d.JobGroup.GroupName)
.ToList()
};
return result;

View File

@@ -11,6 +11,6 @@ namespace PARR.Core.Services.RobotTaskDetailsServices.Interfaces
/// <param name="robot"></param>
/// <param name="task"></param>
/// <returns></returns>
Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task);
Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task, CancellationToken cancellationToken = default);
}
}