feat(api,core,infrastructure): Workload - дополнительная фильтрация по свяанным ЗО и РГ. RedisCacheService - при формирование hash key, дополнительная фильтрация на null, empty.

This commit is contained in:
Mikhail Trubnikov
2026-06-09 10:16:32 +10:00
parent 8cb7a2d599
commit 6fa9c3d445
7 changed files with 102 additions and 77 deletions

View File

@@ -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
{
/// <summary>
/// Тип дополнительно фильтра для работы (ЗО, РГ)
/// Дополнительный фильтр для работ - ЗО
/// </summary>
public WorkloadJobSubfliterType? JobSubfilterType { get; init; }
public string? ResponseArea { get; init; }
/// <summary>
/// Значение дополнительного фильтра для работы
/// Дополнительный фильтр для работ - РГ
/// </summary>
public string? JobSubfilterValue { get; init; }
public string? WorkGroup { get; init; }
}
}

View File

@@ -57,7 +57,7 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.Workload.WorkGroup)]
public async Task<IActionResult> 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
/// <param name="request"></param>
/// <returns></returns>
[HttpGet(ApiRoutes.Workload.WorkGroupJob)]
public async Task<IActionResult> GetWorkGroupJobReport([FromRoute] string workGroup, [FromQuery] TimeZoneOffsetClient offset, [FromQuery] WorkloadBaseQuery request)
public async Task<IActionResult> 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<IActionResult> 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
/// Универсальный метод получения отчетности
/// </summary>
/// <returns></returns>
private async Task<IActionResult> GetReportAsync(WorkloadReportType reportType, int durationDays, TimeSpan offset, string? filter)
//private async Task<IActionResult> GetReportAsync(WorkloadReportType reportType, int durationDays, TimeSpan offset, string? filter)
private async Task<IActionResult> 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<string?>(null, false, new List<ErrorModel> { new ErrorModel { Message = "Необходимо сформировать отчетность." } }, "empty"));

View File

@@ -38,11 +38,11 @@ namespace PARR.API.Controllers.V1.Statistics
/// <summary>
/// Отчет о загруженности. Шаблоны
/// </summary>
/// <param name="reportType"></param>
/// <param name="filter"></param>
/// <param name="state"></param>
/// <param name="date"></param>
/// <param name="offset"></param>
/// <param name="reportType">Тип отчета (ЗО, РГ, Работа)</param>
/// <param name="filter">Фильтр, значиение в зависимости от типа отчета</param>
/// <param name="state">Все, активированные, деактивированные</param>
/// <param name="date">Дата</param>
/// <param name="offset">Часовой пояс</param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
[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<List<StatWorkloadTemplateReportResponse>>(report);

View File

@@ -415,9 +415,9 @@ namespace PARR.Core.Services.Workload.Implementations
/// <param name="offset"></param>
/// <param name="filterParam">Наименование ЗО или РГ или null</param>
/// <returns></returns>
public async Task<WorkloadReport?> GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam)
public async Task<WorkloadReport?> 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<WorkloadReport>(key, true);
}
@@ -433,9 +433,9 @@ namespace PARR.Core.Services.Workload.Implementations
/// <param name="offset"></param>
/// <param name="filterParam">Наименование ЗО или РГ или null</param>
/// <returns></returns>
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
/// <param name="offset"></param>
/// <param name="filterParam"></param>
/// <returns></returns>
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<string>();
@@ -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);

View File

@@ -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<WorkloadReport?> GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam)
public async Task<WorkloadReport?> 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<WorkloadTemplateReport>();
// Если тип - работы, и нет дополнительных фильтров по ЗО или РГ, не полезем в кэш, вернем сразу
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<Guid>();
// Фильтруем из кэша
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<TemplateCache> 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);

View File

@@ -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
/// <param name="dateStart"></param>
/// <param name="durationDays"></param>
/// <param name="offset"></param>
/// <param name="filterParam"></param>
/// <param name="responseArea"></param>
/// <param name="workGroup"></param>
/// <returns></returns>
Task<WorkloadReport?> GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam);
Task<WorkloadReport?> GetWorkloadReportAsync(WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? responseArea, string? workGroup);
/// <summary>
/// Получить отчет - список шаблонов на дату согласно фильтрам
@@ -33,10 +35,18 @@ namespace PARR.Core.Services.Workload.Interfaces
/// <param name="state"></param>
/// <param name="date"></param>
/// <param name="offset"></param>
/// <param name="jobSubfilterType">Дополнительный фильтр при отчете по шаблонам, тип</param>
/// <param name="jobSubfilterValue">Дополнительный фильтр при отчете по шаблонам, значение</param>
/// <param name="responseArea">Дополнительный фильтр при отчете по шаблонам, ЗО</param>
/// <param name="workGorup">Дополнительный фильтр при отчете по шаблонам, РГ</param>
/// <returns></returns>
Task<List<WorkloadTemplateReport>> GetTemplateReportAsync(WorkloadTemplateReportType reportType, string filter, WorkloadTemplateReportState state, DateOnly date, TimeSpan offset, WorkloadJobSubfliterType? jobSubfilterType, string? jobSubfilterValue);
Task<List<WorkloadTemplateReport>> GetTemplateReportAsync(
WorkloadTemplateReportType reportType,
string filter,
WorkloadTemplateReportState state,
DateOnly date,
TimeSpan offset,
string? responseArea,
string? workGroup
);
}
}

View File

@@ -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();