2026-05-26 11:26:52 +10:00
using AutoMapper ;
2026-05-25 16:59:42 +10:00
using Microsoft.EntityFrameworkCore ;
using Microsoft.Extensions.Logging ;
2026-05-26 11:26:52 +10:00
using PARR.BLL.Helpers ;
2026-05-25 16:59:42 +10:00
using PARR.Core.Repositories.Interfaces ;
2026-07-22 16:02:04 +10:00
using PARR.Core.Repositories.Interfaces.TemplateRepositories ;
2026-05-26 11:26:52 +10:00
using PARR.Core.Services.NextRunServices ;
2026-05-25 16:59:42 +10:00
using PARR.Core.Services.RobotTask.Interfaces ;
2026-07-22 16:02:04 +10:00
using PARR.Core.Services.RobotTask.Models ;
2026-05-26 11:26:52 +10:00
using PARR.Core.Services.Shortcodes ;
using PARR.Domain.DTOs.RobotTask ;
2026-05-25 16:59:42 +10:00
using PARR.Domain.Entities ;
2026-05-26 11:26:52 +10:00
using PARR.Domain.Entities.Base.History ;
2026-07-21 11:48:21 +10:00
using PARR.Domain.Entities.RobotEntities ;
2026-05-25 16:59:42 +10:00
using PARR.Domain.Enums ;
using PARR.Domain.Exceptions ;
using PARR.Domain.Settings ;
namespace PARR.Core.Services.RobotTask.Implementations
{
internal class RobotTaskService : IRobotTaskService
{
/// <summary>
/// Количество заданий которые рассматриваем для взятия в работу.
2026-07-22 16:02:04 +10:00
/// Рекомендованное значение, кол-во роботов * 3
2026-05-25 16:59:42 +10:00
/// </summary>
2026-07-22 16:02:04 +10:00
private readonly int TakeTasks = 15 * 3 ;
2026-05-25 16:59:42 +10:00
2026-07-22 16:02:04 +10:00
private readonly ILogger < RobotTaskService > _logger ;
private readonly IRobotConfigurationRepository _robotConfigurationRepository ;
private readonly SettingsFromDb _settingsFromDb ;
private readonly IRobotHistoryRepository _robotHistoryRepository ;
private readonly IMapper _mapper ;
private readonly IShortcodesService _shortcodesService ;
private readonly INextRunService _nextRunService ;
private readonly ITemplateRenamePendingRepository _templateRenamePendingRepository ;
2026-05-25 16:59:42 +10:00
public RobotTaskService (
ILogger < RobotTaskService > logger ,
IRobotConfigurationRepository robotConfigurationRepository ,
SettingsFromDb settingsFromDb ,
2026-05-26 11:26:52 +10:00
IRobotHistoryRepository robotHistoryRepository ,
IMapper mapper ,
IShortcodesService shortcodesService ,
2026-07-22 16:02:04 +10:00
INextRunService nextRunService ,
ITemplateRenamePendingRepository templateRenamePendingRepository
2026-05-25 16:59:42 +10:00
)
{
2026-07-23 10:48:59 +10:00
_logger = logger ;
_robotConfigurationRepository = robotConfigurationRepository ;
_settingsFromDb = settingsFromDb ;
_robotHistoryRepository = robotHistoryRepository ;
_mapper = mapper ;
_shortcodesService = shortcodesService ;
_nextRunService = nextRunService ;
2026-07-22 16:02:04 +10:00
_templateRenamePendingRepository = templateRenamePendingRepository ;
2026-05-25 16:59:42 +10:00
}
2026-05-26 11:26:52 +10:00
public async Task < RobotTaskTemplate > GetTemplateTaskAsync ( TaskStatusEnum taskStatusCode , bool acquireTask , string? robotIp , string? robotId )
{
2026-07-10 14:11:16 +10:00
var templateTask = await GetTaskAsync ( RobotsEnum . TemplateOrder , taskStatusCode , acquireTask , robotIp , robotId , TimeSpan . Zero ) ;
2026-05-26 11:26:52 +10:00
2026-07-22 16:02:04 +10:00
var task = _mapper . Map < RobotTaskTemplate > ( templateTask ) ;
2026-05-26 11:26:52 +10:00
2026-07-22 16:02:04 +10:00
task = task with { FullDescription = NormalizeLineEndingsToCrlf ( await _shortcodesService . ApplyShortcodesAsync ( task . FullDescription , templateTask . Template ! ) ) } ;
task = task with { ShortDescription = await _shortcodesService . ApplyShortcodesAsync ( task . ShortDescription , templateTask . Template ! ) } ;
task = task with { Solution = NormalizeLineEndingsToCrlf ( await _shortcodesService . ApplyShortcodesAsync ( task . Solution , templateTask . Template ! ) ) } ;
task = task with { TnkName = await _shortcodesService . ApplyShortcodesAsync ( task . TnkName , templateTask . Template ! ) } ;
task = task with { WorkName = await _shortcodesService . ApplyShortcodesAsync ( task . WorkName , templateTask . Template ! ) } ;
task = task with { WorkGroup = await _shortcodesService . ApplyShortcodesAsync ( task . WorkGroup , templateTask . Template ! ) } ;
task = task with { ResponseArea = await _shortcodesService . ApplyShortcodesAsync ( task . ResponseArea , templateTask . Template ! ) } ;
2026-05-26 11:26:52 +10:00
2026-07-22 16:02:04 +10:00
task = task with { ClosingCode = _settingsFromDb . ClosingCode } ;
task = task with { Initiator = _settingsFromDb . Initiator } ;
task = task with { Category = _settingsFromDb . Category } ;
2026-05-26 11:26:52 +10:00
return task ;
}
2026-07-10 14:11:16 +10:00
public async Task < RobotTaskSchedule > GetScheduleTaskAsync ( TaskStatusEnum taskStatusCode , bool acquireTask , string? robotIp , string? robotId , IHistoryInitiator historyInitiator , TimeSpan scheduleCooldownDuration )
2026-05-26 11:26:52 +10:00
{
2026-07-10 14:11:16 +10:00
var scheduleTask = await GetTaskAsync ( RobotsEnum . ScheduleOrder , taskStatusCode , acquireTask , robotIp , robotId , scheduleCooldownDuration ) ;
2026-05-26 11:26:52 +10:00
// Проверяем nextRun, lastRun, обновляем их
var resultUpdateNextRun = await UpdateNextRunAsync ( scheduleTask , historyInitiator ) ;
if ( ! resultUpdateNextRun )
{
2026-07-22 16:02:04 +10:00
_logger . LogError ( "Ошибка при расчете NextRun для templateId: {templateId}" , scheduleTask . TemplateId ) ;
2026-05-26 11:26:52 +10:00
throw new NextRunException ( $"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}" ) ;
}
2026-07-22 16:02:04 +10:00
var task = _mapper . Map < RobotTaskSchedule > ( scheduleTask ) ;
2026-05-26 11:26:52 +10:00
2026-07-22 16:02:04 +10:00
task = task with { Timezone = _settingsFromDb . EsppScheduleTimezone } ;
task = task with { WorkGroup = await _shortcodesService . ApplyShortcodesAsync ( task . WorkGroup , scheduleTask . Template ! ) } ;
task = task with { ResponseArea = await _shortcodesService . ApplyShortcodesAsync ( task . ResponseArea , scheduleTask . Template ! ) } ;
2026-05-26 11:26:52 +10:00
//nextRun в часовой зоне У З Робота ЕСПП
2026-07-22 16:02:04 +10:00
var nextRunWithRobotTz = scheduleTask . Template ! . NextRun . Add ( _nextRunService . GetEsppAccountOffset ( ) ) ;
2026-05-26 11:26:52 +10:00
//на всякий случай еще раз проверяем, что дата не устарела и отправляем задание
if ( nextRunWithRobotTz < DateTimeOffset . UtcNow )
{
2026-07-22 16:02:04 +10:00
_logger . LogError ( "Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}" ,
2026-05-26 11:26:52 +10:00
task . TemplateId , nextRunWithRobotTz , DateTimeOffset . UtcNow ) ;
throw new NextRunException ( $"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}" ) ;
}
task = task with { NextStart = EsppScheduleHelpers . GetNextRun ( nextRunWithRobotTz ) } ;
task = task with { GenerationTime = EsppScheduleHelpers . GetGenerationTime ( nextRunWithRobotTz ) } ;
2026-07-22 16:02:04 +10:00
task = task with { RepeatRange = _settingsFromDb . ScheduleRepeatRange } ;
2026-05-26 11:26:52 +10:00
task = task with { } ;
return task ;
}
/// <summary>
/// Получить задачу для робота.
/// Метод может генерировать исключения.
/// </summary>
/// <param name="robotCode"></param>
/// <param name="taskStatusCode"></param>
/// <param name="acquireTask">Взять в работу</param>
/// <param name="robotIp"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
2026-07-10 14:11:16 +10:00
private async Task < RobotConfiguration > GetTaskAsync ( RobotsEnum robotCode , TaskStatusEnum taskStatusCode , bool acquireTask , string? robotIp , string? robotId , TimeSpan scheduleCooldownDuration )
2026-05-25 16:59:42 +10:00
{
// 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
2026-07-22 16:02:04 +10:00
await _robotConfigurationRepository . MarkExpiredTasksAsFailedAsync ( _settingsFromDb . RobotAttemptsNumber , _settingsFromDb . RobotWaitTime ) ;
2026-05-25 16:59:42 +10:00
// 2. Ищем доступные задания
2026-07-10 14:11:16 +10:00
var availableTasks = await GetAvailableTasksAsync ( robotCode , taskStatusCode , scheduleCooldownDuration ) ;
2026-05-25 16:59:42 +10:00
if ( availableTasks . Count = = 0 )
throw new NotFoundException ( "Нет доступных заданий для робота" ) ;
Guid ? acquiredTaskId = null ;
if ( acquireTask )
{
2026-07-10 14:11:16 +10:00
// Берем задание в работу, устанавливаем ему статус "В работе"
2026-05-26 11:26:52 +10:00
acquiredTaskId = await AcquireTaskAsync ( availableTasks , robotIp , robotId ) ;
2026-05-25 16:59:42 +10:00
if ( acquiredTaskId = = null )
2026-05-26 11:26:52 +10:00
throw new Exception ( $"Н е удалось взять ни одну из доступных задач ({availableTasks.Count}) в работу" ) ;
2026-05-25 16:59:42 +10:00
}
else
{
// Берем первую задачу из списка доступных
acquiredTaskId = availableTasks . First ( ) ;
2026-07-22 16:02:04 +10:00
_logger . LogDebug ( "Задача не требует захвата, взята первая из доступных: {TaskId}" , acquiredTaskId ) ;
2026-05-25 16:59:42 +10:00
}
2026-05-26 11:26:52 +10:00
//3. Получаем задачу с о всеми нужными инклудами в зависимости от типа робота
2026-05-25 16:59:42 +10:00
var task = await GetTaskWithAllDataAsync ( acquiredTaskId . Value , robotCode ) ;
2026-05-26 11:26:52 +10:00
return task ;
2026-05-25 16:59:42 +10:00
}
/// <summary>
/// Получить список возможных заданий для взятия в работу.
/// Кол-во заданй ограничено переменной TakeTasks
/// </summary>
/// <param name="robotCode"></param>
/// <param name="taskStatusCode"></param>
/// <returns></returns>
2026-07-10 14:11:16 +10:00
private async Task < List < Guid > > GetAvailableTasksAsync ( RobotsEnum robotCode , TaskStatusEnum taskStatusCode , TimeSpan scheduleCooldownDuration )
2026-05-25 16:59:42 +10:00
{
2026-07-22 16:02:04 +10:00
var query = _robotConfigurationRepository . Get ( )
2026-05-25 16:59:42 +10:00
. AsNoTracking ( )
2026-07-22 16:02:04 +10:00
. Where ( t = > t . RobotCode = = ( int ) robotCode ) ;
2026-05-25 16:59:42 +10:00
// Если это задание для робота расписаний
if ( robotCode = = RobotsEnum . ScheduleOrder )
{
// Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания
2026-07-10 14:11:16 +10:00
query = query . Where ( t = > t . Template ! . RobotConfigurations . Any ( x = > x . RobotCode = = ( int ) RobotsEnum . TemplateOrder & & x . TaskStatusCode = = ( int ) TaskStatusEnum . Ok ) ) ;
// Н е берем шаблоны, у которых lastRun + 3 часа < сейчас, и у них последний инициатор был или nextRun (10) или esppSchedule (5), это условие применяется только к активированным расписаниям
var cooldownThreshold = DateTimeOffset . UtcNow . Add ( - scheduleCooldownDuration ) ;
query = query . Where ( t = >
// Условие кулдауна: проверяем, попадает ли шаблон под ЗАПРЕТ
! (
t . Template ! . IsActiveSchedule
& & ( t . Template . InitiatorParrComponentId = = ParrComponentsEnum . EsppScheduleSync | | t . Template . InitiatorParrComponentId = = ParrComponentsEnum . NextRun )
& & t . Template . LastRun > = cooldownThreshold
)
) ;
2026-05-25 16:59:42 +10:00
}
// Сортируем по nextRun, чтобы те, у кого nextRun ближе к текущей, выполнились скорее
query = query . OrderBy ( t = > t . Template ! . NextRun ) . ThenBy ( t = > t . Template ! . IsActiveSchedule ) . ThenBy ( t = > t . Template ! . IsActiveTemplate ) ;
2026-07-22 16:02:04 +10:00
// Кандидаты заданий, Id задания и имя шаблона
//var tasks = new List<Guid>();
var tasks = new List < RobotTaskDetails > ( ) ;
2026-05-25 16:59:42 +10:00
2026-07-22 16:02:04 +10:00
// Ищем первые TakeTasks заданий в статусе ОЖИДАНИЕ
2026-07-10 14:11:16 +10:00
tasks = await query
. Where ( t = >
t . RobotStatusCode = = ( int ) RobotStatusEnum . Wait
& & t . TaskStatusCode = = ( int ) taskStatusCode
) . Take ( TakeTasks )
2026-07-22 16:02:04 +10:00
//.Select(t => t.Id)
. Select ( t = > new RobotTaskDetails ( t . Id , t . Template ! . Name , t . Template . NextRun ) )
2026-07-10 14:11:16 +10:00
. ToListAsync ( ) ;
2026-05-25 16:59:42 +10:00
2026-07-22 16:02:04 +10:00
_logger . LogDebug ( "Найдено заданий в статусе 'Ожидание' {Count} шт. Робот '{Robot}'" , tasks . Count , robotCode . ToString ( ) ) ;
2026-05-25 16:59:42 +10:00
if ( tasks . Count = = 0 )
{
// Ищем задания в статусе В РАБОТЕ, которые можно перезапустить
// Поиск по `RobotStatusCode` = 22.
// Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`)
// и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись.
2026-07-22 16:02:04 +10:00
var endDate = DateTimeOffset . UtcNow . Add ( - _settingsFromDb . RobotWaitTime ) ;
2026-05-25 16:59:42 +10:00
tasks = await query . Where ( t = > t . RobotStatusCode = = ( int ) RobotStatusEnum . InProgress
2026-07-22 16:02:04 +10:00
& & t . TaskStatusCode = = ( int ) taskStatusCode
& & t . AttemptsNumber < _settingsFromDb . RobotAttemptsNumber
2026-05-25 16:59:42 +10:00
& & t . LastRobotStatusUpdated < endDate )
. Take ( TakeTasks )
2026-07-22 16:02:04 +10:00
//.Select(t => t.Id)
. Select ( t = > new RobotTaskDetails ( t . Id , t . Template ! . Name , t . Template . NextRun ) )
2026-05-25 16:59:42 +10:00
. ToListAsync ( ) ;
2026-07-22 16:02:04 +10:00
_logger . LogDebug ( "Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'" , tasks . Count , robotCode . ToString ( ) ) ;
2026-05-25 16:59:42 +10:00
}
2026-07-22 16:02:04 +10:00
if ( robotCode = = RobotsEnum . TemplateOrder )
{
// Если запрашиваем шаблоны, смотрим корректируем список заданий в зависимости от статуса переименования.
// Это не относится к расписаниям, потому что у переименованных расписаний статус Updating, а оно не возьмется в работу, пока не обновится шаблон
tasks = await ReplaceTemplateTasksForRenameAsync ( tasks , robotCode ) ;
}
return tasks . Select ( t = > t . TaskId ) . ToList ( ) ;
}
/// <summary>
/// Проверяет наличие шаблонов в процессе переименования и заменяет обычные задания на задания по переименованию.
/// Если связанный шаблон не переименован, и у него статус ошибки, целевому шаблону устанавливается статус ошибки.
/// </summary>
/// <param name="tasks"></param>
/// <returns></returns>
private async Task < List < RobotTaskDetails > > ReplaceTemplateTasksForRenameAsync ( List < RobotTaskDetails > tasks , RobotsEnum robotCode )
{
if ( tasks . Count = = 0 | | robotCode ! = RobotsEnum . TemplateOrder )
return tasks ;
// Ищем есть ли связанные шаблоны с таким имененм на переименование
2026-07-23 10:48:59 +10:00
var taskTemplateNames = tasks . Select ( t = > t . TemplateName ) . Distinct ( ) . ToList ( ) ;
// Ищем записи в таблице переименований, где OldName совпадает с именами наших новых задач
2026-07-22 16:02:04 +10:00
var templatesToRename = await _templateRenamePendingRepository . Get ( ) . AsNoTracking ( )
. Where ( t = > taskTemplateNames . Contains ( t . OldName ) )
. ToListAsync ( ) ;
2026-07-23 10:48:59 +10:00
_logger . LogDebug ( "Найдено шаблонов в процессе переименования для текущих задач: {Count} шт." , templatesToRename . Count ) ;
2026-07-22 16:02:04 +10:00
if ( templatesToRename . Count = = 0 )
return tasks ;
2026-07-28 14:41:01 +10:00
// Создаем словарь маппинга TemplateId -> OldName.
var templateIdToOldName = templatesToRename . ToDictionary ( t = > t . TemplateId , t = > t . OldName ) ;
2026-07-23 10:48:59 +10:00
// Ищем конфигурации роботов для СТАРЫХ шаблонов (которые переименовываются) по ИД, смотрим, можем ли взять их в работу
2026-07-22 16:02:04 +10:00
var renameTemplateIds = templatesToRename . Select ( t = > t . TemplateId ) . ToList ( ) ;
var renameTasks = await _robotConfigurationRepository . Get ( )
. AsNoTracking ( )
. Include ( t = > t . Template )
. Where ( t = >
t . RobotCode = = ( int ) robotCode
& & renameTemplateIds . Contains ( t . TemplateId )
// Это может быть только обновление. Так как переименования для создаваемого шаблона быть не может
& & t . TaskStatusCode = = ( int ) TaskStatusEnum . Updating
) . ToListAsync ( ) ;
2026-07-23 10:48:59 +10:00
// --- Блок обработки ошибок ---
// Если старый шаблон в ошибке и лимит попыток исчерпан, ставим ошибку и новому шаблону
var errorTasks = renameTasks
. Where ( t = >
t . RobotStatusCode = = ( int ) RobotStatusEnum . Error
& & t . AttemptsNumber > = _settingsFromDb . RobotAttemptsNumber
) . ToList ( ) ;
2026-07-22 16:02:04 +10:00
var tasksToSetErrorStatus = new List < Guid > ( ) ;
if ( errorTasks . Count > 0 )
{
2026-07-23 10:48:59 +10:00
_logger . LogDebug ( "Найдено старых заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям." , errorTasks . Count ) ;
2026-07-22 16:02:04 +10:00
2026-07-28 14:41:01 +10:00
//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 ( ) ;
2026-07-22 16:02:04 +10:00
// Берем целевые таски, находим в них задания которым надо поставить ошибку
2026-07-23 10:48:59 +10:00
tasksToSetErrorStatus = tasks
2026-07-28 14:41:01 +10:00
. Where ( t = > errorOldNames . Contains ( t . TemplateName ) )
2026-07-23 10:48:59 +10:00
. Select ( t = > t . TaskId )
. ToList ( ) ;
2026-07-22 16:02:04 +10:00
2026-07-23 10:48:59 +10:00
if ( tasksToSetErrorStatus . Count > 0 )
{
// Устанавливаем ошибку целевым + пишем комментарий от робота + нажимаем комит
var logMessage = "[RobotTaskService] Установлен статус ошибки, так как не переименован связанный шаблон" ;
await SetErrorStatusAsync ( tasksToSetErrorStatus , logMessage ) ;
}
2026-07-22 16:02:04 +10:00
}
2026-07-23 10:48:59 +10:00
// --- Блок подмены задач ---
2026-07-22 16:02:04 +10:00
2026-07-23 10:48:59 +10:00
var endDate = DateTimeOffset . UtcNow . Add ( - _settingsFromDb . RobotWaitTime ) ;
2026-07-22 16:02:04 +10:00
2026-07-23 10:48:59 +10:00
// Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InpRogress но которые еще не просрочены)
2026-07-28 14:41:01 +10:00
var allowedTasks = renameTasks . Where ( t = >
2026-07-23 10:48:59 +10:00
t . RobotStatusCode = = ( int ) RobotStatusEnum . Wait
| | ( t . RobotStatusCode = = ( int ) RobotStatusEnum . InProgress
& & t . AttemptsNumber < _settingsFromDb . RobotAttemptsNumber
& & t . LastRobotStatusUpdated < endDate )
) . ToList ( ) ;
2026-07-28 16:15:01 +10:00
// Проверим 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 ) ;
2026-07-23 10:48:59 +10:00
2026-07-28 16:15:01 +10:00
// 3. Оставляем для подмены только те задачи, у которых StatusTypeId == 0
var validAllowedTasks = allowedTasks
. Where ( t = > t . Template ! = null & & t . Template . StatusTypeId = = TemplateStatusTypeEnum . Used )
. ToList ( ) ;
2026-07-23 10:48:59 +10:00
2026-07-28 16:15:01 +10:00
// Формируем список заданий
var originalCount = tasks . Count ;
var errorTaskIdsSet = tasksToSetErrorStatus . ToHashSet ( ) ;
2026-07-28 14:41:01 +10:00
var errorCount = errorTaskIdsSet . Count ;
2026-07-28 16:15:01 +10:00
// Создаем словарь подмены ТОЛЬКО из валидных задач (где 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 )
) ;
2026-07-28 14:41:01 +10:00
2026-07-28 16:15:01 +10:00
// Проходим по ИСХОДНОМУ списку, чтобы сохранить порядок сортировки
2026-07-23 10:48:59 +10:00
var finalTasks = new List < RobotTaskDetails > ( tasks . Count ) ;
int replacedCount = 0 ;
2026-07-28 16:15:01 +10:00
int excludedByStatusCount = 0 ; // Счетчик для логов
2026-07-23 10:48:59 +10:00
foreach ( var task in tasks )
{
2026-07-28 16:15:01 +10:00
// 1. Если задаче нужно поставить ошибку, пропускаем е е
2026-07-23 10:48:59 +10:00
if ( errorTaskIdsSet . Contains ( task . TaskId ) )
{
continue ;
}
2026-07-22 16:02:04 +10:00
2026-07-28 16:15:01 +10:00
// 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. Если для этого имени шаблона есть разрешенная задача на переименование (и она валидна) - вставляем е е
2026-07-23 10:48:59 +10:00
if ( renameTasksToDictionary . TryGetValue ( task . TemplateName , out var renameTask ) )
{
finalTasks . Add ( renameTask ) ;
replacedCount + + ;
}
else
{
// Иначе оставляем исходную задачу на месте
finalTasks . Add ( task ) ;
}
}
2026-07-22 16:02:04 +10:00
2026-07-28 14:41:01 +10:00
// Логируем итоговую статистику трансформации пула задач
2026-07-22 16:02:04 +10:00
_logger . LogInformation (
2026-07-28 14:41:01 +10:00
"Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " +
2026-07-28 16:15:01 +10:00
"Отклонено (ошибка): {ErrorCount} шт. Исключено (невалидный StatusTypeId): {ExcludedCount} шт. " +
"Заменено на старые: {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт." ,
originalCount , errorCount , excludedByStatusCount , replacedCount , finalTasks . Count ) ;
2026-07-22 16:02:04 +10:00
2026-07-28 16:15:01 +10:00
// Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка
2026-07-23 10:48:59 +10:00
return finalTasks ;
2026-07-22 16:02:04 +10:00
}
/// <summary>
/// Установить статус задания - ошибка
/// </summary>
/// <param name="taskIds"></param>
/// <returns></returns>
private async Task SetErrorStatusAsync ( List < Guid > taskIds , string logMessage )
{
2026-07-23 10:48:59 +10:00
if ( taskIds = = null | | taskIds . Count = = 0 )
return ;
2026-07-22 16:02:04 +10:00
var tasks = await _robotConfigurationRepository . Get ( )
. Include ( t = > t . Template )
2026-07-23 10:48:59 +10:00
. Where ( t = > taskIds . Contains ( t . Id ) )
. ToListAsync ( ) ;
2026-07-22 16:02:04 +10:00
if ( tasks . Count = = 0 )
return ;
foreach ( var task in tasks )
{
// Так как это целевой шаблон, то ставим ему сразу максимальное кол-во попыток и ошибку, чтоб больше он не выдавался в заданиях, пока не исправим связанный
// Устанавливаем статус ошибки
_robotConfigurationRepository . SetErrorRobotStatusAndMaxAttempts ( task ) ;
// Пишем в лог роботу
var history = new RobotHistory
{
Id = Guid . NewGuid ( ) ,
HistoryLevel = ( int ) RobotStatusEnum . Error ,
TaskStatusCode = task . TaskStatusCode ,
RobotConfigurationId = task . Id ,
RobotIp = null ,
RobotId = ParrComponentsEnum . Api . ToString ( ) ,
RobotMessage = logMessage
} ;
await _robotHistoryRepository . CreateAsync ( history ) ;
2026-07-23 10:48:59 +10:00
_logger . LogInformation ( "Для целевого задания {TaskId} (шаблон '{TemplateName}') установлен статус ошибки, " +
2026-07-28 16:15:01 +10:00
"так как связанное задание с о старым шаблоном не было успешно выполнено." ,
2026-07-23 10:48:59 +10:00
task . Id , task . Template ! . Name ) ;
2026-07-22 16:02:04 +10:00
}
if ( await _robotHistoryRepository . CommitAsync ( ) )
{
2026-07-23 10:48:59 +10:00
_logger . LogDebug ( "Установлен статус 'Ошибка', для заданий {TaskCount} шт." , tasks . Count ) ;
2026-07-22 16:02:04 +10:00
}
else
{
2026-07-23 10:48:59 +10:00
_logger . LogError ( "Ошибка при установке статуса задания 'Ошибка', для заданий {TaskCount} шт. Транзакция отменена" , tasks . Count ) ;
throw new DbErrorException ( "Н е удалось сохранить изменения статусов заданий при обработке переименования шаблона." ) ;
2026-07-22 16:02:04 +10:00
}
2026-05-25 16:59:42 +10:00
}
/// <summary>
/// Взять задачу в работу
/// </summary>
/// <param name="taskId"></param>
/// <returns></returns>
2026-05-26 11:26:52 +10:00
private async Task < Guid ? > AcquireTaskAsync ( List < Guid > tasks , string? robotIp , string? robotId )
2026-05-25 16:59:42 +10:00
{
foreach ( var taskId in tasks )
{
2026-07-22 16:02:04 +10:00
var isChangedStatus = await _robotConfigurationRepository . SetInProgressStatusAsync ( taskId ) ;
2026-05-25 16:59:42 +10:00
if ( isChangedStatus )
{
2026-07-22 16:02:04 +10:00
_logger . LogDebug ( "Захвачена задача {TaskId}" , taskId ) ;
2026-05-25 16:59:42 +10:00
2026-07-22 16:02:04 +10:00
var task = await _robotConfigurationRepository . Get ( )
2026-05-25 16:59:42 +10:00
. AsNoTracking ( )
. FirstAsync ( t = > t . Id = = taskId ) ;
// пишем в историю робота
var history = new RobotHistory
{
Id = Guid . NewGuid ( ) ,
HistoryLevel = ( int ) RobotHistoryLevelEnum . Start ,
TaskStatusCode = task . TaskStatusCode ,
RobotConfigurationId = taskId ,
2026-05-26 11:43:15 +10:00
RobotIp = robotIp ,
RobotId = robotId
2026-05-25 16:59:42 +10:00
} ;
2026-07-22 16:02:04 +10:00
if ( ! await _robotHistoryRepository . CreateAsync ( history ) | | ! await _robotHistoryRepository . CommitAsync ( ) )
2026-05-25 16:59:42 +10:00
throw new DbErrorException ( "Ошибка при добавлении истории робота, при взятии задания в работу." ) ;
return taskId ;
}
else
{
2026-07-22 16:02:04 +10:00
_logger . LogDebug ( "Н е удалось захватить задачу {TaskId}" , taskId ) ;
2026-05-25 16:59:42 +10:00
}
}
2026-07-22 16:02:04 +10:00
_logger . LogDebug ( "Н е удалось захватить ни одну из доступных задач для робота" ) ;
2026-05-25 16:59:42 +10:00
return null ;
}
/// <summary>
/// Получить задачу с о всем необходимыми полями
/// </summary>
/// <param name="taskId"></param>
/// <param name="robotCode"></param>
/// <returns></returns>
private async Task < RobotConfiguration > GetTaskWithAllDataAsync ( Guid taskId , RobotsEnum robotCode )
{
2026-07-22 16:02:04 +10:00
IQueryable < RobotConfiguration > query = _robotConfigurationRepository . Get ( )
2026-05-26 11:26:52 +10:00
//.AsNoTracking() // нужно обязательно трекать, так как может измениться nextRun и е г о нужно будет сохранить
2026-05-25 16:59:42 +10:00
. AsSingleQuery ( )
2026-05-26 11:26:52 +10:00
// Общие инклуды для шаблонов и расписаний
// Units
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . Unit )
. ThenInclude ( t = > t ! . UnitValues )
. ThenInclude ( t = > t ! . Field )
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . Unit )
. ThenInclude ( t = > t ! . UnitValues )
. ThenInclude ( t = > t ! . Value )
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . UnitsInTemplate )
// Группа с типом
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . Job )
. ThenInclude ( t = > t ! . Group )
. ThenInclude ( t = > t ! . GroupType )
// Т Н К
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . Job )
. ThenInclude ( t = > t ! . Tnk )
. ThenInclude ( t = > t ! . Subprocess )
. ThenInclude ( t = > t ! . Process ) ;
2026-05-25 16:59:42 +10:00
2026-05-26 11:26:52 +10:00
if ( robotCode = = RobotsEnum . ScheduleOrder )
2026-05-25 16:59:42 +10:00
{
2026-05-26 11:26:52 +10:00
// Инклуды только для расписаний
query = query
// Расписание
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . Job )
. ThenInclude ( t = > t ! . Group )
. ThenInclude ( t = > t ! . EsppSchValues )
. ThenInclude ( t = > t ! . EsppSchTypeConfig )
. ThenInclude ( t = > t ! . EsppSchTypeSchedule )
// Исключения по типу
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . Job )
. ThenInclude ( t = > t ! . Group )
. ThenInclude ( t = > t ! . ScheduleExcludeType )
// Исключения по календарю
. Include ( t = > t . Template )
. ThenInclude ( t = > t ! . Job )
. ThenInclude ( t = > t ! . Group )
. ThenInclude ( t = > t ! . ScheduleExcludeTypeCalendar ) ;
2026-05-25 16:59:42 +10:00
}
return await query . FirstAsync ( t = > t . Id = = taskId ) ;
}
2026-05-26 11:26:52 +10:00
/// <summary>
/// Приводит переносы строк в тексте к формату CRLF (\r\n)
/// </summary>
/// <param name="text">Исходный текст</param>
/// <returns>Текст с унифицированными переносами строк</returns>
private string NormalizeLineEndingsToCrlf ( string? text )
{
if ( string . IsNullOrEmpty ( text ) )
return string . Empty ;
// Заменяем любые варианты переносов (\r\n, \r, \n) на единый \r\n
return System . Text . RegularExpressions . Regex . Replace ( text , @"\r\n|\r|\n" , "\r\n" ) ;
}
/// <summary>
/// Обоновить NextRun если он устарел
/// </summary>
/// <param name="task"></param>
/// <returns></returns>
private async Task < bool > UpdateNextRunAsync ( RobotConfiguration task , IHistoryInitiator historyInitiator )
{
var template = task . Template ! ;
//var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job!.GroupId, template!.Job!.Group!.ReferenceDate);
2026-07-22 16:02:04 +10:00
var nextRun = await _nextRunService . GetNextRunForTemplateAsync ( template . Id , false ) ;
2026-05-26 11:26:52 +10:00
if ( ! nextRun . HasValue )
{
2026-07-23 10:48:59 +10:00
_logger . LogError ( "При обновлении nextRun для шаблона {TemplateId}, расчитанный nextRun=null, ошибка в расчетах." , template . Id ) ;
2026-05-26 11:26:52 +10:00
return false ;
}
if ( nextRun . Value < DateTimeOffset . UtcNow )
{
2026-07-23 10:48:59 +10:00
_logger . LogError ( "При обновлении nextRun для шаблона {TemplateId}, расчитанный nextRun<Now [{NextRun}<{Now}], ошибка в расчетах." , template . Id , nextRun . Value , DateTimeOffset . UtcNow ) ;
2026-05-26 11:26:52 +10:00
return false ;
}
if ( nextRun ! = template . NextRun )
{
2026-07-23 10:48:59 +10:00
_logger . LogDebug ( "Для шаблона {TemplateId} обновляю nextRun. Новое: {NewNextRun}, старое: {OldNextRun}" , template . Id , nextRun , template . NextRun ) ;
2026-05-26 11:26:52 +10:00
template . LastRun = template . NextRun ;
template . NextRun = nextRun . Value ;
var suffix = "При получении задания роботом, обновил NextRun" ;
historyInitiator . InitiatorComment =
string . IsNullOrEmpty ( historyInitiator . InitiatorComment )
? suffix
: $"{historyInitiator.InitiatorComment}. {suffix}" ;
2026-07-22 16:02:04 +10:00
if ( ! await _robotConfigurationRepository . CommitAsync ( historyInitiator ) )
2026-05-26 11:26:52 +10:00
throw new DbErrorException ( "Ошибка при сохранении изменения NextRun" ) ;
}
return true ;
}
2026-05-25 16:59:42 +10:00
}
}