feat(api, core): Доделал атомарный захват задачи роботом.

This commit is contained in:
Mikhail Trubnikov
2026-05-26 11:26:52 +10:00
parent ab563f88b9
commit 856b273e6b
13 changed files with 892 additions and 443 deletions

View File

@@ -1,9 +1,14 @@
using InfluxDB.Client.Api.Domain;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
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.DTOs.RobotTask;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
using PARR.Domain.Settings;
@@ -21,21 +26,102 @@ namespace PARR.Core.Services.RobotTask.Implementations
private readonly IRobotConfigurationRepository robotConfigurationRepository;
private readonly SettingsFromDb settingsFromDb;
private readonly IRobotHistoryRepository robotHistoryRepository;
private readonly IMapper mapper;
private readonly IShortcodesService shortcodesService;
private readonly INextRunService nextRunService;
public RobotTaskService(
ILogger<RobotTaskService> logger,
IRobotConfigurationRepository robotConfigurationRepository,
SettingsFromDb settingsFromDb,
IRobotHistoryRepository robotHistoryRepository
IRobotHistoryRepository robotHistoryRepository,
IMapper mapper,
IShortcodesService shortcodesService,
INextRunService nextRunService
)
{
this.logger = logger;
this.robotConfigurationRepository = robotConfigurationRepository;
this.settingsFromDb = settingsFromDb;
this.robotHistoryRepository = robotHistoryRepository;
this.mapper = mapper;
this.shortcodesService = shortcodesService;
this.nextRunService = nextRunService;
}
public async Task GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp)
public async Task<RobotTaskTemplate> GetTemplateTaskAsync(TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId)
{
var templateTask = await GetTaskAsync(RobotsEnum.TemplateOrder, taskStatusCode, acquireTask, robotIp, robotId);
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;
}
public async Task<RobotTaskSchedule> GetScheduleTaskAsync(TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId, IHistoryInitiator historyInitiator)
{
var scheduleTask = await GetTaskAsync(RobotsEnum.ScheduleOrder, taskStatusCode, acquireTask, robotIp, robotId);
// Проверяем 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>
private async Task<RobotConfiguration> GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId)
{
// 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
await robotConfigurationRepository.MarkExpiredTasksAsFailedAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime);
@@ -52,10 +138,10 @@ namespace PARR.Core.Services.RobotTask.Implementations
if (acquireTask)
{
// Берем задание в работу, устанавливаем ей статус "В работе"
acquiredTaskId = await AcquireTaskAsync(availableTasks, robotIp);
acquiredTaskId = await AcquireTaskAsync(availableTasks, robotIp, robotId);
if (acquiredTaskId == null)
throw new NotFoundException($"Не удалось взять ни одну из доступных задач ({availableTasks.Count}) в работу");
throw new Exception($"Не удалось взять ни одну из доступных задач ({availableTasks.Count}) в работу");
}
else
{
@@ -65,13 +151,10 @@ namespace PARR.Core.Services.RobotTask.Implementations
}
//3. Готовим модель ответа
//3. Получаем задачу со всеми нужными инклудами в зависимости от типа робота
var task = await GetTaskWithAllDataAsync(acquiredTaskId.Value, robotCode);
//todo:
return task;
}
@@ -138,7 +221,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
/// </summary>
/// <param name="taskId"></param>
/// <returns></returns>
private async Task<Guid?> AcquireTaskAsync(List<Guid> tasks, string? robotIp)
private async Task<Guid?> AcquireTaskAsync(List<Guid> tasks, string? robotIp, string? robotId)
{
foreach (var taskId in tasks)
{
@@ -159,6 +242,8 @@ namespace PARR.Core.Services.RobotTask.Implementations
TaskStatusCode = task.TaskStatusCode,
RobotConfigurationId = taskId,
RobotIp = robotIp
//todo: Тут добавить RobotId
//RobotId = robotId
};
if (!await robotHistoryRepository.CreateAsync(history) || !await robotHistoryRepository.CommitAsync())
@@ -186,19 +271,118 @@ namespace PARR.Core.Services.RobotTask.Implementations
/// <returns></returns>
private async Task<RobotConfiguration> GetTaskWithAllDataAsync(Guid taskId, RobotsEnum robotCode)
{
var query = robotConfigurationRepository.Get()
.AsNoTracking()
IQueryable<RobotConfiguration> query = robotConfigurationRepository.Get()
//.AsNoTracking() // нужно обязательно трекать, так как может измениться nextRun и его нужно будет сохранить
.AsSingleQuery()
//todo: тут общие инклуды
;
// Общие инклуды для шаблонов и расписаний
// 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);
if(robotCode== RobotsEnum.ScheduleOrder)
if (robotCode == RobotsEnum.ScheduleOrder)
{
// тут инклуды только для расписаний
// Инклуды только для расписаний
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);
}
return await query.FirstAsync(t => t.Id == taskId);
}
/// <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;
}
}
}