From 87db7c5fd66fcf3ce2cdbde63fba8f172d9e8e9e Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Wed, 13 May 2026 16:37:49 +1000 Subject: [PATCH 1/4] =?UTF-8?q?fix(core):=20WorkloadService=20-=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D1=87=D0=B5=D1=82=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BD=D0=B5=D0=B9=20=D0=BE=D1=82=D1=87=D0=B5?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.Core/Services/Workload/Implementations/WorkloadService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs index d057e392..21d43311 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs @@ -115,7 +115,7 @@ namespace PARR.Core.Services.Workload.Implementations /// private async Task> GetReportDays(DateOnly dateStart, int durationDays) { - var dateEnd = dateStart.AddDays(durationDays); + var dateEnd = dateStart.AddDays(durationDays - 1); var days = await nextRunService.GetWorkDaysAsync(dateStart, dateEnd, false); var weekends = await nextRunService.GetWeekendsAsync(dateStart, dateEnd); From 911e6febcd91be3dd96243a166784fb9711c0999 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Fri, 15 May 2026 16:09:49 +1000 Subject: [PATCH 2/4] =?UTF-8?q?fix(api):=20Task=20-=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=81=D0=BE=D0=B2=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87,=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D0=B5=D1=82=D0=BE=D0=B2=20workload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Contracts/V1/ApiRoutes.cs | 9 + .../V1/Responses/ActiveTaskResponse.cs | 9 + .../V1/Responses/TaskItemResponse.cs | 21 ++ .../V1/Responses/TaskItemStatusResponse.cs | 13 ++ .../V1/Responses/TaskItemTypeResponse.cs | 33 +++ .../V1/GeneratorTemplateController.cs | 95 -------- .../V1/JobAutoControlController.cs | 212 ------------------ .../V1/ShortcodeApplyController.cs | 5 +- .../V1/Statistics/StatWorkloadController.cs | 1 - PARR.API/Controllers/V1/SyncTaskController.cs | 5 +- PARR.API/Controllers/V1/TaskController.cs | 51 +++++ .../Controllers/V1/TaskStatusController.cs | 5 +- .../DomainToResponseProfile.cs | 20 ++ .../Mapping/TaskMappingProfile.cs | 36 +++ PARR.Core/PARR.Core.csproj | 4 - .../Implementations/TaskManagementService.cs | 35 ++- .../Interfaces/ITaskManagementService.cs | 11 +- PARR.DAL/Context/DataContext.cs | 2 +- PARR.Domain/DTOs/TaskDTO/ActiveTask.cs | 12 + PARR.Domain/DTOs/TaskDTO/TaskItem.cs | 26 +++ PARR.Domain/DTOs/TaskDTO/TaskStatus.cs | 16 ++ PARR.Domain/DTOs/TaskDTO/TaskType.cs | 36 +++ 22 files changed, 334 insertions(+), 323 deletions(-) create mode 100644 PARR.API/Contracts/V1/Responses/ActiveTaskResponse.cs create mode 100644 PARR.API/Contracts/V1/Responses/TaskItemResponse.cs create mode 100644 PARR.API/Contracts/V1/Responses/TaskItemStatusResponse.cs create mode 100644 PARR.API/Contracts/V1/Responses/TaskItemTypeResponse.cs delete mode 100644 PARR.API/Controllers/V1/GeneratorTemplateController.cs delete mode 100644 PARR.API/Controllers/V1/JobAutoControlController.cs create mode 100644 PARR.API/Controllers/V1/TaskController.cs create mode 100644 PARR.Core/Infrastructure/Mapping/TaskMappingProfile.cs create mode 100644 PARR.Domain/DTOs/TaskDTO/ActiveTask.cs create mode 100644 PARR.Domain/DTOs/TaskDTO/TaskItem.cs create mode 100644 PARR.Domain/DTOs/TaskDTO/TaskStatus.cs create mode 100644 PARR.Domain/DTOs/TaskDTO/TaskType.cs diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index 3108d749..198ab02a 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -564,6 +564,15 @@ #endregion + #region Tasks + + public static class Tasks + { + public const string GetActiveTasks = Base + "/tasks/active"; + } + + #endregion + } } diff --git a/PARR.API/Contracts/V1/Responses/ActiveTaskResponse.cs b/PARR.API/Contracts/V1/Responses/ActiveTaskResponse.cs new file mode 100644 index 00000000..cc7b8310 --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/ActiveTaskResponse.cs @@ -0,0 +1,9 @@ +namespace PARR.API.Contracts.V1.Responses +{ + public record ActiveTaskResponse : TaskItemBaseResponse + { + public required TaskItemTypeResponse Type { get; init; } + + public required TaskItemStatusResponse Status { get; init; } + } +} diff --git a/PARR.API/Contracts/V1/Responses/TaskItemResponse.cs b/PARR.API/Contracts/V1/Responses/TaskItemResponse.cs new file mode 100644 index 00000000..caf99b0d --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/TaskItemResponse.cs @@ -0,0 +1,21 @@ +namespace PARR.API.Contracts.V1.Responses +{ + public record TaskItemBaseResponse + { + public Guid Id { get; init; } + + public DateTimeOffset DateCreated { get; init; } + + public DateTimeOffset? DateModified { get; init; } + + public int RetryCount { get; init; } + + public DateTimeOffset? ProcessedAt { get; init; } + } + + public record TaskItemResponse : TaskItemBaseResponse + { + + } + +} diff --git a/PARR.API/Contracts/V1/Responses/TaskItemStatusResponse.cs b/PARR.API/Contracts/V1/Responses/TaskItemStatusResponse.cs new file mode 100644 index 00000000..69d949cb --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/TaskItemStatusResponse.cs @@ -0,0 +1,13 @@ +using PARR.Domain.Enums; + +namespace PARR.API.Contracts.V1.Responses +{ + public record TaskItemStatusResponse + { + public TaskItemStatusEnum Code { get; init; } + + public required string Name { get; init; } + + public required string Description { get; init; } + } +} diff --git a/PARR.API/Contracts/V1/Responses/TaskItemTypeResponse.cs b/PARR.API/Contracts/V1/Responses/TaskItemTypeResponse.cs new file mode 100644 index 00000000..6539114f --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/TaskItemTypeResponse.cs @@ -0,0 +1,33 @@ +using PARR.Domain.Enums; + +namespace PARR.API.Contracts.V1.Responses +{ + public record TaskItemTypeResponse + { + public TaskTypeEnum Code { get; init; } + + public required string Name { get; init; } + + public required string Description { get; init; } + + /// + /// Максимальное кол-во попыток + /// + public int MaxRetries { get; init; } + + /// + /// Максимальное время выполнения + /// + public int MaxExecutionTimeMinutes { get; init; } + + /// + /// Одновременно может быть только одна задача или несколько + /// + public bool IsSingleton { get; init; } + + /// + /// Сколько дней хранить в БД + /// + public int RetentionDays { get; init; } + } +} diff --git a/PARR.API/Controllers/V1/GeneratorTemplateController.cs b/PARR.API/Controllers/V1/GeneratorTemplateController.cs deleted file mode 100644 index 9a832002..00000000 --- a/PARR.API/Controllers/V1/GeneratorTemplateController.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace PARR.API.Controllers.V1 -{ - //[Authorize(Roles = ParrRoles.Administrator.Role)] - //public class GeneratorTemplateController : BaseApiController - //{ - // private readonly IValidator validator; - // private readonly IRabbitService mqService; - // private readonly IUriService uriService; - // private readonly MqSettings mqSettings; - // private readonly IClientService clientService; - - // public GeneratorTemplateController( - // IValidator validator, - // IRabbitService mqService, - // IUriService uriService, - // MqSettings mqSettings, - // IClientService clientService - // ) - // { - // this.validator = validator; - // this.mqService = mqService; - // this.uriService = uriService; - // this.mqSettings = mqSettings; - // this.clientService = clientService; - // } - - // /// !!!!!!!!!! Это старый контроллер, удалить !!!!!!!!!! - - - // /// - // /// Запрос на генерацию шаблонов - // /// - // /// - // [HttpPost(ApiRoutes.GeneratorTemplate.Create)] - // public async Task Create([FromBody] GeneratorTemplateRequest request) - // { - // var resultValidate = await validator.ValidateAsync(request); - // if (!resultValidate.IsValid) - // return BadRequest(new Response(resultValidate.Errors)); - - // var result = true; - - // request.Ek.ForEach(ek => - // { - // //todo: проверить в ForEach(async status - отрабатывает ли нормально async, возможно надо переделать в обычный foreach - // request.Status.ForEach(async status => - // { - // var obj = new GeneratorTemplateMq - // { - // Action = request.Action, - // ApplicationId = request.ApplicationId, - // Ek = ek, - // WorkId = request.WorkId, - // StatusEk = status, - // IsActiveTemplate = request.IsActiveTemplate, - // IsActiveSchedule = request.IsActiveSchedule, - // HistoryInitiator = new HistoryInitiator { InitiatorIp = clientService.GetClientIp()?.ToString(), InitiatorParrComponentId = ParrComponentsEnum.Api } - // }; - - // //var msg = JsonSerializer.Serialize(obj); - - // //var _result = await mqService.SendAsync(mqSettings.GenerateTemplates, new[] { msg }); - // var _result = await mqService.SendAsync(mqSettings.GenerateTemplates, new List { obj }); - - // if (_result.IsSuccess == false) - // result = false; - - // }); - // }); - - // //var obj = new GeneratorTemplateMq - // //{ - // // Action = request.Action, - // // ApplicationId = request.ApplicationId, - // // Ek = request.Ek, - // // WorkId = request.WorkId, - // // StatusEk = request.Status, - // // IsActiveTemplate = request.IsActiveTemplate, - // // IsActiveSchedule = request.IsActiveSchedule - // //}; - - // //var msg = JsonSerializer.Serialize(obj); - - // //var result = mqService.Send(mqSettings.GenerateTemplates, new[] { msg }); - - // //if (!result.IsSuccess) - // if (!result) - // return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при отправке данных." } })); - - // var createdUri = uriService.GetAllUri(ApiRoutes.Template.GetAll); - - // return Created(createdUri, new Response(null, true, new List(), "Отправлен запрос на генерацию шаблонов.")); - // } - //} -} diff --git a/PARR.API/Controllers/V1/JobAutoControlController.cs b/PARR.API/Controllers/V1/JobAutoControlController.cs deleted file mode 100644 index 813919ea..00000000 --- a/PARR.API/Controllers/V1/JobAutoControlController.cs +++ /dev/null @@ -1,212 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using PARR.API.Controllers.V1.Base; -using PARR.Domain.Common.Roles; - -namespace PARR.API.Controllers.V1 -{ - /// - /// Управление автоматическим созданием/деактивацией/активацией РР - /// - [Authorize(Roles = ParrRoles.Administrator.Role)] - public class JobAutoControlController : BaseApiController - { - //private readonly IJobAutoControlService jobAutoControlService; - //private readonly IApplicationsInWorkService applicationsInWorkService; - //private readonly IMapper mapper; - //private readonly IValidator validator; - //private readonly IUriService uriService; - - //public JobAutoControlController( - // IJobAutoControlService jobAutoControlService, - // IApplicationsInWorkService applicationsInWorkService, - // IMapper mapper, - // IValidator validator, - // IUriService uriService - // ) - //{ - // this.jobAutoControlService = jobAutoControlService; - // this.applicationsInWorkService = applicationsInWorkService; - // this.mapper = mapper; - // this.validator = validator; - // this.uriService = uriService; - //} - - - ///// - ///// Получить настройки автоматического управления РР - ///// - ///// - ///// - //[HttpGet(ApiRoutes.JobAutoControl.Get)] - //public async Task Get([FromRoute] Guid applicationInWorkId) - //{ - // var obj = await jobAutoControlService.Get() - // .Include(t => t.ApplicationsInWork).ThenInclude(t => t.WorkGroups).ThenInclude(t => t.WorkGroup) - // .Include(t => t.JobEkMasks) - // .Include(t => t.JobAutoControlInEkStatuses).ThenInclude(t => t.EkStatus) - // .FirstOrDefaultAsync(t => t.ApplicationInWorkId == applicationInWorkId); - - // if (obj == null) - // { - // // проверим, есть ли вообще такой AppInWork - // var appInWork = await applicationsInWorkService.GetAsync(applicationInWorkId); - // if (appInWork == null) - // return NotFound(); - - // return NoContent(); - // } - - // var response = mapper.Map(obj); - - // return Ok(new Response(response, true)); - //} - - - ///// - ///// Создать настройки автоматического управления РР - ///// - ///// - ///// - ///// - //[HttpPost(ApiRoutes.JobAutoControl.Create)] - //public async Task Create([FromRoute] Guid applicationInWorkId, [FromBody] JobAutoControlRequest request) - //{ - // var exist = await jobAutoControlService.Get().FirstOrDefaultAsync(t => t.ApplicationInWorkId == applicationInWorkId); - // if (exist != null) - // return BadRequest(new Response(false, new List { - // new ErrorModel { FieldName = nameof(applicationInWorkId), Message = $"Запись с {nameof(applicationInWorkId)}: {applicationInWorkId} уже существует." } - // })); - - // var resultValidate = await validator.ValidateAsync(request); - // if (!resultValidate.IsValid) - // return BadRequest(new Response(resultValidate.Errors)); - - // var newObj = new DAL.Models.JobAutoControl - // { - // Id = Guid.NewGuid(), - // ApplicationInWorkId = applicationInWorkId, - // CreateNew = request.CreateNew, - // Deactivate = request.Deactivate, - // Activate = request.Activate, - // OnTemplateIsActive = request.OnTemplateIsActive, - // OnScheduleIsActive = request.OnScheduleIsActive, - // OffTemplateIsActive = request.OffTemplateIsActive, - // OffScheduleIsActive = request.OffScheduleIsActive - // }; - - // // убираем пробелы и дубли - // request.EkMasks.Where(t => t.Trim() != string.Empty).Distinct().ToList().ForEach(mask => newObj.JobEkMasks.Add(new JobEkMask - // { - // Id = Guid.NewGuid(), - // DateCreated = DateTimeOffset.UtcNow, - // Name = mask.Trim(), - // JobAutoControlId = newObj.Id - // })); - - // request.EnabledEkStatuses.ForEach(status => newObj.JobAutoControlInEkStatuses.Add(new JobAutoControlInEkStatus - // { - // EkStatusCode = (int)status, - // JobAutoControlId = newObj.Id - // })); - - // if (!await jobAutoControlService.CreateAsync(newObj) || !await jobAutoControlService.CommitAsync()) - // return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при создани записи." } })); - - // var locationUri = uriService.GetUri(ApiRoutes.JobAutoControl.Get, ApiRoutes.JobAutoControl.appInWorkId, applicationInWorkId); - - // var createdObj = await jobAutoControlService.Get() - // .Include(t => t.ApplicationsInWork).ThenInclude(t => t.WorkGroups).ThenInclude(t => t.WorkGroup) - // .Include(t => t.JobEkMasks) - // .Include(t => t.JobAutoControlInEkStatuses).ThenInclude(t => t.EkStatus) - // .FirstOrDefaultAsync(t => t.ApplicationInWorkId == applicationInWorkId); - - // return Created(locationUri, new Response(mapper.Map(createdObj), true)); - //} - - - ///// - ///// Обновить настройки автоматического управления РР - ///// - ///// - ///// - ///// - //[HttpPut(ApiRoutes.JobAutoControl.Update)] - //public async Task Update([FromRoute] Guid applicationInWorkId, [FromBody] JobAutoControlRequest request) - //{ - // var resultValidate = await validator.ValidateAsync(request); - // if (!resultValidate.IsValid) - // return BadRequest(new Response(resultValidate.Errors)); - - // var orig = await jobAutoControlService.Get() - // .Include(t => t.JobEkMasks) - // .Include(t => t.JobAutoControlInEkStatuses) - // .FirstOrDefaultAsync(t => t.ApplicationInWorkId == applicationInWorkId); - - // if (orig == null) - // return BadRequest(new Response(false, new List { - // new ErrorModel { FieldName = nameof(applicationInWorkId), Message = $"Запись с {nameof(applicationInWorkId)}: {applicationInWorkId} не существует." } - // })); - - - // orig.DateModified = DateTimeOffset.UtcNow; - // orig.CreateNew = request.CreateNew; - // orig.Deactivate = request.Deactivate; - // orig.Activate = request.Activate; - // orig.OnTemplateIsActive = request.OnTemplateIsActive; - // orig.OnScheduleIsActive = request.OnScheduleIsActive; - // orig.OffTemplateIsActive = request.OffTemplateIsActive; - // orig.OffScheduleIsActive = request.OffScheduleIsActive; - - // orig.JobEkMasks.Clear(); - // // убираем пробелы и дубли - // request.EkMasks.Where(t => t.Trim() != string.Empty).Distinct().ToList().ForEach(mask => orig.JobEkMasks.Add(new JobEkMask - // { - // //Id = Guid.NewGuid(), // не пойму почему не нужно это поле. с ним ошибка. - // DateCreated = DateTimeOffset.UtcNow, - // Name = mask.Trim(), - // JobAutoControlId = orig.Id - // })); - - // orig.JobAutoControlInEkStatuses.Clear(); - // request.EnabledEkStatuses.ForEach(status => orig.JobAutoControlInEkStatuses.Add(new JobAutoControlInEkStatus - // { - // EkStatusCode = (int)status, - // JobAutoControlId = orig.Id - // })); - - // if (!await jobAutoControlService.CommitAsync()) - // return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при изменении записи." } })); - - - // var updatedObj = await jobAutoControlService.Get() - // .Include(t => t.ApplicationsInWork).ThenInclude(t => t.WorkGroups).ThenInclude(t => t.WorkGroup) - // .Include(t => t.JobEkMasks) - // .Include(t => t.JobAutoControlInEkStatuses).ThenInclude(t => t.EkStatus) - // .FirstOrDefaultAsync(t => t.ApplicationInWorkId == applicationInWorkId); - - // return Ok(new Response(mapper.Map(updatedObj), true)); - //} - - - ///// - ///// Удалить настройки автоматического управления РР - ///// - ///// - ///// - //[HttpDelete(ApiRoutes.JobAutoControl.Delete)] - //public async Task Delete([FromRoute] Guid applicationInWorkId) - //{ - // var obj = await jobAutoControlService.Get().FirstOrDefaultAsync(t => t.ApplicationInWorkId == applicationInWorkId); - - // if (obj == null) - // return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при удалении настроект. Не найден настройки с {nameof(applicationInWorkId)}: {applicationInWorkId}" } })); - - // if (!jobAutoControlService.Delete(obj) || !await jobAutoControlService.CommitAsync()) - // return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при удалении настроек." } })); - - // return NoContent(); - //} - - - } -} diff --git a/PARR.API/Controllers/V1/ShortcodeApplyController.cs b/PARR.API/Controllers/V1/ShortcodeApplyController.cs index d80060d7..205eda63 100644 --- a/PARR.API/Controllers/V1/ShortcodeApplyController.cs +++ b/PARR.API/Controllers/V1/ShortcodeApplyController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests; @@ -7,9 +8,11 @@ using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Controllers.V1.Base; using PARR.Core.Repositories.Interfaces; using PARR.Core.Services.Shortcodes; +using PARR.Domain.Common.Roles; namespace PARR.API.Controllers.V1 { + [Authorize(Roles = ParrRoles.Administrator.Role)] public class ShortcodeApplyController : BaseApiController { private readonly ILogger logger; diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs index 0da2dea9..0bb953e4 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs @@ -1,7 +1,6 @@ using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using NodaTime; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests.BaseRequests; using PARR.API.Contracts.V1.Requests.Queries; diff --git a/PARR.API/Controllers/V1/SyncTaskController.cs b/PARR.API/Controllers/V1/SyncTaskController.cs index 9b7425b9..2ff7528a 100644 --- a/PARR.API/Controllers/V1/SyncTaskController.cs +++ b/PARR.API/Controllers/V1/SyncTaskController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests; using PARR.API.Contracts.V1.Responses.Base; @@ -8,11 +9,13 @@ using PARR.API.Settings; using PARR.Core.Common.Interfaces.RabbitServices; using PARR.Core.Services.MatchingStatusService; using PARR.Domain.Common.Rabbit.Messages; +using PARR.Domain.Common.Roles; using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; namespace PARR.API.Controllers.V1 { + [Authorize(Roles = ParrRoles.Administrator.Role)] public class SyncTaskController : BaseApiController { private readonly ILogger logger; diff --git a/PARR.API/Controllers/V1/TaskController.cs b/PARR.API/Controllers/V1/TaskController.cs new file mode 100644 index 00000000..70a60e4f --- /dev/null +++ b/PARR.API/Controllers/V1/TaskController.cs @@ -0,0 +1,51 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PARR.API.Contracts.V1; +using PARR.API.Contracts.V1.Responses; +using PARR.API.Contracts.V1.Responses.Base; +using PARR.API.Controllers.V1.Base; +using PARR.Core.Services.TaskServices.Interfaces; +using PARR.Domain.Common.Roles; +using PARR.Domain.Enums; + +namespace PARR.API.Controllers.V1 +{ + /// + /// Контроллер по управлению Task (задания, очереди) + /// + [Authorize(Roles = ParrRoles.Administrator.Role)] + public class TaskController : BaseApiController + { + private readonly ITaskManagementService taskManagementService; + private readonly IMapper mapper; + + public TaskController( + ITaskManagementService taskManagementService, + IMapper mapper + ) + { + this.taskManagementService = taskManagementService; + this.mapper = mapper; + } + + + /// + /// Получить список активных задач (задания, очереди) + /// + /// + [HttpGet(ApiRoutes.Tasks.GetActiveTasks)] + public async Task GetActiveTasks([FromQuery] TaskTypeEnum? typeCode) + { + var activeTasks = await taskManagementService.GetActiveTasksAsync(typeCode); + + if (activeTasks.Count == 0) + return NoContent(); + + var response = mapper.Map>(activeTasks); + + return Ok(new Response>(response, true)); + } + + } +} diff --git a/PARR.API/Controllers/V1/TaskStatusController.cs b/PARR.API/Controllers/V1/TaskStatusController.cs index d7cb6293..e31764fb 100644 --- a/PARR.API/Controllers/V1/TaskStatusController.cs +++ b/PARR.API/Controllers/V1/TaskStatusController.cs @@ -11,6 +11,9 @@ using PARR.Domain.Common.Roles; namespace PARR.API.Controllers.V1 { + /// + /// Статусы заданий роботам + /// [Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)] public class TaskStatusController : BaseApiController { @@ -27,7 +30,7 @@ namespace PARR.API.Controllers.V1 } /// - /// Получить список статусов заданий + /// Получить список статусов заданий роботам /// /// [HttpGet(ApiRoutes.TaskStatus.GetAll)] diff --git a/PARR.API/MappingProfiles/DomainToResponseProfile.cs b/PARR.API/MappingProfiles/DomainToResponseProfile.cs index e6847037..f156da2d 100644 --- a/PARR.API/MappingProfiles/DomainToResponseProfile.cs +++ b/PARR.API/MappingProfiles/DomainToResponseProfile.cs @@ -6,6 +6,7 @@ using PARR.API.MappingProfiles.Resolvers; using PARR.Core.Repositories.Interfaces.Schedule; using PARR.Domain.DTOs.Matching; using PARR.Domain.DTOs.Shortcode; +using PARR.Domain.DTOs.TaskDTO; using PARR.Domain.DTOs.Workload; using PARR.Domain.Entities; using PARR.Domain.Entities.Base.History; @@ -445,6 +446,25 @@ namespace PARR.API.MappingProfiles #endregion + + #region Task + + //CreateMap() + // .Include() + // .Include(); + + //CreateMap(); + + CreateMap(); + + // --- + + CreateMap(); + + CreateMap(); + + #endregion + } } diff --git a/PARR.Core/Infrastructure/Mapping/TaskMappingProfile.cs b/PARR.Core/Infrastructure/Mapping/TaskMappingProfile.cs new file mode 100644 index 00000000..53b96ab0 --- /dev/null +++ b/PARR.Core/Infrastructure/Mapping/TaskMappingProfile.cs @@ -0,0 +1,36 @@ +using AutoMapper; +using PARR.Domain.DTOs.TaskDTO; + +namespace PARR.Core.Infrastructure.Mapping +{ + /// + /// Маппинг все что касается Task + /// + internal class TaskMappingProfile : Profile + { + public TaskMappingProfile() + { + #region TaskItem + CreateMap() + .Include() + .Include(); + + CreateMap(); + #endregion + + #region ActiveTask + CreateMap() + .ForMember(d => d.Type, o => o.MapFrom(s => s.TaskType)) + .ForMember(d => d.Status, o => o.MapFrom(s => s.TaskStatus)); + #endregion + + #region TaskType + CreateMap(); + #endregion + + #region TaskStatus + CreateMap(); + #endregion + } + } +} diff --git a/PARR.Core/PARR.Core.csproj b/PARR.Core/PARR.Core.csproj index ede68966..6b9219aa 100644 --- a/PARR.Core/PARR.Core.csproj +++ b/PARR.Core/PARR.Core.csproj @@ -6,10 +6,6 @@ enable - - - - diff --git a/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs b/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs index 51c0496c..5db6eb93 100644 --- a/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs +++ b/PARR.Core/Services/TaskServices/Implementations/TaskManagementService.cs @@ -1,11 +1,12 @@ -using Microsoft.EntityFrameworkCore; +using AutoMapper; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces.RabbitServices; using PARR.Core.Repositories.Interfaces.TaskRepositories; using PARR.Core.Services.TaskServices.Interfaces; using PARR.Domain.Common.Rabbit.Messages; +using PARR.Domain.DTOs.TaskDTO; using PARR.Domain.Entities.Base.History; -using PARR.Domain.Entities.TaskEntities; using PARR.Domain.Enums; using PARR.Domain.Settings; using System.Text.Encodings.Web; @@ -19,24 +20,29 @@ namespace PARR.Core.Services.TaskServices.Implementations private readonly ITaskTypeRepository taskTypeRepository; private readonly ITaskRepository taskRepository; private readonly IRabbitService rabbitService; + private readonly IMapper mapper; public TaskManagementService( ILogger logger, ITaskTypeRepository taskTypeRepository, ITaskRepository taskRepository, - IRabbitService rabbitService + IRabbitService rabbitService, + IMapper mapper ) { this.logger = logger; this.taskTypeRepository = taskTypeRepository; this.taskRepository = taskRepository; this.rabbitService = rabbitService; + this.mapper = mapper; } public async Task CreateTaskAsync(TaskTypeEnum typeCode, T payload, IHistoryInitiator initiator, IMqSettings mqSettings) { - var taskType = await taskTypeRepository.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Code == typeCode); + var taskType = await taskTypeRepository.Get() + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Code == typeCode); if (taskType == null) { @@ -59,7 +65,7 @@ namespace PARR.Core.Services.TaskServices.Implementations var payloadStr = PayloadToString(payload); // Создаем новую запись задачи - var task = new TaskItem + var task = new Domain.Entities.TaskEntities.TaskItem { Id = Guid.NewGuid(), TypeCode = typeCode, @@ -99,12 +105,29 @@ namespace PARR.Core.Services.TaskServices.Implementations } + public async Task> GetActiveTasksAsync(TaskTypeEnum? typeCode = null) + { + var query = taskRepository.Get() + .AsNoTracking() + .Include(t => t.TaskStatus) + .Include(t => t.TaskType) + .Where(t => t.StatusCode == TaskItemStatusEnum.Pending || t.StatusCode == TaskItemStatusEnum.Processing); + + if (typeCode.HasValue) + query = query.Where(t => t.TypeCode == typeCode.Value); + + var tasks = await query.OrderByDescending(t => t.DateCreated).ToListAsync(); + + return mapper.Map>(tasks); + } + + /// /// Получить активную singleton задачу указанного типа /// /// /// - private async Task GetActiveSingletonTaskAsync(TaskTypeEnum typeCode) + private async Task GetActiveSingletonTaskAsync(TaskTypeEnum typeCode) { return await taskRepository.Get().AsNoTracking() .FirstOrDefaultAsync(t => t.TypeCode == typeCode && ( diff --git a/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs b/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs index e558d175..24370534 100644 --- a/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs +++ b/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs @@ -1,4 +1,5 @@ -using PARR.Domain.Entities.Base.History; +using PARR.Domain.DTOs.TaskDTO; +using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; using PARR.Domain.Settings; @@ -19,5 +20,13 @@ namespace PARR.Core.Services.TaskServices.Interfaces /// Настройки очереди /// Task CreateTaskAsync(TaskTypeEnum typeCode, T payload, IHistoryInitiator initiator, IMqSettings mqSettings); + + + /// + /// Получить список активных задач + /// + /// + /// + Task> GetActiveTasksAsync(TaskTypeEnum? typeCode = null); } } diff --git a/PARR.DAL/Context/DataContext.cs b/PARR.DAL/Context/DataContext.cs index ab342b67..36b68b74 100644 --- a/PARR.DAL/Context/DataContext.cs +++ b/PARR.DAL/Context/DataContext.cs @@ -579,7 +579,7 @@ namespace PARR.DAL.Context { f.HasData(new Domain.Entities.TaskEntities.TaskType[] { - new() { Code = TaskTypeEnum.Workload, Name = TaskTypeEnum.Workload.ToString(), Description = "Формирование данных для отчетности - Загруженность", MaxRetries = 2, IsSingleton=true, MaxExecutionTimeMinutes=60, RetentionDays=90 } + new() { Code = TaskTypeEnum.Workload, Name = TaskTypeEnum.Workload.ToString(), Description = "Формирование данных для отчетности - Загруженность", MaxRetries = 2, IsSingleton=true, MaxExecutionTimeMinutes=90, RetentionDays=90 } } ); }); diff --git a/PARR.Domain/DTOs/TaskDTO/ActiveTask.cs b/PARR.Domain/DTOs/TaskDTO/ActiveTask.cs new file mode 100644 index 00000000..057e54a7 --- /dev/null +++ b/PARR.Domain/DTOs/TaskDTO/ActiveTask.cs @@ -0,0 +1,12 @@ +namespace PARR.Domain.DTOs.TaskDTO +{ + /// + /// DTO + /// + public record ActiveTask : TaskItemBase + { + public required TaskType Type { get; init; } + + public required TaskStatus Status { get; init; } + } +} diff --git a/PARR.Domain/DTOs/TaskDTO/TaskItem.cs b/PARR.Domain/DTOs/TaskDTO/TaskItem.cs new file mode 100644 index 00000000..4095ca3c --- /dev/null +++ b/PARR.Domain/DTOs/TaskDTO/TaskItem.cs @@ -0,0 +1,26 @@ +namespace PARR.Domain.DTOs.TaskDTO +{ + /// + /// DTO + /// + public record TaskItemBase + { + public Guid Id { get; init; } + + public DateTimeOffset DateCreated { get; init; } + + public DateTimeOffset? DateModified { get; init; } + + public int RetryCount { get; init; } + + public DateTimeOffset? ProcessedAt { get; init; } + } + + /// + /// DTO + /// + public record TaskItem : TaskItemBase + { + //todo: + } +} diff --git a/PARR.Domain/DTOs/TaskDTO/TaskStatus.cs b/PARR.Domain/DTOs/TaskDTO/TaskStatus.cs new file mode 100644 index 00000000..6313aaee --- /dev/null +++ b/PARR.Domain/DTOs/TaskDTO/TaskStatus.cs @@ -0,0 +1,16 @@ +using PARR.Domain.Enums; + +namespace PARR.Domain.DTOs.TaskDTO +{ + /// + /// DTO + /// + public record TaskStatus + { + public TaskItemStatusEnum Code { get; init; } + + public required string Name { get; init; } + + public required string Description { get; init; } + } +} diff --git a/PARR.Domain/DTOs/TaskDTO/TaskType.cs b/PARR.Domain/DTOs/TaskDTO/TaskType.cs new file mode 100644 index 00000000..696d7a03 --- /dev/null +++ b/PARR.Domain/DTOs/TaskDTO/TaskType.cs @@ -0,0 +1,36 @@ +using PARR.Domain.Enums; + +namespace PARR.Domain.DTOs.TaskDTO +{ + /// + /// DTO + /// + public record TaskType + { + public TaskTypeEnum Code { get; init; } + + public required string Name { get; init; } + + public required string Description { get; init; } + + /// + /// Максимальное кол-во попыток + /// + public int MaxRetries { get; init; } + + /// + /// Максимальное время выполнения + /// + public int MaxExecutionTimeMinutes { get; init; } + + /// + /// Одновременно может быть только одна задача или несколько + /// + public bool IsSingleton { get; init; } + + /// + /// Сколько дней хранить в БД + /// + public int RetentionDays { get; init; } + } +} From e5535857d442d1e5ce8525169f0cd0738e15ff6c Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Mon, 18 May 2026 16:46:42 +1000 Subject: [PATCH 3/4] =?UTF-8?q?feat(api,=20core):=20Workload=20-=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=83=D1=81=20=D1=84=D0=BE=D1=80=D0=BC=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BE=D1=82=D1=87?= =?UTF-8?q?=D0=B5=D1=82=D0=BD=D0=BE=D1=81=D1=82=D0=B8.=20RedisCacheService?= =?UTF-8?q?=20-=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B2=D1=83=D1=8E=20=D0=B7=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D1=8C=20=D0=B8=D0=B7=20=D0=BA=D1=8D=D1=88=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D0=B0=20Hash.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Contracts/V1/ApiRoutes.cs | 2 + .../StatWorkloadCacheInfoResponse.cs | 14 +++++++ .../V1/Statistics/StatWorkloadController.cs | 21 ++++++++-- .../DomainToResponseProfile.cs | 4 ++ .../Common/Interfaces/IRedisCacheService.cs | 8 ++++ .../Implementations/WorkloadCacheService.cs | 12 ++++++ .../Implementations/WorkloadService.cs | 40 ++++++++++++++++++- .../Workload/Interfaces/IWorkloadService.cs | 6 +++ PARR.Domain/Cache/Models/Base/IBaseCache.cs | 11 ++++- .../DTOs/Workload/WorkloadCacheInfo.cs | 36 +++++++++++++++++ .../Redis/RedisCacheService.cs | 32 +++++++++++++++ 11 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadCacheInfoResponse.cs create mode 100644 PARR.Domain/DTOs/Workload/WorkloadCacheInfo.cs diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index 198ab02a..2743bcc0 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -303,6 +303,8 @@ public const string WorkGroup = BaseStat + "/workload/work-groups"; public const string ResponseArea = BaseStat + "/workload/response-areas"; + + public const string ReportInfo = BaseStat + "/workload/report-info"; } #endregion diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadCacheInfoResponse.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadCacheInfoResponse.cs new file mode 100644 index 00000000..00798498 --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadCacheInfoResponse.cs @@ -0,0 +1,14 @@ +using PARR.Domain.DTOs.Workload; + +namespace PARR.API.Contracts.V1.Responses.Statistics +{ + /// + /// Инфо о формирование КЭШ для отчета Workload + /// + public record StatWorkloadCacheInfoResponse + { + public WorkloadCacheInfo.StatusEnum Status { get; set; } + + public DateTimeOffset? Date { get; init; } + } +} diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs index 0bb953e4..f57ad973 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs @@ -70,8 +70,6 @@ namespace PARR.API.Controllers.V1.Statistics } - - /// /// Сформировать отчетность /// @@ -110,6 +108,22 @@ namespace PARR.API.Controllers.V1.Statistics } + /// + /// Получить информацию о формировании КЭШ + /// + /// + [HttpGet(ApiRoutes.Workload.ReportInfo)] + public async Task GetReportInfo() + { + var report = await workloadService.GetReportInfoAsync(); + + var response = mapper.Map(report); + + return Ok(new Response(response, true)); + } + + + /// /// Универсальный метод получения отчетности /// @@ -123,7 +137,8 @@ namespace PARR.API.Controllers.V1.Statistics var report = await workloadService.GetWorkloadReport(reportType, dateStart, durationDays, offset, filter); if (report == null) - return BadRequest(new Response(false, new List { new ErrorModel { Message = "Необходимо сформировать отчетность." } })); + //return BadRequest(new Response(false, new List { new ErrorModel { Message = "Необходимо сформировать отчетность." } })); + return BadRequest(new Response(null, false, new List { new ErrorModel { Message = "Необходимо сформировать отчетность." } }, "empty")); var resonse = mapper.Map(report); diff --git a/PARR.API/MappingProfiles/DomainToResponseProfile.cs b/PARR.API/MappingProfiles/DomainToResponseProfile.cs index f156da2d..aa478cf1 100644 --- a/PARR.API/MappingProfiles/DomainToResponseProfile.cs +++ b/PARR.API/MappingProfiles/DomainToResponseProfile.cs @@ -444,6 +444,10 @@ namespace PARR.API.MappingProfiles CreateMap(); + + + CreateMap(); + #endregion diff --git a/PARR.Core/Common/Interfaces/IRedisCacheService.cs b/PARR.Core/Common/Interfaces/IRedisCacheService.cs index f6bf40b2..92c7ef3c 100644 --- a/PARR.Core/Common/Interfaces/IRedisCacheService.cs +++ b/PARR.Core/Common/Interfaces/IRedisCacheService.cs @@ -94,6 +94,14 @@ /// Task GetHashFieldAsync(string hashKey, string field); + /// + /// Получить случайную первую запись из Hash + /// + /// + /// + /// + Task<(string Field, T? Value)?> GetFirstHashFieldAsync(string hashKey); + /// /// Получить все значения из Hash /// diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs index 6053b5c5..aeadd86d 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs @@ -328,6 +328,18 @@ namespace PARR.Core.Services.Workload.Implementations } + /// + /// Вернуть первую запись из TamplateCache если есть + /// + /// + public async Task GetFirstTemplateCacheAsync() + { + var cache = await redisCacheService.GetFirstHashFieldAsync(redisCacheService.GetKey(cacheTemplateKey)); + + return cache?.Value; + } + + /// /// Формирует ключ для отчета Workload /// diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs index 21d43311..7e6ebefc 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.Logging; using PARR.Core.Services.NextRunServices; +using PARR.Core.Services.TaskServices.Interfaces; using PARR.Core.Services.Workload.Interfaces; using PARR.Core.Services.Workload.Models; +using PARR.Domain.DTOs.TaskDTO; using PARR.Domain.DTOs.Workload; using PARR.Domain.Enums; @@ -12,16 +14,19 @@ namespace PARR.Core.Services.Workload.Implementations private readonly ILogger logger; private readonly WorkloadCacheService workloadCacheService; private readonly INextRunService nextRunService; + private readonly ITaskManagementService taskManagementService; public WorkloadService( ILogger logger, WorkloadCacheService workloadCacheService, - INextRunService nextRunService + INextRunService nextRunService, + ITaskManagementService taskManagementService ) { this.logger = logger; this.workloadCacheService = workloadCacheService; this.nextRunService = nextRunService; + this.taskManagementService = taskManagementService; } @@ -107,6 +112,39 @@ namespace PARR.Core.Services.Workload.Implementations return report; } + + public async Task GetReportInfoAsync() + { + // может быть три статуса + // 1 - отчет сейчас формируется (дата начала формирования отчета) + // 2 - отчет сформирован (вернуть дату формирования кэш) + // 3 - отчет сейчас не формируется, кэша нет (date = null) + + + // 1. проверить что выполнились все задания на обновление кэш + var activeTasks = await taskManagementService.GetActiveTasksAsync(TaskTypeEnum.Workload); + if (activeTasks.Count > 0) + { + // идет формирование отчета + // задача может быт только одна, так как у нее такой тип + var task = activeTasks.First(); + + return new WorkloadCacheInfo { Status = WorkloadCacheInfo.StatusEnum.Processing, Date = task.DateCreated }; + } + + // 2. получить дату кэш + // Если нет активных задач, значит может быть кэш существует, пробуем получить дату из кэш + var templateCache = await workloadCacheService.GetFirstTemplateCacheAsync(); + if (templateCache != null) + { + return new WorkloadCacheInfo { Status = WorkloadCacheInfo.StatusEnum.Generated, Date = templateCache.Timestamp }; + } + + // Если кэш нет, то возвращаем null + return new WorkloadCacheInfo { Status = WorkloadCacheInfo.StatusEnum.NotFound }; + } + + /// /// Получить список дней для отчета, с отметкой выходной/рабочий /// diff --git a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs index aeb775dd..aae196c5 100644 --- a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs +++ b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs @@ -8,6 +8,12 @@ namespace PARR.Core.Services.Workload.Interfaces /// public interface IWorkloadService { + /// + /// Получить статус кэш/отчета + /// + /// + Task GetReportInfoAsync(); + /// /// Получить отчет о загруженности /// diff --git a/PARR.Domain/Cache/Models/Base/IBaseCache.cs b/PARR.Domain/Cache/Models/Base/IBaseCache.cs index ea268a3b..61481099 100644 --- a/PARR.Domain/Cache/Models/Base/IBaseCache.cs +++ b/PARR.Domain/Cache/Models/Base/IBaseCache.cs @@ -5,10 +5,19 @@ /// public interface IBaseCache { + /// + /// Объект + /// public T Data { get; set; } - + + /// + /// Дата формирования Cache + /// public DateTimeOffset Timestamp { get; set; } + /// + /// Источник Cache (class) + /// public string Source { get; set; } } } diff --git a/PARR.Domain/DTOs/Workload/WorkloadCacheInfo.cs b/PARR.Domain/DTOs/Workload/WorkloadCacheInfo.cs new file mode 100644 index 00000000..7101e296 --- /dev/null +++ b/PARR.Domain/DTOs/Workload/WorkloadCacheInfo.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace PARR.Domain.DTOs.Workload +{ + /// + /// Инфо о состоянии КЭШ для отчета Workload + /// + public record WorkloadCacheInfo + { + /// + /// Статус отчета + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum StatusEnum + { + /// + /// Отчет формируется + /// + Processing = 1, + + /// + /// Отчет сформирован + /// + Generated = 2, + + /// + /// Нет отчета + /// + NotFound = 3 + } + + public StatusEnum Status { get; init; } + + public DateTimeOffset? Date { get; init; } + } +} diff --git a/PARR.Infrastructure/Redis/RedisCacheService.cs b/PARR.Infrastructure/Redis/RedisCacheService.cs index fba9f1ae..d76685a5 100644 --- a/PARR.Infrastructure/Redis/RedisCacheService.cs +++ b/PARR.Infrastructure/Redis/RedisCacheService.cs @@ -213,6 +213,38 @@ namespace PARR.Infrastructure.Redis return JsonSerializer.Deserialize(value); } + public async Task<(string Field, T? Value)?> GetFirstHashFieldAsync(string hashKey) + { + logger.LogDebug("Запрос первого поля из хеша '{HashKey}'", hashKey); + + int attempts = 0; + int maxAttempts = 5; + + await foreach (var entry in redis.HashScanAsync(hashKey, pageSize: 1)) + { + if (++attempts > maxAttempts) + { + logger.LogWarning("Превышен лимит попыток ({Max}) для хеша '{HashKey}'", maxAttempts, hashKey); + return null; + } + + if (entry.Value.IsNullOrEmpty) + { + logger.LogDebug("Поле '{Field}' в хеше '{HashKey}' пустое, пропускаем", entry.Name, hashKey); + continue; + } + + var deserialized = JsonSerializer.Deserialize(entry.Value); + + logger.LogDebug("Получено поле '{Field}' из хеша '{HashKey}', тип: {Type}", entry.Name, hashKey, typeof(T).Name); + + return (entry.Name.ToString(), deserialized); + } + + logger.LogDebug("Хеш '{HashKey}' пуст или не существует", hashKey); + return null; + } + public async Task> GetAllHashFieldsAsync(string hashKey) { // получить все записи из Hash From 178d136dbaa1d5b0554abfc9cdd507f8966c67db Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 19 May 2026 09:45:38 +1000 Subject: [PATCH 4/4] =?UTF-8?q?feat(docker,=20api,=20core,=20dal,=20domain?= =?UTF-8?q?,=20workloadBuilder):=20WorkloadBuillder=20-=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BA=D1=8D=D1=88=D0=B0=20=D0=BF=D0=BE=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B0=D0=BB=D1=83.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V1/Statistics/StatWorkloadController.cs | 1 - .../Middleware/GlobalExceptionHandler.cs | 9 +++ PARR.API/appsettings.json | 2 +- PARR.Core/DependencyInjection.cs | 2 + .../Interfaces/ITaskManagementService.cs | 3 +- .../WorkloadIntervalService.cs | 55 +++++++++++++++++++ .../Interfaces/IWorkloadIntervalService.cs | 18 ++++++ PARR.DAL/Context/DataContext.cs | 3 +- PARR.Domain/Enums/ParrComponentsEnum.cs | 7 ++- PARR.WorkloadBuilderWorker/IntervalWorker.cs | 25 +++++++++ PARR.WorkloadBuilderWorker/Program.cs | 19 ++++++- .../{Worker.cs => RabbitWorker.cs} | 7 ++- .../Settings/WorkerSettings.cs | 8 +++ PARR.WorkloadBuilderWorker/appsettings.json | 3 +- docker-compose.dcproj | 2 + docker-compose.workload-builder.yml | 27 ++++++++- 16 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs create mode 100644 PARR.Core/Services/Workload/Implementations/WorkloadIntervalService.cs create mode 100644 PARR.Core/Services/Workload/Interfaces/IWorkloadIntervalService.cs create mode 100644 PARR.WorkloadBuilderWorker/IntervalWorker.cs rename PARR.WorkloadBuilderWorker/{Worker.cs => RabbitWorker.cs} (69%) diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs index f57ad973..ef330478 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs @@ -137,7 +137,6 @@ namespace PARR.API.Controllers.V1.Statistics var report = await workloadService.GetWorkloadReport(reportType, dateStart, durationDays, offset, filter); if (report == null) - //return BadRequest(new Response(false, new List { new ErrorModel { Message = "Необходимо сформировать отчетность." } })); return BadRequest(new Response(null, false, new List { new ErrorModel { Message = "Необходимо сформировать отчетность." } }, "empty")); var resonse = mapper.Map(report); diff --git a/PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs b/PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs new file mode 100644 index 00000000..50009b57 --- /dev/null +++ b/PARR.API/Infrastructure/Middleware/GlobalExceptionHandler.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Diagnostics; + +namespace PARR.API.Infrastructure.Middleware +{ + public class GlobalExceptionHandler//: IExceptionHandler + { + //todo: + } +} diff --git a/PARR.API/appsettings.json b/PARR.API/appsettings.json index 1ce93432..01a1a2c6 100644 --- a/PARR.API/appsettings.json +++ b/PARR.API/appsettings.json @@ -111,7 +111,7 @@ } ], "RabbitMq": { - "ThresholdConnections": 28 + "ThresholdConnections": 29 } } } diff --git a/PARR.Core/DependencyInjection.cs b/PARR.Core/DependencyInjection.cs index 89a31080..74f867ac 100644 --- a/PARR.Core/DependencyInjection.cs +++ b/PARR.Core/DependencyInjection.cs @@ -84,6 +84,8 @@ namespace PARR.Core services.AddScoped(); services.AddScoped(); + services.AddSingleton(); + #endregion #region NextRun diff --git a/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs b/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs index 24370534..b9b83851 100644 --- a/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs +++ b/PARR.Core/Services/TaskServices/Interfaces/ITaskManagementService.cs @@ -11,7 +11,8 @@ namespace PARR.Core.Services.TaskServices.Interfaces public interface ITaskManagementService { /// - /// Создать новую задачу в БД и опубликовать в очередь + /// Создать новую задачу в БД и опубликовать в очередь. + /// В случае ошибок генерирует Exception. /// /// /// Тип задачи diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadIntervalService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadIntervalService.cs new file mode 100644 index 00000000..9bc6126d --- /dev/null +++ b/PARR.Core/Services/Workload/Implementations/WorkloadIntervalService.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PARR.Core.Common.Interfaces; +using PARR.Core.Services.TaskServices.Interfaces; +using PARR.Core.Services.Workload.Interfaces; +using PARR.Domain.Entities.Base.History; +using PARR.Domain.Settings; + +namespace PARR.Core.Services.Workload.Implementations +{ + internal class WorkloadIntervalService : IWorkloadIntervalService + { + private readonly ILogger logger; + private readonly IIntervalService intervalService; + private readonly IServiceProvider serviceProvider; + + public WorkloadIntervalService( + ILogger logger, + IIntervalService intervalService, + IServiceProvider serviceProvider + ) + { + this.logger = logger; + this.intervalService = intervalService; + this.serviceProvider = serviceProvider; + } + + public async Task StartAsync(TimeSpan interval, IMqSettings mqSettings) + { + logger.LogInformation("Запуск сервиса формирования кэш отчетов Workload по интервалу."); + + await intervalService.IntervalInitAsync(async () => + { + + await using (var scope = serviceProvider.CreateAsyncScope()) + { + var taskManagementService = scope.ServiceProvider.GetRequiredService(); + + var initiator = new HistoryInitiator { InitiatorParrComponentId = Domain.Enums.ParrComponentsEnum.WorkloadBuilder, InitiatorComment = $"Генерация кэш workload по интервалу '{interval}'" }; + + try + { + var taskId = await taskManagementService.CreateTaskAsync(Domain.Enums.TaskTypeEnum.Workload, default, initiator, mqSettings); + logger.LogInformation("Создана задача {taskId} на формирование кэш отчета.", taskId); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при отправке задания на формирование кэш отчета."); + } + } + + }, interval); + } + } +} diff --git a/PARR.Core/Services/Workload/Interfaces/IWorkloadIntervalService.cs b/PARR.Core/Services/Workload/Interfaces/IWorkloadIntervalService.cs new file mode 100644 index 00000000..facaabae --- /dev/null +++ b/PARR.Core/Services/Workload/Interfaces/IWorkloadIntervalService.cs @@ -0,0 +1,18 @@ +using PARR.Domain.Settings; + +namespace PARR.Core.Services.Workload.Interfaces +{ + /// + /// Формирование кэш отчетов по интервалу + /// + public interface IWorkloadIntervalService + { + /// + /// Запуск формирования по интервалу + /// + /// Интервал + /// Настройки очереди куда отправлять задание + /// + Task StartAsync(TimeSpan interval, IMqSettings mqSettings); + } +} diff --git a/PARR.DAL/Context/DataContext.cs b/PARR.DAL/Context/DataContext.cs index 36b68b74..84749f65 100644 --- a/PARR.DAL/Context/DataContext.cs +++ b/PARR.DAL/Context/DataContext.cs @@ -518,7 +518,8 @@ namespace PARR.DAL.Context new() { Id = ParrComponentsEnum.NextRun, Name = ParrComponentsEnum.NextRun.ToString(), Description = "Рассчет даты следующего срабатывания" }, new() { Id = ParrComponentsEnum.TemplateActivator, Name = ParrComponentsEnum.TemplateActivator.ToString(), Description = "Активатор шаблонов / расписаний" }, new() { Id = ParrComponentsEnum.TemplateDistributor, Name = ParrComponentsEnum.TemplateDistributor.ToString(), Description = "Автораспределение РР" }, - new() { Id = ParrComponentsEnum.TemplateTaskGenerator, Name = ParrComponentsEnum.TemplateTaskGenerator.ToString(), Description = "Генератор шаблонов" } + new() { Id = ParrComponentsEnum.TemplateTaskGenerator, Name = ParrComponentsEnum.TemplateTaskGenerator.ToString(), Description = "Генератор шаблонов" }, + new() { Id = ParrComponentsEnum.WorkloadBuilder, Name = ParrComponentsEnum.WorkloadBuilder.ToString(), Description = "Генератор кэша отчетов workload" } ); }); diff --git a/PARR.Domain/Enums/ParrComponentsEnum.cs b/PARR.Domain/Enums/ParrComponentsEnum.cs index 725b1688..d829acb4 100644 --- a/PARR.Domain/Enums/ParrComponentsEnum.cs +++ b/PARR.Domain/Enums/ParrComponentsEnum.cs @@ -79,6 +79,11 @@ namespace PARR.Domain.Enums /// /// Генератор Template /// - TemplateTaskGenerator = 13 + TemplateTaskGenerator = 13, + + /// + /// Генератор кэша отчетов Workload + /// + WorkloadBuilder = 14 } } diff --git a/PARR.WorkloadBuilderWorker/IntervalWorker.cs b/PARR.WorkloadBuilderWorker/IntervalWorker.cs new file mode 100644 index 00000000..4107e248 --- /dev/null +++ b/PARR.WorkloadBuilderWorker/IntervalWorker.cs @@ -0,0 +1,25 @@ +using PARR.Core.Services.Workload.Interfaces; +using PARR.WorkloadBuilderWorker.Settings; + +namespace PARR.WorkloadBuilderWorker +{ + public class IntervalWorker : BackgroundService + { + private readonly ILogger logger; + private readonly IWorkloadIntervalService workloadIntervalService; + private readonly WorkerSettings workerSettings; + + public IntervalWorker(ILogger logger, IWorkloadIntervalService workloadIntervalService, WorkerSettings workerSettings) + { + this.logger = logger; + this.workloadIntervalService = workloadIntervalService; + this.workerSettings = workerSettings; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Запуск {workerName}", nameof(IntervalWorker)); + await workloadIntervalService.StartAsync(workerSettings.RepeatEvery, workerSettings.MqSettings); + } + } +} diff --git a/PARR.WorkloadBuilderWorker/Program.cs b/PARR.WorkloadBuilderWorker/Program.cs index 3c9414ff..30e9c2da 100644 --- a/PARR.WorkloadBuilderWorker/Program.cs +++ b/PARR.WorkloadBuilderWorker/Program.cs @@ -46,8 +46,25 @@ builder.Configuration.AddDalConfigurations(builder.Services); builder.Services.AddDallSettings(builder.Configuration); #endregion +// Задается через переменные окружеия +var runRabbit = builder.Configuration.GetValue("ENABLE_RABBIT", true); +var runInterval = builder.Configuration.GetValue("ENABLE_INTERVAL", true); -builder.Services.AddHostedService(); +if (runRabbit) +{ + builder.Services.AddHostedService(); +} + +if (runInterval) +{ + builder.Services.AddHostedService(); +} + +// Настройка краша при ошибке. Если хоть один из воркеров упадет, то падает целиком проект +builder.Services.Configure(options => +{ + options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.StopHost; +}); var host = builder.Build(); host.Run(); diff --git a/PARR.WorkloadBuilderWorker/Worker.cs b/PARR.WorkloadBuilderWorker/RabbitWorker.cs similarity index 69% rename from PARR.WorkloadBuilderWorker/Worker.cs rename to PARR.WorkloadBuilderWorker/RabbitWorker.cs index 730de234..d6bca1f5 100644 --- a/PARR.WorkloadBuilderWorker/Worker.cs +++ b/PARR.WorkloadBuilderWorker/RabbitWorker.cs @@ -3,13 +3,15 @@ using PARR.WorkloadBuilderWorker.Settings; namespace PARR.WorkloadBuilderWorker { - public class Worker : BackgroundService + public class RabbitWorker : BackgroundService { + private readonly ILogger logger; private readonly IWorkloadCacheBuilderService workloadCacheBuilderService; private readonly WorkerSettings workerSettings; - public Worker(ILogger logger, IWorkloadCacheBuilderService workloadCacheBuilderService, WorkerSettings WorkerSettings) + public RabbitWorker(ILogger logger, IWorkloadCacheBuilderService workloadCacheBuilderService, WorkerSettings WorkerSettings) { + this.logger = logger; this.workloadCacheBuilderService = workloadCacheBuilderService; this.workerSettings = WorkerSettings; } @@ -17,6 +19,7 @@ namespace PARR.WorkloadBuilderWorker protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + logger.LogInformation("Запуск {workerName}", nameof(RabbitWorker)); await workloadCacheBuilderService.ProcessMessagesAsync(workerSettings.MqSettings); } diff --git a/PARR.WorkloadBuilderWorker/Settings/WorkerSettings.cs b/PARR.WorkloadBuilderWorker/Settings/WorkerSettings.cs index 5ab5e0b0..c55c5cdf 100644 --- a/PARR.WorkloadBuilderWorker/Settings/WorkerSettings.cs +++ b/PARR.WorkloadBuilderWorker/Settings/WorkerSettings.cs @@ -4,6 +4,14 @@ namespace PARR.WorkloadBuilderWorker.Settings { public record WorkerSettings { + /// + /// Настройки MQ + /// public MqSettingsBase MqSettings { get; init; } = new(); + + /// + /// Интервал повторений + /// + public TimeSpan RepeatEvery { get; init; } } } diff --git a/PARR.WorkloadBuilderWorker/appsettings.json b/PARR.WorkloadBuilderWorker/appsettings.json index 496c9590..b1debb85 100644 --- a/PARR.WorkloadBuilderWorker/appsettings.json +++ b/PARR.WorkloadBuilderWorker/appsettings.json @@ -28,6 +28,7 @@ "QueueName": "parr-task-workload", "User": "task_workload_reader", "Password": "sdjfgIYFdifyKJFDhvsad@!314d" - } + }, + "RepeatEvery": "12:00:00" } } diff --git a/docker-compose.dcproj b/docker-compose.dcproj index 825b0fcb..a150e6c7 100644 --- a/docker-compose.dcproj +++ b/docker-compose.dcproj @@ -9,6 +9,7 @@ parr.api + @@ -25,6 +26,7 @@ + diff --git a/docker-compose.workload-builder.yml b/docker-compose.workload-builder.yml index 6994cb4b..7c0805c8 100644 --- a/docker-compose.workload-builder.yml +++ b/docker-compose.workload-builder.yml @@ -2,11 +2,13 @@ version: '3.4' # workload builder services: - parr-workload-builder: + parr-workload-builder-interval: image: harbor.dvgd.rzd/parr/parr-workload-builder:${tag:-latest} environment: - ASPNETCORE_ENVIRONMENT=Production - TZ=Europe/Moscow + - ENABLE_RABBIT=false + - ENABLE_INTERVAL=true logging: driver: fluentd options: @@ -15,7 +17,28 @@ services: fluentd-max-retries: '30' fluentd-async: 'true' fluentd-buffer-limit: '52428800' - tag: parr.workload-builder.serilog + tag: parr.workload-builder-interval.serilog + deploy: + replicas: 1 + networks: + - parr-network + + parr-workload-builder-rabbit: + image: harbor.dvgd.rzd/parr/parr-workload-builder:${tag:-latest} + environment: + - ASPNETCORE_ENVIRONMENT=Production + - TZ=Europe/Moscow + - ENABLE_RABBIT=true + - ENABLE_INTERVAL=false + logging: + driver: fluentd + options: + fluentd-address: dvgd-efk-01.dvgd.oao.rzd:24224 + fluentd-retry-wait: '10s' + fluentd-max-retries: '30' + fluentd-async: 'true' + fluentd-buffer-limit: '52428800' + tag: parr.workload-builder-rabbit.serilog deploy: replicas: 1 networks: