From ab563f88b9903572de582d898bdf23bbecb3c619 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Mon, 25 May 2026 16:59:42 +1000 Subject: [PATCH] =?UTF-8?q?feat(api,core,dal):=20RobotTasks=20-=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B8=20=D0=B2=20Core,=20=D0=BE=D0=BF=D1=82?= =?UTF-8?q?=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B2=D0=B7?= =?UTF-8?q?=D1=8F=D1=82=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=80=D0=BE=D0=B1=D0=BE=D1=82=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V1/Requests/Queries/RobotTaskQuery.cs | 4 +- .../Controllers/V1/RobotTaskController.cs | 288 +-------------- .../Controllers/V1/RobotTaskOldController.cs | 330 ++++++++++++++++++ PARR.Core/DependencyInjection.cs | 9 +- .../IRobotConfigurationRepository.cs | 9 +- .../Implementations/RobotTaskService.cs | 204 +++++++++++ .../RobotTask/Interfaces/IRobotTaskService.cs | 9 + .../RobotConfigurationRepository.cs | 18 +- 8 files changed, 595 insertions(+), 276 deletions(-) create mode 100644 PARR.API/Controllers/V1/RobotTaskOldController.cs create mode 100644 PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs create mode 100644 PARR.Core/Services/RobotTask/Interfaces/IRobotTaskService.cs diff --git a/PARR.API/Contracts/V1/Requests/Queries/RobotTaskQuery.cs b/PARR.API/Contracts/V1/Requests/Queries/RobotTaskQuery.cs index c503ff61..84a040a9 100644 --- a/PARR.API/Contracts/V1/Requests/Queries/RobotTaskQuery.cs +++ b/PARR.API/Contracts/V1/Requests/Queries/RobotTaskQuery.cs @@ -1,10 +1,10 @@ namespace PARR.API.Contracts.V1.Requests.Queries { - public class RobotTaskQuery + public record RobotTaskQuery { /// /// Установить статус задания - InProgress (Робот взял в работу) /// - public bool? SetInProgressStatus { get; set; } + public bool? SetInProgressStatus { get; init; } } } diff --git a/PARR.API/Controllers/V1/RobotTaskController.cs b/PARR.API/Controllers/V1/RobotTaskController.cs index 4293d917..1b6182cb 100644 --- a/PARR.API/Controllers/V1/RobotTaskController.cs +++ b/PARR.API/Controllers/V1/RobotTaskController.cs @@ -1,336 +1,86 @@ using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests.Queries; -using PARR.API.Contracts.V1.Responses; -using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Controllers.V1.Base; using PARR.API.Services.Interfaces; -using PARR.BLL.Helpers; using PARR.Core.Repositories.Interfaces; using PARR.Core.Services.NextRunServices; +using PARR.Core.Services.RobotTask.Interfaces; using PARR.Core.Services.Shortcodes; using PARR.Domain.Common.Roles; -using PARR.Domain.Entities; -using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; using PARR.Domain.Settings; namespace PARR.API.Controllers.V1 { + /// + /// Формирование заданий роботам + /// [Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)] public class RobotTaskController : BaseApiController { private readonly IMapper mapper; private readonly SettingsFromDb settingsFromDb; private readonly IRobotConfigurationRepository robotConfigurationService; - //private readonly IEsppScheduleTransformService esppScheduleTransformService; - private readonly ILogger logger; + private readonly ILogger logger; private readonly IClientService clientService; private readonly IRobotHistoryRepository robotHistoryService; private readonly IShortcodesService shortcodesService; private readonly INextRunService nextRunService; - //private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; + private readonly IRobotTaskService robotTaskService; public RobotTaskController( IMapper mapper, SettingsFromDb settingsFromDb, IRobotConfigurationRepository robotConfigurationService, - //IEsppScheduleTransformService esppScheduleTransformService, - ILogger logger, + ILogger logger, IClientService clientService, IRobotHistoryRepository robotHistoryService, IShortcodesService shortcodesService, - INextRunService nextRunService - //IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService + INextRunService nextRunService, + + + IRobotTaskService robotTaskService ) { this.mapper = mapper; this.settingsFromDb = settingsFromDb; this.robotConfigurationService = robotConfigurationService; - //this.esppScheduleTransformService = esppScheduleTransformService; this.logger = logger; this.clientService = clientService; this.robotHistoryService = robotHistoryService; this.shortcodesService = shortcodesService; this.nextRunService = nextRunService; - //this.scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService; + this.robotTaskService = robotTaskService; } /// - /// Получить задание для робота по коду робота и по статусу задания + /// Получить задание для робота по коду робота и статусу задания /// - /// - /// + /// Код робота + /// Статус задания + /// Параметры /// [HttpGet(ApiRoutes.RobotTask.GetByRobotAndStatusTask)] public async Task GetByRobotAndStatusTask([FromRoute] RobotsEnum robotCode, [FromRoute] TaskStatusEnum taskStatusCode, [FromQuery] RobotTaskQuery requestQuery) { - //Ищем все задания с превышенным кол-вом попыток и с просроченным временем и ставим им статус ошибки - await robotConfigurationService.FindUnfulfilledTaskAndSetRobotErrorStatusAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime); + + //robotTaskService - var query = robotConfigurationService.Get().AsSingleQuery() - .Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode); - - switch (robotCode) - { - case RobotsEnum.TemplateOrder: - // шаблоны - query = query - .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(a => a!.Job) - .ThenInclude(t => t!.Group) - .ThenInclude(g => g.GroupType) - .Include(t => t.Template) - .ThenInclude(w => w!.Job) - .ThenInclude(t => t!.Tnk) - .ThenInclude(s => s!.Subprocess) - .ThenInclude(p => p!.Process); - - query = query - .Include(t => t.Template) - .ThenInclude(t => t!.UnitsInTemplate); - - break; - - case RobotsEnum.ScheduleOrder: - //расписание - query = query - .AsSingleQuery() - .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(a => a!.Job) - .ThenInclude(t => t!.Group) - .ThenInclude(g => g.GroupType) - .Include(t => t.Template) - .ThenInclude(a => a!.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) - .Include(t => t.Template) - .ThenInclude(t => t!.Job) - .ThenInclude(t => t!.Tnk) - .Include(t => t.Template) - .ThenInclude(t => t!.UnitsInTemplate); - - //выбираем только записи с созданными шаблонами (у которых статус 20 или 30), а только потом у них ищем расписания - var createdTemplates = robotConfigurationService.Get() - .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && (t.TaskStatusCode == (int)TaskStatusEnum.Ok)) - .Select(t => t.TemplateId); - query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && createdTemplates.Contains(t.TemplateId)); - // query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && t.TemplateId==Guid.Parse("7cb7c3be-506d-40e0-a63f-4554edb52459")); - break; - - default: - break; - } - // сортируем по NextRun, чтобы те у которых дата след срабатывания ближе к текущей, выполнились скорее - query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template.IsActiveTemplate); - - RobotConfiguration? task = null; - - //ищем задание в ожидании, если нашли, выбираем его - task = await query.FirstOrDefaultAsync(t => t.RobotStatusCode == (int)RobotStatusEnum.Wait); - - if (task == null) - { - //ищем задания в работе, которые можно перезапустить - //Поиск по `RobotStatusCode` = 22. - //Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`) - //и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись. - - var endDate = DateTimeOffset.UtcNow.Add(-settingsFromDb.RobotWaitTime); - task = await query - .FirstOrDefaultAsync(t => - t.RobotStatusCode == (int)RobotStatusEnum.InProgress - && t.AttemptsNumber < settingsFromDb.RobotAttemptsNumber - && t.LastRobotStatusUpdated < endDate - ); - } - - if (task == null) - return NotFound(); - - if (requestQuery?.SetInProgressStatus == true) - { - var resultSetStatus = await SetInProgressStatusAsync(task.Id); - if (resultSetStatus == false) - { - logger.LogError($"Ошибка при установке статуса {RobotStatusEnum.InProgress.ToString()} для задания RobotConfigutationId {task.Id} (при выдаче задания роботу)"); - return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при выдаче задания." } })); - } - } - - switch (robotCode) - { - case RobotsEnum.TemplateOrder: - { //RobotTaskTemplateResponse - var robotTaskTemplateResponse = mapper.Map(task); - - robotTaskTemplateResponse.FullDescription = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.FullDescription, task.Template!)); - robotTaskTemplateResponse.ShortDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.ShortDescription, task.Template!); - robotTaskTemplateResponse.Solution = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.Solution, task.Template!)); - robotTaskTemplateResponse.TnkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.TnkName, task.Template!); - robotTaskTemplateResponse.WorkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkName, task.Template!); - robotTaskTemplateResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkGroup, task.Template!); - robotTaskTemplateResponse.ResponseArea = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.ResponseArea, task.Template!); - - return Ok(new Response(robotTaskTemplateResponse, true)); - } - case RobotsEnum.ScheduleOrder: - { // если был запрос на расписание, проверяем у него nextRun, lastRun, обновляем их - var resultUpdateNextRun = await UpdateNextRunAsync(task); - if (!resultUpdateNextRun) - { - logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}", task.TemplateId); - return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при расчете NextRun" } })); - } - - //RobotTaskScheduleResponse - var robotTaskScheduleResponse = mapper.Map(task); - - robotTaskScheduleResponse.Timezone = settingsFromDb.EsppScheduleTimezone; - - robotTaskScheduleResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.WorkGroup, task.Template!); - robotTaskScheduleResponse.ResponseArea = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.ResponseArea, task.Template!); - - //var nextRunWithRobotTz = nextRunService.GetNextRunWithTimezoneEsppAndResponseArea(task.Template!.NextRun, task.Template!.Job?.Group?.IsResponseAreaTimezone, robotTaskScheduleResponse.ResponseArea); - //nextRun в часовой зоне УЗ Робота ЕСПП - var nextRunWithRobotTz = task.Template!.NextRun.Add(nextRunService.GetEsppAccountOffset()); - - //на всякий случай еще раз проверяем, что дата не устарела и отправляем задание - if (nextRunWithRobotTz < DateTimeOffset.UtcNow) - { - logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}", - task.TemplateId, nextRunWithRobotTz, DateTimeOffset.UtcNow); - return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при расчете NextRun" } })); - } - - robotTaskScheduleResponse.NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithRobotTz); - robotTaskScheduleResponse.GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithRobotTz); - - return Ok(new Response(robotTaskScheduleResponse, true)); - } - default: - break; - } - - return BadRequest(); + return Ok(); } - /// - /// Обоновить NextRun если он устарел - /// - /// - /// - private async Task UpdateNextRunAsync(RobotConfiguration task) - { - 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 - /// Устанавливаем статус "Робот взял в работу", пишем в историю работы роботов инф о начале работ - /// - /// - /// - private async Task SetInProgressStatusAsync(Guid taskId) - { - var config = await robotConfigurationService.GetAsync(taskId); - - //изменение статуса робота - robotConfigurationService.ChangeRobotStatus(RobotStatusEnum.InProgress, config!); - - if (!await robotConfigurationService.CommitAsync()) - return false; - - //записываем в лог робота - var history = new RobotHistory - { - Id = Guid.NewGuid(), - HistoryLevel = (int)RobotHistoryLevelEnum.Start, - TaskStatusCode = config.TaskStatusCode, - RobotConfigurationId = config.Id, - RobotIp = clientService.GetClientIp()?.ToString() - }; - - if (!await robotHistoryService.CreateAsync(history) || !await robotHistoryService.CommitAsync()) - return false; - - return true; - } - /// - /// Приводит переносы строк в тексте к формату CRLF (\r\n) - /// - /// Исходный текст - /// Текст с унифицированными переносами строк - 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"); - } } } diff --git a/PARR.API/Controllers/V1/RobotTaskOldController.cs b/PARR.API/Controllers/V1/RobotTaskOldController.cs new file mode 100644 index 00000000..e522c2d8 --- /dev/null +++ b/PARR.API/Controllers/V1/RobotTaskOldController.cs @@ -0,0 +1,330 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using PARR.API.Contracts.V1; +using PARR.API.Contracts.V1.Requests.Queries; +using PARR.API.Contracts.V1.Responses; +using PARR.API.Contracts.V1.Responses.Base; +using PARR.API.Controllers.V1.Base; +using PARR.API.Services.Interfaces; +using PARR.BLL.Helpers; +using PARR.Core.Repositories.Interfaces; +using PARR.Core.Services.NextRunServices; +using PARR.Core.Services.Shortcodes; +using PARR.Domain.Common.Roles; +using PARR.Domain.Entities; +using PARR.Domain.Entities.Base.History; +using PARR.Domain.Enums; +using PARR.Domain.Settings; + +namespace PARR.API.Controllers.V1 +{ + [Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)] + public class RobotTaskOldController : BaseApiController + { + private readonly IMapper mapper; + private readonly SettingsFromDb settingsFromDb; + private readonly IRobotConfigurationRepository robotConfigurationService; + private readonly ILogger logger; + private readonly IClientService clientService; + private readonly IRobotHistoryRepository robotHistoryService; + private readonly IShortcodesService shortcodesService; + private readonly INextRunService nextRunService; + + public RobotTaskOldController( + IMapper mapper, + SettingsFromDb settingsFromDb, + IRobotConfigurationRepository robotConfigurationService, + ILogger logger, + IClientService clientService, + IRobotHistoryRepository robotHistoryService, + IShortcodesService shortcodesService, + INextRunService nextRunService + ) + { + this.mapper = mapper; + this.settingsFromDb = settingsFromDb; + this.robotConfigurationService = robotConfigurationService; + this.logger = logger; + this.clientService = clientService; + this.robotHistoryService = robotHistoryService; + this.shortcodesService = shortcodesService; + this.nextRunService = nextRunService; + } + + + /// + /// Получить задание для робота по коду робота и по статусу задания + /// + /// + /// + /// + [HttpGet(ApiRoutes.RobotTask.GetByRobotAndStatusTask)] + public async Task GetByRobotAndStatusTask([FromRoute] RobotsEnum robotCode, [FromRoute] TaskStatusEnum taskStatusCode, [FromQuery] RobotTaskQuery requestQuery) + { + //Ищем все задания с превышенным кол-вом попыток и с просроченным временем и ставим им статус ошибки + await robotConfigurationService.MarkExpiredTasksAsFailedAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime); + + + var query = robotConfigurationService.Get() + .AsSingleQuery() + .Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode); + + switch (robotCode) + { + case RobotsEnum.TemplateOrder: + // шаблоны + query = query + .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(a => a!.Job) + .ThenInclude(t => t!.Group) + .ThenInclude(g => g.GroupType) + .Include(t => t.Template) + .ThenInclude(w => w!.Job) + .ThenInclude(t => t!.Tnk) + .ThenInclude(s => s!.Subprocess) + .ThenInclude(p => p!.Process); + + query = query + .Include(t => t.Template) + .ThenInclude(t => t!.UnitsInTemplate); + + break; + + case RobotsEnum.ScheduleOrder: + //расписание + query = query + .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(a => a!.Job) + .ThenInclude(t => t!.Group) + .ThenInclude(g => g.GroupType) + .Include(t => t.Template) + .ThenInclude(a => a!.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) + .Include(t => t.Template) + .ThenInclude(t => t!.Job) + .ThenInclude(t => t!.Tnk) + .Include(t => t.Template) + .ThenInclude(t => t!.UnitsInTemplate); + + //выбираем только записи с созданными шаблонами (у которых статус 20 или 30), а только потом у них ищем расписания + var createdTemplates = robotConfigurationService.Get() + .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && (t.TaskStatusCode == (int)TaskStatusEnum.Ok)) + .Select(t => t.TemplateId); + query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && createdTemplates.Contains(t.TemplateId)); + // query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && t.TemplateId==Guid.Parse("7cb7c3be-506d-40e0-a63f-4554edb52459")); + break; + + default: + break; + } + + + // сортируем по NextRun, чтобы те у которых дата след срабатывания ближе к текущей, выполнились скорее + query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template.IsActiveTemplate); + + RobotConfiguration? task = null; + + //ищем задание в ожидании, если нашли, выбираем его + task = await query.FirstOrDefaultAsync(t => t.RobotStatusCode == (int)RobotStatusEnum.Wait); + + if (task == null) + { + //ищем задания в работе, которые можно перезапустить + //Поиск по `RobotStatusCode` = 22. + //Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`) + //и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись. + + var endDate = DateTimeOffset.UtcNow.Add(-settingsFromDb.RobotWaitTime); + task = await query + .FirstOrDefaultAsync(t => + t.RobotStatusCode == (int)RobotStatusEnum.InProgress + && t.AttemptsNumber < settingsFromDb.RobotAttemptsNumber + && t.LastRobotStatusUpdated < endDate + ); + } + + if (task == null) + return NotFound(); + + if (requestQuery?.SetInProgressStatus == true) + { + var resultSetStatus = await SetInProgressStatusAsync(task.Id); + if (resultSetStatus == false) + { + logger.LogError($"Ошибка при установке статуса {RobotStatusEnum.InProgress.ToString()} для задания RobotConfigutationId {task.Id} (при выдаче задания роботу)"); + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при выдаче задания." } })); + } + } + + switch (robotCode) + { + case RobotsEnum.TemplateOrder: + { //RobotTaskTemplateResponse + var robotTaskTemplateResponse = mapper.Map(task); + + robotTaskTemplateResponse.FullDescription = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.FullDescription, task.Template!)); + robotTaskTemplateResponse.ShortDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.ShortDescription, task.Template!); + robotTaskTemplateResponse.Solution = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.Solution, task.Template!)); + robotTaskTemplateResponse.TnkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.TnkName, task.Template!); + robotTaskTemplateResponse.WorkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkName, task.Template!); + robotTaskTemplateResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkGroup, task.Template!); + robotTaskTemplateResponse.ResponseArea = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.ResponseArea, task.Template!); + + return Ok(new Response(robotTaskTemplateResponse, true)); + } + case RobotsEnum.ScheduleOrder: + { // если был запрос на расписание, проверяем у него nextRun, lastRun, обновляем их + var resultUpdateNextRun = await UpdateNextRunAsync(task); + if (!resultUpdateNextRun) + { + logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}", task.TemplateId); + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при расчете NextRun" } })); + } + + //RobotTaskScheduleResponse + var robotTaskScheduleResponse = mapper.Map(task); + + robotTaskScheduleResponse.Timezone = settingsFromDb.EsppScheduleTimezone; + + robotTaskScheduleResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.WorkGroup, task.Template!); + robotTaskScheduleResponse.ResponseArea = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.ResponseArea, task.Template!); + + //var nextRunWithRobotTz = nextRunService.GetNextRunWithTimezoneEsppAndResponseArea(task.Template!.NextRun, task.Template!.Job?.Group?.IsResponseAreaTimezone, robotTaskScheduleResponse.ResponseArea); + //nextRun в часовой зоне УЗ Робота ЕСПП + var nextRunWithRobotTz = task.Template!.NextRun.Add(nextRunService.GetEsppAccountOffset()); + + //на всякий случай еще раз проверяем, что дата не устарела и отправляем задание + if (nextRunWithRobotTz < DateTimeOffset.UtcNow) + { + logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}", + task.TemplateId, nextRunWithRobotTz, DateTimeOffset.UtcNow); + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при расчете NextRun" } })); + } + + robotTaskScheduleResponse.NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithRobotTz); + robotTaskScheduleResponse.GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithRobotTz); + + return Ok(new Response(robotTaskScheduleResponse, true)); + } + default: + break; + } + + return BadRequest(); + } + + + /// + /// Обоновить NextRun если он устарел + /// + /// + /// + private async Task UpdateNextRunAsync(RobotConfiguration task) + { + 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 + /// Устанавливаем статус "Робот взял в работу", пишем в историю работы роботов инф о начале работ + /// + /// + /// + private async Task SetInProgressStatusAsync(Guid taskId) + { + var config = await robotConfigurationService.GetAsync(taskId); + + //изменение статуса робота + robotConfigurationService.ChangeRobotStatus(RobotStatusEnum.InProgress, config!); + + if (!await robotConfigurationService.CommitAsync()) + return false; + + //записываем в лог робота + var history = new RobotHistory + { + Id = Guid.NewGuid(), + HistoryLevel = (int)RobotHistoryLevelEnum.Start, + TaskStatusCode = config.TaskStatusCode, + RobotConfigurationId = config.Id, + RobotIp = clientService.GetClientIp()?.ToString() + }; + + if (!await robotHistoryService.CreateAsync(history) || !await robotHistoryService.CommitAsync()) + return false; + + return true; + } + + + /// + /// Приводит переносы строк в тексте к формату CRLF (\r\n) + /// + /// Исходный текст + /// Текст с унифицированными переносами строк + 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"); + } + } +} diff --git a/PARR.Core/DependencyInjection.cs b/PARR.Core/DependencyInjection.cs index 2d5a2249..ef703236 100644 --- a/PARR.Core/DependencyInjection.cs +++ b/PARR.Core/DependencyInjection.cs @@ -7,6 +7,8 @@ using PARR.Core.Common.Interfaces; using PARR.Core.Services.MatchingStatusService; using PARR.Core.Services.NextRunServices; using PARR.Core.Services.NextRunServices.Subservices; +using PARR.Core.Services.RobotTask.Implementations; +using PARR.Core.Services.RobotTask.Interfaces; using PARR.Core.Services.Shortcodes; using PARR.Core.Services.Shortcodes.Handlers; using PARR.Core.Services.TaskServices.Handlers; @@ -78,8 +80,6 @@ namespace PARR.Core #endregion - #region Services - #region Shortсodes services.AddScoped(); @@ -95,10 +95,13 @@ namespace PARR.Core services.AddScoped(); #endregion + #region Services + services.AddTransient(); - //services.AddScoped(); + services.AddScoped(); + //services.AddScoped(); #endregion diff --git a/PARR.Core/Repositories/Interfaces/IRobotConfigurationRepository.cs b/PARR.Core/Repositories/Interfaces/IRobotConfigurationRepository.cs index 06b019c8..5bc2d65a 100644 --- a/PARR.Core/Repositories/Interfaces/IRobotConfigurationRepository.cs +++ b/PARR.Core/Repositories/Interfaces/IRobotConfigurationRepository.cs @@ -26,7 +26,7 @@ namespace PARR.Core.Repositories.Interfaces /// /// /// - Task FindUnfulfilledTaskAndSetRobotErrorStatusAsync(int robotAttemptsNumber, TimeSpan robotWaitTime); + Task MarkExpiredTasksAsFailedAsync(int robotAttemptsNumber, TimeSpan robotWaitTime); /// /// Получить конфигурацию из шаблона по RobotCode. У шаблона обязательно должен быть Include таблицы RobotConfiguration @@ -42,5 +42,12 @@ namespace PARR.Core.Repositories.Interfaces /// /// true - если изменил статус, false - нельзя изменить статус bool SetUpdateTaskStatusIfAllow(RobotConfiguration configuration); + + /// + /// Атомарно установить статус задачи "В работе" если текущий статус "В ожидании". + /// + /// + /// + Task SetInProgressStatusAsync(Guid id); } } diff --git a/PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs b/PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs new file mode 100644 index 00000000..5650b697 --- /dev/null +++ b/PARR.Core/Services/RobotTask/Implementations/RobotTaskService.cs @@ -0,0 +1,204 @@ +using InfluxDB.Client.Api.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.Core.Repositories.Interfaces; +using PARR.Core.Services.RobotTask.Interfaces; +using PARR.Domain.Entities; +using PARR.Domain.Enums; +using PARR.Domain.Exceptions; +using PARR.Domain.Settings; + +namespace PARR.Core.Services.RobotTask.Implementations +{ + internal class RobotTaskService : IRobotTaskService + { + /// + /// Количество заданий которые рассматриваем для взятия в работу. + /// + private readonly int TakeTasks = 10; + + private readonly ILogger logger; + private readonly IRobotConfigurationRepository robotConfigurationRepository; + private readonly SettingsFromDb settingsFromDb; + private readonly IRobotHistoryRepository robotHistoryRepository; + + public RobotTaskService( + ILogger logger, + IRobotConfigurationRepository robotConfigurationRepository, + SettingsFromDb settingsFromDb, + IRobotHistoryRepository robotHistoryRepository + ) + { + this.logger = logger; + this.robotConfigurationRepository = robotConfigurationRepository; + this.settingsFromDb = settingsFromDb; + this.robotHistoryRepository = robotHistoryRepository; + } + + public async Task GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp) + { + // 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки + await robotConfigurationRepository.MarkExpiredTasksAsFailedAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime); + + + // 2. Ищем доступные задания + var availableTasks = await GetAvailableTasksAsync(robotCode, taskStatusCode); + + if (availableTasks.Count == 0) + throw new NotFoundException("Нет доступных заданий для робота"); + + Guid? acquiredTaskId = null; + + if (acquireTask) + { + // Берем задание в работу, устанавливаем ей статус "В работе" + acquiredTaskId = await AcquireTaskAsync(availableTasks, robotIp); + + if (acquiredTaskId == null) + throw new NotFoundException($"Не удалось взять ни одну из доступных задач ({availableTasks.Count}) в работу"); + } + else + { + // Берем первую задачу из списка доступных + acquiredTaskId = availableTasks.First(); + logger.LogDebug("Задача не требует захвата, взята первая из доступных: {TaskId}", acquiredTaskId); + } + + + //3. Готовим модель ответа + var task = await GetTaskWithAllDataAsync(acquiredTaskId.Value, robotCode); + + //todo: + + + + } + + + /// + /// Получить список возможных заданий для взятия в работу. + /// Кол-во заданй ограничено переменной TakeTasks + /// + /// + /// + /// + private async Task> GetAvailableTasksAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode) + { + var query = robotConfigurationRepository.Get() + .AsNoTracking() + .Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode); + + // Если это задание для робота расписаний + if (robotCode == RobotsEnum.ScheduleOrder) + { + // Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания + 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)); + } + + // Сортируем по nextRun, чтобы те, у кого nextRun ближе к текущей, выполнились скорее + query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template!.IsActiveTemplate); + + // Кандидаты заданий + var tasks = new List(); + + // Ещем первые 10 заданий в статусе ОЖИДАНИЕ + tasks = await query.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.Wait).Take(TakeTasks).Select(t => t.Id).ToListAsync(); + + 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 + && t.AttemptsNumber < settingsFromDb.RobotAttemptsNumber + && t.LastRobotStatusUpdated < endDate) + .Take(TakeTasks) + .Select(t => t.Id) + .ToListAsync(); + + logger.LogDebug("Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString()); + } + + return tasks; + } + + + /// + /// Взять задачу в работу + /// + /// + /// + private async Task AcquireTaskAsync(List tasks, string? robotIp) + { + 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, + RobotIp = robotIp + }; + + if (!await robotHistoryRepository.CreateAsync(history) || !await robotHistoryRepository.CommitAsync()) + throw new DbErrorException("Ошибка при добавлении истории робота, при взятии задания в работу."); + + return taskId; + } + else + { + logger.LogDebug("Не удалось захватить задачу {TaskId}", taskId); + } + } + + logger.LogDebug("Не удалось захватить ни одну из доступных задач для робота"); + + return null; + } + + + /// + /// Получить задачу со всем необходимыми полями + /// + /// + /// + /// + private async Task GetTaskWithAllDataAsync(Guid taskId, RobotsEnum robotCode) + { + var query = robotConfigurationRepository.Get() + .AsNoTracking() + .AsSingleQuery() + //todo: тут общие инклуды + ; + + if(robotCode== RobotsEnum.ScheduleOrder) + { + // тут инклуды только для расписаний + } + + return await query.FirstAsync(t => t.Id == taskId); + } + + } +} diff --git a/PARR.Core/Services/RobotTask/Interfaces/IRobotTaskService.cs b/PARR.Core/Services/RobotTask/Interfaces/IRobotTaskService.cs new file mode 100644 index 00000000..c4ce047f --- /dev/null +++ b/PARR.Core/Services/RobotTask/Interfaces/IRobotTaskService.cs @@ -0,0 +1,9 @@ +namespace PARR.Core.Services.RobotTask.Interfaces +{ + /// + /// Управление заданиями роботам + /// + public interface IRobotTaskService + { + } +} diff --git a/PARR.DAL/Repositories/RobotConfigurationRepository.cs b/PARR.DAL/Repositories/RobotConfigurationRepository.cs index d8845aaa..dc37a2e7 100644 --- a/PARR.DAL/Repositories/RobotConfigurationRepository.cs +++ b/PARR.DAL/Repositories/RobotConfigurationRepository.cs @@ -102,6 +102,22 @@ namespace PARR.DAL.Repositories } } + + public async Task SetInProgressStatusAsync(Guid id) + { + // Если задание все еще в статусе Wait (RobotStatusEnum.Wait), установить ему статус "InProgress" + + // Выполняем атомарный UPDATE напрямую в базе данных + var affectedRows = await EntitySet.Where(t => t.Id == id && t.RobotStatusCode == (int)RobotStatusEnum.Wait) + .ExecuteUpdateAsync(s => s + .SetProperty(t => t.RobotStatusCode, (int)RobotStatusEnum.InProgress) + .SetProperty(t => t.LastRobotStatusUpdated, DateTimeOffset.UtcNow) + ); + + return affectedRows > 0; + } + + public RobotConfiguration GetFromTemplateByRobotCode(RobotsEnum robotsEnum, Template template) { var config = template.RobotConfigurations.FirstOrDefault(t => t.RobotCode == (int)robotsEnum); @@ -117,7 +133,7 @@ namespace PARR.DAL.Repositories // Поиск невыполненных заданий и установка им статуса ошибки робота - public async Task FindUnfulfilledTaskAndSetRobotErrorStatusAsync(int robotAttemptsNumber, TimeSpan robotWaitTime) + public async Task MarkExpiredTasksAsFailedAsync(int robotAttemptsNumber, TimeSpan robotWaitTime) { //Ищем `RobotStatusCode` = 22 и `LastStatusUpdated` истекло и `AttemptsNumber` >= допустимого значения из настроек, //ставим всем этим записям `RobotStatusCode`= 33