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