feat(api,core,domain): StatWorkloadTemplate - статистика загруженности, шаблоны
This commit is contained in:
@@ -6,9 +6,10 @@ using PARR.Core.Services.Shortcodes;
|
||||
using PARR.Core.Services.Workload.Models;
|
||||
using PARR.Domain.Cache;
|
||||
using PARR.Domain.Cache.Models;
|
||||
using PARR.Domain.DTOs.UnitDto;
|
||||
using PARR.Domain.DTOs.Workload;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Enums.Workload;
|
||||
|
||||
namespace PARR.Core.Services.Workload.Implementations
|
||||
{
|
||||
@@ -175,6 +176,47 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
await redisCacheService.DeleteKeysByPatternAsync(redisCacheService.GetKey(CacheKeys.Workload.WorkloadWorkGroupItem()) + "*");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить список шаблонов по ИД из кэш/бд.
|
||||
/// Если каких то данных не хватает в кэше, дополнить кэш.
|
||||
/// </summary>
|
||||
/// <param name="templateIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<List<TemplateCache>?> GetTemplateReportDataByIdsAsync(IReadOnlySet<Guid> templateIds)
|
||||
{
|
||||
if (templateIds == null || templateIds.Count == 0)
|
||||
return null;
|
||||
|
||||
// Формируем ключи
|
||||
var keys = templateIds.Select(t =>
|
||||
{
|
||||
var (hashKey, fieldKey) = (redisCacheService.GetKey(CacheKeys.Workload.TemplateHash()), redisCacheService.GetKey(CacheKeys.Workload.Template(t)));
|
||||
return (HashKey: hashKey, FieldKey: fieldKey);
|
||||
}).ToList();
|
||||
|
||||
// Получаем из кэш
|
||||
var cacheData = await redisCacheService.GetHashFieldsAsync<TemplateCache>(keys);
|
||||
|
||||
var resultDictionary = cacheData.ToDictionary(t => t.Data.TemplateId, t => t);
|
||||
|
||||
if (resultDictionary.Count < templateIds.Count)
|
||||
{
|
||||
// Догружаем недостающие данные
|
||||
// Находим id которых нет в кэше
|
||||
var missingIds = templateIds.Where(id => !resultDictionary.ContainsKey(id)).ToHashSet();
|
||||
|
||||
foreach (var templateId in missingIds)
|
||||
{
|
||||
var newCacheItem = await GetTemplatePropsAsync(templateId);
|
||||
resultDictionary[templateId] = newCacheItem;
|
||||
}
|
||||
}
|
||||
|
||||
return resultDictionary.Select(t => t.Value).ToList();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить кэш РГ и ЗО всех шаблонов, получить недостающие поля из БД.
|
||||
/// Сравнит кол-во записей в БД и в КЭШ, если не хватает до 1000 шаблонов, доформирует кэш,
|
||||
@@ -196,14 +238,9 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
return null;
|
||||
}
|
||||
|
||||
// Сравним кол-во записей в БД и в КЭШ, вдруг там разница больше допустимой
|
||||
var templatesCount = await templateRepository.Get().AsNoTracking().CountAsync();
|
||||
if (templatesCount - cacheData.Count > maxCacheDiff)
|
||||
{
|
||||
logger.LogInformation("Разница в количестве шаблонов в БД ({TemplateCount}) и в КЭШ ({CacheCount}) больше допустимой {MaxCacheDiff}. Считаем КЭШ неактуальным. Требуется повторное формирование КЭШа.",
|
||||
templatesCount, cacheData.Count, maxCacheDiff);
|
||||
// Кэш актуальный?
|
||||
if (!await IsTemplateReportCacheActualAsync(cacheData.Count))
|
||||
return null;
|
||||
}
|
||||
|
||||
// Получаем из БД только нужные данные для отчета, попадающие в интервал
|
||||
var dtStart = new DateTimeOffset(dateStart.Year, dateStart.Month, dateStart.Day, 0, 0, 0, offset);
|
||||
@@ -251,7 +288,11 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
}
|
||||
else
|
||||
{
|
||||
(workGroup, responseArea, jobId) = await GetTemplatePropsAsync(template.Id);
|
||||
//(workGroup, responseArea, jobId) = await GetTemplatePropsAsync(template.Id);
|
||||
var newCacheItem = await GetTemplatePropsAsync(template.Id);
|
||||
workGroup = newCacheItem.Data.WorkGroup;
|
||||
responseArea = newCacheItem.Data.ResponseArea;
|
||||
jobId = newCacheItem.Data.JobId;
|
||||
}
|
||||
|
||||
var data = new TemplateReportData(
|
||||
@@ -272,12 +313,35 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
return reportData;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Проверка кэша на акутальность.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> IsTemplateReportCacheActualAsync(long? cacheCount = null)
|
||||
{
|
||||
long finalCacheCount = cacheCount ?? await redisCacheService.GetHashLengthAsync(redisCacheService.GetKey(CacheKeys.Workload.TemplateHash()));
|
||||
|
||||
// Сравним кол-во записей в БД и в КЭШ, вдруг там разница больше допустимой
|
||||
var templatesCount = await templateRepository.Get().AsNoTracking().CountAsync();
|
||||
if (Math.Abs(templatesCount - finalCacheCount) > maxCacheDiff)
|
||||
{
|
||||
logger.LogInformation("Разница в количестве шаблонов в БД ({TemplateCount}) и в КЭШ ({CacheCount}) больше допустимой {MaxCacheDiff}. Считаем КЭШ неактуальным. Требуется повторное формирование КЭШа.",
|
||||
templatesCount, cacheCount, maxCacheDiff);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить РГ и ЗО из шорткода. Сохранить в КЭШ.
|
||||
/// </summary>
|
||||
/// <param name="templateId"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<(string WorkGroup, string ResponseArea, Guid jobId)> GetTemplatePropsAsync(Guid templateId)
|
||||
//private async Task<(string WorkGroup, string ResponseArea, Guid jobId)> GetTemplatePropsAsync(Guid templateId)
|
||||
private async Task<TemplateCache> GetTemplatePropsAsync(Guid templateId)
|
||||
{
|
||||
logger.LogDebug("Дозагружаю недостающие данные из БД для шаблона {TemplateId}", templateId);
|
||||
|
||||
@@ -290,7 +354,20 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
if (template == null)
|
||||
{
|
||||
logger.LogWarning("Шаблон {TemplateId} не найден в БД", templateId);
|
||||
return ($"РГ {templateId}", $"ЗО {templateId}", templateId);
|
||||
//return ($"РГ {templateId}", $"ЗО {templateId}", templateId);
|
||||
return new TemplateCache
|
||||
{
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Source = "",
|
||||
Data = new TemplateCacheDto
|
||||
{
|
||||
JobGroupId = Guid.Empty,
|
||||
JobId = Guid.Empty,
|
||||
TemplateId = templateId,
|
||||
ResponseArea = $"ЗО {templateId}",
|
||||
WorkGroup = $"РГ {templateId}"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var (workGroup, responseArea) = await ApplyShortcodesAsync(template);
|
||||
@@ -311,7 +388,8 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
);
|
||||
logger.LogDebug("Добавлена недостающая запись в кэш. {TemplateId}, '{WorkGroup}', '{ResponseArea}'", templateId, workGroup, responseArea);
|
||||
|
||||
return (workGroup, responseArea, template.JobId);
|
||||
//return (workGroup, responseArea, template.JobId);
|
||||
return cacheItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Core.Repositories.Interfaces.TaskRepositories;
|
||||
using PARR.Core.Services.NextRunServices;
|
||||
@@ -9,6 +10,7 @@ using PARR.Core.Services.Workload.Interfaces;
|
||||
using PARR.Core.Services.Workload.Models;
|
||||
using PARR.Domain.DTOs.Workload;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Enums.Workload;
|
||||
|
||||
namespace PARR.Core.Services.Workload.Implementations
|
||||
{
|
||||
@@ -20,8 +22,7 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
private readonly ITaskRepository taskRepository;
|
||||
private readonly IMapper mapper;
|
||||
private readonly IJobRepository jobRepository;
|
||||
|
||||
//private readonly ITaskManagementService taskManagementService;
|
||||
private readonly ITemplateRepository templateRepository;
|
||||
|
||||
public WorkloadService(
|
||||
ILogger<WorkloadService> logger,
|
||||
@@ -29,8 +30,8 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
INextRunService nextRunService,
|
||||
ITaskRepository taskRepository,
|
||||
IMapper mapper,
|
||||
IJobRepository jobRepository
|
||||
//ITaskManagementService taskManagementService
|
||||
IJobRepository jobRepository,
|
||||
ITemplateRepository templateRepository
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
@@ -39,11 +40,11 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
this.taskRepository = taskRepository;
|
||||
this.mapper = mapper;
|
||||
this.jobRepository = jobRepository;
|
||||
//this.taskManagementService = taskManagementService;
|
||||
this.templateRepository = templateRepository;
|
||||
}
|
||||
|
||||
|
||||
public async Task<WorkloadReport?> GetWorkloadReport(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam)
|
||||
public async Task<WorkloadReport?> GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam)
|
||||
{
|
||||
logger.LogDebug("Начинаю формировать отчет о загруженности по параметрам: reportType '{ReportType}', dateStart {DateStart}, durationDays {DurationDays}, offset {Offset}",
|
||||
reportType.ToString(), dateStart, durationDays, offset);
|
||||
@@ -174,6 +175,97 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<WorkloadTemplateReport>> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
logger.LogWarning("Не передано значение фильтра");
|
||||
throw new ArgumentException(nameof(filter), "Не передано значение фильтра.");
|
||||
}
|
||||
|
||||
// Template cache вообще есть, он актуальный?
|
||||
if (!await workloadCacheService.IsTemplateReportCacheActualAsync())
|
||||
{
|
||||
logger.LogDebug("Не могу рассчитать отчет Workload, так как нет основного кэш или он не актуальный.");
|
||||
return new List<WorkloadTemplateReport>();
|
||||
}
|
||||
|
||||
var trimmedFilter = filter.Trim();
|
||||
|
||||
|
||||
// Формируем дату и время начала и конца
|
||||
var dtStart = new DateTimeOffset(date.Year, date.Month, date.Day, 0, 0, 0, offset);
|
||||
var dtEnd = dtStart.AddDays(1);
|
||||
|
||||
// Конвертируем в UTC для совместимости с PostgreSQL timestamptz
|
||||
var dtStartUtc = dtStart.ToUniversalTime();
|
||||
var dtEndUtc = dtEnd.ToUniversalTime();
|
||||
|
||||
// Запрос к БД
|
||||
var query = templateRepository.Get().AsNoTracking()
|
||||
.Where(t => dtStartUtc <= t.NextRun && t.NextRun < dtEndUtc);
|
||||
|
||||
// Фильтруем по состоянию
|
||||
switch (state)
|
||||
{
|
||||
case WorkloadTemplateReportState.All:
|
||||
break;
|
||||
case WorkloadTemplateReportState.Activated:
|
||||
query = query.Where(t => t.IsActiveTemplate && t.IsActiveSchedule);
|
||||
break;
|
||||
case WorkloadTemplateReportState.Deactivated:
|
||||
query = query.Where(t => !t.IsActiveTemplate || !t.IsActiveSchedule);
|
||||
break;
|
||||
}
|
||||
|
||||
// Если тип - работы
|
||||
if (reportType == WorkloadTemplateReportType.Job)
|
||||
{
|
||||
if (!Guid.TryParse(trimmedFilter, out Guid jobId))
|
||||
throw new ArgumentException("Значение фильтра не является Guid");
|
||||
|
||||
return await query.Where(t => t.JobId == jobId)
|
||||
.OrderBy(t => t.Name)
|
||||
.Select(t => new WorkloadTemplateReport(t.Id, t.Name, t.NextRun, t.IsActiveTemplate, t.IsActiveSchedule))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
// Тип "не работы", загружаем шаблоны
|
||||
var dbTemplates = await query.OrderBy(t => t.Name)
|
||||
.Select(t => new WorkloadTemplateReport(t.Id, t.Name, t.NextRun, t.IsActiveTemplate, t.IsActiveSchedule))
|
||||
.ToListAsync();
|
||||
|
||||
if (dbTemplates.Count == 0)
|
||||
return new List<WorkloadTemplateReport>();
|
||||
|
||||
// Получаем из кэша шаблоны (если их в кэше нет, догружаем в кэш)
|
||||
var cacheData = await workloadCacheService.GetTemplateReportDataByIdsAsync(dbTemplates.Select(t => t.TemplateId).ToHashSet());
|
||||
|
||||
if (cacheData == null)
|
||||
throw new Exception("Из кэша не получили значения.");
|
||||
|
||||
if (dbTemplates.Count != cacheData.Count)
|
||||
logger.LogWarning("Количество шаблонов в БД {DbCount} не совпадает с количеством шаблонов полученных из кэша {CacheCount}", dbTemplates.Count, cacheData.Count);
|
||||
|
||||
// Фильтр по типу
|
||||
switch (reportType)
|
||||
{
|
||||
case WorkloadTemplateReportType.WorkGroup:
|
||||
var cacheTemplatesWg = cacheData.Where(t => string.Equals(t.Data.WorkGroup, trimmedFilter, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => t.Data.TemplateId).ToHashSet();
|
||||
return dbTemplates.Where(t => cacheTemplatesWg.Contains(t.TemplateId)).ToList();
|
||||
case WorkloadTemplateReportType.ResponseArea:
|
||||
var cacheTemplatesRa = cacheData.Where(t => string.Equals(t.Data.ResponseArea, trimmedFilter, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => t.Data.TemplateId).ToHashSet();
|
||||
return dbTemplates.Where(t => cacheTemplatesRa.Contains(t.TemplateId)).ToList();
|
||||
}
|
||||
|
||||
logger.LogWarning("Не обработанный тип отчета {ReportType}", reportType);
|
||||
|
||||
return new List<WorkloadTemplateReport>();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить список дней для отчета, с отметкой выходной/рабочий
|
||||
/// </summary>
|
||||
@@ -262,6 +354,5 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
|
||||
return new WorkloadStatisticItem { DailyMetrics = dailyMetrics, Summary = summary, Title = title };
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using PARR.Domain.DTOs.Workload;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Enums.Workload;
|
||||
|
||||
namespace PARR.Core.Services.Workload.Interfaces
|
||||
{
|
||||
@@ -23,6 +23,18 @@ namespace PARR.Core.Services.Workload.Interfaces
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="filterParam"></param>
|
||||
/// <returns></returns>
|
||||
Task<WorkloadReport?> GetWorkloadReport(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam);
|
||||
Task<WorkloadReport?> GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam);
|
||||
|
||||
/// <summary>
|
||||
/// Получить отчет - список шаблонов на дату согласно фильтрам
|
||||
/// </summary>
|
||||
/// <param name="reportType"></param>
|
||||
/// <param name="filter">Фильтр, значение в зависимости от reportType</param>
|
||||
/// <param name="state"></param>
|
||||
/// <param name="date"></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<WorkloadTemplateReport>> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user