feat(api, core): Workload - статус формирования отчетности. RedisCacheService - получить первую запись из кэш типа Hash.

This commit is contained in:
Mikhail Trubnikov
2026-05-18 16:46:42 +10:00
parent 911e6febcd
commit e5535857d4
11 changed files with 181 additions and 5 deletions

View File

@@ -303,6 +303,8 @@
public const string WorkGroup = BaseStat + "/workload/work-groups"; public const string WorkGroup = BaseStat + "/workload/work-groups";
public const string ResponseArea = BaseStat + "/workload/response-areas"; public const string ResponseArea = BaseStat + "/workload/response-areas";
public const string ReportInfo = BaseStat + "/workload/report-info";
} }
#endregion #endregion

View File

@@ -0,0 +1,14 @@
using PARR.Domain.DTOs.Workload;
namespace PARR.API.Contracts.V1.Responses.Statistics
{
/// <summary>
/// Инфо о формирование КЭШ для отчета Workload
/// </summary>
public record StatWorkloadCacheInfoResponse
{
public WorkloadCacheInfo.StatusEnum Status { get; set; }
public DateTimeOffset? Date { get; init; }
}
}

View File

@@ -70,8 +70,6 @@ namespace PARR.API.Controllers.V1.Statistics
} }
/// <summary> /// <summary>
/// Сформировать отчетность /// Сформировать отчетность
/// </summary> /// </summary>
@@ -110,6 +108,22 @@ namespace PARR.API.Controllers.V1.Statistics
} }
/// <summary>
/// Получить информацию о формировании КЭШ
/// </summary>
/// <returns></returns>
[HttpGet(ApiRoutes.Workload.ReportInfo)]
public async Task<IActionResult> GetReportInfo()
{
var report = await workloadService.GetReportInfoAsync();
var response = mapper.Map<StatWorkloadCacheInfoResponse>(report);
return Ok(new Response<StatWorkloadCacheInfoResponse>(response, true));
}
/// <summary> /// <summary>
/// Универсальный метод получения отчетности /// Универсальный метод получения отчетности
/// </summary> /// </summary>
@@ -123,7 +137,8 @@ namespace PARR.API.Controllers.V1.Statistics
var report = await workloadService.GetWorkloadReport(reportType, dateStart, durationDays, offset, filter); var report = await workloadService.GetWorkloadReport(reportType, dateStart, durationDays, offset, filter);
if (report == null) if (report == null)
return BadRequest(new Response(false, new List<ErrorModel> { new ErrorModel { Message = "Необходимо сформировать отчетность." } })); //return BadRequest(new Response(false, new List<ErrorModel> { new ErrorModel { Message = "Необходимо сформировать отчетность." } }));
return BadRequest(new Response<string?>(null, false, new List<ErrorModel> { new ErrorModel { Message = "Необходимо сформировать отчетность." } }, "empty"));
var resonse = mapper.Map<StatWorkloadReportResponse>(report); var resonse = mapper.Map<StatWorkloadReportResponse>(report);

View File

@@ -444,6 +444,10 @@ namespace PARR.API.MappingProfiles
CreateMap<WorkloadDailyItem, WorkloadDailyItemResponse>(); CreateMap<WorkloadDailyItem, WorkloadDailyItemResponse>();
CreateMap<WorkloadCacheInfo, StatWorkloadCacheInfoResponse>();
#endregion #endregion

View File

@@ -94,6 +94,14 @@
/// <returns></returns> /// <returns></returns>
Task<T?> GetHashFieldAsync<T>(string hashKey, string field); Task<T?> GetHashFieldAsync<T>(string hashKey, string field);
/// <summary>
/// Получить случайную первую запись из Hash
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="hashKey"></param>
/// <returns></returns>
Task<(string Field, T? Value)?> GetFirstHashFieldAsync<T>(string hashKey);
/// <summary> /// <summary>
/// Получить все значения из Hash /// Получить все значения из Hash
/// </summary> /// </summary>

View File

@@ -328,6 +328,18 @@ namespace PARR.Core.Services.Workload.Implementations
} }
/// <summary>
/// Вернуть первую запись из TamplateCache если есть
/// </summary>
/// <returns></returns>
public async Task<TemplateCache?> GetFirstTemplateCacheAsync()
{
var cache = await redisCacheService.GetFirstHashFieldAsync<TemplateCache>(redisCacheService.GetKey(cacheTemplateKey));
return cache?.Value;
}
/// <summary> /// <summary>
/// Формирует ключ для отчета Workload /// Формирует ключ для отчета Workload
/// </summary> /// </summary>

View File

@@ -1,7 +1,9 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Core.Services.NextRunServices; using PARR.Core.Services.NextRunServices;
using PARR.Core.Services.TaskServices.Interfaces;
using PARR.Core.Services.Workload.Interfaces; using PARR.Core.Services.Workload.Interfaces;
using PARR.Core.Services.Workload.Models; using PARR.Core.Services.Workload.Models;
using PARR.Domain.DTOs.TaskDTO;
using PARR.Domain.DTOs.Workload; using PARR.Domain.DTOs.Workload;
using PARR.Domain.Enums; using PARR.Domain.Enums;
@@ -12,16 +14,19 @@ namespace PARR.Core.Services.Workload.Implementations
private readonly ILogger<WorkloadService> logger; private readonly ILogger<WorkloadService> logger;
private readonly WorkloadCacheService workloadCacheService; private readonly WorkloadCacheService workloadCacheService;
private readonly INextRunService nextRunService; private readonly INextRunService nextRunService;
private readonly ITaskManagementService taskManagementService;
public WorkloadService( public WorkloadService(
ILogger<WorkloadService> logger, ILogger<WorkloadService> logger,
WorkloadCacheService workloadCacheService, WorkloadCacheService workloadCacheService,
INextRunService nextRunService INextRunService nextRunService,
ITaskManagementService taskManagementService
) )
{ {
this.logger = logger; this.logger = logger;
this.workloadCacheService = workloadCacheService; this.workloadCacheService = workloadCacheService;
this.nextRunService = nextRunService; this.nextRunService = nextRunService;
this.taskManagementService = taskManagementService;
} }
@@ -107,6 +112,39 @@ namespace PARR.Core.Services.Workload.Implementations
return report; return report;
} }
public async Task<WorkloadCacheInfo> 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 };
}
/// <summary> /// <summary>
/// Получить список дней для отчета, с отметкой выходной/рабочий /// Получить список дней для отчета, с отметкой выходной/рабочий
/// </summary> /// </summary>

View File

@@ -8,6 +8,12 @@ namespace PARR.Core.Services.Workload.Interfaces
/// </summary> /// </summary>
public interface IWorkloadService public interface IWorkloadService
{ {
/// <summary>
/// Получить статус кэш/отчета
/// </summary>
/// <returns></returns>
Task<WorkloadCacheInfo> GetReportInfoAsync();
/// <summary> /// <summary>
/// Получить отчет о загруженности /// Получить отчет о загруженности
/// </summary> /// </summary>

View File

@@ -5,10 +5,19 @@
/// </summary> /// </summary>
public interface IBaseCache<T> public interface IBaseCache<T>
{ {
/// <summary>
/// Объект
/// </summary>
public T Data { get; set; } public T Data { get; set; }
/// <summary>
/// Дата формирования Cache
/// </summary>
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
/// <summary>
/// Источник Cache (class)
/// </summary>
public string Source { get; set; } public string Source { get; set; }
} }
} }

View File

@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace PARR.Domain.DTOs.Workload
{
/// <summary>
/// Инфо о состоянии КЭШ для отчета Workload
/// </summary>
public record WorkloadCacheInfo
{
/// <summary>
/// Статус отчета
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum StatusEnum
{
/// <summary>
/// Отчет формируется
/// </summary>
Processing = 1,
/// <summary>
/// Отчет сформирован
/// </summary>
Generated = 2,
/// <summary>
/// Нет отчета
/// </summary>
NotFound = 3
}
public StatusEnum Status { get; init; }
public DateTimeOffset? Date { get; init; }
}
}

View File

@@ -213,6 +213,38 @@ namespace PARR.Infrastructure.Redis
return JsonSerializer.Deserialize<T>(value); return JsonSerializer.Deserialize<T>(value);
} }
public async Task<(string Field, T? Value)?> GetFirstHashFieldAsync<T>(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<T>(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<Dictionary<string, T>> GetAllHashFieldsAsync<T>(string hashKey) public async Task<Dictionary<string, T>> GetAllHashFieldsAsync<T>(string hashKey)
{ {
// получить все записи из Hash // получить все записи из Hash