diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index f5aeaa65..f4b6d161 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -302,6 +302,8 @@ public const string WorkGroup = BaseStat + "/workload/work-groups"; + public const string WorkGroupJob = BaseStat + "/workload/work-groups/{workGroup}/jobs"; + public const string ResponseArea = BaseStat + "/workload/response-areas"; public const string ResponseAreaWorkGroups = BaseStat + "/workload/response-areas/{responseArea}/work-groups"; diff --git a/PARR.API/Controllers/V1/Statistics/StatWorkWorkLoadController.cs b/PARR.API/Controllers/V1/Statistics/StatWorkWorkLoadController.cs deleted file mode 100644 index c051499e..00000000 --- a/PARR.API/Controllers/V1/Statistics/StatWorkWorkLoadController.cs +++ /dev/null @@ -1,162 +0,0 @@ -using AutoMapper; -using PARR.API.Controllers.V1.Base; -using PARR.API.Services.Interfaces; -using PARR.Core.Repositories.Interfaces; - -namespace PARR.API.Controllers.V1.Statistics -{ - /// - /// Статистика загрузки регламентными работами по Видам работ(ApplicationInWorks) - /// - public class StatWorkWorkLoadController : BaseApiController - { - private readonly ITemplateRepository templateService; - //private readonly IWorkLoadService workLoadService; - private readonly IMapper mapper; - private readonly ILogger logger; - //private readonly IWorkGroupService workGroupService; - - public StatWorkWorkLoadController( - ITemplateRepository templateService, - //IWorkLoadService workLoadService, - IMapper mapper, - ILogger logger - //IWorkGroupService workGroupService - ) - { - this.templateService = templateService; - //this.workLoadService = workLoadService; - this.mapper = mapper; - this.logger = logger; - //this.workGroupService = workGroupService; - } - - - ///// - ///// Статистика загрузки регламентными работами по Видам работ(ApplicationInWorks) - ///// - ///// - ///// - ///// - //[HttpGet(ApiRoutes.StatWorkWorkLoad.Get)] - //public async Task Get([FromRoute] Guid workGroupId, [FromQuery] WorkloadBaseQuery requestQuery) - //{ - // var response = await GetStatisticsAsync(workGroupId, requestQuery); - - // return Ok(new Response>(response, true)); - //} - - - ///// - ///// Статистика загрузки РР по списку РР - ///// - ///// - ///// - //[HttpPost(ApiRoutes.StatWorkWorkLoad.GetByApplicationInWork)] - //public async Task GetByApplicationInWork([FromBody] StatWorkWorkLoadRequest requestBody) - //{ - // var response = await GetStatisticsAsync(null, requestBody.Filter, requestBody.JobsRequests); - - // return Ok(new Response>(response, true)); - //} - - - //private async Task> GetStatisticsAsync(Guid? workGroupId, WorkloadBaseQuery requestQuery, List? jobsRequests = null) - //{ - // //защита от дурака, чтоб не выгрузить всю БД - // if (workGroupId == null && (jobsRequests == null || jobsRequests.Count() == 0)) - // { - // logger.LogWarning($"Пытались выполнить запрос получения статситики с нулевыми параметрами {nameof(workGroupId)}, {nameof(jobsRequests)}. Controller {nameof(StatWorkWorkLoadController)}"); - // return new List(); - // } - - - // var daysList = workLoadService.GetDatesForPeriod(requestQuery); - - // IQueryable query = templateService.Get() - // .Include(t => t.Host); - - // query = workLoadService.FilterTemplatesQuery(query, requestQuery); - - // if (workGroupId.HasValue) - // query = query.Where(t => t.Host!.WorkGroupId == workGroupId.Value); - - // if (jobsRequests != null && jobsRequests.Count() > 0) - // //query = query.Where(t => jobsRequests.Any(x => x.JobId == t.ApplicationInWorkId && x.WorkGroupId == t.Host!.WorkGroupId.Value)); /*&& x.WorkGroupId == t.Host!.WorkGroupId.Value*/ - // query = query.Where(t => jobsRequests.Select(t => t.JobId).Any(x => x == t.ApplicationInWorkId) && jobsRequests.Select(t => t.WorkGroupId).Any(x => x == t.Host!.WorkGroupId.Value)); - - // //var groupedQuery = query.GroupBy(t => new { t.NextRun.DateTime.Date, t.ApplicationInWorkId, t.Host.WorkGroupId }) - // var groupedQuery = query.GroupBy(t => new { t.NextRun.AddHours(requestQuery.TimeZoneOffsetHours).DateTime.Date, t.ApplicationInWorkId, t.Host.WorkGroupId }) - // .Select(t => new - // { - // ApplicationInWorkId = t.Key.ApplicationInWorkId, - // WorkGroupId = t.Key.WorkGroupId, - // Date = t.Key.Date, - // TemplateCount = t.Count() - // }); - - // var queryResult = await groupedQuery.ToListAsync(); - - - // // получаем только работы по которым есть шаблоны (активированные/деакт) и за любой период - // IQueryable appInWorksQuery = templateService.Get() - // .Include(t => t.Host) - // .Include(t => t.ApplicationsInWork).ThenInclude(t => t.Application).ThenInclude(t => t.ApplicationType); - - // IQueryable workGroupsQuery = workGroupService.Get() - // .Include(t => t.ResponseArea); - - - // if (workGroupId.HasValue) - // { - // appInWorksQuery = appInWorksQuery.Where(t => t.Host.WorkGroupId == workGroupId); - // workGroupsQuery = workGroupsQuery.Where(t => t.Id == workGroupId.Value); - // } - - // if (jobsRequests != null && jobsRequests.Count() > 0) - // { - // //appInWorksQuery = appInWorksQuery.Where(t => jobsRequests.Any(x => x.JobId == t.ApplicationInWorkId)); - // appInWorksQuery = appInWorksQuery.Where(t => jobsRequests.Select(j => j.JobId).Any(x => x == t.ApplicationInWorkId)); - // workGroupsQuery = workGroupsQuery.Where(t => jobsRequests.Select(w => w.WorkGroupId).Any(x => x == t.Id)); - // } - - - // var appInWorks = await appInWorksQuery.Select(t => t.ApplicationsInWork) - // .Distinct() - // .ToListAsync(); - - // var workGroups = await workGroupsQuery.Distinct().ToListAsync(); - - - // var response = new List(); - - // appInWorks.ForEach(appInWork => - // { - // workGroups.ForEach(workGroup => - // { - // var workLoads = daysList.Select(t => new StatWorkLoadDataResponse - // { - // Date = t, - // TemplateCount = queryResult.FirstOrDefault(x => x.ApplicationInWorkId == appInWork.Id && DateOnly.FromDateTime(x.Date) == t && x.WorkGroupId == workGroup.Id)?.TemplateCount ?? 0 - // }).OrderBy(t => t.Date).ToList(); - - // response.Add(new StatWorkWorkLoadResponse - // { - // Job = mapper.Map(appInWork), - // WorkGroup = mapper.Map(workGroup), - // WorkLoad = workLoads - // }); - // }); - // }); - - // // убираем не нужные значения так как запрос в SQL (query) был с несколькими Any - // if (jobsRequests != null && jobsRequests.Count() > 0) - // response = response.Where(t => jobsRequests.Any(x => x.WorkGroupId == t.WorkGroup.Id && x.JobId == t.Job.Id)).ToList(); - - // response = response.OrderBy(t => t.Job.ShortDescription).ThenBy(t => t.Job.Application?.Name).ThenBy(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 9332551c..fe83179c 100644 --- a/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatWorkloadController.cs @@ -15,6 +15,7 @@ using PARR.Domain.Common.Roles; using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; using PARR.Domain.Exceptions; +using static PARR.API.Contracts.V1.ApiRoutes; namespace PARR.API.Controllers.V1.Statistics { @@ -60,6 +61,23 @@ namespace PARR.API.Controllers.V1.Statistics } + /// + /// Отчет о загруженности по РГ - работы + /// + /// + /// + /// + /// + [HttpGet(ApiRoutes.Workload.WorkGroupJob)] + public async Task GetWorkGroupJobReport([FromRoute] string workGroup, [FromQuery] TimeZoneOffsetClient offset, [FromQuery] WorkloadBaseQuery request) + { + if (string.IsNullOrWhiteSpace(workGroup)) + throw new AppValidationException("Не указано значение рабочей группы"); + + return await GetReportAsync(WorkloadReportType.WorkGroupJob, request.DurationDays, offset.TimeZoneOffset, workGroup); + } + + /// /// Отчет о загруженности по всем ЗО /// diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs index 3f491e8a..7f384ae5 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs @@ -37,10 +37,6 @@ namespace PARR.Core.Services.Workload.Implementations //todo:----------- Настройки, вынести ----------- - ///// - ///// Ключ КЭШ шаблонов - ///// - //private readonly string[] cacheTemplateKey = { "template", "report", "params" }; /// /// TTL кэша шаблонов @@ -52,26 +48,6 @@ namespace PARR.Core.Services.Workload.Implementations /// private readonly int maxCacheDiff = 1000; - ///// - ///// Ключ КЭШ, отчет о загруженности, все ЗО - ///// - //private readonly string[] cacheWorkloadResponseAreaAllKey = { "workload", "response area", "all" }; - - ///// - ///// Ключ КЭШ, отчет о загруженности, одна ЗО - ///// - //private readonly string[] cacheWorkloadResponseAreaItemKey = { "workload", "response area", "item" }; - - ///// - ///// Ключ КЭШ, отчет о загруженности, все РГ - ///// - //private readonly string[] cacheWorkloadWorkGroupAllKey = { "workload", "work group", "all" }; - - ///// - ///// Ключ КЭШ, отчет о загруженности, одна РГ - ///// - //private readonly string[] cacheWorkloadWorkGroupItemKey = { "workload", "work group", "item" }; - /// /// TTL кэша отчетов workload /// @@ -213,7 +189,6 @@ namespace PARR.Core.Services.Workload.Implementations logger.LogDebug("Получаю данные для формирования отчета. DateStart {DateStart}, DurationDays {DurationDays}, Offset {Offset}", dateStart, durationDays, offset); // Получим кэш целиком - //var cacheData = await redisCacheService.GetAllHashFieldsAsync(redisCacheService.GetKey(cacheTemplateKey)); var cacheData = await redisCacheService.GetAllHashFieldsAsync(redisCacheService.GetKey(CacheKeys.Workload.TemplateHash())); if (cacheData.Count == 0) { @@ -222,7 +197,7 @@ namespace PARR.Core.Services.Workload.Implementations } // Сравним кол-во записей в БД и в КЭШ, вдруг там разница больше допустимой - var templatesCount = await templateRepository.Get().Select(t => t.Id).CountAsync(); + var templatesCount = await templateRepository.Get().AsNoTracking().CountAsync(); if (templatesCount - cacheData.Count > maxCacheDiff) { logger.LogInformation("Разница в количестве шаблонов в БД ({TemplateCount}) и в КЭШ ({CacheCount}) больше допустимой {MaxCacheDiff}. Считаем КЭШ неактуальным. Требуется повторное формирование КЭШа.", @@ -243,7 +218,14 @@ namespace PARR.Core.Services.Workload.Implementations var templates = await templateRepository.Get() .AsNoTracking() .Where(t => dtStartUtc <= t.NextRun && t.NextRun < dtEndUtc) - .Select(t => new { Id = t.Id, IsActiveTemplate = t.IsActiveTemplate, IsActiveSchedule = t.IsActiveSchedule, Name = t.Name, NextRun = t.NextRun }) + .Select(t => new + { + Id = t.Id, + IsActiveTemplate = t.IsActiveTemplate, + IsActiveSchedule = t.IsActiveSchedule, + Name = t.Name, + NextRun = t.NextRun + }) .ToListAsync(); logger.LogDebug("Загружено шаблонов из БД {Count}", templates.Count); @@ -258,16 +240,18 @@ namespace PARR.Core.Services.Workload.Implementations var (workGroup, responseArea) = ("", ""); var cacheDate = DateTimeOffset.UtcNow; + var jobId = Guid.Empty; if (cache != null) { workGroup = cache.Data.WorkGroup; responseArea = cache.Data.ResponseArea; cacheDate = cache.Timestamp; + jobId = cache.Data.JobId; } else { - (workGroup, responseArea) = await GetTemplateProps(template.Id); + (workGroup, responseArea, jobId) = await GetTemplatePropsAsync(template.Id); } var data = new TemplateReportData( @@ -278,7 +262,8 @@ namespace PARR.Core.Services.Workload.Implementations template.NextRun, template.IsActiveTemplate, template.IsActiveSchedule, - cacheDate); + cacheDate, + jobId); reportData.Add(data); } @@ -292,7 +277,7 @@ namespace PARR.Core.Services.Workload.Implementations /// /// /// - private async Task<(string WorkGroup, string ResponseArea)> GetTemplateProps(Guid templateId) + private async Task<(string WorkGroup, string ResponseArea, Guid jobId)> GetTemplatePropsAsync(Guid templateId) { logger.LogDebug("Дозагружаю недостающие данные из БД для шаблона {TemplateId}", templateId); @@ -305,7 +290,7 @@ namespace PARR.Core.Services.Workload.Implementations if (template == null) { logger.LogWarning("Шаблон {TemplateId} не найден в БД", templateId); - return ($"РГ {templateId}", $"ЗО {templateId}"); + return ($"РГ {templateId}", $"ЗО {templateId}", templateId); } var (workGroup, responseArea) = await ApplyShortcodesAsync(template); @@ -326,7 +311,7 @@ namespace PARR.Core.Services.Workload.Implementations ); logger.LogDebug("Добавлена недостающая запись в кэш. {TemplateId}, '{WorkGroup}', '{ResponseArea}'", templateId, workGroup, responseArea); - return (workGroup, responseArea); + return (workGroup, responseArea, template.JobId); } /// @@ -418,7 +403,7 @@ namespace PARR.Core.Services.Workload.Implementations case WorkloadReportType.WorkGroupAll: keyList.AddRange(CacheKeys.Workload.WorkloadWorkGroupAll()); break; - case WorkloadReportType.WorkGroupItem: + case WorkloadReportType.WorkGroupJob: keyList.AddRange(CacheKeys.Workload.WorkloadWorkGroupItem()); break; default: diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs index 6bb693a7..2511396a 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadService.cs @@ -1,5 +1,7 @@ using AutoMapper; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.TaskRepositories; using PARR.Core.Services.NextRunServices; using PARR.Core.Services.TaskServices.Helpers; @@ -17,6 +19,7 @@ namespace PARR.Core.Services.Workload.Implementations private readonly INextRunService nextRunService; private readonly ITaskRepository taskRepository; private readonly IMapper mapper; + private readonly IJobRepository jobRepository; //private readonly ITaskManagementService taskManagementService; @@ -25,7 +28,8 @@ namespace PARR.Core.Services.Workload.Implementations WorkloadCacheService workloadCacheService, INextRunService nextRunService, ITaskRepository taskRepository, - IMapper mapper + IMapper mapper, + IJobRepository jobRepository //ITaskManagementService taskManagementService ) { @@ -34,6 +38,7 @@ namespace PARR.Core.Services.Workload.Implementations this.nextRunService = nextRunService; this.taskRepository = taskRepository; this.mapper = mapper; + this.jobRepository = jobRepository; //this.taskManagementService = taskManagementService; } @@ -43,7 +48,7 @@ namespace PARR.Core.Services.Workload.Implementations logger.LogDebug("Начинаю формировать отчет о загруженности по параметрам: reportType '{ReportType}', dateStart {DateStart}, durationDays {DurationDays}, offset {Offset}", reportType.ToString(), dateStart, durationDays, offset); - if ((reportType == WorkloadReportType.WorkGroupItem || reportType == WorkloadReportType.ResponseAreaWorkGroups) && string.IsNullOrEmpty(filterParam)) + if ((reportType == WorkloadReportType.WorkGroupJob || reportType == WorkloadReportType.ResponseAreaWorkGroups) && string.IsNullOrEmpty(filterParam)) { logger.LogError("Для типа отчета {ReportType} не передан параметр.", reportType.ToString()); throw new ArgumentNullException(nameof(filterParam), $"Для отчета типа {reportType.ToString()} параметр является обязательным."); @@ -80,15 +85,15 @@ namespace PARR.Core.Services.Workload.Implementations { case WorkloadReportType.ResponseAreaAll: displayName = "По зонам ответственности"; - foreach (var group in templates.GroupBy(t => t.ResponseArea)) + foreach (var group in templates.GroupBy(t => t.ResponseArea, StringComparer.OrdinalIgnoreCase)) statistics.Add(BuildStatisticItem(group.Key, group, reportDays, offset)); break; case WorkloadReportType.ResponseAreaWorkGroups: displayName = "По зоне ответственности"; // Фильтруем по ЗО - var filteredRA = templates.Where(t => t.ResponseArea.Equals(filterParam, StringComparison.OrdinalIgnoreCase)).ToList(); + var filteredRA = templates.Where(t => string.Equals(t.ResponseArea, filterParam, StringComparison.OrdinalIgnoreCase)).ToList(); // Группируем работы по РГ - foreach (var group in filteredRA.GroupBy(t => t.WorkGroup)) + foreach (var group in filteredRA.GroupBy(t => t.WorkGroup, StringComparer.OrdinalIgnoreCase)) statistics.Add(BuildStatisticItem(group.Key, group, reportDays, offset)); //statistics.Add(BuildStatisticItem(filterParam!, filteredRA, reportDays, offset)); break; @@ -97,11 +102,23 @@ namespace PARR.Core.Services.Workload.Implementations foreach (var group in templates.GroupBy(t => t.WorkGroup)) statistics.Add(BuildStatisticItem(group.Key, group, reportDays, offset)); break; - case WorkloadReportType.WorkGroupItem: + case WorkloadReportType.WorkGroupJob: displayName = "По рабочей группе"; - //todo: тут нужно вернуть другой отчет, либо группы работ, либо список работ - var filteredWG = templates.Where(t => t.WorkGroup.Equals(filterParam, StringComparison.OrdinalIgnoreCase)).ToList(); - statistics.Add(BuildStatisticItem(filterParam!, filteredWG, reportDays, offset)); + // Фильтруем по РГ + var filteredWG = templates.Where(t => string.Equals(t.WorkGroup, filterParam, StringComparison.OrdinalIgnoreCase)).ToList(); + + // Получим все JobName из бд + var jobIds = filteredWG.Select(t => t.JobId).ToHashSet(); + var jobs = await jobRepository.Get().AsNoTracking() + .Where(t => jobIds.Contains(t.Id)) + .ToDictionaryAsync(t => t.Id, t => t.Name); + + // Группируем работы по JobId + foreach (var group in filteredWG.GroupBy(t => t.JobId)) + { + var jobName = jobs.GetValueOrDefault(group.Key) ?? "Работа"; + statistics.Add(BuildStatisticItem(jobName, group, reportDays, offset)); + } break; } diff --git a/PARR.Core/Services/Workload/Models/TemplateReportData.cs b/PARR.Core/Services/Workload/Models/TemplateReportData.cs index 0fa2b5fe..f68b8df5 100644 --- a/PARR.Core/Services/Workload/Models/TemplateReportData.cs +++ b/PARR.Core/Services/Workload/Models/TemplateReportData.cs @@ -19,6 +19,7 @@ DateTimeOffset NextRun, bool IsActiveTemplate, bool IsActiveSchedule, - DateTimeOffset CacheDate + DateTimeOffset CacheDate, + Guid JobId ); } diff --git a/PARR.Domain/Enums/WorkloadReportType.cs b/PARR.Domain/Enums/WorkloadReportType.cs index c3126ae3..57be84a9 100644 --- a/PARR.Domain/Enums/WorkloadReportType.cs +++ b/PARR.Domain/Enums/WorkloadReportType.cs @@ -24,8 +24,8 @@ namespace PARR.Domain.Enums WorkGroupAll = 2, /// - /// Отчет по одной РГ + /// Отчет по одной РГ - список работ /// - WorkGroupItem = 3 + WorkGroupJob = 3 } } diff --git a/PARR.Infrastructure/Redis/RedisCacheService.cs b/PARR.Infrastructure/Redis/RedisCacheService.cs index 90781f73..55f30ff5 100644 --- a/PARR.Infrastructure/Redis/RedisCacheService.cs +++ b/PARR.Infrastructure/Redis/RedisCacheService.cs @@ -504,7 +504,9 @@ namespace PARR.Infrastructure.Redis if (keyPartsToHash != null && keyPartsToHash.Length > 0) { - var partsToHashStr = string.Join(mainSeparator, keyPartsToHash).Replace(" ", wordSeparator); + var partsToHashStr = string.Join(mainSeparator, keyPartsToHash) + .Replace(" ", wordSeparator) + .ToLower(); using var sha256 = System.Security.Cryptography.SHA256.Create(); var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(partsToHashStr));