feat(templateMatcher): refactoring

This commit is contained in:
Mikhail Kuznetsov
2025-12-26 21:47:53 +10:00
parent e04d2b628c
commit e8db55400c
11 changed files with 679 additions and 554 deletions

View File

@@ -12,6 +12,10 @@ namespace PARR.DAL.DomainServices.Implementations
{
internal class UnitFilterService : IUnitFilterService
{
#if DEBUG
private readonly Guid targetUnitId = Guid.Parse("d4322a08-246b-4380-8953-8ce4a8446235");
#endif
private readonly ILogger<UnitFilterService> logger;
private readonly IJobService jobService;
private readonly IUnitService unitService;
@@ -118,6 +122,19 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count);
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в initialUnitIds
if (initialUnitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в initialUnitIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в initialUnitIds.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
#endif
if (!initialUnitIds.Any())
{
logger.LogDebug("Фильтр #{Index}: 0 юнитов после UnitFilter. Пропускаем.", filterNumber);
@@ -249,14 +266,42 @@ namespace PARR.DAL.DomainServices.Implementations
candidateUnits = ApplyFieldFilter(candidateUnits, fieldFilter);
}
logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateUnits.Count());
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter
var candidateListAfterFieldFilter = candidateUnits.ToList();
if (candidateListAfterFieldFilter.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в candidateUnits после FieldFilter.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после FieldFilter.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateListAfterFieldFilter.Count);
#endif
foreach (var relFilter in filter.RelationshipFilters)
{
candidateUnits = ApplyRelationshipFilterToQuery(candidateUnits, relFilter);
}
logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateUnits.Count());
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter
var candidateListAfterRelFilter = candidateUnits.ToList();
if (candidateListAfterRelFilter.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в candidateUnits после RelationshipFilter.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после RelationshipFilter.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateListAfterRelFilter.Count);
#endif
// 9 Umbrella-фильтр
var finalUnits = candidateUnits.AsEnumerable();
@@ -264,6 +309,19 @@ namespace PARR.DAL.DomainServices.Implementations
{
finalUnits = ApplyRelationshipCountFilter(finalUnits, job, filter.RelationshipFilters);
logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count());
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в finalUnits после Umbrella
if (finalUnits.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в finalUnits после Umbrella-фильтра.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в finalUnits после Umbrella-фильтра.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
#endif
}
// 10 Взять ID
@@ -272,6 +330,19 @@ namespace PARR.DAL.DomainServices.Implementations
.Take(remaining)
.ToList();
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в newIds
if (newIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в newIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в newIds.", targetUnitId);
// ❌ юнит отсеялся на этапе Take(remaining), если его не было в finalUnits
}
#endif
collectedIds.UnionWith(newIds);
logger.LogDebug("Фильтр #{Index}: найдено {Count} Unit'ов. Всего: {Total}",
filterNumber, newIds.Count, collectedIds.Count);
@@ -287,6 +358,18 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogInformation("Job {JobId}: из {FilterCount} фильтров получено {UnitCount} уникальных Unit'ов",
job.Id, job.UnitFilters.Count, result.Count);
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в result
if (result.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в финальном результате.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в финальном результате.", targetUnitId);
}
#endif
return result;
}
@@ -403,15 +486,45 @@ namespace PARR.DAL.DomainServices.Implementations
return query.AsEnumerable().Where(dto =>
{
#if DEBUG
// ✅ Отладка: проверить, это нужный юнит
if (dto.Id == targetUnitId)
{
logger.LogDebug("Проверяем юнит {TargetUnitId} с {ParentCount} родителями и {ChildCount} детьми.",
targetUnitId, dto.Parents.Count, dto.Children.Count);
}
#endif
var links = isParent ? dto.Parents : dto.Children;
if (links == null || !links.Any())
{
#if DEBUG
// ✅ Отладка: юнит не имеет связей
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: нет связей ({Direction}), результат фильтра: {Result}",
targetUnitId, isParent ? "Parent" : "Child", isFullMatch && isInverse);
}
#endif
// Для IsFullMatch/IsInverse — "все подходят", т.е. true
// Для остальных — false
return isFullMatch && isInverse;
}
#if DEBUG
// ✅ Отладка: проверить, какие связи у юнита
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: {Count} связей ({Direction}).", targetUnitId, links.Count, isParent ? "Parent" : "Child");
foreach (var link in links)
{
logger.LogDebug(" Связь: UnitId={LinkUnitId}, Name={LinkName}, Values=[{Values}]", link.UnitId, link.Name, string.Join(", ", link.Values.Select(v => $"{v.FieldId}={v.Value}")));
}
}
#endif
var hasMatchingLinks = links.Any(link =>
{
var hasMatch = link.Values.Any(v =>
@@ -423,9 +536,25 @@ namespace PARR.DAL.DomainServices.Implementations
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
);
#if DEBUG
// ✅ Отладка: проверить, какая связь подходит
if (dto.Id == targetUnitId)
{
logger.LogDebug(" Проверка связи {LinkUnitId}: hasMatch={HasMatch} (ValueMask={ValueMask}, FieldId={FieldId})", link.UnitId, hasMatch, valueMask, fieldId);
}
#endif
return isInverse ? !hasMatch : hasMatch;
});
#if DEBUG
// ✅ Отладка: результат hasMatchingLinks
if (dto.Id == targetUnitId)
{
logger.LogDebug(" hasMatchingLinks = {HasMatchingLinks}, isInverse = {IsInverse}", hasMatchingLinks, isInverse);
}
#endif
if (isFullMatch)
{
var allMatch = links.All(link =>
@@ -439,9 +568,25 @@ namespace PARR.DAL.DomainServices.Implementations
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
);
#if DEBUG
// ✅ Отладка: проверить, все ли связи подходят
if (dto.Id == targetUnitId)
{
logger.LogDebug(" Проверка связи {LinkUnitId} для FullMatch: hasMatch={HasMatch}", link.UnitId, hasMatch);
}
#endif
return isInverse ? !hasMatch : hasMatch;
});
#if DEBUG
// ✅ Отладка: результат allMatch
if (dto.Id == targetUnitId)
{
logger.LogDebug(" allMatch = {AllMatch}, isFullMatch = {IsFullMatch}", allMatch, isFullMatch);
}
#endif
return allMatch;
}

View File

@@ -5,8 +5,6 @@ using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain;
using PARR.Constants;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.DomainServices.Shortcodes.Models;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit;
@@ -18,12 +16,14 @@ using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
using System.Text.Json;
namespace PARR.TemplateMatcher.Services.Implemetaions;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private const bool DefaultUnusedTemplateState = false;
private const bool DefaultUnusedScheduleState = false;
#if DEBUG
private readonly Guid targetUnitId = Guid.Parse("d4322a08-246b-4380-8953-8ce4a8446235");
#endif
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
@@ -37,11 +37,12 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateService templateService;
private readonly IJobGroupService jobGroupService;
private readonly ITemplateReuser templateReuser;
private readonly IShortcodesService shortcodesService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
private readonly IUnitRegionalEkPtkGroupService regionalEkPtkGroupService;
private readonly IUnitFieldService unitFieldService;
private readonly IJobService jobService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
@@ -54,11 +55,12 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
ITemplateService templateService,
IJobGroupService jobGroupService,
ITemplateReuser templateReuser,
IShortcodesService shortcodesService,
IEsppScheduleTransformService esppScheduleTransformService,
IUnitRegionalEkPtkGroupService regionalEkPtkGroupService,
IUnitFieldService unitFieldService,
IJobService jobService
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender
)
{
this.logger = logger;
@@ -71,11 +73,12 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
this.templateService = templateService;
this.jobGroupService = jobGroupService;
this.templateReuser = templateReuser;
this.shortcodesService = shortcodesService;
this.esppScheduleTransformService = esppScheduleTransformService;
this.regionalEkPtkGroupService = regionalEkPtkGroupService;
this.unitFieldService = unitFieldService;
this.jobService = jobService;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
@@ -108,7 +111,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
var jobsInGroup = jobGroup.Jobs.ToList();
// --- Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗАК" ---
// --- НОВАЯ ЛОГИКА: Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗАК" ---
var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
if (workGroupField == null)
{
@@ -123,17 +126,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", regionalGroupValueIds.Count);
// Загружаем заранее подготовленный Job (например, для неиспользуемых шаблонов)
var unusedJobId = Guid.Parse("8f85a91c-a223-4686-bb69-1f0ee73624f2");
var unusedJob = await jobService.Get()
.AsNoTracking()
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g.GroupType)
.FirstOrDefaultAsync(j => j.Id == unusedJobId);
// 2. Найти Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
@@ -162,6 +154,19 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
return;
}
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в expectedUnitIds
if (expectedUnitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в expectedUnitIds.", targetUnitId);
//return; // ❌ юнит отсеялся на этом этапе
}
#endif
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId (дополнительный фильтр)
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
@@ -185,6 +190,19 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.Select(u => u.Id)
.ToHashSet();
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet
if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
//return; // ❌ юнит отсеялся на этом этапе
}
#endif
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
if (!unitIdsWithValidGroupingFieldSet.Any())
@@ -204,6 +222,19 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.Select(u => u.Id) // Выбираем Id юнита
.ToHashSet(); // И снова в HashSet
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet
if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
//return; // ❌ юнит отсеялся на этом этапе
}
#endif
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count);
if (!unitIdsWithValidWorkGroupFieldSet.Any())
@@ -225,6 +256,20 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
.ToListAsync();
#if DEBUG
// ✅ Отладка: проверить, юнит участвует в potentialUnitInUnitLinks
var potentialLinksContainingTarget = potentialUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
if (potentialLinksContainingTarget.Any())
{
logger.LogDebug("Юнит {TargetUnitId} участвует в {Count} потенциальных связях UnitInUnit.", targetUnitId, potentialLinksContainingTarget.Count);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ участвует в потенциальных связях UnitInUnit.", targetUnitId);
// ❌ юнит отсеялся на этом этапе, если связи не требовались
}
#endif
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
// Загрузить UnitInValue для всех ParentUnitId и ChildUnitId из potentialUnitInUnitLinks
@@ -288,6 +333,20 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}
}
#if DEBUG
// ✅ Отладка: проверить, юнит участвует в filteredUnitInUnitLinks
var filteredLinksContainingTarget = filteredUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
if (filteredLinksContainingTarget.Any())
{
logger.LogDebug("Юнит {TargetUnitId} участвует в {Count} отфильтрованных связях UnitInUnit.", targetUnitId, filteredLinksContainingTarget.Count);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ участвует в отфильтрованных связях UnitInUnit.", targetUnitId);
// ❌ юнит отсеялся на этом этапе, если связи требовались
}
#endif
logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count);
// --- Сгруппировать юниты из unitIdsWithValidGroupingFieldSet по связанному юниту ---
@@ -317,6 +376,22 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count);
#if DEBUG
// ✅ Отладка: проверить, юнит есть в groupedRelationships.Values
var allUnitsInGroups = groupedRelationships.Values.SelectMany(x => x).ToList();
if (allUnitsInGroups.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в groupedRelationships.Values до разрешения конфликтов.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в groupedRelationships.Values до разрешения конфликтов.", targetUnitId);
// ❌ юнит отсеялся на этапе группировки
}
logger.LogDebug("Содержимое groupedRelationships до разрешения конфликтов: [{Groups}]", string.Join(", ", groupedRelationships.Select(kvp => $"Key: {kvp.Key}, Values: [{string.Join(", ", kvp.Value)}]")));
#endif
// --- Разрешение конфликта - один юнит из unitIdsWithValidGroupingFieldSet только в одном списке значений ---
var unitToKeys = new Dictionary<Guid, List<Guid>>(); // Карта: юнит из списка -> список ключей, где он встречается
@@ -334,9 +409,34 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}
}
#if DEBUG
// ✅ Отладка: проверить, юнит есть в unitToKeys до поиска конфликтов
if (unitToKeys.ContainsKey(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в unitToKeys до поиска конфликтов, находится в {Count} группах: [{Groups}]", targetUnitId, unitToKeys[targetUnitId].Count, string.Join(", ", unitToKeys[targetUnitId]));
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitToKeys до поиска конфликтов.", targetUnitId);
}
#endif
// Найти юниты, которые находятся в нескольких списках
var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList();
#if DEBUG
// ✅ Отладка: проверить, юнит в conflictedUnits
var targetConflictedEntry = conflictedUnits.FirstOrDefault(c => c.Key == targetUnitId);
if (targetConflictedEntry.Key != default)
{
logger.LogDebug("Юнит {TargetUnitId} находится в {Count} группах (конфликт).", targetUnitId, targetConflictedEntry.Value.Count);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ находится в конфликте (не в нескольких группах).", targetUnitId);
}
#endif
foreach (var conflictedUnitEntry in conflictedUnits)
{
var unitId = conflictedUnitEntry.Key;
@@ -367,6 +467,20 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}
}
#if DEBUG
// ✅ Отладка: проверить, юнит есть в groupedRelationships.Values после разрешения конфликтов
var allUnitsInGroupsAfterConflictResolution = groupedRelationships.Values.SelectMany(x => x).ToList();
if (allUnitsInGroupsAfterConflictResolution.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в groupedRelationships.Values после разрешения конфликтов.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в groupedRelationships.Values после разрешения конфликтов.", targetUnitId);
// ❌ юнит отсеялся на этапе разрешения конфликтов
}
#endif
// Удаляем ключи, у которых список стал пустым после разрешения конфликтов
var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList();
foreach (var key in keysToRemove)
@@ -377,11 +491,26 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count);
#if DEBUG
logger.LogDebug("Содержимое groupedRelationships после разрешения конфликтов: [{Groups}]", string.Join(", ", groupedRelationships.Select(kvp => $"Key: {kvp.Key}, Values: [{string.Join(", ", kvp.Value)}]")));
#endif
// 7. Разбить каждую группу и сопоставить с Job
foreach (var kvp in groupedRelationships)
{
var relationshipUnitId = kvp.Key; // Связанный юнит (не из unitIdsWithValidGroupingFieldSet)
var childUnitIds = kvp.Value; // Юниты из unitIdsWithValidGroupingFieldSet, связанные с regionalUnitId
if (childUnitIds.Count == 0)
continue;
#if DEBUG
// ✅ Отладка: проверить, есть ли ВРТ-AOS-05-ДВС в childUnitIds
var childUnitNames = childUnitIds.Select(id => unitService.Get().AsNoTracking().Where(u => u.Id == id).Select(u => u.Name).FirstOrDefaultAsync().GetAwaiter().GetResult() ?? id.ToString()).ToList();
if (childUnitNames.Contains("ВРТ-AOS-05-ДВС"))
{
var relationshipUnitName = await unitService.Get().AsNoTracking().Where(u => u.Id == relationshipUnitId).Select(u => u.Name).FirstOrDefaultAsync();
logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юнит 'ВРТ-AOS-05-ДВС' в childUnitIds: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNames));
}
#endif
logger.LogDebug("Обработка связанного юнита {RegionalUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
@@ -431,7 +560,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) актуален по юнитам.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i);
// Проверить, изменилось ли имя шаблона (например, из-за %МАКС:...% или %ТНК-КРАТКО%)
var expectedName = await GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup);
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup);
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.Name, expectedName);
@@ -448,13 +577,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
LastRun = existingTemplateForSubGroup.LastRun,
NextRun = nextRun,
Index = i ,
Index = i,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = subGroup
};
await SendTemplateUpdateMessage(updateRequest);
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
@@ -485,7 +614,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i);
var expectedName = await GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup);
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup);
var nextRun = await GetNextRunAsync(targetJob);
var updateRequest = new TemplateUpdaterMq
@@ -503,7 +632,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
UnitsInTemplate = subGroup
};
await SendTemplateUpdateMessage(updateRequest);
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
@@ -553,7 +682,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (!expectedTemplateKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Regional {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await DeactivateTemplateAsync(existingTemplate, existingTemplate.JobId, unusedJob, initiator);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
@@ -613,7 +742,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
return;
}
var expectedName = await GetNormalizedTemplateNameAsync(targetJob, template.UnitId, template.Index, newUnitIds);
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, template.UnitId, template.Index, newUnitIds);
var nextRun = await GetNextRunAsync(targetJob, template.NextRun);
var updateRequest = new TemplateUpdaterMq
@@ -632,7 +761,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
UnitsInTemplate = newUnitIds
};
await SendTemplateUpdateMessage(updateRequest);
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
private async Task CreateGroupedTemplateAsync(Guid jobId, Guid regionalUnitId, List<Guid> unitIds, int index, HistoryInitiator initiator)
@@ -655,132 +784,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, связанного юнита {RegionalUnitId}, Index {Index}.", jobId, regionalUnitId, index);
}
private async Task<bool> DeactivateTemplateAsync(
Template template,
Guid jobId,
Job unusedJob,
HistoryInitiator initiator)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
return true;
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
{
logger.LogDebug("Шаблон {TemplateId} уже неактивен (Unused), пропускаем деактивацию.", template.Id);
return true;
}
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.",
template.Id, template.UnitId);
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
return false;
}
if (unusedJob == null)
{
logger.LogError("Job для деактивированных шаблонов не найден.");
return false;
}
// Вычисляем имя шаблона с новым Job
var expectedName = await GetNormalizedTemplateNameAsync(unusedJob, template.UnitId);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = unusedJob.Id,
UnitId = template.UnitId,
Name = GetTemplateNameForUnused(expectedName),
IsActiveTemplate = DefaultUnusedTemplateState,
IsActiveSchedule = DefaultUnusedScheduleState,
LastRun = template.LastRun,
NextRun = template.NextRun,
Index = null,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator,
UnitsInTemplate = new List<Guid>()
};
await SendTemplateUpdateMessage(updateRequest);
return true;
}
private async Task SendTemplateUpdateMessage(TemplateUpdaterMq updateRequest)
{
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
var msg = JsonSerializer.Serialize(updateRequest);
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new[] { msg });
if (result.IsSuccess)
{
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
}
else
{
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.",
updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName);
}
}
private string GetTemplateNameForUnused(string templateName)
{
return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
private async Task<string> GetNormalizedTemplateNameAsync(Job targetJob, Guid unitId, int? index = null, List<Guid>? templateUnitIds = null)
{
// Подготовка объекта TemplateForShortcodes для передачи в ShortcodesService
var templateForShortcodes = new TemplateForShortcodes
{
Id = Guid.Empty, // Не используется в подстановке, но нужен для структуры
Index = index+1,
JobId = targetJob.Id,
UnitId = unitId,
Job = new JobForShortcodes
{
Group = targetJob.Group != null ? new JobGroupForShortcodes
{
Id = targetJob.Group.Id,
GroupingUnitFieldId = targetJob.Group.GroupingUnitFieldId,
GroupType = targetJob.Group.GroupType != null ? new JobGroupTypeForShortcodes
{
Code = targetJob.Group.GroupType.Code
} : null,
GroupName = targetJob.Group.GroupName
} : null,
Tnk = targetJob.Tnk != null ? new TnkForShortcodes
{
Name = targetJob.Tnk.Name,
ShortName = targetJob.Tnk.ShortName ?? ""
} : null,
WorkName = targetJob.WorkName,
Name = targetJob.Name
},
// Преобразование List<Guid> в List<UnitInTemplateForShortcodes>
UnitsInTemplate = templateUnitIds?.Select(id => new UnitInTemplateForShortcodes { UnitId = id }).ToList() ?? new List<UnitInTemplateForShortcodes>()
};
var rawName = await shortcodesService.ApplyShortcodesAsync(targetJob.TemplateNameMask, templateForShortcodes);
return rawName.ToUpper();
}
private async Task<DateTimeOffset> GetNextRunAsync(Job targetJob, DateTimeOffset? currentNextRun = null)
{
var now = DateTimeOffset.UtcNow;
if (currentNextRun.HasValue && currentNextRun.Value > now)
{
return currentNextRun.Value;
}
var referenceDate = targetJob.Group?.ReferenceDate ?? now;
var referenceDate = targetJob.Group?.ReferenceDate ?? DateTimeOffset.UtcNow;
return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate);
}
}

View File

@@ -4,25 +4,25 @@ using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain;
using PARR.Constants;
using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.DomainServices.Shortcodes.Models;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
using PARR.DAL.TransformServices;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
using System.Text.Json;
namespace PARR.TemplateMatcher.Services.Implemetaions;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{
private const bool DefaultUnusedTemplateState = false;
private const bool DefaultUnusedScheduleState = false;
#if DEBUG
private readonly Guid targetUnitId = Guid.Parse("358437ac-1eeb-4c00-840c-998326f657ac");
#endif
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
@@ -33,19 +33,30 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateService templateService;
private readonly IJobService jobService;
private readonly ITemplateReuser templateReuser;
private readonly IShortcodesService shortcodesService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
public SimpleTemplateSynchronizer(
ILogger<SimpleTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService,
IUnitService unitService,
MqSettings mqSettings,
IMqService mqService,
ITemplateService templateService,
IJobService jobService,
ITemplateReuser templateReuser,
IShortcodesService shortcodesService,
IEsppScheduleTransformService esppScheduleTransformService)
IEsppScheduleTransformService esppScheduleTransformService,
IUnitRegionalEkPtkGroupService regionalEkPtkGroupService,
IUnitFieldService unitFieldService,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender
)
{
this.logger = logger;
this.unitFilterService = unitFilterService;
@@ -54,120 +65,82 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
this.templateService = templateService;
this.jobService = jobService;
this.templateReuser = templateReuser;
this.shortcodesService = shortcodesService;
this.esppScheduleTransformService = esppScheduleTransformService;
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroupId {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
// Не делаем ничего
return;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId);
logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId);
// Загружаем заранее подготовленный Job (например, для неиспользуемых шаблонов)
var unusedJobId = Guid.Parse("8f85a91c-a223-4686-bb69-1f0ee73624f2");
var unusedJob = await jobService.Get()
var job = await jobService.Get()
.AsNoTracking()
.Include(j => j.AutoControl)
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g.GroupType)
.FirstOrDefaultAsync(j => j.Id == unusedJobId);
.ThenInclude(g => g!.GroupType)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId);
var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet<Guid>();
logger.LogDebug("JobId {JobId}: найдено {Count} UnitId по фильтрам.", jobId, expectedUnitIds.Count);
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null)
{
logger.LogError("Job с Id {JobId} не найден.", jobId);
logger.LogWarning("Job {JobId} не найден.", jobId);
return;
}
// Проверяем, является ли Job "групповым"
bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group;
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
if (unitIds == null || !unitIds.Any())
{
logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId);
return; // Ничего не делаем для группового Job
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
return;
}
var existingTemplates = await templateService.Get()
.AsNoTracking()
.Where(t => t.JobId == jobId)
.ToListAsync();
logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.",
jobId, expectedUnitIds.Count, existingTemplates.Count);
// Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны
if (!expectedUnitIds.Any())
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId))
{
if (existingTemplates.Any())
{
logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.",
jobId, existingTemplates.Count);
foreach (var template in existingTemplates)
{
await DeactivateTemplateAsync(template, jobId, unusedJob, initiator);
}
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
}
else
{
logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", jobId);
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
return; // ❌ юнит отсеялся на этом этапе
}
#endif
logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", jobId);
return;
}
// Деактивация шаблонов, которые вышли из фильтра
var templatesToDeactivate = existingTemplates
.Where(t => !expectedUnitIds.Contains(t.UnitId))
.ToList();
foreach (var template in templatesToDeactivate)
{
await DeactivateTemplateAsync(template, jobId, unusedJob, initiator);
}
// Перечитываем шаблоны после деактивации
existingTemplates = await templateService.Get()
var existingTemplates = await templateService.Get()
.AsNoTracking()
.Include(t => t.Job)
.ThenInclude(t => t.Tnk)
.Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == jobId)
.ToListAsync();
var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t);
var existingUnitIds = existingTemplates
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used)
.Select(t => t.UnitId)
.ToHashSet();
// UnitId без шаблона → попытка переиспользования или создание
var unitIdsMissingTemplates = expectedUnitIds
.Where(unitId => !unitToTemplate.ContainsKey(unitId))
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
var unusedTemplates = existingTemplates
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used && !unitIds.Contains(t.UnitId))
.ToList();
var unitIdsToCreateFresh = new List<Guid>();
foreach (var unitId in unitIdsMissingTemplates)
foreach (var unitId in newUnitIds)
{
var reused = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
if (reused != null) // если захват успешен
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId);
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
var expectedName = await GetNormalizedTemplateNameAsync(job, unitId);
var nextRun = await GetNextRunAsync(job); // всегда пересчитываем для нового назначения
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId);
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(job, unitId);
var nextRun = await GetNextRunAsync(job);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reused.Id,
TemplateId = reusableTemplate.Id,
JobId = jobId,
UnitId = unitId,
Name = expectedName,
@@ -176,238 +149,93 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
NextRun = nextRun,
UnitsInTemplate = new List<Guid>() // Для простого шаблона
UnitsInTemplate = new List<Guid>() // для простого шаблона
};
await SendTemplateUpdateMessage(updateRequest);
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId);
unitIdsToCreateFresh.Add(unitId);
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
await CreateSimpleTemplateAsync(jobId, unitId, initiator);
}
}
// Обновление/реактивация шаблонов, оставшихся в фильтре
var templatesInFilter = existingTemplates
.Where(t => expectedUnitIds.Contains(t.UnitId))
.ToList();
foreach (var template in templatesInFilter)
foreach (var unusedTemplate in unusedTemplates)
{
var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId);
await ReactivateOrRenameTemplateAsync(template, job, expectedName, initiator);
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
}
// Создание новых шаблонов
foreach (var unitId in unitIdsToCreateFresh)
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
await SendTemplateGeneratorMessageAsync(jobId, unitId, initiator);
}
logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId);
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
// Загружаем заранее подготовленный Job (например, для неиспользуемых шаблонов)
var unusedJobId = Guid.Parse("8f85a91c-a223-4686-bb69-1f0ee73624f2");
var unusedJob = await jobService.Get()
var job = await jobService.Get()
.AsNoTracking()
.Include(j => j.AutoControl)
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g.GroupType)
.FirstOrDefaultAsync(j => j.Id == unusedJobId);
.ThenInclude(g => g!.GroupType)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId);
if (job == null)
{
logger.LogWarning("Job {JobId} не найден.", jobId);
return;
}
var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
if (unitIds == null || !unitIds.Any())
{
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
return;
}
#if DEBUG
// ✅ Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
return; // ❌ юнит отсеялся на этом этапе
}
#endif
var existingTemplates = await templateService.Get()
.AsNoTracking()
.Where(t => t.JobId == jobId)
.Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync();
if (!existingTemplates.Any()) return;
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null) return;
var currentUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet<Guid>();
logger.LogDebug("JobId {JobId}: {Count} UnitId по текущему фильтру.", jobId, currentUnitIds.Count);
foreach (var template in existingTemplates)
{
logger.LogDebug("Обработка шаблона {TemplateId} (UnitId {UnitId}).", template.Id, template.UnitId);
bool unitStillInFilter = currentUnitIds.Contains(template.UnitId);
var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId);
TemplateStatusTypeEnum targetStatus;
bool targetIsActiveTemplate;
bool targetIsActiveSchedule;
if (unitStillInFilter)
if (unitIds.Contains(template.UnitId))
{
targetStatus = TemplateStatusTypeEnum.Used;
targetIsActiveTemplate = template.IsActiveTemplate;
targetIsActiveSchedule = template.IsActiveSchedule;
}
else
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(job, template.UnitId);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
targetStatus = TemplateStatusTypeEnum.Unused;
targetIsActiveTemplate = DefaultUnusedTemplateState;
targetIsActiveSchedule = DefaultUnusedScheduleState;
// ✅ Вычисляем имя шаблона с новым unusedJob
if (unusedJob != null)
{
expectedName = await GetNormalizedTemplateNameAsync(unusedJob, template.UnitId);
}
else
{
logger.LogError("Job для деактивированных шаблонов не найден.");
expectedName = GetTemplateNameForUnused(template.Name);
}
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId);
}
bool needsUpdate = template.StatusTypeId != TemplateStatusTypeEnum.Updating &&
(
// 1. Статус изменился (например, Used → Unused или Unused → Used)
template.StatusTypeId != targetStatus ||
// 2. Для Used — имя должно соответствовать шаблону
(targetStatus == TemplateStatusTypeEnum.Used && template.Name != expectedName) ||
// 3. Флаги изменились (редко, но возможно через AutoControl изменение)
template.IsActiveTemplate != targetIsActiveTemplate ||
template.IsActiveSchedule != targetIsActiveSchedule
);
if (!needsUpdate) continue;
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
continue;
}
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName);
var nextRun = await GetNextRunAsync(job, template.NextRun);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = unitStillInFilter ? jobId : unusedJob?.Id ?? jobId, // ✅ Используем unusedJob.Id, если деактивация
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = targetIsActiveTemplate,
IsActiveSchedule = targetIsActiveSchedule,
LastRun = template.LastRun,
NextRun = nextRun,
Index = template.Index,
StatusTypeId = targetStatus,
Initiator = initiator,
UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() // Для простого шаблона это список из одного элемента или пустой
};
await SendTemplateUpdateMessage(updateRequest);
}
logger.LogInformation("Обновление шаблонов завершено для JobId {JobId}.", jobId);
}
// --- Вспомогательные методы ---
private async Task<bool> DeactivateTemplateAsync(
Template template,
Guid jobId,
Job unusedJob,
HistoryInitiator initiator)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
return true; // уже в обработке
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
{
logger.LogDebug("Шаблон {TemplateId} уже неактивен (Unused), пропускаем деактивацию.", template.Id);
return true;
}
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.",
template.Id, template.UnitId);
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
return false;
}
if (unusedJob == null)
{
logger.LogError("Job для деактивированных шаблонов не найден.");
return false;
}
// ✅ Вычисляем имя шаблона с новым Job
var expectedName = await GetNormalizedTemplateNameAsync(unusedJob, template.UnitId);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = unusedJob.Id, // ✅ Используем unusedJob.Id
UnitId = template.UnitId,
Name = GetTemplateNameForUnused(expectedName), // ✅ Используем новое имя
IsActiveTemplate = DefaultUnusedTemplateState,
IsActiveSchedule = DefaultUnusedScheduleState,
LastRun = template.LastRun,
NextRun = template.NextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator,
UnitsInTemplate = new List<Guid>() // Для простого шаблона
};
await SendTemplateUpdateMessage(updateRequest);
return true;
}
private async Task<bool> ReactivateOrRenameTemplateAsync(
Template template,
Job job,
string expectedName,
HistoryInitiator initiator)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
return true;
bool needsUpdate = template.Name != expectedName
|| template.StatusTypeId != TemplateStatusTypeEnum.Used;
if (!needsUpdate) return true;
logger.LogInformation("Шаблон {TemplateId}: требуется обновление имени или реактивация.", template.Id);
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
return false;
}
var nextRun = await GetNextRunAsync(job, template.NextRun);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = job.Id,
JobId = jobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
@@ -417,121 +245,39 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() // Для простого шаблона это список из одного элемента
UnitsInTemplate = new List<Guid>() // для простого шаблона
};
await SendTemplateUpdateMessage(updateRequest);
return true;
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
}
}
private async Task<bool> SendTemplateGeneratorMessageAsync(
Guid jobId,
Guid unitId,
HistoryInitiator initiator)
logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId);
}
private async Task CreateSimpleTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator)
{
logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId);
logger.LogInformation("Создание нового простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = unitId,
HistoryInitiator = initiator,
UnitsInTemplate = new List<Guid>() // Для простого шаблона
UnitsInTemplate = new List<Guid>(), // для простого шаблона
HistoryInitiator = initiator
};
var msg = JsonSerializer.Serialize(mqRequest);
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания шаблона для UnitId {UnitId}.", unitId);
return result.IsSuccess;
}
private async Task SendTemplateUpdateMessage(TemplateUpdaterMq updateRequest)
{
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
var msg = JsonSerializer.Serialize(updateRequest);
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new[] { msg });
if (result.IsSuccess)
{
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
}
else
{
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.",
updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName);
}
}
private string GetTemplateNameForUnused(string templateName)
{
return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{
return await jobService.Get()
.AsNoTracking()
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(j => j.GroupType)
.Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId);
}
private async Task<HashSet<Guid>> GetExpectedUnitIdsAsync(Guid jobId)
{
var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
return units?.ToHashSet() ?? new HashSet<Guid>();
}
private async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId)
{
// Создаём TemplateForShortcodes "на лету", без запроса к БД
var templateForShortcodes = new TemplateForShortcodes
{
Id = Guid.Empty, // шаблон ещё не создан
Index = null,
JobId = job.Id,
UnitId = unitId,
Job = new JobForShortcodes
{
Group = job.Group == null ? null : new JobGroupForShortcodes
{
GroupingUnitFieldId = job.Group.GroupingUnitFieldId,
GroupType = job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes
{
Code = job.Group.GroupType.Code
},
GroupName = job.Group.GroupName
},
Tnk = job.Tnk == null ? null : new TnkForShortcodes
{
Name = job.Tnk.Name,
ShortName = job.Tnk.ShortName ?? ""
},
WorkName = job.WorkName,
Name = job.Name
},
UnitsInTemplate = new List<UnitInTemplateForShortcodes>() // для простого шаблона
};
var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, templateForShortcodes);
return rawName.ToUpper();
logger.LogError("Ошибка отправки команды создания простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
}
private async Task<DateTimeOffset> GetNextRunAsync(Job job, DateTimeOffset? currentNextRun = null)
{
var now = DateTimeOffset.UtcNow;
if (currentNextRun.HasValue && currentNextRun.Value > now)
{
return currentNextRun.Value;
}
var referenceDate = job.Group?.ReferenceDate ?? now;
var referenceDate = job.Group?.ReferenceDate ?? DateTimeOffset.UtcNow;
return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate);
}
}

View File

@@ -0,0 +1,101 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain;
using PARR.Constants;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
using System.Text.Json;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateDeactivator : ITemplateDeactivator
{
private const bool DefaultUnusedTemplateState = false;
private const bool DefaultUnusedScheduleState = false;
private readonly ILogger<TemplateDeactivator> logger;
private readonly ITemplateService templateService;
private readonly IJobService jobService;
private readonly ITemplateNameNormalizer namenormalizer;
private readonly ITemplateUpdaterMqSender sender;
private readonly Guid unusedJobId = Guid.Parse("8f85a91c-a223-4686-bb69-1f0ee73624f2");
public TemplateDeactivator(
ILogger<TemplateDeactivator> logger,
ITemplateService templateService,
IJobService jobService,
ITemplateNameNormalizer namenormalizer,
ITemplateUpdaterMqSender sender)
{
this.logger = logger;
this.templateService = templateService;
this.jobService = jobService;
this.namenormalizer = namenormalizer;
this.sender = sender;
}
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
return true; // уже в обработке
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
{
logger.LogDebug("Шаблон {TemplateId} уже неактивен (Unused), пропускаем деактивацию.", template.Id);
return true;
}
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.",
template.Id, template.UnitId);
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
return false;
}
var unusedJob = await jobService.Get()
.AsNoTracking()
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g!.GroupType)
.FirstOrDefaultAsync(j => j.Id == unusedJobId);
if (unusedJob == null)
{
logger.LogError("Job для деактивированных шаблонов не найден.");
return false;
}
// Вычисляем имя шаблона с новым Job
var expectedName = await namenormalizer.GetNormalizedTemplateNameAsync(unusedJob, template.UnitId);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = unusedJob.Id,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = DefaultUnusedTemplateState,
IsActiveSchedule = DefaultUnusedScheduleState,
LastRun = template.LastRun,
NextRun = template.NextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator,
UnitsInTemplate = new List<Guid>()
};
await sender.SendTemplateUpdateMessageAsync(updateRequest);
return true;
}
}

View File

@@ -0,0 +1,51 @@
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.DomainServices.Shortcodes.Models;
using PARR.DAL.Models.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateNameNormalizer : ITemplateNameNormalizer
{
private readonly IShortcodesService shortcodesService;
public TemplateNameNormalizer(IShortcodesService shortcodesService)
{
this.shortcodesService = shortcodesService;
}
public async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null, List<Guid>? templateUnitIds = null)
{
var templateForShortcodes = new TemplateForShortcodes
{
Id = Guid.Empty,
Index = (index != null) ? index + 1 : index,
JobId = job.Id,
UnitId = unitId,
Job = new JobForShortcodes
{
Group = job.Group != null ? new JobGroupForShortcodes
{
Id = job.Group.Id,
GroupingUnitFieldId = job.Group.GroupingUnitFieldId,
GroupType = job.Group.GroupType != null ? new JobGroupTypeForShortcodes
{
Code = job.Group.GroupType.Code
} : null,
GroupName = job.Group.GroupName
} : null,
Tnk = job.Tnk != null ? new TnkForShortcodes
{
Name = job.Tnk.Name,
ShortName = job.Tnk.ShortName ?? ""
} : null,
WorkName = job.WorkName,
Name = job.Name
},
UnitsInTemplate = templateUnitIds?.Select(id => new UnitInTemplateForShortcodes { UnitId = id }).ToList() ?? new List<UnitInTemplateForShortcodes>()
};
var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, templateForShortcodes);
return rawName.ToUpper();
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.Extensions.Logging;
using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
using System.Text.Json;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateUpdaterMqSender : ITemplateUpdaterMqSender
{
private readonly ILogger<TemplateUpdaterMqSender> logger;
private readonly IMqService mqService;
private readonly MqSettings mqSettings;
public TemplateUpdaterMqSender(
ILogger<TemplateUpdaterMqSender> logger,
IMqService mqService,
MqSettings mqSettings)
{
this.logger = logger;
this.mqService = mqService;
this.mqSettings = mqSettings;
}
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMq updateRequest)
{
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
var msg = JsonSerializer.Serialize(updateRequest);
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new[] { msg });
if (result.IsSuccess)
{
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
}
else
{
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.",
updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName);
}
}
}

View File

@@ -0,0 +1,10 @@
using PARR.Common.Domain;
using PARR.DAL.Models;
namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface ITemplateDeactivator
{
Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator);
}
}

View File

@@ -0,0 +1,9 @@
using PARR.DAL.Models.Job;
namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface ITemplateNameNormalizer
{
Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null, List<Guid>? templateUnitIds = null);
}
}

View File

@@ -0,0 +1,9 @@
using PARR.BLL.Domain.Mq;
namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface ITemplateUpdaterMqSender
{
Task SendTemplateUpdateMessageAsync(TemplateUpdaterMq updateRequest);
}
}

View File

@@ -4,6 +4,7 @@ using PARR.Common.Domain;
using PARR.DAL.Contracts;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces.Job;
using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Implemetaions;
using PARR.TemplateMatcher.Services.Interfaces;
@@ -42,7 +43,7 @@ namespace PARR.TemplateMatcher
// Проверяем, является ли Job "групповым"
bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group;
if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue)
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{
logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer.", jobId);
// Находим нужный синхронизатор
@@ -103,7 +104,7 @@ namespace PARR.TemplateMatcher
// Проверяем, является ли Job "групповым"
bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group;
if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue)
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{
logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer для Update.", jobId);
// Находим нужный синхронизатор
@@ -144,7 +145,7 @@ namespace PARR.TemplateMatcher
return await jobService.Get()
.AsNoTracking() // Добавлено
.Include(j => j.Group)
.ThenInclude(j => j.GroupType)
.ThenInclude(j => j!.GroupType)
.Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId);
}

View File

@@ -25,6 +25,9 @@ namespace PARR.TemplateMatcher
services.AddTransient<IJobGroupValidatorService, JobGroupValidatorService>();
services.AddTransient<ITemplateMatcher, TemplateMatcher>();
services.AddTransient<ITemplateReuser, TemplateReuser>();
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
}