feat(core): Добавлены помощники по клонированию JobGroup и синхронизации Job в JobGroup по старшему MaxValueRelationships

This commit is contained in:
Mikhail Kuznetsov
2026-05-19 09:47:35 +10:00
parent 3f3ab8617d
commit b267dfe7c5
6 changed files with 380 additions and 11 deletions

View File

@@ -0,0 +1,182 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Schedule;
namespace PARR.Core.Common.Helpers;
/// <summary>
/// Сервис для полного клонирования JobGroup со всеми вложенными Job, фильтрами, AutoControl и EsppSchValues.
/// Шаблоны (Template) не клонируются. Идентификаторы генерируются стратегией EF Core.
/// </summary>
public class JobGroupCloneHelper
{
private readonly IJobGroupRepository groupRepository;
private readonly IJobRepository jobRepository;
private readonly ILogger<JobGroupCloneHelper> logger;
public JobGroupCloneHelper(
IJobGroupRepository groupRepository,
IJobRepository jobRepository,
ILogger<JobGroupCloneHelper> logger)
{
this.groupRepository = groupRepository;
this.jobRepository = jobRepository;
this.logger = logger;
}
public async Task<Guid> CloneAsync(
Guid sourceJobGroupId,
string? newGroupName,
HistoryInitiator initiator,
CancellationToken ct = default)
{
var sourceGroup = await groupRepository.Get()
.AsNoTracking()
.Include(g => g.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.FieldFilters)
.Include(g => g.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.Include(g => g.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(g => g.EsppSchValues)
.FirstOrDefaultAsync(g => g.Id == sourceJobGroupId, ct);
if (sourceGroup == null)
{
throw new InvalidOperationException($"JobGroup {sourceJobGroupId} not found.");
}
logger.LogInformation("Начало клонирования JobGroup {JobGroupId}.", sourceJobGroupId);
var newGroup = MapJobGroup(sourceGroup, newGroupName);
foreach (var sourceJob in sourceGroup.Jobs ?? Enumerable.Empty<Job>())
{
var newJob = MapJob(sourceJob);
newGroup.Jobs.Add(newJob);
CloneJobFilters(sourceJob.UnitFilters, newJob);
if (sourceJob.AutoControl != null)
{
newJob.AutoControl = MapAutoControl(sourceJob.AutoControl);
}
}
// Клонирование значений расписания
foreach (var sourceSchValue in sourceGroup.EsppSchValues ?? Enumerable.Empty<EsppSchValue>())
{
newGroup.EsppSchValues.Add(new EsppSchValue
{
TypeValueId = sourceSchValue.TypeValueId,
TypeConfigId = sourceSchValue.TypeConfigId
});
}
var isSuccess = await groupRepository.CreateAsync(newGroup);
if (!isSuccess)
{
throw new InvalidOperationException("Не удалось сохранить клон JobGroup в базу данных.");
}
await groupRepository.CommitAsync(initiator);
logger.LogInformation("JobGroup успешно клонирована. Новый ID: {NewGroupId}.", newGroup.Id);
return newGroup.Id;
}
private JobGroup MapJobGroup(JobGroup source, string? newName)
{
string finalName = string.IsNullOrWhiteSpace(newName)
? $"{source.GroupName?.TrimEnd()} КЛОН"
: newName.Trim();
return new JobGroup
{
GroupName = finalName,
ShortDescription = source.ShortDescription,
FullDescription = source.FullDescription,
Solution = source.Solution,
TemplateDuration = source.TemplateDuration,
ReferenceDate = source.ReferenceDate,
UserTimeZoneOffsetMinutes = source.UserTimeZoneOffsetMinutes,
IsResponseAreaTimezone = source.IsResponseAreaTimezone,
IsAutoDistributionEnabled = source.IsAutoDistributionEnabled,
IsAgent = source.IsAgent,
AgentName = source.AgentName,
AgentTimeOutSec = source.AgentTimeOutSec,
AgentScript = source.AgentScript,
GroupTypeId = source.GroupTypeId,
GroupingUnitFieldId = source.GroupingUnitFieldId,
IsGroupByResponsible = source.IsGroupByResponsible,
ScheduleExcludeTypeId = source.ScheduleExcludeTypeId,
ScheduleExcludeTypeCalendarId = source.ScheduleExcludeTypeCalendarId,
DateCreated = DateTimeOffset.UtcNow,
Jobs = new HashSet<Job>(),
EsppSchValues = new HashSet<EsppSchValue>()
};
}
private Job MapJob(Job source)
{
return new Job
{
Name = source.Name,
WorkName = source.WorkName,
MinValueRelationships = source.MinValueRelationships,
MaxValueRelationships = source.MaxValueRelationships,
IsParentRelationships = source.IsParentRelationships,
TemplateNameMask = source.TemplateNameMask,
WorkGroupMask = source.WorkGroupMask,
ResponseAreaMask = source.ResponseAreaMask,
TnkId = source.TnkId,
DateCreated = DateTimeOffset.UtcNow,
UnitFilters = new HashSet<JobUnitFilter>()
};
}
private JobAutoControl MapAutoControl(JobAutoControl source)
{
return new JobAutoControl
{
InitUsedTemplateState = source.InitUsedTemplateState,
InitUsedScheduleState = source.InitUsedScheduleState
};
}
private void CloneJobFilters(IEnumerable<JobUnitFilter>? sourceFilters, Job targetJob)
{
if (sourceFilters == null) return;
foreach (var sourceFilter in sourceFilters)
{
var newFilter = new JobUnitFilter
{
UnitFilter = sourceFilter.UnitFilter,
DateCreated = DateTimeOffset.UtcNow,
FieldFilters = sourceFilter.FieldFilters?.Select(f => new JobFieldFilter
{
FieldId = f.FieldId,
ValueMask = f.ValueMask,
IsInverse = f.IsInverse,
DateCreated = DateTimeOffset.UtcNow
}).ToList() ?? new List<JobFieldFilter>(),
RelationshipFilters = sourceFilter.RelationshipFilters?.Select(r => new JobRelationshipFilter
{
FieldId = r.FieldId,
ValueMask = r.ValueMask,
IsInverse = r.IsInverse,
IsParent = r.IsParent,
IsFullMatch = r.IsFullMatch
}).ToList() ?? new List<JobRelationshipFilter>()
};
targetJob.UnitFilters.Add(newFilter);
}
}
}

View File

@@ -0,0 +1,188 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
namespace PARR.Core.Common.Helpers;
public class JobGroupUnitFilterSynchronizeHelper
{
private readonly IJobRepository jobRepository;
private readonly ILogger<JobGroupUnitFilterSynchronizeHelper> logger;
public JobGroupUnitFilterSynchronizeHelper(IJobRepository jobRepository, ILogger<JobGroupUnitFilterSynchronizeHelper> logger)
{
this.jobRepository = jobRepository;
this.logger = logger;
}
public async Task SynchronizeGroupFiltersAsync(Guid jobGroupId, HistoryInitiator initiator, CancellationToken ct = default)
{
var jobs = await jobRepository.Get()
.AsNoTracking()
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.FieldFilters)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.Where(j => j.GroupId == jobGroupId)
.ToListAsync(ct);
if (jobs == null || jobs.Count < 2)
{
logger.LogDebug("JobGroup {JobGroupId} содержит менее двух Job. Синхронизация фильтров не требуется.", jobGroupId);
return;
}
var referenceJob = jobs.OrderByDescending(j => j.MaxValueRelationships).FirstOrDefault();
if (referenceJob == null)
{
logger.LogWarning("Не удалось определить эталонный Job для JobGroup {JobGroupId}.", jobGroupId);
return;
}
logger.LogInformation("Начало синхронизации фильтров для JobGroup {JobGroupId}. Эталонный Job: {ReferenceJobId}.", jobGroupId, referenceJob.Id);
foreach (var job in jobs)
{
ct.ThrowIfCancellationRequested();
if (job.Id == referenceJob.Id) continue;
if (!AreUnitFiltersIdentical(job.UnitFilters, referenceJob.UnitFilters))
{
logger.LogDebug("Фильтры Job {JobId} отличаются от эталона. Запуск обновления.", job.Id);
await ApplyFilterCorrectionAsync(job.Id, referenceJob.UnitFilters ?? Enumerable.Empty<JobUnitFilter>(), initiator, ct);
}
else
{
logger.LogDebug("Фильтры Job {JobId} идентичны эталону. Пропуск.", job.Id);
}
}
logger.LogInformation("Синхронизация фильтров для JobGroup {JobGroupId} завершена.", jobGroupId);
}
private async Task ApplyFilterCorrectionAsync(Guid jobId, IEnumerable<JobUnitFilter> referenceFilters, HistoryInitiator initiator, CancellationToken ct)
{
var job = await jobRepository.Get()
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.FieldFilters)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId, ct);
if (job == null) return;
if (job.UnitFilters == null)
{
job.UnitFilters = new List<JobUnitFilter>();
}
else
{
job.UnitFilters.Clear();
}
var clonedFilters = CloneFilters(referenceFilters, job.Id);
foreach (var filter in clonedFilters)
{
job.UnitFilters!.Add(filter);
}
await jobRepository.CommitAsync(initiator);
logger.LogInformation("Фильтры Job {JobId} успешно обновлены.", jobId);
}
private bool AreUnitFiltersIdentical(IEnumerable<JobUnitFilter>? current, IEnumerable<JobUnitFilter>? reference)
{
var currentList = (current ?? Enumerable.Empty<JobUnitFilter>()).ToList();
var refList = (reference ?? Enumerable.Empty<JobUnitFilter>()).ToList();
if (currentList.Count != refList.Count) return false;
for (int i = 0; i < currentList.Count; i++)
{
if (!AreFiltersEqual(currentList[i], refList[i]))
return false;
}
return true;
}
private bool AreFiltersEqual(JobUnitFilter a, JobUnitFilter b)
{
if (!AreFieldFiltersEqual(a.FieldFilters, b.FieldFilters)) return false;
if (!AreRelationshipFiltersEqual(a.RelationshipFilters, b.RelationshipFilters)) return false;
return true;
}
private bool AreFieldFiltersEqual(IEnumerable<JobFieldFilter>? a, IEnumerable<JobFieldFilter>? b)
{
var listA = (a ?? Enumerable.Empty<JobFieldFilter>()).OrderBy(f => f.FieldId).ToList();
var listB = (b ?? Enumerable.Empty<JobFieldFilter>()).OrderBy(f => f.FieldId).ToList();
if (listA.Count != listB.Count) return false;
for (int i = 0; i < listA.Count; i++)
{
if (listA[i].FieldId != listB[i].FieldId ||
!string.Equals(listA[i].ValueMask, listB[i].ValueMask, StringComparison.OrdinalIgnoreCase) ||
listA[i].IsInverse != listB[i].IsInverse)
{
return false;
}
}
return true;
}
private bool AreRelationshipFiltersEqual(IEnumerable<JobRelationshipFilter>? a, IEnumerable<JobRelationshipFilter>? b)
{
var listA = (a ?? Enumerable.Empty<JobRelationshipFilter>()).OrderBy(f => f.FieldId).ToList();
var listB = (b ?? Enumerable.Empty<JobRelationshipFilter>()).OrderBy(f => f.FieldId).ToList();
if (listA.Count != listB.Count) return false;
for (int i = 0; i < listA.Count; i++)
{
if (listA[i].FieldId != listB[i].FieldId ||
!string.Equals(listA[i].ValueMask, listB[i].ValueMask, StringComparison.OrdinalIgnoreCase) ||
listA[i].IsInverse != listB[i].IsInverse ||
listA[i].IsParent != listB[i].IsParent ||
listA[i].IsFullMatch != listB[i].IsFullMatch)
{
return false;
}
}
return true;
}
private List<JobUnitFilter> CloneFilters(IEnumerable<JobUnitFilter> source, Guid targetJobId)
{
var result = new List<JobUnitFilter>();
foreach (var src in source ?? Enumerable.Empty<JobUnitFilter>())
{
var newFilter = new JobUnitFilter
{
JobId = targetJobId,
UnitFilter = src.UnitFilter!,
DateCreated = DateTimeOffset.UtcNow,
FieldFilters = src.FieldFilters?.Select(f => new JobFieldFilter
{
FieldId = f.FieldId,
ValueMask = f.ValueMask,
IsInverse = f.IsInverse,
DateCreated = DateTimeOffset.UtcNow
}).ToList() ?? new List<JobFieldFilter>(),
RelationshipFilters = src.RelationshipFilters?.Select(r => new JobRelationshipFilter
{
FieldId = r.FieldId,
ValueMask = r.ValueMask,
IsInverse = r.IsInverse,
IsParent = r.IsParent,
IsFullMatch = r.IsFullMatch
}).ToList() ?? new List<JobRelationshipFilter>()
};
result.Add(newFilter);
}
return result;
}
}