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-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-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-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>
/// Количество заданий которые рассматриваем для взятия в работу.
/// </summary>
private readonly int TakeTasks = 10 ;
private readonly ILogger < RobotTaskService > logger ;
private readonly IRobotConfigurationRepository robotConfigurationRepository ;
private readonly SettingsFromDb settingsFromDb ;
private readonly IRobotHistoryRepository robotHistoryRepository ;
2026-05-26 11:26:52 +10:00
private readonly IMapper mapper ;
private readonly IShortcodesService shortcodesService ;
private readonly INextRunService nextRunService ;
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 ,
INextRunService nextRunService
2026-05-25 16:59:42 +10:00
)
{
this . logger = logger ;
this . robotConfigurationRepository = robotConfigurationRepository ;
this . settingsFromDb = settingsFromDb ;
this . robotHistoryRepository = robotHistoryRepository ;
2026-05-26 11:26:52 +10:00
this . mapper = mapper ;
this . shortcodesService = shortcodesService ;
this . nextRunService = nextRunService ;
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
var task = mapper . Map < RobotTaskTemplate > ( templateTask ) ;
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 ! ) } ;
task = task with { ClosingCode = settingsFromDb . ClosingCode } ;
task = task with { Initiator = settingsFromDb . Initiator } ;
task = task with { Category = settingsFromDb . Category } ;
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 )
{
logger . LogError ( "Ошибка при расчете NextRun для templateId: {templateId}" , scheduleTask . TemplateId ) ;
throw new NextRunException ( $"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}" ) ;
}
var task = mapper . Map < RobotTaskSchedule > ( scheduleTask ) ;
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 ! ) } ;
//nextRun в часовой зоне У З Робота ЕСПП
var nextRunWithRobotTz = scheduleTask . Template ! . NextRun . Add ( nextRunService . GetEsppAccountOffset ( ) ) ;
//на всякий случай еще раз проверяем, что дата не устарела и отправляем задание
if ( nextRunWithRobotTz < DateTimeOffset . UtcNow )
{
logger . LogError ( "Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}" ,
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 ) } ;
task = task with { RepeatRange = settingsFromDb . ScheduleRepeatRange } ;
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. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
await robotConfigurationRepository . MarkExpiredTasksAsFailedAsync ( settingsFromDb . RobotAttemptsNumber , settingsFromDb . RobotWaitTime ) ;
// 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 ( ) ;
logger . LogDebug ( "Задача не требует захвата, взята первая из доступных: {TaskId}" , acquiredTaskId ) ;
}
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
{
var query = robotConfigurationRepository . Get ( )
. AsNoTracking ( )
2026-07-10 14:11:16 +10:00
. Where ( t = > t . RobotCode = = ( int ) robotCode /* && t.TaskStatusCode == (int)taskStatusCode*/ ) ;
2026-05-25 16:59:42 +10:00
// Если это задание для робота расписаний
if ( robotCode = = RobotsEnum . ScheduleOrder )
{
// Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания
2026-07-10 14:11:16 +10:00
#region С т а р ы й н е о п т и м и з и р о в а н н ы й з а п р о с
//var createdTemplates = robotConfigurationRepository.Get()
// .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && t.TaskStatusCode == (int)TaskStatusEnum.Ok)
// .Select(t => t.TemplateId);
//query = query.Where(t => createdTemplates.Contains(t.TemplateId));
#endregion
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 ) ;
// Кандидаты заданий
var tasks = new List < Guid > ( ) ;
// Ещем первые 10 заданий в статусе ОЖИДАНИЕ
2026-07-10 14:11:16 +10:00
tasks = await query
. Where ( t = >
t . RobotStatusCode = = ( int ) RobotStatusEnum . Wait
& & t . TaskStatusCode = = ( int ) taskStatusCode
) . Take ( TakeTasks )
. Select ( t = > t . Id )
. ToListAsync ( ) ;
2026-05-25 16:59:42 +10:00
logger . LogDebug ( "Найдено заданий в статусе 'Ожидание' {Count} шт. Робот '{Robot}'" , tasks . Count , robotCode . ToString ( ) ) ;
if ( tasks . Count = = 0 )
{
// Ищем задания в статусе В РАБОТЕ, которые можно перезапустить
// Поиск по `RobotStatusCode` = 22.
// Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`)
// и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись.
var endDate = DateTimeOffset . UtcNow . Add ( - settingsFromDb . RobotWaitTime ) ;
tasks = await query . Where ( t = > t . RobotStatusCode = = ( int ) RobotStatusEnum . InProgress
2026-07-10 18:35:18 +10:00
& & t . TaskStatusCode = = ( int ) taskStatusCode
2026-05-25 16:59:42 +10:00
& & t . AttemptsNumber < settingsFromDb . RobotAttemptsNumber
& & t . LastRobotStatusUpdated < endDate )
. Take ( TakeTasks )
. Select ( t = > t . Id )
. ToListAsync ( ) ;
logger . LogDebug ( "Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'" , tasks . Count , robotCode . ToString ( ) ) ;
}
return tasks ;
}
/// <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 )
{
var isChangedStatus = await robotConfigurationRepository . SetInProgressStatusAsync ( taskId ) ;
if ( isChangedStatus )
{
logger . LogDebug ( "Захвачена задача {TaskId}" , taskId ) ;
var task = await robotConfigurationRepository . Get ( )
. 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
} ;
if ( ! await robotHistoryRepository . CreateAsync ( history ) | | ! await robotHistoryRepository . CommitAsync ( ) )
throw new DbErrorException ( "Ошибка при добавлении истории робота, при взятии задания в работу." ) ;
return taskId ;
}
else
{
logger . LogDebug ( "Н е удалось захватить задачу {TaskId}" , taskId ) ;
}
}
logger . LogDebug ( "Н е удалось захватить ни одну из доступных задач для робота" ) ;
return null ;
}
/// <summary>
/// Получить задачу с о всем необходимыми полями
/// </summary>
/// <param name="taskId"></param>
/// <param name="robotCode"></param>
/// <returns></returns>
private async Task < RobotConfiguration > GetTaskWithAllDataAsync ( Guid taskId , RobotsEnum robotCode )
{
2026-05-26 11:26:52 +10:00
IQueryable < RobotConfiguration > query = robotConfigurationRepository . Get ( )
//.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);
var nextRun = await nextRunService . GetNextRunForTemplateAsync ( template . Id , false ) ;
if ( ! nextRun . HasValue )
{
logger . LogError ( "При обновлении nextRun для шаблона {templateId}, расчитанный nextRun=null, ошибка в расчетах." , template . Id ) ;
return false ;
}
if ( nextRun . Value < DateTimeOffset . UtcNow )
{
logger . LogError ( "При обновлении nextRun для шаблона {templateId}, расчитанный nextRun<Now [{nextRun}<{now}], ошибка в расчетах." , template . Id , nextRun . Value , DateTimeOffset . UtcNow ) ;
return false ;
}
if ( nextRun ! = template . NextRun )
{
logger . LogDebug ( $"Для шаблона id {template.Id} обновляю nextRun, новое значение {nextRun}, старое значение {template.NextRun}" ) ;
template . LastRun = template . NextRun ;
template . NextRun = nextRun . Value ;
var suffix = "При получении задания роботом, обновил NextRun" ;
historyInitiator . InitiatorComment =
string . IsNullOrEmpty ( historyInitiator . InitiatorComment )
? suffix
: $"{historyInitiator.InitiatorComment}. {suffix}" ;
if ( ! await robotConfigurationRepository . CommitAsync ( historyInitiator ) )
throw new DbErrorException ( "Ошибка при сохранении изменения NextRun" ) ;
}
return true ;
}
2026-05-25 16:59:42 +10:00
}
}