diff --git a/PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs b/PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs index 53273ac1..8a7c9d17 100644 --- a/PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs +++ b/PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs @@ -1,17 +1,15 @@ -using PARR.Domain.Enums.Workload; - -namespace PARR.API.Contracts.V1.Requests.Queries +namespace PARR.API.Contracts.V1.Requests.Queries { public record StatWorkloadTemplateReqportQuery { /// - /// Тип дополнительно фильтра для работы (ЗО, РГ) + /// Дополнительный фильтр для работ - ЗО /// - public WorkloadJobSubfliterType? JobSubfilterType { get; init; } + public string? ResponseArea { get; init; } /// - /// Значение дополнительного фильтра для работы + /// Дополнительный фильтр для работ - РГ /// - public string? JobSubfilterValue { get; init; } + public string? WorkGroup { get; init; } } } diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs index ed47af74..0a8dade1 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs @@ -57,7 +57,7 @@ namespace PARR.API.Controllers.V1.Statistics [HttpGet(ApiRoutes.Workload.WorkGroup)] public async Task GetWorkGroupReport([FromQuery] TimeZoneOffsetClient offset, [FromQuery] WorkloadBaseQuery request) { - return await GetReportAsync(WorkloadReportType.WorkGroupAll, request.DurationDays, offset.TimeZoneOffset, null); + return await GetReportAsync(WorkloadReportType.WorkGroupAll, request.DurationDays, offset.TimeZoneOffset, null, null); } @@ -69,12 +69,12 @@ namespace PARR.API.Controllers.V1.Statistics /// /// [HttpGet(ApiRoutes.Workload.WorkGroupJob)] - public async Task GetWorkGroupJobReport([FromRoute] string workGroup, [FromQuery] TimeZoneOffsetClient offset, [FromQuery] WorkloadBaseQuery request) + public async Task GetWorkGroupJobReport([FromRoute] string workGroup, [FromQuery] string? responseArea, [FromQuery] TimeZoneOffsetClient offset, [FromQuery] WorkloadBaseQuery request) { if (string.IsNullOrWhiteSpace(workGroup)) throw new AppValidationException("Не указано значение рабочей группы"); - return await GetReportAsync(WorkloadReportType.WorkGroupJob, request.DurationDays, offset.TimeZoneOffset, workGroup); + return await GetReportAsync(WorkloadReportType.WorkGroupJob, request.DurationDays, offset.TimeZoneOffset, responseArea, workGroup); } @@ -85,7 +85,7 @@ namespace PARR.API.Controllers.V1.Statistics [HttpGet(ApiRoutes.Workload.ResponseArea)] public async Task GetResponseAreaReport([FromQuery] TimeZoneOffsetClient offset, [FromQuery] WorkloadBaseQuery request) { - return await GetReportAsync(WorkloadReportType.ResponseAreaAll, request.DurationDays, offset.TimeZoneOffset, null); + return await GetReportAsync(WorkloadReportType.ResponseAreaAll, request.DurationDays, offset.TimeZoneOffset, null, null); } @@ -102,7 +102,7 @@ namespace PARR.API.Controllers.V1.Statistics if (string.IsNullOrWhiteSpace(responseArea)) throw new AppValidationException("Не указано значение зоны ответственности"); - return await GetReportAsync(WorkloadReportType.ResponseAreaWorkGroups, request.DurationDays, offset.TimeZoneOffset, responseArea); + return await GetReportAsync(WorkloadReportType.ResponseAreaWorkGroups, request.DurationDays, offset.TimeZoneOffset, responseArea, null); } @@ -147,13 +147,14 @@ namespace PARR.API.Controllers.V1.Statistics /// Универсальный метод получения отчетности /// /// - private async Task GetReportAsync(WorkloadReportType reportType, int durationDays, TimeSpan offset, string? filter) + //private async Task GetReportAsync(WorkloadReportType reportType, int durationDays, TimeSpan offset, string? filter) + private async Task GetReportAsync(WorkloadReportType reportType, int durationDays, TimeSpan offset, string? responseArea, string? workGroup) { var utcNow = DateTimeOffset.UtcNow; var localTime = utcNow.ToOffset(offset); var dateStart = DateOnly.FromDateTime(localTime.Date); - var report = await workloadService.GetWorkloadReportAsync(reportType, dateStart, durationDays, offset, filter?.Trim()); + var report = await workloadService.GetWorkloadReportAsync(reportType, dateStart, durationDays, offset, responseArea?.Trim(), workGroup?.Trim()); if (report == null) return BadRequest(new Response(null, false, new List { new ErrorModel { Message = "Необходимо сформировать отчетность." } }, "empty")); diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs index b443fa5e..7ae670fa 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs @@ -38,11 +38,11 @@ namespace PARR.API.Controllers.V1.Statistics /// /// Отчет о загруженности. Шаблоны /// - /// - /// - /// - /// - /// + /// Тип отчета (ЗО, РГ, Работа) + /// Фильтр, значиение в зависимости от типа отчета + /// Все, активированные, деактивированные + /// Дата + /// Часовой пояс /// /// [HttpGet(ApiRoutes.StatWorkloadTemplate.GetWorkloadTemplateReport)] @@ -61,13 +61,7 @@ namespace PARR.API.Controllers.V1.Statistics throw new ArgumentException(nameof(filter), "Не передано значение фильтра."); } - if (query.JobSubfilterType.HasValue && string.IsNullOrWhiteSpace(query.JobSubfilterValue)) - { - logger.LogWarning("Не передано значение дополнительного фильтра для работы"); - throw new ArgumentException(nameof(filter), "Не передано значение дополнительного фильтра для работы."); - } - - var report = await workloadService.GetTemplateReportAsync(reportType, filter, state, date, offset.TimeZoneOffset, query.JobSubfilterType, query.JobSubfilterValue); + var report = await workloadService.GetTemplateReportAsync(reportType, filter.Trim(), state, date, offset.TimeZoneOffset, query.ResponseArea?.Trim(), query.WorkGroup?.Trim()); var response = mapper.Map>(report); diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs index 812d89db..a4d8cc85 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs @@ -415,9 +415,9 @@ namespace PARR.Core.Services.Workload.Implementations /// /// Наименование ЗО или РГ или null /// - public async Task GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam) + public async Task GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? responseArea, string? workGroup) { - var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, filterParam); + var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, responseArea, workGroup); return await redisCacheService.GetCachedDataAsync(key, true); } @@ -433,9 +433,9 @@ namespace PARR.Core.Services.Workload.Implementations /// /// Наименование ЗО или РГ или null /// - public async Task SetWorkloadReport(WorkloadReport report, WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam) + public async Task SetWorkloadReport(WorkloadReport report, WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? responseArea, string? workGroup) { - var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, filterParam); + var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, responseArea, workGroup); await redisCacheService.SetCachedDataAsync(key, report, cacheWorkloadTtl, true); } @@ -462,10 +462,10 @@ namespace PARR.Core.Services.Workload.Implementations /// /// /// - private string GetReportCacheKey(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam = null) + private string GetReportCacheKey(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? responseArea, string? workGroup) { // формируе ключ, вида: основа + дата начала + продолжительность + смещение - // Если есть параметр (filterParam), добавляется параметр в виде Hash + // Если есть параметр (responseArea, workGroup), добавляется параметр в виде Hash var keyList = new List(); @@ -493,10 +493,10 @@ namespace PARR.Core.Services.Workload.Implementations var keyStr = string.Empty; - if (filterParam == null) + if (responseArea == null && workGroup == null) keyStr = redisCacheService.GetKey(keyList.ToArray()); else - keyStr = redisCacheService.GetKey(keyList.ToArray(), new string[] { filterParam }); + keyStr = redisCacheService.GetKey(keyList.ToArray(), new string[] { responseArea ?? string.Empty, workGroup ?? string.Empty }); logger.LogDebug("Сформирован ключ для кэш '{Key}'", keyStr); diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs index 07f98fe3..c1531fa7 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs @@ -8,6 +8,7 @@ using PARR.Core.Services.NextRunServices; using PARR.Core.Services.TaskServices.Helpers; using PARR.Core.Services.Workload.Interfaces; using PARR.Core.Services.Workload.Models; +using PARR.Domain.Cache.Models; using PARR.Domain.DTOs.Workload; using PARR.Domain.Entities; using PARR.Domain.Enums; @@ -45,19 +46,25 @@ namespace PARR.Core.Services.Workload.Implementations } - public async Task GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam) + public async Task GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? responseArea, string? workGroup) { logger.LogDebug("Начинаю формировать отчет о загруженности по параметрам: reportType '{ReportType}', dateStart {DateStart}, durationDays {DurationDays}, offset {Offset}", reportType.ToString(), dateStart, durationDays, offset); - if ((reportType == WorkloadReportType.WorkGroupJob || reportType == WorkloadReportType.ResponseAreaWorkGroups) && string.IsNullOrEmpty(filterParam)) + if (reportType == WorkloadReportType.WorkGroupJob && string.IsNullOrEmpty(workGroup)) { logger.LogError("Для типа отчета {ReportType} не передан параметр.", reportType.ToString()); - throw new ArgumentNullException(nameof(filterParam), $"Для отчета типа {reportType.ToString()} параметр является обязательным."); + throw new ArgumentNullException(nameof(workGroup), $"Для отчета типа {reportType.ToString()} параметр является обязательным."); + } + + if (reportType == WorkloadReportType.ResponseAreaWorkGroups && string.IsNullOrEmpty(responseArea)) + { + logger.LogError("Для типа отчета {ReportType} не передан параметр.", reportType.ToString()); + throw new ArgumentNullException(nameof(responseArea), $"Для отчета типа {reportType.ToString()} параметр является обязательным."); } // Если есть отчет в КЭШ, вернуть - var cacheReport = await workloadCacheService.GetWorkloadReportAsync(reportType, dateStart, durationDays, offset, filterParam); + var cacheReport = await workloadCacheService.GetWorkloadReportAsync(reportType, dateStart, durationDays, offset, responseArea, workGroup); if (cacheReport != null) { logger.LogDebug("Вернул отчет из кэша."); @@ -92,11 +99,10 @@ namespace PARR.Core.Services.Workload.Implementations case WorkloadReportType.ResponseAreaWorkGroups: displayName = "По зоне ответственности"; // Фильтруем по ЗО - var filteredRA = templates.Where(t => string.Equals(t.ResponseArea, filterParam, StringComparison.OrdinalIgnoreCase)).ToList(); + var filteredRA = templates.Where(t => string.Equals(t.ResponseArea, responseArea, StringComparison.OrdinalIgnoreCase)).ToList(); // Группируем работы по РГ foreach (var group in filteredRA.GroupBy(t => t.WorkGroup, StringComparer.OrdinalIgnoreCase)) statistics.Add(BuildStatisticItem(group.Key, null, group, reportDays, offset)); - //statistics.Add(BuildStatisticItem(filterParam!, filteredRA, reportDays, offset)); break; case WorkloadReportType.WorkGroupAll: displayName = "По рабочим группам"; @@ -106,7 +112,12 @@ namespace PARR.Core.Services.Workload.Implementations case WorkloadReportType.WorkGroupJob: displayName = "По рабочей группе"; // Фильтруем по РГ - var filteredWG = templates.Where(t => string.Equals(t.WorkGroup, filterParam, StringComparison.OrdinalIgnoreCase)).ToList(); + var filteredWG = templates.Where(t => string.Equals(t.WorkGroup, workGroup, StringComparison.OrdinalIgnoreCase)).ToList(); + if (!string.IsNullOrWhiteSpace(responseArea)) + { + // Дополнительно фильтруем по ЗО + filteredWG = filteredWG.Where(t => string.Equals(t.ResponseArea, responseArea, StringComparison.OrdinalIgnoreCase)).ToList(); + } // Получим все JobName из бд var jobIds = filteredWG.Select(t => t.JobId).ToHashSet(); @@ -123,11 +134,12 @@ namespace PARR.Core.Services.Workload.Implementations break; } + var report = new WorkloadReport { Type = reportType, DisplayName = displayName, - SubfilterName = filterParam, + SubfilterName = string.Join(", ", new[] { responseArea, workGroup }), ReportCacheCreatedAt = DateTimeOffset.UtcNow, TemplateCacheCreatedAt = templates.FirstOrDefault()?.CacheDate ?? DateTimeOffset.MinValue, Days = reportDays, @@ -135,7 +147,7 @@ namespace PARR.Core.Services.Workload.Implementations }; // Сохраним в кэш - await workloadCacheService.SetWorkloadReport(report, reportType, dateStart, durationDays, offset, filterParam); + await workloadCacheService.SetWorkloadReport(report, reportType, dateStart, durationDays, offset, responseArea, workGroup); logger.LogDebug("Отчет построен и сохранен в кэш"); return report; @@ -181,8 +193,8 @@ namespace PARR.Core.Services.Workload.Implementations WorkloadTemplateReportState state, DateOnly date, TimeSpan offset, - WorkloadJobSubfliterType? jobSubfilterType, - string? jobSubfilterValue + string? responseArea, + string? workGroup ) { if (string.IsNullOrWhiteSpace(filter)) @@ -243,8 +255,8 @@ namespace PARR.Core.Services.Workload.Implementations if (dbTemplates.Count == 0) return new List(); - // Если тип - работы, и нет дополнительных фильтров по ЗО или РГ, не полезем в кэш, вернем сразу - if (reportType == WorkloadTemplateReportType.Job && (!jobSubfilterType.HasValue || string.IsNullOrWhiteSpace(jobSubfilterValue))) + // Если тип - работы, и нет дополнительных фильтров по ЗО и РГ, не полезем в кэш, вернем сразу + if (reportType == WorkloadTemplateReportType.Job && string.IsNullOrWhiteSpace(responseArea) && string.IsNullOrWhiteSpace(workGroup)) { // Тут кэш не нужен, вернем сразу из бд (нет фильтров по РГ и ЗО, нужен список всех работ на дату) return FilterWorkloadJobTemplateReport(dbTemplates, null); @@ -259,41 +271,51 @@ namespace PARR.Core.Services.Workload.Implementations if (dbTemplates.Count != cacheData.Count) logger.LogWarning("Количество шаблонов в БД {DbCount} не совпадает с количеством шаблонов полученных из кэша {CacheCount}", dbTemplates.Count, cacheData.Count); + // Дополнительные фильтры + var trimmedResponseArea = responseArea?.Trim(); + var trimmedWorkGroup = workGroup?.Trim(); + // Фильтр по типу switch (reportType) { case WorkloadTemplateReportType.Job: // Смотрим есть ли дополнительные фильтры - if (jobSubfilterType.HasValue && !string.IsNullOrWhiteSpace(jobSubfilterValue)) + if (!string.IsNullOrEmpty(trimmedResponseArea) || !string.IsNullOrEmpty(trimmedWorkGroup)) { - var cacheTemplatesJobs = new HashSet(); // Фильтруем из кэша - var trimmedJobSubfilterValue = jobSubfilterValue.Trim(); - switch (jobSubfilterType) - { - case WorkloadJobSubfliterType.ResponseArea: - cacheTemplatesJobs = cacheData.Where(t => string.Equals(t.Data.ResponseArea, trimmedJobSubfilterValue, StringComparison.OrdinalIgnoreCase)) - .Select(t => t.Data.TemplateId).ToHashSet(); - break; - case WorkloadJobSubfliterType.WorkGroup: - cacheTemplatesJobs = cacheData.Where(t => string.Equals(t.Data.WorkGroup, trimmedJobSubfilterValue, StringComparison.OrdinalIgnoreCase)) - .Select(t => t.Data.TemplateId).ToHashSet(); - break; - default: - throw new ArgumentException("Неверный тип фильтра", nameof(jobSubfilterType)); - } + IEnumerable cacheTemplatesJobs = cacheData; - return FilterWorkloadJobTemplateReport(dbTemplates, cacheTemplatesJobs); + // Фильтруем по ЗО если есть + if (!string.IsNullOrEmpty(trimmedResponseArea)) + cacheTemplatesJobs = cacheTemplatesJobs.Where(t => string.Equals(t.Data.ResponseArea, trimmedResponseArea, StringComparison.OrdinalIgnoreCase)); + + // Дополнительный фильтр по РГ, если есть + if (!string.IsNullOrEmpty(trimmedWorkGroup)) + cacheTemplatesJobs = cacheTemplatesJobs.Where(t => string.Equals(t.Data.WorkGroup, trimmedWorkGroup, StringComparison.OrdinalIgnoreCase)); + + var filteredTemplateJobIds = cacheTemplatesJobs.Select(t => t.Data.TemplateId).ToHashSet(); + + return FilterWorkloadJobTemplateReport(dbTemplates, filteredTemplateJobIds); } return FilterWorkloadJobTemplateReport(dbTemplates, null); 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(); + var cacheTemplatesWg = cacheData.Where(t => string.Equals(t.Data.WorkGroup, trimmedFilter, StringComparison.OrdinalIgnoreCase)); + // Дополнительный фильтр по ЗО, если есть + if (!string.IsNullOrEmpty(trimmedResponseArea)) + cacheTemplatesWg = cacheTemplatesWg.Where(t => string.Equals(t.Data.ResponseArea, trimmedResponseArea, StringComparison.OrdinalIgnoreCase)); + + var filteredTemplateWgIds = cacheTemplatesWg.Select(t => t.Data.TemplateId).ToHashSet(); + + return dbTemplates.Where(t => filteredTemplateWgIds.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(); + var cacheTemplatesRa = cacheData.Where(t => string.Equals(t.Data.ResponseArea, trimmedFilter, StringComparison.OrdinalIgnoreCase)); + // Дополнительный фильтр по РГ если есть + if (!string.IsNullOrEmpty(trimmedWorkGroup)) + cacheTemplatesRa = cacheTemplatesRa.Where(t => string.Equals(t.Data.WorkGroup, trimmedWorkGroup, StringComparison.OrdinalIgnoreCase)); + + var filteredTemplateRaIds = cacheTemplatesRa.Select(t => t.Data.TemplateId).ToHashSet(); + + return dbTemplates.Where(t => filteredTemplateRaIds.Contains(t.TemplateId)).ToList(); } logger.LogWarning("Не обработанный тип отчета {ReportType}", reportType); diff --git a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs index 12c905f3..be742bed 100644 --- a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs +++ b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs @@ -1,4 +1,5 @@ -using PARR.Domain.DTOs.Workload; +using Microsoft.Extensions.Primitives; +using PARR.Domain.DTOs.Workload; using PARR.Domain.Enums.Workload; namespace PARR.Core.Services.Workload.Interfaces @@ -21,9 +22,10 @@ namespace PARR.Core.Services.Workload.Interfaces /// /// /// - /// + /// + /// /// - Task GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam); + Task GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? responseArea, string? workGroup); /// /// Получить отчет - список шаблонов на дату согласно фильтрам @@ -33,10 +35,18 @@ namespace PARR.Core.Services.Workload.Interfaces /// /// /// - /// Дополнительный фильтр при отчете по шаблонам, тип - /// Дополнительный фильтр при отчете по шаблонам, значение + /// Дополнительный фильтр при отчете по шаблонам, ЗО + /// Дополнительный фильтр при отчете по шаблонам, РГ /// - Task> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset, WorkloadJobSubfliterType? jobSubfilterType, string? jobSubfilterValue); + Task> GetTemplateReportAsync( + WorkloadTemplateReportType reportType, + string filter, + WorkloadTemplateReportState state, + DateOnly date, + TimeSpan offset, + string? responseArea, + string? workGroup + ); } } diff --git a/PARR.Infrastructure/Redis/RedisCacheService.cs b/PARR.Infrastructure/Redis/RedisCacheService.cs index 55f30ff5..3a3820d7 100644 --- a/PARR.Infrastructure/Redis/RedisCacheService.cs +++ b/PARR.Infrastructure/Redis/RedisCacheService.cs @@ -504,7 +504,7 @@ namespace PARR.Infrastructure.Redis if (keyPartsToHash != null && keyPartsToHash.Length > 0) { - var partsToHashStr = string.Join(mainSeparator, keyPartsToHash) + var partsToHashStr = string.Join(mainSeparator, keyPartsToHash.Where(t => !string.IsNullOrWhiteSpace(t))) .Replace(" ", wordSeparator) .ToLower();