From 1baa8f22963d4cddce4a99168e1cbb7f2ae460d8 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Thu, 4 Jun 2026 17:20:16 +1000 Subject: [PATCH 1/3] =?UTF-8?q?feat(api,core,domain):=20StatWorkloadTempla?= =?UTF-8?q?te=20-=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8,=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Contracts/V1/ApiRoutes.cs | 5 + .../Statistics/StatWorkloadReportResponse.cs | 2 +- .../StatWorkloadTemplateReportResponse.cs | 4 + .../StatWorkGroupWorkLoadController.cs | 142 ------------------ .../V1/Statistics/StatWorkloadController.cs | 4 +- .../StatWorkloadTemplateController.cs | 65 ++++++++ .../DomainToResponseProfile.cs | 6 + .../Implementations/WorkloadCacheService.cs | 102 +++++++++++-- .../Implementations/WorkloadService.cs | 105 ++++++++++++- .../Workload/Interfaces/IWorkloadService.cs | 16 +- PARR.Domain/DTOs/Workload/WorkloadReport.cs | 2 +- .../DTOs/Workload/WorkloadTemplateReport.cs | 10 ++ .../{ => Workload}/WorkloadReportType.cs | 2 +- .../Workload/WorkloadTemplateReportState.cs | 26 ++++ .../Workload/WorkloadTemplateReportType.cs | 26 ++++ 15 files changed, 349 insertions(+), 168 deletions(-) create mode 100644 PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadTemplateReportResponse.cs delete mode 100644 PARR.API/Controllers/V1/Statistics/StatWorkGroupWorkLoadController.cs create mode 100644 PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs create mode 100644 PARR.Domain/DTOs/Workload/WorkloadTemplateReport.cs rename PARR.Domain/Enums/{ => Workload}/WorkloadReportType.cs (95%) create mode 100644 PARR.Domain/Enums/Workload/WorkloadTemplateReportState.cs create mode 100644 PARR.Domain/Enums/Workload/WorkloadTemplateReportType.cs diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index f4b6d161..d75ef393 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -311,6 +311,11 @@ public const string ReportInfo = BaseStat + "/workload/report-info"; } + public static class StatWorkloadTemplate + { + public const string GetWorkloadTemplateReport = BaseStat + "/workload/templates/{reportType}/{filter}/{state}/{date}"; + } + #endregion #region Наряды diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs index 560cf3e4..6d266c49 100644 --- a/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs +++ b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs @@ -1,4 +1,4 @@ -using PARR.Domain.Enums; +using PARR.Domain.Enums.Workload; namespace PARR.API.Contracts.V1.Responses.Statistics { diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadTemplateReportResponse.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadTemplateReportResponse.cs new file mode 100644 index 00000000..95acebbc --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadTemplateReportResponse.cs @@ -0,0 +1,4 @@ +namespace PARR.API.Contracts.V1.Responses.Statistics +{ + public record StatWorkloadTemplateReportResponse(Guid TemplateId, string Name, DateTimeOffset NextRun, bool IsActiveTemplate, bool IsActiveSchedule); +} diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkGroupWorkLoadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkGroupWorkLoadController.cs deleted file mode 100644 index 9fe2a151..00000000 --- a/PARR.API/Controllers/V1/Statistics/StatWorkGroupWorkLoadController.cs +++ /dev/null @@ -1,142 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using PARR.API.Contracts.V1; -using PARR.API.Contracts.V1.Requests; -using PARR.API.Contracts.V1.Requests.Queries; -using PARR.API.Contracts.V1.Responses; -using PARR.API.Contracts.V1.Responses.Base; -using PARR.API.Contracts.V1.Responses.Statistics; -using PARR.API.Controllers.V1.Base; -using PARR.API.Services.Interfaces; -using PARR.Core.Repositories.Interfaces; - -namespace PARR.API.Controllers.V1.Statistics -{ - /// - /// Статистика загрузки регламентными работами по рабочим группам - /// - public class StatWorkGroupWorkLoadController : BaseApiController - { - private readonly ITemplateRepository templateService; - //private readonly IWorkLoadService workLoadService; - //private readonly IWorkGroupService workGroupService; - private readonly IMapper mapper; - private readonly ILogger logger; - - public StatWorkGroupWorkLoadController( - ITemplateRepository templateService, - //IWorkLoadService workLoadService, - //IWorkGroupService workGroupService, - IMapper mapper, - ILogger logger - ) - { - this.templateService = templateService; - //this.workLoadService = workLoadService; - //this.workGroupService = workGroupService; - this.mapper = mapper; - this.logger = logger; - } - - /* - /// - /// Статистика загрузки регламентными работами рабочих групп по ЗО - /// - /// - /// - /// - [HttpGet(ApiRoutes.StatWorkGroupWorkLoad.Get)] - public async Task Get([FromRoute] int responseAreaCode, [FromQuery] WorkloadBaseQuery requestQuery) - { - var response = await GetStatisticsAsync(responseAreaCode, requestQuery); - - return Ok(new Response>(response, true)); - } - - - /// - /// Статистика загрузки РГ по списку РГ - /// - /// - [HttpPost(ApiRoutes.StatWorkGroupWorkLoad.GetByWorkGroups)] - public async Task GetByWorkGroups([FromBody] StatWorkGroupsWorkLoadRequest request) - { - var response = await GetStatisticsAsync(null, request.Filter, request.WorkGroups); - - return Ok(new Response>(response, true)); - } - - - private async Task> GetStatisticsAsync(int? responseAreaCode, WorkloadBaseQuery requestQuery, List? workGroups = null) - { - //защита от дурака, чтоб не выгрузить всю БД - if (responseAreaCode == null && (workGroups == null || workGroups.Count() == 0)) - { - logger.LogWarning($"Пытались выполнить запрос получения статситики с нулевыми параметрами {nameof(responseAreaCode)}, {nameof(workGroups)}. Controller {nameof(StatWorkGroupWorkLoadController)}"); - return new List(); - } - - - var daysList = workLoadService.GetDatesForPeriod(requestQuery); - - IQueryable query = templateService.Get() - .Include(t => t.Host); - - query = workLoadService.FilterTemplatesQuery(query, requestQuery); - - if (responseAreaCode.HasValue) - query = query.Where(t => t.Host!.WorkGroup!.ResponseAreaCode == responseAreaCode.Value); - - if (workGroups != null && workGroups.Count() > 0) - query = query.Where(t => workGroups.Any(x => x == t.Host!.WorkGroupId.Value)); - - - // var groupedQuery = query.GroupBy(t => new { t.NextRun.DateTime.Date, t.Host!.WorkGroupId }) - var groupedQuery = query.GroupBy(t => new { t.NextRun.AddHours(requestQuery.TimeZoneOffsetHours).DateTime.Date, t.Host!.WorkGroupId }) - .Select(t => new - { - WorkGroupId = t.Key.WorkGroupId, - Date = t.Key.Date, - TemplateCount = t.Count() - }); - - var queryResult = await groupedQuery.ToListAsync(); - - - IQueryable workGroupsQuery = workGroupService.Get() - .Include(t => t.ResponseArea); - - if (responseAreaCode.HasValue) - workGroupsQuery = workGroupsQuery.Where(t => t.ResponseAreaCode == responseAreaCode); - - if (workGroups != null && workGroups.Count() > 0) - workGroupsQuery = workGroupsQuery.Where(t => workGroups.Any(x => x == t.Id)); - - - var allWorkGroups = await workGroupsQuery.ToListAsync(); - - var response = new List(); - - allWorkGroups.ForEach(wg => - { - var workLoads = daysList.Select(t => new StatWorkLoadDataResponse - { - Date = t, - TemplateCount = queryResult.FirstOrDefault(x => x.WorkGroupId == wg.Id && DateOnly.FromDateTime(x.Date) == t)?.TemplateCount ?? 0 - }).OrderBy(t => t.Date).ToList(); - - response.Add(new StatWorkGroupWorkLoadResponse - { - WorkGroup = mapper.Map(wg), - WorkLoad = workLoads - }); - }); - - response = response.OrderBy(t => t.WorkGroup.Name).ToList(); - - return response; - } - */ - } -} diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs index fe83179c..ed47af74 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs @@ -14,8 +14,8 @@ using PARR.Core.Services.Workload.Interfaces; using PARR.Domain.Common.Roles; using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; +using PARR.Domain.Enums.Workload; using PARR.Domain.Exceptions; -using static PARR.API.Contracts.V1.ApiRoutes; namespace PARR.API.Controllers.V1.Statistics { @@ -153,7 +153,7 @@ namespace PARR.API.Controllers.V1.Statistics var localTime = utcNow.ToOffset(offset); var dateStart = DateOnly.FromDateTime(localTime.Date); - var report = await workloadService.GetWorkloadReport(reportType, dateStart, durationDays, offset, filter?.Trim()); + var report = await workloadService.GetWorkloadReportAsync(reportType, dateStart, durationDays, offset, filter?.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 new file mode 100644 index 00000000..b5b33e9a --- /dev/null +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs @@ -0,0 +1,65 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PARR.API.Contracts.V1; +using PARR.API.Contracts.V1.Requests.BaseRequests; +using PARR.API.Contracts.V1.Responses.Base; +using PARR.API.Contracts.V1.Responses.Statistics; +using PARR.API.Controllers.V1.Base; +using PARR.Core.Services.Workload.Interfaces; +using PARR.Domain.Common.Roles; +using PARR.Domain.Enums.Workload; + +namespace PARR.API.Controllers.V1.Statistics +{ + /// + /// Статситика загруженности - шаблоны + /// + [Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)] + public class StatWorkloadTemplateController : BaseApiController + { + private readonly IMapper mapper; + private readonly IWorkloadService workloadService; + private readonly ILogger logger; + + public StatWorkloadTemplateController( + IMapper mapper, + IWorkloadService workloadService, + ILogger logger + ) + { + this.mapper = mapper; + this.workloadService = workloadService; + this.logger = logger; + } + + + /// + /// Отчет о загруженности. Шаблоны + /// + /// + /// + /// + /// + /// + /// + /// + [HttpGet(ApiRoutes.StatWorkloadTemplate.GetWorkloadTemplateReport)] + public async Task GetWorkloadTemplateReport([FromRoute] WorkloadTemplateReportType reportType, [FromRoute] string filter, [FromRoute] WorkloadTemplateReportState state, [FromRoute] DateOnly date, [FromQuery] TimeZoneOffsetClient offset) + { + if (string.IsNullOrWhiteSpace(filter)) + { + logger.LogWarning("Не передано значение фильтра"); + throw new ArgumentException(nameof(filter), "Не передано значение фильтра."); + } + + var report = await workloadService.GetTemplateReportAsync(reportType, filter, state, date, offset.TimeZoneOffset); + + var response = mapper.Map>(report); + + return Ok(new Response>(response, true)); + } + + + } +} diff --git a/PARR.API/MappingProfiles/DomainToResponseProfile.cs b/PARR.API/MappingProfiles/DomainToResponseProfile.cs index 5735968c..f71c1276 100644 --- a/PARR.API/MappingProfiles/DomainToResponseProfile.cs +++ b/PARR.API/MappingProfiles/DomainToResponseProfile.cs @@ -460,6 +460,12 @@ namespace PARR.API.MappingProfiles #endregion + #region StatWorkloadTemplate + + CreateMap(); + + #endregion + #region Task diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs index 7f384ae5..812d89db 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs @@ -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()) + "*"); } + + /// + /// Получить список шаблонов по ИД из кэш/бд. + /// Если каких то данных не хватает в кэше, дополнить кэш. + /// + /// + /// + public async Task?> GetTemplateReportDataByIdsAsync(IReadOnlySet 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(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(); + } + + /// /// Получить кэш РГ и ЗО всех шаблонов, получить недостающие поля из БД. /// Сравнит кол-во записей в БД и в КЭШ, если не хватает до 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; } + + /// + /// Проверка кэша на акутальность. + /// + /// + public async Task 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; + } + + /// /// Получить РГ и ЗО из шорткода. Сохранить в КЭШ. /// /// /// - 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 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; } /// diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs index 2511396a..8f533012 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs @@ -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 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 GetWorkloadReport(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam) + public async Task 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> 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(); + } + + 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(); + + // Получаем из кэша шаблоны (если их в кэше нет, догружаем в кэш) + 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(); + } + + /// /// Получить список дней для отчета, с отметкой выходной/рабочий /// @@ -262,6 +354,5 @@ namespace PARR.Core.Services.Workload.Implementations return new WorkloadStatisticItem { DailyMetrics = dailyMetrics, Summary = summary, Title = title }; } - } } diff --git a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs index aae196c5..f44595c4 100644 --- a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs +++ b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs @@ -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 /// /// /// - Task GetWorkloadReport(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam); + Task GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam); + + /// + /// Получить отчет - список шаблонов на дату согласно фильтрам + /// + /// + /// Фильтр, значение в зависимости от reportType + /// + /// + /// + /// + Task> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset); + } } diff --git a/PARR.Domain/DTOs/Workload/WorkloadReport.cs b/PARR.Domain/DTOs/Workload/WorkloadReport.cs index ed312a08..e3956864 100644 --- a/PARR.Domain/DTOs/Workload/WorkloadReport.cs +++ b/PARR.Domain/DTOs/Workload/WorkloadReport.cs @@ -1,4 +1,4 @@ -using PARR.Domain.Enums; +using PARR.Domain.Enums.Workload; namespace PARR.Domain.DTOs.Workload { diff --git a/PARR.Domain/DTOs/Workload/WorkloadTemplateReport.cs b/PARR.Domain/DTOs/Workload/WorkloadTemplateReport.cs new file mode 100644 index 00000000..6753cb0b --- /dev/null +++ b/PARR.Domain/DTOs/Workload/WorkloadTemplateReport.cs @@ -0,0 +1,10 @@ +namespace PARR.Domain.DTOs.Workload +{ + /// + /// Отчет о шаблонах, для отчета о нагруженности + /// + /// + /// + /// + public record WorkloadTemplateReport(Guid TemplateId, string Name, DateTimeOffset NextRun, bool IsActiveTemplate, bool IsActiveSchedule); +} diff --git a/PARR.Domain/Enums/WorkloadReportType.cs b/PARR.Domain/Enums/Workload/WorkloadReportType.cs similarity index 95% rename from PARR.Domain/Enums/WorkloadReportType.cs rename to PARR.Domain/Enums/Workload/WorkloadReportType.cs index 57be84a9..bbc1c4eb 100644 --- a/PARR.Domain/Enums/WorkloadReportType.cs +++ b/PARR.Domain/Enums/Workload/WorkloadReportType.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace PARR.Domain.Enums +namespace PARR.Domain.Enums.Workload { /// /// Тип отчета о загруженности diff --git a/PARR.Domain/Enums/Workload/WorkloadTemplateReportState.cs b/PARR.Domain/Enums/Workload/WorkloadTemplateReportState.cs new file mode 100644 index 00000000..7b2c1a3d --- /dev/null +++ b/PARR.Domain/Enums/Workload/WorkloadTemplateReportState.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace PARR.Domain.Enums.Workload +{ + /// + /// Тип отчета о загруженности, состояние + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum WorkloadTemplateReportState + { + /// + /// Все + /// + All = 0, + + /// + /// Активированные + /// + Activated = 1, + + /// + /// Деактивированные + /// + Deactivated = 2 + } +} diff --git a/PARR.Domain/Enums/Workload/WorkloadTemplateReportType.cs b/PARR.Domain/Enums/Workload/WorkloadTemplateReportType.cs new file mode 100644 index 00000000..b8680aab --- /dev/null +++ b/PARR.Domain/Enums/Workload/WorkloadTemplateReportType.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace PARR.Domain.Enums.Workload +{ + /// + /// Тип отчета о загруженности, шаблоны + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum WorkloadTemplateReportType + { + /// + /// Зона ответственности + /// + ResponseArea = 0, + + /// + /// Рабочая группа + /// + WorkGroup = 1, + + /// + /// Работа + /// + Job = 2 + } +} From 8cb7a2d59907bd79dd477798f1093eb7cf29ee3c Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Fri, 5 Jun 2026 10:28:15 +1000 Subject: [PATCH 2/3] =?UTF-8?q?feat(api,=20domain,=20core):=20StatWorkload?= =?UTF-8?q?Template=20-=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=A0?= =?UTF-8?q?=D0=93=20=D0=B8=20=D0=97=D0=9E.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StatWorkloadTemplateReqportQuery.cs | 17 +++++ .../Statistics/StatWorkloadReportResponse.cs | 2 + .../StatWorkloadTemplateController.cs | 18 ++++- .../Implementations/WorkloadService.cs | 74 +++++++++++++++---- .../Workload/Interfaces/IWorkloadService.cs | 4 +- PARR.Domain/DTOs/Workload/WorkloadReport.cs | 6 ++ .../Workload/WorkloadJobSubfliterType.cs | 22 ++++++ 7 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs create mode 100644 PARR.Domain/Enums/Workload/WorkloadJobSubfliterType.cs diff --git a/PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs b/PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs new file mode 100644 index 00000000..53273ac1 --- /dev/null +++ b/PARR.API/Contracts/V1/Requests/Queries/StatWorkloadTemplateReqportQuery.cs @@ -0,0 +1,17 @@ +using PARR.Domain.Enums.Workload; + +namespace PARR.API.Contracts.V1.Requests.Queries +{ + public record StatWorkloadTemplateReqportQuery + { + /// + /// Тип дополнительно фильтра для работы (ЗО, РГ) + /// + public WorkloadJobSubfliterType? JobSubfilterType { get; init; } + + /// + /// Значение дополнительного фильтра для работы + /// + public string? JobSubfilterValue { get; init; } + } +} diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs index 6d266c49..c7dac959 100644 --- a/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs +++ b/PARR.API/Contracts/V1/Responses/Statistics/StatWorkloadReportResponse.cs @@ -72,6 +72,8 @@ namespace PARR.API.Contracts.V1.Responses.Statistics { public required string Title { get; init; } + public Guid? ObjId { get; init; } + public required WorkloadSummaryResponse Summary { get; init; } public required List DailyMetrics { get; init; } diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs index b5b33e9a..b443fa5e 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadTemplateController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests.BaseRequests; +using PARR.API.Contracts.V1.Requests.Queries; using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Contracts.V1.Responses.Statistics; using PARR.API.Controllers.V1.Base; @@ -45,7 +46,14 @@ namespace PARR.API.Controllers.V1.Statistics /// /// [HttpGet(ApiRoutes.StatWorkloadTemplate.GetWorkloadTemplateReport)] - public async Task GetWorkloadTemplateReport([FromRoute] WorkloadTemplateReportType reportType, [FromRoute] string filter, [FromRoute] WorkloadTemplateReportState state, [FromRoute] DateOnly date, [FromQuery] TimeZoneOffsetClient offset) + public async Task GetWorkloadTemplateReport( + [FromRoute] WorkloadTemplateReportType reportType, + [FromRoute] string filter, + [FromRoute] WorkloadTemplateReportState state, + [FromRoute] DateOnly date, + [FromQuery] TimeZoneOffsetClient offset, + [FromQuery] StatWorkloadTemplateReqportQuery query + ) { if (string.IsNullOrWhiteSpace(filter)) { @@ -53,7 +61,13 @@ namespace PARR.API.Controllers.V1.Statistics throw new ArgumentException(nameof(filter), "Не передано значение фильтра."); } - var report = await workloadService.GetTemplateReportAsync(reportType, filter, state, date, offset.TimeZoneOffset); + 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 response = mapper.Map>(report); diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs index 8f533012..07f98fe3 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs @@ -9,6 +9,7 @@ using PARR.Core.Services.TaskServices.Helpers; using PARR.Core.Services.Workload.Interfaces; using PARR.Core.Services.Workload.Models; using PARR.Domain.DTOs.Workload; +using PARR.Domain.Entities; using PARR.Domain.Enums; using PARR.Domain.Enums.Workload; @@ -55,7 +56,6 @@ namespace PARR.Core.Services.Workload.Implementations throw new ArgumentNullException(nameof(filterParam), $"Для отчета типа {reportType.ToString()} параметр является обязательным."); } - // Если есть отчет в КЭШ, вернуть var cacheReport = await workloadCacheService.GetWorkloadReportAsync(reportType, dateStart, durationDays, offset, filterParam); if (cacheReport != null) @@ -87,7 +87,7 @@ namespace PARR.Core.Services.Workload.Implementations case WorkloadReportType.ResponseAreaAll: displayName = "По зонам ответственности"; foreach (var group in templates.GroupBy(t => t.ResponseArea, StringComparer.OrdinalIgnoreCase)) - statistics.Add(BuildStatisticItem(group.Key, group, reportDays, offset)); + statistics.Add(BuildStatisticItem(group.Key, null, group, reportDays, offset)); break; case WorkloadReportType.ResponseAreaWorkGroups: displayName = "По зоне ответственности"; @@ -95,13 +95,13 @@ namespace PARR.Core.Services.Workload.Implementations var filteredRA = templates.Where(t => string.Equals(t.ResponseArea, filterParam, StringComparison.OrdinalIgnoreCase)).ToList(); // Группируем работы по РГ foreach (var group in filteredRA.GroupBy(t => t.WorkGroup, StringComparer.OrdinalIgnoreCase)) - statistics.Add(BuildStatisticItem(group.Key, group, reportDays, offset)); + statistics.Add(BuildStatisticItem(group.Key, null, group, reportDays, offset)); //statistics.Add(BuildStatisticItem(filterParam!, filteredRA, reportDays, offset)); break; case WorkloadReportType.WorkGroupAll: displayName = "По рабочим группам"; foreach (var group in templates.GroupBy(t => t.WorkGroup)) - statistics.Add(BuildStatisticItem(group.Key, group, reportDays, offset)); + statistics.Add(BuildStatisticItem(group.Key, null, group, reportDays, offset)); break; case WorkloadReportType.WorkGroupJob: displayName = "По рабочей группе"; @@ -118,7 +118,7 @@ namespace PARR.Core.Services.Workload.Implementations foreach (var group in filteredWG.GroupBy(t => t.JobId)) { var jobName = jobs.GetValueOrDefault(group.Key) ?? "Работа"; - statistics.Add(BuildStatisticItem(jobName, group, reportDays, offset)); + statistics.Add(BuildStatisticItem(jobName, group.Key, group, reportDays, offset)); } break; } @@ -175,7 +175,15 @@ namespace PARR.Core.Services.Workload.Implementations } - public async Task> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset) + public async Task> GetTemplateReportAsync( + WorkloadTemplateReportType reportType, + string filter, + WorkloadTemplateReportState state, + DateOnly date, + TimeSpan offset, + WorkloadJobSubfliterType? jobSubfilterType, + string? jobSubfilterValue + ) { if (string.IsNullOrWhiteSpace(filter)) { @@ -218,19 +226,16 @@ namespace PARR.Core.Services.Workload.Implementations 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(); + query = query.Where(t => t.JobId == jobId); } - // Тип "не работы", загружаем шаблоны + // Загружаем шаблоны var dbTemplates = await query.OrderBy(t => t.Name) .Select(t => new WorkloadTemplateReport(t.Id, t.Name, t.NextRun, t.IsActiveTemplate, t.IsActiveSchedule)) .ToListAsync(); @@ -238,6 +243,13 @@ namespace PARR.Core.Services.Workload.Implementations if (dbTemplates.Count == 0) return new List(); + // Если тип - работы, и нет дополнительных фильтров по ЗО или РГ, не полезем в кэш, вернем сразу + if (reportType == WorkloadTemplateReportType.Job && (!jobSubfilterType.HasValue || string.IsNullOrWhiteSpace(jobSubfilterValue))) + { + // Тут кэш не нужен, вернем сразу из бд (нет фильтров по РГ и ЗО, нужен список всех работ на дату) + return FilterWorkloadJobTemplateReport(dbTemplates, null); + } + // Получаем из кэша шаблоны (если их в кэше нет, догружаем в кэш) var cacheData = await workloadCacheService.GetTemplateReportDataByIdsAsync(dbTemplates.Select(t => t.TemplateId).ToHashSet()); @@ -250,6 +262,30 @@ namespace PARR.Core.Services.Workload.Implementations // Фильтр по типу switch (reportType) { + case WorkloadTemplateReportType.Job: + // Смотрим есть ли дополнительные фильтры + if (jobSubfilterType.HasValue && !string.IsNullOrWhiteSpace(jobSubfilterValue)) + { + 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)); + } + + return FilterWorkloadJobTemplateReport(dbTemplates, cacheTemplatesJobs); + } + 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(); @@ -266,6 +302,16 @@ namespace PARR.Core.Services.Workload.Implementations } + private List FilterWorkloadJobTemplateReport(List dbTemplates, HashSet? filteredCacheTemplates) + { + if (filteredCacheTemplates == null) + return dbTemplates; + + // Отсекаем шаблоны которых нет в кэше + return dbTemplates.Where(t => filteredCacheTemplates.Contains(t.TemplateId)).ToList(); + } + + /// /// Получить список дней для отчета, с отметкой выходной/рабочий /// @@ -347,12 +393,12 @@ namespace PARR.Core.Services.Workload.Implementations /// /// /// - private WorkloadStatisticItem BuildStatisticItem(string title, IEnumerable groupTemplates, List reportDays, TimeSpan offset) + private WorkloadStatisticItem BuildStatisticItem(string title, Guid? objId, IEnumerable groupTemplates, List reportDays, TimeSpan offset) { var dailyMetrics = GetDailyMetrics(groupTemplates.ToList(), reportDays, offset); var summary = GetSummary(dailyMetrics, reportDays.Count); - return new WorkloadStatisticItem { DailyMetrics = dailyMetrics, Summary = summary, Title = title }; + return new WorkloadStatisticItem { DailyMetrics = dailyMetrics, Summary = summary, Title = title, ObjId = objId }; } } } diff --git a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs index f44595c4..12c905f3 100644 --- a/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs +++ b/PARR.Core/Services/Workload/Interfaces/IWorkloadService.cs @@ -33,8 +33,10 @@ namespace PARR.Core.Services.Workload.Interfaces /// /// /// + /// Дополнительный фильтр при отчете по шаблонам, тип + /// Дополнительный фильтр при отчете по шаблонам, значение /// - Task> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset); + Task> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset, WorkloadJobSubfliterType? jobSubfilterType, string? jobSubfilterValue); } } diff --git a/PARR.Domain/DTOs/Workload/WorkloadReport.cs b/PARR.Domain/DTOs/Workload/WorkloadReport.cs index e3956864..862577ab 100644 --- a/PARR.Domain/DTOs/Workload/WorkloadReport.cs +++ b/PARR.Domain/DTOs/Workload/WorkloadReport.cs @@ -73,6 +73,12 @@ namespace PARR.Domain.DTOs.Workload { public required string Title { get; init; } + /// + /// ИД строки, если есть. + /// Например JobId + /// + public Guid? ObjId { get; init; } + public required WorkloadSummary Summary { get; init; } public required List DailyMetrics { get; init; } diff --git a/PARR.Domain/Enums/Workload/WorkloadJobSubfliterType.cs b/PARR.Domain/Enums/Workload/WorkloadJobSubfliterType.cs new file mode 100644 index 00000000..0d0954f0 --- /dev/null +++ b/PARR.Domain/Enums/Workload/WorkloadJobSubfliterType.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace PARR.Domain.Enums.Workload +{ + /// + /// Дополнительный фильтр для работ. + /// Отчет Workload, шаблоны + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum WorkloadJobSubfliterType + { + /// + /// ЗО + /// + ResponseArea = 0, + + /// + /// РГ + /// + WorkGroup=1 + } +} From 6fa9c3d4454c3618c23ccf71d594c5c6e34f6071 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 9 Jun 2026 10:16:32 +1000 Subject: [PATCH 3/3] =?UTF-8?q?feat(api,core,infrastructure):=20Workload?= =?UTF-8?q?=20-=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D1=81=D0=B2?= =?UTF-8?q?=D1=8F=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=20=D0=97=D0=9E=20=D0=B8=20?= =?UTF-8?q?=D0=A0=D0=93.=20RedisCacheService=20-=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20hash=20key,=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD?= =?UTF-8?q?=D0=B0=20null,=20empty.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StatWorkloadTemplateReqportQuery.cs | 12 +-- .../V1/Statistics/StatWorkloadController.cs | 15 +-- .../StatWorkloadTemplateController.cs | 18 ++-- .../Implementations/WorkloadCacheService.cs | 16 ++-- .../Implementations/WorkloadService.cs | 94 ++++++++++++------- .../Workload/Interfaces/IWorkloadService.cs | 22 +++-- .../Redis/RedisCacheService.cs | 2 +- 7 files changed, 102 insertions(+), 77 deletions(-) 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();