From e5535857d442d1e5ce8525169f0cd0738e15ff6c Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Mon, 18 May 2026 16:46:42 +1000 Subject: [PATCH] =?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