feat(api): JobGroup - управление юнит фильтрами

This commit is contained in:
Mikhail Trubnikov
2026-09-24 16:03:57 +10:00
parent 2a3afb9a5d
commit f624b7f4a7
9 changed files with 284 additions and 199 deletions

View File

@@ -1,6 +1,6 @@
namespace PARR.API.Contracts.V1.Requests
{
public record JobGroupRequest
public record JobGroupRequest : UnitFiltersBaseRequest
{
public required string Name { get; init; }

View File

@@ -1,20 +1,13 @@
namespace PARR.API.Contracts.V1.Requests
{
public record JobRequest
public record JobRequest : UnitFiltersBaseRequest
{
public required Guid TnkId { get; init; }
public required Guid GroupId { get; init; }
public required string Name { get; init; }
//public int? MinValueRelationships { get; init; }
//public int? MaxValueRelationships { get; init; }
//public bool? IsParentRelationships { get; init; }
public JobRelationships? Relationships { get; init; }
public required string TemplateNameMask { get; init; }
@@ -25,19 +18,9 @@
public required string ResponseAreaMask { get; init; }
//#region AutoControl
//public bool IsEnableAutoControl { get; set; }
//public bool InitUsedTemplateState { get; set; }
//public bool InitUsedScheduleState { get; set; }
//#endregion
public JobAutoControlRequest? AutoControl { get; init; }
public required List<UnitFilterRequest> UnitFilters { get; init; }
//public required List<UnitFilterRequest> UnitFilters { get; init; }
}
public record JobRelationships
@@ -49,43 +32,38 @@
public bool IsParentRelationships { get; init; }
}
public record UnitFilterRequest
{
///// <summary>
///// Id = null в методе Create, в Update обязателен
///// </summary>
//public Guid? Id { get; set; }
//public record UnitFilterRequest
//{
// public required string UnitFilterMask { get; init; }
public required string UnitFilterMask { get; init; }
// public List<FieldFilterRequest>? FieldFilters { get; init; }
public List<FieldFilterRequest>? FieldFilters { get; init; }
public List<RelationshipFilterRequest>? RelationshipFilters { get; init; }
}
// public List<RelationshipFilterRequest>? RelationshipFilters { get; init; }
//}
public record FieldFilterRequest
{
//public record FieldFilterRequest
//{
public Guid FieldId { get; init; }
// public Guid FieldId { get; init; }
public string? ValueMask { get; init; }
// public string? ValueMask { get; init; }
public bool IsInverse { get; init; } = false;
// public bool IsInverse { get; init; } = false;
}
//}
public record RelationshipFilterRequest
{
public Guid FieldId { get; init; }
//public record RelationshipFilterRequest
//{
// public Guid FieldId { get; init; }
public bool? IsParent { get; init; }
// public bool? IsParent { get; init; }
public string? ValueMask { get; init; }
// public string? ValueMask { get; init; }
public bool? IsFullMatch { get; init; }
// public bool? IsFullMatch { get; init; }
public bool? IsInverse { get; init; }
}
// public bool? IsInverse { get; init; }
//}
}

View File

@@ -0,0 +1,42 @@
namespace PARR.API.Contracts.V1.Requests
{
/// <summary>
/// Unit фильтры для Job и JobGroup
/// </summary>
public record UnitFiltersBaseRequest
{
public required List<UnitFilterRequest> UnitFilters { get; init; }
}
public record UnitFilterRequest
{
public required string UnitFilterMask { get; init; }
public List<FieldFilterRequest>? FieldFilters { get; init; }
public List<RelationshipFilterRequest>? RelationshipFilters { get; init; }
}
public record FieldFilterRequest
{
public Guid FieldId { get; init; }
public string? ValueMask { get; init; }
public bool IsInverse { get; init; } = false;
}
public record RelationshipFilterRequest
{
public Guid FieldId { get; init; }
public bool? IsParent { get; init; }
public string? ValueMask { get; init; }
public bool? IsFullMatch { get; init; }
public bool? IsInverse { get; init; }
}
}

View File

@@ -361,7 +361,7 @@ namespace PARR.API.Controllers.V1
.Include(t => t.Group).ThenInclude(t => t.GroupingUnitField)
.Include(t => t.UnitFilters)
.ThenInclude(t => t.FieldFilters)
.ThenInclude(t => t.UnitField)
.ThenInclude(t => t.UnitField)
.Include(t => t.UnitFilters)
.ThenInclude(t => t.RelationshipFilters)
.Include(t => t.AutoControl)
@@ -411,124 +411,6 @@ namespace PARR.API.Controllers.V1
orig.UnitFilters.Add(newUnitFilter);
}
#region old
/* Писал полноценный апдейт, но Миша сказал что нахрен это - просто все удаляем, а потом создаём заново... Ох уж этот Миша...
//Сразу удаляем UnitFilter которых нет
var toDelete = orig.UnitFilters.Where(t => !mappedRequest.UnitFilters.Any(e => e.Id == t.Id));
foreach (var item in toDelete)
orig.UnitFilters.Remove(item);
foreach (var mappedUnitFilter in mappedRequest.UnitFilters)
{
var origUnitFilter = orig.UnitFilters.FirstOrDefault(t => t.Id == mappedUnitFilter.Id);
if (origUnitFilter == null)
{
var newUnitFilter = new JobUnitFilter
{
UnitFilter = mappedUnitFilter.UnitFilter,
DateCreated = DateTimeOffset.UtcNow,
JobId = orig.Id
};
foreach (var newRequestFieldFilter in mappedUnitFilter.FieldFilters)
{
var newFieldFilter = CreateFieldFilter(newUnitFilter.Id, newRequestFieldFilter.FieldId, newRequestFieldFilter.ValueMask);
newUnitFilter.FieldFilters.Add(newFieldFilter);
}
foreach (var newRequestRelationshipFilter in mappedUnitFilter.RelationshipFilters)
{
var newRelationshipFilter = CreateRelationshipFilter(newRequestRelationshipFilter.FieldId,
newRequestRelationshipFilter.IsParent,
newRequestRelationshipFilter.IsFullMatch,
newRequestRelationshipFilter.IsInverse,
newRequestRelationshipFilter.ValueMask);
newUnitFilter.RelationshipFilters.Add(newRelationshipFilter);
}
orig.UnitFilters.Add(newUnitFilter);
}
else
{
#region Непосредственно UnitFilter
if (origUnitFilter.UnitFilter != mappedUnitFilter.UnitFilter)
{
origUnitFilter.UnitFilter = mappedUnitFilter.UnitFilter;
origUnitFilter.DateModified = DateTimeOffset.UtcNow;
}
#endregion
#region FieldFilters
//Сразу удаляем неактуальные
var fieldFiltersToDelete = origUnitFilter.FieldFilters.Where(t => !mappedUnitFilter.FieldFilters.Any(m => m.FieldId == t.FieldId));
foreach (var item in fieldFiltersToDelete)
origUnitFilter.FieldFilters.Remove(item);
//Перебираем FieldFilter
foreach (var mappedFieldFilter in mappedUnitFilter.FieldFilters)
{
var origFieldFilter = origUnitFilter.FieldFilters.FirstOrDefault(t => t.FieldId == mappedFieldFilter.FieldId);
if (origFieldFilter == null)//Нет в БД? не проблема - создадим
{
var newFieldFilter = CreateFieldFilter(origUnitFilter.Id, mappedFieldFilter.FieldId, mappedFieldFilter.ValueMask);
origUnitFilter.FieldFilters.Add(newFieldFilter);
}
else
{
if (origFieldFilter.ValueMask != mappedFieldFilter.ValueMask)
{
origFieldFilter.ValueMask = mappedFieldFilter.ValueMask;
origFieldFilter.DateModified = DateTimeOffset.UtcNow;
}
}
}
#endregion
#region RelationshipFilters
//Сразу удаляем неактуальные
var relationshipFiltersToDelete = origUnitFilter.RelationshipFilters.Where(t => !mappedUnitFilter.RelationshipFilters.Any(m => m.FieldId == t.FieldId));
foreach (var item in relationshipFiltersToDelete)
origUnitFilter.RelationshipFilters.Remove(item);
//Перебираем RelationshipFilters
foreach (var mappedRelationshipFilter in mappedUnitFilter.RelationshipFilters)
{
var origRelationshipFilter = origUnitFilter.RelationshipFilters.FirstOrDefault(t => t.FieldId == mappedRelationshipFilter.FieldId);
if (origRelationshipFilter == null)//Нет в БД? не проблема - создадим
{
var newRelationshipFilter = CreateRelationshipFilter(mappedRelationshipFilter.FieldId,
mappedRelationshipFilter.IsParent,
mappedRelationshipFilter.IsFullMatch,
mappedRelationshipFilter.IsInverse,
mappedRelationshipFilter.ValueMask);
origUnitFilter.RelationshipFilters.Add(newRelationshipFilter);
}
else
{
if (origRelationshipFilter.IsParent != mappedRelationshipFilter.IsParent)
origRelationshipFilter.IsParent = mappedRelationshipFilter.IsParent;
if (origRelationshipFilter.ValueMask != mappedRelationshipFilter.ValueMask)
origRelationshipFilter.ValueMask = mappedRelationshipFilter.ValueMask;
if (origRelationshipFilter.IsFullMatch != mappedRelationshipFilter.IsFullMatch)
origRelationshipFilter.IsFullMatch = mappedRelationshipFilter.IsFullMatch;
if (origRelationshipFilter.IsInverse != mappedRelationshipFilter.IsInverse)
origRelationshipFilter.IsInverse = mappedRelationshipFilter.IsInverse;
}
}
#endregion
}
}*/
#endregion
}
private static JobRelationshipFilter CreateRelationshipFilter(Guid fieldId, bool isParent, bool isFullMatch, bool isInverse, string valueMask)

View File

@@ -177,7 +177,8 @@ namespace PARR.API.Controllers.V1
ScheduleExcludeTypeCalendarId = request.ScheduleExcludeTypeCalendarId,
IsAutoDistributionEnabled = request.IsAutoDistributionEnabled,
IsResponseAreaTimezone = request.IsWorkGroupTimezone,
IsGroupByResponsible = request.IsGroupByResponsible
IsGroupByResponsible = request.IsGroupByResponsible,
UnitFilters = new List<JobGroupUnitFilter>()
//IsAgent = request.IsAgent,
//AgentName = request.AgentName,
//AgentTimeOutSec = request.AgentTimeOutSec,
@@ -214,6 +215,12 @@ namespace PARR.API.Controllers.V1
#endregion
#region Фильтры
jobGroup.UnitFilters = MapUnitFilters(request.UnitFilters, jobGroup.Id);
#endregion
//Добавляем настройки планировщика
request.Schedule.ForEach(item =>
{
@@ -264,6 +271,10 @@ namespace PARR.API.Controllers.V1
.Include(t => t.EsppSchValues)
.Include(t => t.AutoControl)
.Include(t => t.DistributionConfig).ThenInclude(t => t.DistributionPeriod)
.Include(t => t.UnitFilters)
.ThenInclude(t => t.FieldFilters)
.Include(t => t.UnitFilters)
.ThenInclude(t => t.RelationshipFilters)
.FirstOrDefaultAsync(t => t.Id == id);
if (orig == null)
@@ -274,7 +285,6 @@ namespace PARR.API.Controllers.V1
throw new AppValidationException("Запрещено изменение поля 'Тип работ'");
//Расписание было изменено, ниже добавим задание в очередь на обновление расписаний у связанных шаблонов
var isScheduleChanged = IsScheduleChanged(orig, request);
@@ -371,6 +381,14 @@ namespace PARR.API.Controllers.V1
#endregion
#region Фильтры
orig.UnitFilters.Clear();
orig.UnitFilters = MapUnitFilters(request.UnitFilters, orig.Id);
#endregion
//обновляем планировщик
orig.EsppSchValues.Clear();
request.Schedule.ForEach(item =>
@@ -465,6 +483,48 @@ namespace PARR.API.Controllers.V1
return NoContent();
}
/// <summary>
/// Обновление юнит фильтров
/// </summary>
/// <param name="requestFilters"></param>
/// <param name="jobGroupId"></param>
/// <returns></returns>
private List<JobGroupUnitFilter> MapUnitFilters(List<UnitFilterRequest> requestFilters, Guid jobGroupId)
{
return requestFilters.Select(requestFilter =>
{
var filterId = Guid.NewGuid();
return new JobGroupUnitFilter
{
Id = filterId,
DateCreated = DateTimeOffset.UtcNow,
JobGroupId = jobGroupId, // Явно связываем с группой работ
UnitFilter = requestFilter.UnitFilterMask,
FieldFilters = requestFilter.FieldFilters?.Select(t => new JobGroupFieldFilter
{
Id = Guid.NewGuid(),
DateCreated = DateTimeOffset.UtcNow,
UnitFilterId = filterId,
FieldId = t.FieldId,
ValueMask = t.ValueMask ?? string.Empty,
IsInverse = t.IsInverse
}).ToList() ?? new List<JobGroupFieldFilter>(),
RelationshipFilters = requestFilter.RelationshipFilters?.Select(t => new JobGroupRelationshipFilter
{
UnitFilterId = filterId,
FieldId = t.FieldId,
IsParent = t.IsParent ?? false,
ValueMask = t.ValueMask ?? string.Empty,
IsFullMatch = t.IsFullMatch ?? false,
IsInverse = t.IsInverse ?? false
}).ToList() ?? new List<JobGroupRelationshipFilter>()
};
}).ToList();
}
/// <summary>
/// Удалить настройки автоконтроля для связанных Job
/// </summary>

View File

@@ -16,7 +16,8 @@ namespace PARR.API.Validators
IUnitFieldRepository unitFieldRepository,
IScheduleExcludeTypeRepository scheduleExcludeTypeRepository,
IScheduleExcludeTypeCalendarRepository scheduleExcludeTypeCalendarRepository,
IDistributionPeriodRepository distributionPeriodRepository
IDistributionPeriodRepository distributionPeriodRepository,
UnitFiltersBaseRequestValidator unitFilterValidator
)
{
RuleFor(t => t.Name)
@@ -38,6 +39,9 @@ namespace PARR.API.Validators
.MustAsync(async (entity, value, c) => await jobGroupTypeRepository.GetAsync(value) != null)
.WithMessage("Указан несуществующий Id типа");
// Валидация фильтров (они могут быть пустые, мы так договорились)
Include(unitFilterValidator);
//Проверяем существует ли такой GroupingUnitFieldId в unitField
RuleFor(t => t.GroupingUnitFieldId)
.MustAsync(async (entity, value, c) =>
@@ -149,7 +153,6 @@ namespace PARR.API.Validators
return false;
})
.WithMessage("Некорректные параметры автоконтроля");
}
}
}

View File

@@ -17,7 +17,8 @@ namespace PARR.API.Validators
public JobRequestValidator(
ITnkRepository tnkRepository,
IJobGroupRepository jobGroupRepository,
IUnitFieldRepository unitFieldRepository
IUnitFieldRepository unitFieldRepository,
UnitFiltersBaseRequestValidator unitFilterValidator
)
{
_tnkRepository = tnkRepository;
@@ -38,12 +39,15 @@ namespace PARR.API.Validators
.MustAsync(async (entity, value, c) => await IsGroupExist(entity))
.WithMessage("Указан несуществующий Id группы работ({PropertyValue})");
RuleForEach(t => t.UnitFilters)
.NotNull().NotEmpty();
//RuleForEach(t => t.UnitFilters)
// .NotNull().NotEmpty();
RuleForEach(t => t.UnitFilters)
.MustAsync(async (entity, value, c) => await IsUnitFiltersCorrect(entity))
.WithMessage("Неверно заданы параметры фильтров. Внимательнее, пожалуйста!");
// Валидация фильтров (они могут быть пустые, мы так договорились)
Include(unitFilterValidator);
//RuleForEach(t => t.UnitFilters)
// .MustAsync(async (entity, value, c) => await IsUnitFiltersCorrect(entity))
// .WithMessage("Неверно заданы параметры фильтров.");
RuleFor(t => t.Relationships)
.MustAsync(async (entity, value, c) =>
@@ -95,35 +99,64 @@ namespace PARR.API.Validators
.WithMessage("Некорректные параметры автоконтроля");
}
private async Task<bool> IsUnitFiltersCorrect(JobRequest entity)
{
foreach (var unitFilter in entity.UnitFilters)
{
if (string.IsNullOrEmpty(unitFilter.UnitFilterMask))
return false;
//private async Task<bool> IsUnitFiltersCorrect(JobRequest entity)
//{
// // 1. Проверим маски, они не должны быть пустыми
// foreach (var unitFilter in entity.UnitFilters)
// {
// if (string.IsNullOrEmpty(unitFilter.UnitFilterMask))
// return false;
// }
//if (unitFilter.FieldFilters == null || !unitFilter.FieldFilters.Any())
// return false;
// // 2. Собираем все уникальные FieldId из всех фильтров в памяти
// var requestedFieldIds = entity.UnitFilters
// .SelectMany(uf =>
// (uf.FieldFilters ?? Enumerable.Empty<FieldFilterRequest>()).Select(ff => ff.FieldId)
// .Concat((uf.RelationshipFilters ?? Enumerable.Empty<RelationshipFilterRequest>()).Select(rf => rf.FieldId)))
// .Distinct()
// .ToList();
if (unitFilter.FieldFilters != null && unitFilter.FieldFilters.Any())
foreach (var fieldFilter in unitFilter.FieldFilters)
{
var fieldId = fieldFilter.FieldId;
if (await _unitFieldRepository.GetAsync(fieldId) == null)
return false;
}
// // Если фильтров с ID нет, то и проверять в БД нечего
// if (!requestedFieldIds.Any())
// return true;
if (unitFilter.RelationshipFilters != null && unitFilter.RelationshipFilters.Any())
foreach (var relationshipFilter in unitFilter.RelationshipFilters!)
{
var fieldId = relationshipFilter.FieldId;
if (await _unitFieldRepository.GetAsync(fieldId) == null)
return false;
}
}
// // 3. Выполняем ОДИН запрос к БД.
// var existingFieldIds = await _unitFieldRepository.Get()
// .Where(field => requestedFieldIds.Contains(field.Id))
// .Select(field => field.Id)
// .Distinct()
// .ToListAsync();
return true;
}
// // 4. Если количество найденных ID в БД меньше, чем мы запрашивали,
// // значит какого-то fieldId не существует
// return requestedFieldIds.Count == existingFieldIds.Count;
// #region Old
// //foreach (var unitFilter in entity.UnitFilters)
// //{
// // if (string.IsNullOrEmpty(unitFilter.UnitFilterMask))
// // return false;
// // if (unitFilter.FieldFilters != null && unitFilter.FieldFilters.Any())
// // foreach (var fieldFilter in unitFilter.FieldFilters)
// // {
// // var fieldId = fieldFilter.FieldId;
// // if (await _unitFieldRepository.GetAsync(fieldId) == null)
// // return false;
// // }
// // if (unitFilter.RelationshipFilters != null && unitFilter.RelationshipFilters.Any())
// // foreach (var relationshipFilter in unitFilter.RelationshipFilters!)
// // {
// // var fieldId = relationshipFilter.FieldId;
// // if (await _unitFieldRepository.GetAsync(fieldId) == null)
// // return false;
// // }
// //}
// //return true;
// #endregion
//}
private async Task<bool> IsGroupExist(JobRequest entity)
{

View File

@@ -0,0 +1,85 @@
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using PARR.API.Contracts.V1.Requests;
using PARR.Core.Repositories.Interfaces.Unit;
namespace PARR.API.Validators
{
/// <summary>
/// Валидация фильтроп вдя JobGroup, Job
/// </summary>
public class UnitFiltersBaseRequestValidator: AbstractValidator<UnitFiltersBaseRequest>
{
private readonly IUnitFieldRepository _unitFieldRepository;
public UnitFiltersBaseRequestValidator(IUnitFieldRepository unitFieldRepository)
{
_unitFieldRepository = unitFieldRepository;
// Это правило будет применяться ко ВСЕМ, кто наследует UnitFiltersBaseRequest
RuleForEach(t => t.UnitFilters)
.MustAsync(async (entity, value, c) => await IsUnitFiltersCorrect(entity))
.WithMessage("Неверно заданы параметры фильтров.");
}
private async Task<bool> IsUnitFiltersCorrect(UnitFiltersBaseRequest entity)
{
// 1. Проверим маски, они не должны быть пустыми
foreach (var unitFilter in entity.UnitFilters)
{
if (string.IsNullOrEmpty(unitFilter.UnitFilterMask))
return false;
}
// 2. Собираем все уникальные FieldId из всех фильтров в памяти
var requestedFieldIds = entity.UnitFilters
.SelectMany(uf =>
(uf.FieldFilters ?? Enumerable.Empty<FieldFilterRequest>()).Select(ff => ff.FieldId)
.Concat((uf.RelationshipFilters ?? Enumerable.Empty<RelationshipFilterRequest>()).Select(rf => rf.FieldId)))
.Distinct()
.ToList();
// Если фильтров с ID нет, то и проверять в БД нечего
if (!requestedFieldIds.Any())
return true;
// 3. Выполняем ОДИН запрос к БД.
var existingFieldIds = await _unitFieldRepository.Get()
.Where(field => requestedFieldIds.Contains(field.Id))
.Select(field => field.Id)
.Distinct()
.ToListAsync();
// 4. Если количество найденных ID в БД меньше, чем мы запрашивали,
// значит какого-то fieldId не существует
return requestedFieldIds.Count == existingFieldIds.Count;
#region Old
//foreach (var unitFilter in entity.UnitFilters)
//{
// if (string.IsNullOrEmpty(unitFilter.UnitFilterMask))
// return false;
// if (unitFilter.FieldFilters != null && unitFilter.FieldFilters.Any())
// foreach (var fieldFilter in unitFilter.FieldFilters)
// {
// var fieldId = fieldFilter.FieldId;
// if (await _unitFieldRepository.GetAsync(fieldId) == null)
// return false;
// }
// if (unitFilter.RelationshipFilters != null && unitFilter.RelationshipFilters.Any())
// foreach (var relationshipFilter in unitFilter.RelationshipFilters!)
// {
// var fieldId = relationshipFilter.FieldId;
// if (await _unitFieldRepository.GetAsync(fieldId) == null)
// return false;
// }
//}
//return true;
#endregion
}
}
}