diff --git a/PARR.API/Controllers/V1/RobotTaskController.cs b/PARR.API/Controllers/V1/RobotTaskController.cs index 0dcc7853..9f3f1d3c 100644 --- a/PARR.API/Controllers/V1/RobotTaskController.cs +++ b/PARR.API/Controllers/V1/RobotTaskController.cs @@ -11,7 +11,8 @@ using PARR.API.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.Services.Interfaces; using PARR.DAL.TransformServices; @@ -68,55 +69,61 @@ namespace PARR.API.Controllers.V1 var query = robotConfigurationService.Get() .Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode); - //var query = robotConfigurationService.GetAsync(); - switch (robotCode) { case RobotsEnum.TemplateOrder: // шаблоны - query = query.Include(t => t.Template) - .ThenInclude(t => t!.Unit) - .ThenInclude(t => t!.UnitValues) - .ThenInclude(t => t.Field) - .Include(t => t.Template) - .ThenInclude(t => t!.Unit) - .ThenInclude(t => t!.UnitValues) - .ThenInclude(t => t.Value) - .Include(t => t.Template) - .ThenInclude(a => a!.Job) - .ThenInclude(t => t!.Group) - .Include(t => t.Template) - .ThenInclude(w => w!.Job) - .ThenInclude(t => t!.Tnk) - .ThenInclude(s => s!.Subprocess) - .ThenInclude(p => p!.Process); + query = query + .Include(t => t.Template) + .ThenInclude(t => t!.Unit) + .ThenInclude(t => t!.UnitValues) + .ThenInclude(t => t.Field) + .Include(t => t.Template) + .ThenInclude(t => t!.Unit) + .ThenInclude(t => t!.UnitValues) + .ThenInclude(t => t.Value) + .Include(t => t.Template) + .ThenInclude(a => a!.Job) + .ThenInclude(t => t!.Group) + .Include(t => t.Template) + .ThenInclude(w => w!.Job) + .ThenInclude(t => t!.Tnk) + .ThenInclude(s => s!.Subprocess) + .ThenInclude(p => p!.Process); + + query = query + .Include(t => t.Template) + .ThenInclude(t => t.UnitsInTemplate); + break; + case RobotsEnum.ScheduleOrder: //расписание - // тут не делаем AsSplitQuery, не может подтянуть все таблицы - query = query.Include(t => t.Template) - .ThenInclude(t => t!.Unit) - .ThenInclude(t => t!.UnitValues) - .ThenInclude(t => t.Field) - .Include(t => t.Template) - .ThenInclude(t => t!.Unit) - .ThenInclude(t => t!.UnitValues) - .ThenInclude(t => t.Value) - .Include(t => t.Template) - .ThenInclude(a => a!.Job) - .ThenInclude(t => t!.Group) - .ThenInclude(t => t!.EsppSchValues) - .ThenInclude(t => t!.EsppSchTypeConfig) - .ThenInclude(t => t!.EsppSchTypeSchedule); + query = query + .Include(t => t.Template) + .ThenInclude(t => t!.Unit) + .ThenInclude(t => t!.UnitValues) + .ThenInclude(t => t.Field) + .Include(t => t.Template) + .ThenInclude(t => t!.Unit) + .ThenInclude(t => t!.UnitValues) + .ThenInclude(t => t.Value) + .Include(t => t.Template) + .ThenInclude(a => a!.Job) + .ThenInclude(t => t!.Group) + .ThenInclude(t => t!.EsppSchValues) + .ThenInclude(t => t!.EsppSchTypeConfig) + .ThenInclude(t => t!.EsppSchTypeSchedule) + .Include(t => t.Template) + .ThenInclude(t => t.UnitsInTemplate); //выбираем только записи с созданными шаблонами (у которых статус 20 или 30), а только потом у них ищем расписания - // сначала находим шаблоны со статусом 30 var createdTemplates = robotConfigurationService.Get() - .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && (t.TaskStatusCode == (int)TaskStatusEnum.Ok)) - .Select(t => t.TemplateId); - //находим задания с расписанием по списку шаблонов createdTemplates + .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && (t.TaskStatusCode == (int)TaskStatusEnum.Ok)) + .Select(t => t.TemplateId); query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && createdTemplates.Contains(t.TemplateId)); break; + default: break; } @@ -161,34 +168,96 @@ namespace PARR.API.Controllers.V1 switch (robotCode) { case RobotsEnum.TemplateOrder: - //RobotTaskTemplateResponse - var robotTaskTemplateResponse = mapper.Map(task); + { //RobotTaskTemplateResponse + var robotTaskTemplateResponse = mapper.Map(task); - //TODO Вынести в отдельный метод ShortcodesService - if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.FullDescription)) - robotTaskTemplateResponse.FullDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.FullDescription, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.ShortDescription)) - robotTaskTemplateResponse.ShortDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.ShortDescription, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.Solution)) - robotTaskTemplateResponse.Solution = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.Solution, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.TnkName)) - robotTaskTemplateResponse.TnkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.TnkName, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.WorkName)) - robotTaskTemplateResponse.WorkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkName, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.WorkGroup)) - robotTaskTemplateResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkGroup, task!.Template!.UnitId, task!.Template!.JobId); + //TODO Вынести в отдельный метод ShortcodesService - return Ok(new Response(robotTaskTemplateResponse, true)); + // ✅ Построим TemplateForShortcodes из уже загруженного task.Template + var templateForShortcodes = new TemplateForShortcodes + { + Id = task.Template!.Id, + Index = task.Template.Index, + JobId = task.Template.JobId, + UnitId = task.Template.UnitId, + Job = task.Template.Job == null ? null : new JobForShortcodes + { + Group = task.Template.Job.Group == null ? null : new JobGroupForShortcodes + { + GroupingUnitFieldId = task.Template.Job.Group.GroupingUnitFieldId, + GroupType = task.Template.Job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes + { + Code = task.Template.Job.Group.GroupType.Code + }, + GroupName = task.Template.Job.Group.GroupName + }, + Tnk = task.Template.Job.Tnk == null ? null : new TnkForShortcodes + { + Name = task.Template.Job.Tnk.Name, + ShortName = task.Template.Job.Tnk.ShortName ?? "" + }, + WorkName = task.Template.Job.WorkName, + Name = task.Template.Job.Name + }, + UnitsInTemplate = task.Template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes { UnitId = uit.UnitId }).ToList() ?? new List() + }; + + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.FullDescription)) + robotTaskTemplateResponse.FullDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.FullDescription , templateForShortcodes); + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.ShortDescription)) + robotTaskTemplateResponse.ShortDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.ShortDescription, templateForShortcodes); + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.Solution)) + robotTaskTemplateResponse.Solution = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.Solution, templateForShortcodes); + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.TnkName)) + robotTaskTemplateResponse.TnkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.TnkName, templateForShortcodes); + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.WorkName)) + robotTaskTemplateResponse.WorkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkName, templateForShortcodes); + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.WorkGroup)) + robotTaskTemplateResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkGroup, templateForShortcodes); + + return Ok(new Response(robotTaskTemplateResponse, true)); + } case RobotsEnum.ScheduleOrder: - // если был запрос на расписание, проверяем у него nextRun, lastRun, обновляем их - await UpdateLastNextRunDate(task); + { // если был запрос на расписание, проверяем у него nextRun, lastRun, обновляем их + await UpdateLastNextRunDate(task); - //RobotTaskScheduleResponse - var robotTaskScheduleResponse = mapper.Map(task); - if (shortcodesService.IsAnyShortcodes(robotTaskScheduleResponse.WorkGroup)) - robotTaskScheduleResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.WorkGroup, task!.Template!.UnitId, task!.Template!.JobId); + //RobotTaskScheduleResponse + var robotTaskScheduleResponse = mapper.Map(task); - return Ok(new Response(robotTaskScheduleResponse, true)); + // Построим TemplateForShortcodes из уже загруженного task.Template + var templateForShortcodes = new TemplateForShortcodes + { + Id = task.Template!.Id, + Index = task.Template.Index, + JobId = task.Template.JobId, + UnitId = task.Template.UnitId, + Job = task.Template.Job == null ? null : new JobForShortcodes + { + Group = task.Template.Job.Group == null ? null : new JobGroupForShortcodes + { + GroupingUnitFieldId = task.Template.Job.Group.GroupingUnitFieldId, + GroupType = task.Template.Job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes + { + Code = task.Template.Job.Group.GroupType.Code + }, + GroupName = task.Template.Job.Group.GroupName + }, + Tnk = task.Template.Job.Tnk == null ? null : new TnkForShortcodes + { + Name = task.Template.Job.Tnk.Name, + ShortName = task.Template.Job.Tnk.ShortName + }, + WorkName = task.Template.Job.WorkName, + Name = task.Template.Job.Name + }, + UnitsInTemplate = task.Template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes { UnitId = uit.UnitId }).ToList() ?? new List() + }; + + if (shortcodesService.IsAnyShortcodes(robotTaskScheduleResponse.WorkGroup)) + robotTaskScheduleResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.WorkGroup, templateForShortcodes); + + return Ok(new Response(robotTaskScheduleResponse, true)); + } default: break; } diff --git a/PARR.API/Controllers/V1/ShortcodeController.cs b/PARR.API/Controllers/V1/ShortcodeController.cs index 4720fdda..3f44c42d 100644 --- a/PARR.API/Controllers/V1/ShortcodeController.cs +++ b/PARR.API/Controllers/V1/ShortcodeController.cs @@ -6,7 +6,7 @@ using PARR.API.Contracts.V1.Responses; using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Controllers.V1.Base; using PARR.Constants; -using PARR.DAL.DomainServices.Interfaces; +using PARR.DAL.DomainServices.Shortcodes; namespace PARR.API.Controllers.V1 { diff --git a/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs b/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs index 242f9ac3..6a4318ff 100644 --- a/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs +++ b/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs @@ -2,8 +2,8 @@ using MockQueryable; using Moq; using PARR.DAL.Contracts; -using PARR.DAL.DomainServices.Implementations; using PARR.DAL.DomainServices.Interfaces; +using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.Models.Job; using PARR.DAL.Models.Unit; using PARR.DAL.Services.Interfaces.Job; @@ -21,32 +21,10 @@ namespace PARR.DAL.Tests.DomainServices.Implementations private readonly Mock unitFieldServiceMock = new(); private readonly Mock unitFilterServiceMock = new(); - private ShortcodesService CreateService() - => new(loggerMock.Object, settingsMock.Object, jobServiceMock.Object, - unitServiceMock.Object, unitFilterServiceMock.Object, - unitInValueServiceMock.Object, unitFieldServiceMock.Object); - [Fact] public async Task ApplyShortcodesAsync_WithEc_ShouldReplaceEc() { - // Arrange - var unitId = Guid.NewGuid(); - var jobId = Guid.NewGuid(); - var unit = new Unit { Id = unitId, Name = "Сервер_01" }; - var job = new Job { Id = jobId, Name = "SAN ТО-1 Проверка работы фабрики SAN", TemplateNameMask = "%ЭК%", WorkGroupMask = "", WorkName = "Аудит" }; - - // ✅ БЕЗ .Object — ключевое! - unitServiceMock.Setup(x => x.Get()).Returns(new[] { unit }.BuildMock()); - jobServiceMock.Setup(x => x.Get()).Returns(new[] { job }.BuildMock()); - - var service = CreateService(); - - // Act - var result = await service.ApplyShortcodesAsync("%ЭК%", unitId, jobId); - - // Assert - Assert.Equal("Сервер_01", result); } } } \ No newline at end of file diff --git a/PARR.DAL/DomainServices/Implementations/GroupedShortcodesCacheService.cs b/PARR.DAL/DomainServices/Implementations/GroupedShortcodesCacheService.cs new file mode 100644 index 00000000..099f2067 --- /dev/null +++ b/PARR.DAL/DomainServices/Implementations/GroupedShortcodesCacheService.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Logging; +using PARR.DAL.CacheServices; +using PARR.DAL.DomainServices.Interfaces; +using PARR.DAL.Settings; +using System.Text.RegularExpressions; + +namespace PARR.DAL.DomainServices.Implementations +{ + public class GroupedShortcodesCacheService : IGroupedShortcodesCacheService + { + private readonly IRedisCacheService cacheService; + private readonly GroupedShortcodesCacheSettings settings; + private readonly ILogger logger; + + public GroupedShortcodesCacheService( + IRedisCacheService cacheService, + GroupedShortcodesCacheSettings settings, + ILogger logger) + { + this.cacheService = cacheService; + this.settings = settings; + this.logger = logger; + } + + public async Task GetAggregatedValueAsync( + Guid unitId, + string shortcode, + Func> computeIfMissing) + { + if (string.IsNullOrEmpty(shortcode)) + throw new ArgumentException("Ключ шорткода должен быть указан.", nameof(shortcode)); + + var cacheKey = GetCacheKey(unitId, shortcode); + + try + { + var cachedValue = await cacheService.GetCachedDataAsync(cacheKey); + if (cachedValue != null) + { + logger.LogDebug( + "Попадание в кэш для шорткода '{ShortcodeKey}': unit={UnitId} → '{Value}'", + shortcode, unitId, cachedValue); + return cachedValue; + } + + logger.LogDebug( + "Промах кэша для шорткода '{ShortcodeKey}': unit={UnitId}. Вычисление...", + shortcode, unitId); + + var computedValue = await computeIfMissing(); + + await cacheService.SetCachedDataAsync(cacheKey, computedValue, settings.ValueTtl); + + logger.LogDebug( + "Вычислено и сохранено значение для '{ShortcodeKey}': unit={UnitId} → '{Value}' (срок хранения={Ttl})", + shortcode, unitId, computedValue, settings.ValueTtl); + + return computedValue; + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "Ошибка при получении или вычислении значения для шорткода '{ShortcodeKey}' (unit={UnitId}). Возвращена пустая строка.", + shortcode, unitId); + + return string.Empty; + } + } + + private static string GetCacheKey(Guid unitId, string shortcodeKey) + { + var safeKey = shortcodeKey + .Trim() + .Replace(":", "_") + .Replace(" ", "_") + .Replace(".", "_") + .Replace("%", "") + .Replace("[", "_") + .Replace("]", "_") + .Replace("/", "_") + .Replace("\\", "_"); + + safeKey = Regex.Replace(safeKey, @"[^a-zA-Z0-9_-]", "_"); + + return $"gr_shcd_{unitId:N}_{safeKey}"; // :N — без дефисов в Guid + } + } +} \ No newline at end of file diff --git a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs b/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs deleted file mode 100644 index 7b9f482e..00000000 --- a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs +++ /dev/null @@ -1,287 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using PARR.Constants; -using PARR.DAL.Contracts; -using PARR.DAL.DomainModels; -using PARR.DAL.DomainServices.Interfaces; -using PARR.DAL.Models.Job; -using PARR.DAL.Models.Unit; -using PARR.DAL.Services.Interfaces.Job; -using PARR.DAL.Services.Interfaces.Unit; -using System.Text.RegularExpressions; - -namespace PARR.DAL.DomainServices.Implementations -{ - internal class ShortcodesService : IShortcodesService - { - private const string shortcodePattern = "%[^%\\s]+%"; - - private static readonly HashSet SupportedShortcodes = new(StringComparer.OrdinalIgnoreCase) - { - "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%СВЯЗИ%", "%ТНК-КРАТКО%", "%СВЯЗИ-ПН%", "%ИНДЕКС%" - }; - - private readonly ILogger logger; - private readonly SettingsFromDb settingsFromDb; - private readonly IJobService jobService; - private readonly IUnitService unitService; - private readonly IUnitInValueService unitInValueService; - private readonly IUnitFieldService unitFieldService; - private readonly IUnitFilterService unitFilterService; - - public ShortcodesService( - ILogger logger, - SettingsFromDb settingsFromDb, - IJobService jobService, - IUnitService unitService, - IUnitFilterService unitFilterService, - IUnitInValueService unitInValueService, - IUnitFieldService unitFieldService - ) - { - this.logger = logger; - this.settingsFromDb = settingsFromDb; - this.jobService = jobService; - this.unitService = unitService; - this.unitInValueService = unitInValueService; - this.unitFieldService = unitFieldService; - this.unitFilterService = unitFilterService; - } - - public async Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null) - { - logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', unitId={UnitId}, jobId={JobId}", str, unitId, jobId); - - var job = await jobService - .Get().AsNoTracking() - .Include(j => j.Tnk) - .Include(j => j.Group) - .Include(j => j.UnitFilters) - .ThenInclude(uf => uf.RelationshipFilters) - .FirstOrDefaultAsync(j => j.Id == jobId); - - var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == unitId); - - if (job == null || unit == null || string.IsNullOrEmpty(str)) - { - logger.LogError("Переданы некорректные данные для подстановки динамических записей"); - return str; - } - - var resultName = str; - var shortcodesInMask = GetShortCodes(resultName); - - // 1. Статические константы - var nameConstants = settingsFromDb.TemplateNameConstantPartsList; - if (shortcodesInMask.Any(m => nameConstants.Any(c => $"%{c.Name}%".Equals(m.Value, StringComparison.OrdinalIgnoreCase)))) - { - resultName = ReplaceConstants(nameConstants, resultName); - } - - // 2. Стандартные шорткоды — с поддержкой вложенных (%РАБОТА% → "Мониторинг | %ЭК%") - const int MaxStandardIterations = 3; - var iteration = 0; - - while (iteration < MaxStandardIterations) - { - // Ищем ТОЛЬКО поддерживаемые шорткоды в текущей строке - var remainingShortcodes = GetShortCodes(resultName) - .Select(m => m.Value) - .Where(s => SupportedShortcodes.Contains(s)) - .ToList(); - - if (!remainingShortcodes.Any()) - break; - - // Делаем замену - var oldResult = resultName; - resultName = ReplaceStandardShortcodes(job, unit, resultName, index); - iteration++; - - // Защита от "бесполезных" итераций (строка не изменилась) - if (resultName == oldResult) - { - logger.LogDebug("Замена стандартных шорткодов не изменила строку на итерации {Iteration}. Останов.", iteration); - break; - } - } - - if (iteration >= MaxStandardIterations) - { - logger.LogWarning( - "Достигнуто максимальное число итераций ({Max}) при замене стандартных шорткодов. Текущий результат: {Result}", - MaxStandardIterations, resultName); - } - - // 3. %СВЯЗИ% или %СВЯЗИ-ПН% - List? relatedUnitNames = null; - - // 3.1. %СВЯЗИ% - if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase))) - { - relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(jobId, unitId); - var linksText = string.Join("\n", relatedUnitNames); - resultName = Regex.Replace(resultName, "%СВЯЗИ%", linksText, RegexOptions.IgnoreCase); - } - - // 3.2. %СВЯЗИ-ПН% - if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase))) - { - relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(jobId, unitId); - var linksText = string.Join("\n", relatedUnitNames.Select((name, i) => $"{i + 1}. {name}")); - resultName = Regex.Replace(resultName, "%СВЯЗИ-ПН%", linksText, RegexOptions.IgnoreCase); - } - - // 4. Поля (оставшиеся %FIELD_NAME%) - выполняем запрос ТОЛЬКО если после предыдущих замен остались необработанные шорткоды - shortcodesInMask = GetShortCodes(resultName); - if (shortcodesInMask.Count > 0) - resultName = await ReplaceFieldValues(unitId, resultName, shortcodesInMask); - - logger.LogDebug("Подстановка завершена. Результат: '{Result}'", resultName); - - return resultName; - } - - - public bool IsAnyShortcodes(string str) - { - return Regex.IsMatch(str, shortcodePattern); - } - - - private static List GetShortCodes(string resultName) - { - var shortcodesInMask = Regex.Matches(resultName, shortcodePattern).ToList(); - return shortcodesInMask; - } - - public async Task> GetAvailableShortcodesAsync() - { - var result = new List(); - - // 1. Статические константы — из settingsFromDb - foreach (var constant in settingsFromDb.TemplateNameConstantPartsList) - { - result.Add(new ShortcodeInfoDto - { - Shortcode = $"%{constant.Name}%", - Description = $"Константа: {constant.Value ?? "(пусто)"}", - Type = ShortcodeTypeEnum.Static - }); - } - - // 2. Стандартные шорткоды - result.AddRange(new[] - { - new ShortcodeInfoDto { Shortcode = "%ЭК%", - Description = "Наименование ЭК(Код поиска)", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto { Shortcode = "%ГРУППА_РАБОТ%", - Description = "Наименование группы работ", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto { Shortcode = "%РАБОТА%", - Description = "Наименование работы в АСУ ЕСПП", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto { Shortcode = "%ТНК%", - Description = "Полное наименование ТНК", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto { Shortcode = "%ТНК-КРАТКО%", - Description = "Краткое наименование ТНК", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto { Shortcode = "%ИНДЕКС%", - Description = "Порядковый индекс шаблона для групповых работ", - Type = ShortcodeTypeEnum.Standart } - }); - - // 3. Связи - result.AddRange(new[] - { - new ShortcodeInfoDto { Shortcode = "%СВЯЗИ%", - Description = "Связанные ЭК (по одному на строку), выбираются только при настроенном фильтре по полям в связанных ЭК", - Type = ShortcodeTypeEnum.Relationship }, - new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", - Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК", - Type = ShortcodeTypeEnum.Relationship } - }); - - // 4. Все доступные поля из UnitField - var fieldNames = await unitFieldService.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync(); - foreach (var fieldName in fieldNames.OrderBy(n => n.AihitName)) - { - result.Add(new ShortcodeInfoDto - { - Shortcode = $"%{fieldName.AihitName}%", - Description = $"Атрибут: {fieldName.DisplayName ?? fieldName.AihitName}", - Type = ShortcodeTypeEnum.FieldValue - }); - } - return result; - } - - - private async Task ReplaceFieldValues(Guid unitId, string resultName, List shortcodesInMask) - { - // 1. Извлекаем имена полей из шорткодов - var requiredFieldNames = shortcodesInMask - .Select(m => m.Value.Trim('%').ToUpperInvariant()) - .ToList(); - - if (requiredFieldNames.Count == 0) - return resultName; - - // 2. Получаем значения (может быть дубль) - var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames); - - // 3. Группируем по FieldName → список значений - var fieldValuesMap = fieldValues - .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase) - .ToDictionary( - g => g.Key, - g => g.Select(x => x.Value).ToList(), // список значений (может быть null) - StringComparer.OrdinalIgnoreCase); - - // 4. Объединяем значения через запятую (null → "null") и подставляем - foreach (var match in shortcodesInMask) - { - var fieldName = match.Value.Trim('%').ToUpperInvariant(); - - if (fieldValuesMap.TryGetValue(fieldName, out var values)) - { - // Объединяем все значения через запятую, null заменяем на строку "null" - var combinedValue = string.Join(", ", values.Select(v => v ?? "null")); - resultName = resultName.Replace(match.Value, combinedValue); - } - else - { - logger.LogWarning("Поле '{FieldName}' не найдено для unitId={UnitId} при подстановке шорткода '{Shortcode}'", - fieldName, unitId, match.Value); - } - } - - return resultName; - } - - - private static string ReplaceStandardShortcodes(Job job, Unit unit, string input, int? index = null) - { - return input - .Replace("%ЭК%", unit.Name, StringComparison.OrdinalIgnoreCase) - .Replace("%ГРУППА_РАБОТ%", job.Group?.GroupName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("%РАБОТА%", job.WorkName, StringComparison.OrdinalIgnoreCase) - .Replace("%ТНК%", job.Tnk?.Name ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("%ТНК-КРАТКО%", job.Tnk?.ShortName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("%ИНДЕКС%", index?.ToString() ?? "", StringComparison.OrdinalIgnoreCase); - } - - - private static string ReplaceConstants(List nameConstants, string resultName) - { - foreach (var item in nameConstants) - { - resultName = resultName.Replace($"%{item.Name}%", item.Value); - } - - return resultName; - } - } -} diff --git a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs index a57c0176..a60c1296 100644 --- a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs @@ -123,13 +123,9 @@ namespace PARR.DAL.DomainServices.Implementations var parentRelFilters = filter.RelationshipFilters.Where(rf => rf.IsParent).ToList(); var childRelFilters = filter.RelationshipFilters.Where(rf => !rf.IsParent).ToList(); - var parentLinks = parentRelFilters.Any() - ? await unitInUnitService.GetParentLinksByChildIdsAsync(initialUnitIds) - : new List(); + var parentLinks = await unitInUnitService.GetParentLinksByChildIdsAsync(initialUnitIds); - var childLinks = childRelFilters.Any() - ? await unitInUnitService.GetChildLinksByParentIdsAsync(initialUnitIds) - : new List(); + var childLinks = await unitInUnitService.GetChildLinksByParentIdsAsync(initialUnitIds); // 4️ ID родителей и детей var parentUnitIds = parentLinks.Select(l => l.ParentUnitId).ToHashSet(); @@ -251,7 +247,7 @@ namespace PARR.DAL.DomainServices.Implementations logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateUnits.Count()); - // 9️⃣ Umbrella-фильтр + // 9️ Umbrella-фильтр var finalUnits = candidateUnits.AsEnumerable(); if (job.Group.GroupType.Code == JobGroupTypesEnum.Umbrella) { @@ -452,45 +448,71 @@ namespace PARR.DAL.DomainServices.Implementations var isParentDirection = job.IsParentRelationships == true; if (min == 0 && max == int.MaxValue) + { + logger.LogDebug("RelationshipCountFilter: Min и Max не заданы — пропускаем фильтр."); return units; + } + logger.LogDebug("RelationshipCountFilter: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, isParentDirection); + + // Определяем, есть ли фильтры по полям var activeFilters = relationshipFilters .Where(rf => rf.IsParent == isParentDirection && !string.IsNullOrWhiteSpace(rf.ValueMask)) .ToList(); - if (activeFilters.Count == 0) - return units; + bool hasFieldFilters = activeFilters.Count > 0; - logger.LogDebug("RelationshipFilter: Min={Min}, Max={Max}, IsParent={IsParent}, Filters={Count}", - min, max, isParentDirection, activeFilters.Count); + logger.LogDebug("RelationshipCountFilter: Найдено {Count} фильтров по полям для направления {Direction}", activeFilters.Count, isParentDirection ? "Parent" : "Child"); return units.Where(dto => { var links = isParentDirection ? dto.Parents : dto.Children; if (links == null || !links.Any()) - return min == 0; + { + var result = min == 0; + logger.LogDebug("UnitId {UnitId}: связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result); + return result; + } + + logger.LogDebug("UnitId {UnitId}: {Count} связей до фильтрации", dto.Id, links.Count); int matchingCount = 0; - foreach (var link in links) + if (hasFieldFilters) { - bool hasMatch = link.Values.Any(v => - activeFilters.Any(f => - v.FieldId == f.FieldId && - v.Value != null && - v.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase) - ) - ); + // Есть фильтры по полям → считаем только связанные юниты, подходящие под фильтр + foreach (var link in links) + { + bool hasMatch = link.Values.Any(v => + activeFilters.Any(f => + v.FieldId == f.FieldId && + v.Value != null && + v.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase) + ) + ); - if (hasMatch) - matchingCount++; + if (hasMatch) + matchingCount++; - if (matchingCount > max) - break; + if (matchingCount > max) + { + logger.LogDebug("UnitId {UnitId}: matchingCount ({Count}) > max ({Max}) — прерываем подсчёт", dto.Id, matchingCount, max); + break; + } + } + } + else + { + // Нет фильтров по полям → считаем общее количество связей (без учёта значений) + matchingCount = links.Count; + logger.LogDebug("UnitId {UnitId}: нет фильтров по полям — matchingCount = links.Count = {Count}", dto.Id, matchingCount); } - return matchingCount >= min && matchingCount <= max; + var finalResult = matchingCount >= min && matchingCount <= max; + logger.LogDebug("UnitId {UnitId}: matchingCount={Count}, Min={Min}, Max={Max}, результат фильтра: {Result}", dto.Id, matchingCount, min, max, finalResult); + + return finalResult; }); } diff --git a/PARR.DAL/DomainServices/Interfaces/IGroupedShortcodesCacheService.cs b/PARR.DAL/DomainServices/Interfaces/IGroupedShortcodesCacheService.cs new file mode 100644 index 00000000..210917ce --- /dev/null +++ b/PARR.DAL/DomainServices/Interfaces/IGroupedShortcodesCacheService.cs @@ -0,0 +1,10 @@ +namespace PARR.DAL.DomainServices.Interfaces +{ + public interface IGroupedShortcodesCacheService + { + Task GetAggregatedValueAsync( + Guid unitId, + string shortcodeKey, + Func> computeIfMissing); + } +} diff --git a/PARR.DAL/DomainServices/Interfaces/ITemplateNameGeneratorService.cs b/PARR.DAL/DomainServices/Interfaces/ITemplateNameGeneratorService.cs deleted file mode 100644 index eab86a19..00000000 --- a/PARR.DAL/DomainServices/Interfaces/ITemplateNameGeneratorService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using PARR.DAL.Models.Job; -using PARR.DAL.Models.Unit; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace PARR.DAL.DomainServices.Interfaces -{ - public interface ITemplateNameGeneratorService - { - Task GetTemplateNameAsync(Guid jobId, Guid unitId); - } -} diff --git a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs b/PARR.DAL/DomainServices/Shortcodes/IShortcodesService.cs similarity index 52% rename from PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs rename to PARR.DAL/DomainServices/Shortcodes/IShortcodesService.cs index c57fc0ad..820e62cf 100644 --- a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs +++ b/PARR.DAL/DomainServices/Shortcodes/IShortcodesService.cs @@ -1,10 +1,11 @@ using PARR.DAL.DomainModels; +using PARR.DAL.DomainServices.Shortcodes.Models; -namespace PARR.DAL.DomainServices.Interfaces +namespace PARR.DAL.DomainServices.Shortcodes { public interface IShortcodesService { - Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null); + Task ApplyShortcodesAsync(string str, TemplateForShortcodes template); bool IsAnyShortcodes(string str); diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/JobForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/JobForShortcodes.cs new file mode 100644 index 00000000..0dab9e42 --- /dev/null +++ b/PARR.DAL/DomainServices/Shortcodes/Models/JobForShortcodes.cs @@ -0,0 +1,10 @@ +namespace PARR.DAL.DomainServices.Shortcodes.Models +{ + public class JobForShortcodes + { + public JobGroupForShortcodes? Group { get; set; } + public TnkForShortcodes? Tnk { get; set; } + public string WorkName { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + } +} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupForShortcodes.cs new file mode 100644 index 00000000..1105892c --- /dev/null +++ b/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupForShortcodes.cs @@ -0,0 +1,10 @@ +namespace PARR.DAL.DomainServices.Shortcodes.Models +{ + public class JobGroupForShortcodes + { + public Guid Id { get; set; } + public Guid? GroupingUnitFieldId { get; set; } + public JobGroupTypeForShortcodes? GroupType { get; set; } + public string GroupName { get; set; } = string.Empty; + } +} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupTypeForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupTypeForShortcodes.cs new file mode 100644 index 00000000..f1596b7c --- /dev/null +++ b/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupTypeForShortcodes.cs @@ -0,0 +1,9 @@ +using PARR.DAL.Contracts; + +namespace PARR.DAL.DomainServices.Shortcodes.Models +{ + public class JobGroupTypeForShortcodes + { + public JobGroupTypesEnum Code { get; set; } + } +} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/TemplateForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/TemplateForShortcodes.cs new file mode 100644 index 00000000..76a5119b --- /dev/null +++ b/PARR.DAL/DomainServices/Shortcodes/Models/TemplateForShortcodes.cs @@ -0,0 +1,13 @@ +namespace PARR.DAL.DomainServices.Shortcodes.Models +{ + public class TemplateForShortcodes + { + public Guid Id { get; set; } + public int? Index { get; set; } + public Guid JobId { get; set; } + public Guid UnitId { get; set; } + + public JobForShortcodes? Job { get; set; } + public List UnitsInTemplate { get; set; } = new(); + } +} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/TnkForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/TnkForShortcodes.cs new file mode 100644 index 00000000..1e284b63 --- /dev/null +++ b/PARR.DAL/DomainServices/Shortcodes/Models/TnkForShortcodes.cs @@ -0,0 +1,8 @@ +namespace PARR.DAL.DomainServices.Shortcodes.Models +{ + public class TnkForShortcodes + { + public string Name { get; set; } = string.Empty; + public string ShortName { get; set; } = string.Empty; + } +} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/UnitInTemplateForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/UnitInTemplateForShortcodes.cs new file mode 100644 index 00000000..985aa973 --- /dev/null +++ b/PARR.DAL/DomainServices/Shortcodes/Models/UnitInTemplateForShortcodes.cs @@ -0,0 +1,7 @@ +namespace PARR.DAL.DomainServices.Shortcodes.Models +{ + public class UnitInTemplateForShortcodes + { + public Guid UnitId { get; set; } + } +} diff --git a/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs b/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs new file mode 100644 index 00000000..02ee6121 --- /dev/null +++ b/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs @@ -0,0 +1,470 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.Constants; +using PARR.DAL.Contracts; +using PARR.DAL.DomainModels; +using PARR.DAL.DomainServices.Interfaces; +using PARR.DAL.DomainServices.Shortcodes.Models; +using PARR.DAL.Models.Unit; +using PARR.DAL.Services.Interfaces; +using PARR.DAL.Services.Interfaces.Job; +using PARR.DAL.Services.Interfaces.Unit; +using System.Text.RegularExpressions; + +namespace PARR.DAL.DomainServices.Shortcodes +{ + internal class ShortcodesService : IShortcodesService + { + private const string shortcodePattern = "%[^%\\s]+%"; + private const string maxShortcodePattern = @"%МАКС:([а-яА-Яa-zA-Z0-9_]+)%"; + private const string lettersShortcodePattern = @"%БУКВЫ:([^%]+)%"; + + private static readonly HashSet SupportedShortcodes = new(StringComparer.OrdinalIgnoreCase) + { + "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%СВЯЗИ%", "%ТНК-КРАТКО%", "%СВЯЗИ-ПН%", "%ИНДЕКС%" + }; + + private readonly ILogger logger; + private readonly SettingsFromDb settingsFromDb; + private readonly IJobService jobService; + private readonly IUnitService unitService; + private readonly IUnitInValueService unitInValueService; + private readonly IUnitFieldService unitFieldService; + private readonly ITemplateService templateService; + private readonly IGroupedShortcodesCacheService groupedShortcodesCacheService; + private readonly IUnitFilterService unitFilterService; + + public ShortcodesService( + ILogger logger, + SettingsFromDb settingsFromDb, + IJobService jobService, + IUnitService unitService, + IUnitFilterService unitFilterService, + IUnitInValueService unitInValueService, + IUnitFieldService unitFieldService, + ITemplateService templateService, + IGroupedShortcodesCacheService groupedShortcodesCacheService + ) + { + this.logger = logger; + this.settingsFromDb = settingsFromDb; + this.jobService = jobService; + this.unitService = unitService; + this.unitInValueService = unitInValueService; + this.unitFieldService = unitFieldService; + this.templateService = templateService; + this.groupedShortcodesCacheService = groupedShortcodesCacheService; + this.unitFilterService = unitFilterService; + } + + public async Task ApplyShortcodesAsync(string str, TemplateForShortcodes template) + { + logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', templateId={TemplateId}, index={Index}", str, template.Id, template.Index); + + var job = template.Job; + if (job == null) + { + logger.LogError("Шаблон {TemplateId} не содержит Job. Подстановка прервана.", template.Id); + return str; + } + + var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == template.UnitId); + + if (unit == null || string.IsNullOrEmpty(str)) + { + logger.LogError("Переданы некорректные данные для подстановки динамических записей"); + return str; + } + + var resultName = str; + var shortcodesInMask = GetShortCodes(resultName); + + // 1. Статические константы + var nameConstants = settingsFromDb.TemplateNameConstantPartsList; + if (shortcodesInMask.Any(m => nameConstants.Any(c => $"%{c.Name}%".Equals(m.Value, StringComparison.OrdinalIgnoreCase)))) + { + resultName = ReplaceConstants(nameConstants, resultName); + } + + // 2. Стандартные шорткоды — с поддержкой вложенных (%РАБОТА% → "Мониторинг | %ЭК%") + const int MaxStandardIterations = 3; + var iteration = 0; + + while (iteration < MaxStandardIterations) + { + var remainingShortcodes = GetShortCodes(resultName) + .Select(m => m.Value) + .Where(s => SupportedShortcodes.Contains(s)) + .ToList(); + + if (!remainingShortcodes.Any()) + break; + + var oldResult = resultName; + resultName = ReplaceStandardShortcodes(job, unit, resultName, template.Index); + iteration++; + + if (resultName == oldResult) + { + logger.LogDebug("Замена стандартных шорткодов не изменила строку на итерации {Iteration}. Останов.", iteration); + break; + } + } + + if (iteration >= MaxStandardIterations) + { + logger.LogWarning( + "Достигнуто максимальное число итераций ({Max}) при замене стандартных шорткодов. Текущий результат: {Result}", + MaxStandardIterations, resultName); + } + + // 2.5. %МАКС:FIELD% — только для групповых job (JobGroup.Type == Group) + if (job.Group != null && job.Group.GroupType != null && job.Group.GroupType!.Code == JobGroupTypesEnum.Group) + { + var maxShortcodes = Regex.Matches(resultName, maxShortcodePattern); + if (maxShortcodes.Count > 0) + { + resultName = await ReplaceMaxShortcodesAsync(job.Group.Id, template.UnitId, resultName, maxShortcodes); // ✅ Исправлено: job.GroupId + } + } + + // 2.6. %БУКВЫ:FIELD% — извлекает только буквы из значения поля + var lettersShortcodes = Regex.Matches(resultName, lettersShortcodePattern); + if (lettersShortcodes.Count > 0) + { + resultName = await ReplaceLettersShortcodesAsync(template.UnitId, resultName, lettersShortcodes); + } + + // 2.7. %ГР_ПОЛЕ-ПН% — нумерованный список UnitsInTemplate с GroupingUnitFieldId + if (shortcodesInMask.Any(m => string.Equals(m.Value, "%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase))) + { + var unitsInTemplate = template.UnitsInTemplate; + if (unitsInTemplate == null || !unitsInTemplate.Any()) + { + logger.LogDebug("Шаблон {TemplateId} не содержит UnitsInTemplate. %ГР_ПОЛЕ-ПН% заменён на пустую строку.", template.Id); + resultName = Regex.Replace(resultName, "%ГР_ПОЛЕ-ПН%", "", RegexOptions.IgnoreCase); + } + else + { + var unitIds = unitsInTemplate.Select(uit => uit.UnitId).ToList(); + + var units = await unitService.Get() + .AsNoTracking() + .Where(u => unitIds.Contains(u.Id)) + .ToListAsync(); + + var groupingFieldId = job.Group?.GroupingUnitFieldId; + Dictionary valuesByUnit = new(); + + if (groupingFieldId.HasValue) + { + var fieldValues = await unitInValueService.Get() + .AsNoTracking() + .Include(uv => uv.Value) + .Where(uv => + uv.FieldId == groupingFieldId.Value && + unitIds.Contains(uv.UnitId) && + uv.Value != null && + !string.IsNullOrWhiteSpace(uv.Value.Value)) + .Select(uv => new { uv.UnitId, Value = uv.Value.Value }) + .ToListAsync(); + + valuesByUnit = fieldValues + .GroupBy(x => x.UnitId) + .ToDictionary( + g => g.Key, + g => string.Join(", ", g.Select(v => v.Value).OrderBy(v => v)) + ); + } + + var lines = unitsInTemplate + .Select((uit, indexInList) => + { + var unitInList = units.FirstOrDefault(u => u.Id == uit.UnitId); + var unitName = unitInList?.Name ?? $"(UnitId={uit.UnitId})"; + var valuesStr = valuesByUnit.TryGetValue(uit.UnitId, out var vals) ? vals : ""; + return $"{indexInList + 1}. {unitName} ({valuesStr})"; + }) + .ToList(); + + var resultText = string.Join("\n", lines); + resultName = Regex.Replace(resultName, "%ГР_ПОЛЕ-ПН%", resultText, RegexOptions.IgnoreCase); + } + } + + // 3. %СВЯЗИ% или %СВЯЗИ-ПН% (если всё ещё зависят от jobId/unitId) + List? relatedUnitNames = null; + + if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase))) + { + relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId); + var linksText = string.Join("\n", relatedUnitNames); + resultName = Regex.Replace(resultName, "%СВЯЗИ%", linksText, RegexOptions.IgnoreCase); + } + + if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase))) + { + relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId); + var linksText = string.Join("\n", relatedUnitNames.Select((name, i) => $"{i + 1}. {name}")); + resultName = Regex.Replace(resultName, "%СВЯЗИ-ПН%", linksText, RegexOptions.IgnoreCase); + } + + // 4. Поля (оставшиеся %FIELD_NAME%) + shortcodesInMask = GetShortCodes(resultName); + if (shortcodesInMask.Count > 0) + resultName = await ReplaceFieldValues(template.UnitId, resultName, shortcodesInMask); + + logger.LogDebug("Подстановка завершена. Результат: '{Result}'", resultName); + + return resultName; + } + + public bool IsAnyShortcodes(string str) + { + return Regex.IsMatch(str, shortcodePattern); + } + + private static List GetShortCodes(string resultName) + { + var shortcodesInMask = Regex.Matches(resultName, shortcodePattern).ToList(); + return shortcodesInMask; + } + + public async Task> GetAvailableShortcodesAsync() + { + var result = new List(); + + // 1. Статические константы — из settingsFromDb + foreach (var constant in settingsFromDb.TemplateNameConstantPartsList) + { + result.Add(new ShortcodeInfoDto + { + Shortcode = $"%{constant.Name}%", + Description = $"Константа: {constant.Value ?? "(пусто)"}", + Type = ShortcodeTypeEnum.Static + }); + } + + // 2. Стандартные шорткоды + result.AddRange(new[] + { + new ShortcodeInfoDto { Shortcode = "%ЭК%", + Description = "Наименование ЭК(Код поиска)", + Type = ShortcodeTypeEnum.Standart }, + new ShortcodeInfoDto { Shortcode = "%ГРУППА_РАБОТ%", + Description = "Наименование группы работ", + Type = ShortcodeTypeEnum.Standart }, + new ShortcodeInfoDto { Shortcode = "%РАБОТА%", + Description = "Наименование работы в АСУ ЕСПП", + Type = ShortcodeTypeEnum.Standart }, + new ShortcodeInfoDto { Shortcode = "%ТНК%", + Description = "Полное наименование ТНК", + Type = ShortcodeTypeEnum.Standart }, + new ShortcodeInfoDto { Shortcode = "%ТНК-КРАТКО%", + Description = "Краткое наименование ТНК", + Type = ShortcodeTypeEnum.Standart }, + new ShortcodeInfoDto { Shortcode = "%ИНДЕКС%", + Description = "Порядковый индекс шаблона для групповых работ", + Type = ShortcodeTypeEnum.Standart }, + new ShortcodeInfoDto { Shortcode = "%МАКС:ИМЯ АТРИБУТА%", + Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК%", + Type = ShortcodeTypeEnum.Standart }, + new ShortcodeInfoDto { Shortcode = "%ГР_ПОЛЕ-ПН%", + Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.", + Type = ShortcodeTypeEnum.Relationship }, + new ShortcodeInfoDto{ Shortcode = "%БУКВЫ:ИМЯ АТРИБУТА%", + Description = "Извлекает только буквы из значения поля. Пример: %БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ% → ПРИВ", + Type = ShortcodeTypeEnum.Standart + } + }); + + // 3. Связи + result.AddRange(new[] + { + new ShortcodeInfoDto { Shortcode = "%СВЯЗИ%", + Description = "Связанные ЭК (по одному на строку), выбираются только при настроенном фильтре по полям в связанных ЭК", + Type = ShortcodeTypeEnum.Relationship }, + new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", + Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК", + Type = ShortcodeTypeEnum.Relationship } + }); + + // 4. Все доступные поля из UnitField + var fieldNames = await unitFieldService.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync(); + foreach (var fieldName in fieldNames.OrderBy(n => n.AihitName)) + { + result.Add(new ShortcodeInfoDto + { + Shortcode = $"%{fieldName.AihitName}%", + Description = $"Атрибут: {fieldName.DisplayName ?? fieldName.AihitName}", + Type = ShortcodeTypeEnum.FieldValue + }); + } + return result; + } + + private async Task ReplaceFieldValues(Guid unitId, string resultName, List shortcodesInMask) + { + var requiredFieldNames = shortcodesInMask + .Select(m => m.Value.Trim('%').ToUpperInvariant()) + .ToList(); + + if (requiredFieldNames.Count == 0) + return resultName; + + var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames); + + var fieldValuesMap = fieldValues + .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + g => g.Key, + g => g.Select(x => x.Value).ToList(), + StringComparer.OrdinalIgnoreCase); + + foreach (var match in shortcodesInMask) + { + var fieldName = match.Value.Trim('%').ToUpperInvariant(); + + if (fieldValuesMap.TryGetValue(fieldName, out var values)) + { + var combinedValue = string.Join(", ", values.Select(v => v ?? "null")); + resultName = resultName.Replace(match.Value, combinedValue); + } + else + { + logger.LogWarning("Поле '{FieldName}' не найдено для unitId={UnitId} при подстановке шорткода '{Shortcode}'", + fieldName, unitId, match.Value); + } + } + + return resultName; + } + + private static string ReplaceStandardShortcodes(JobForShortcodes job, Unit unit, string input, int? index = null) + { + return input + .Replace("%ЭК%", unit.Name, StringComparison.OrdinalIgnoreCase) + .Replace("%ГРУППА_РАБОТ%", job.Group?.GroupName ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("%РАБОТА%", job.WorkName, StringComparison.OrdinalIgnoreCase) + .Replace("%ТНК%", job.Tnk?.Name ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("%ТНК-КРАТКО%", job.Tnk?.ShortName ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("%ИНДЕКС%", index?.ToString() ?? "", StringComparison.OrdinalIgnoreCase); + } + + private static string ReplaceConstants(List nameConstants, string resultName) + { + foreach (var item in nameConstants) + resultName = resultName.Replace($"%{item.Name}%", item.Value); + + return resultName; + } + + private async Task ReplaceMaxShortcodesAsync(Guid jobGroupId, Guid unitId, string input, MatchCollection maxShortcodes) + { + var shortcodeToMatches = maxShortcodes + .Cast() + .GroupBy(m => m.Value, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in shortcodeToMatches) + { + var fullShortcode = kvp.Key; + var matches = kvp.Value; + var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim(); // "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" + + logger.LogDebug("Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}", + fullShortcode, jobGroupId, unitId); + + var mostFrequentValue = await groupedShortcodesCacheService.GetAggregatedValueAsync( + unitId, + fullShortcode, + async () => + { + // Логика вычисления, если кэш пуст + var unitIds = await jobService.Get() + .Where(j => j.GroupId == jobGroupId) + .Join( + templateService.Get() + .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used) + .Include(t => t.UnitsInTemplate), + job => job.Id, + template => template.JobId, + (job, template) => template + ) + .SelectMany(template => template.UnitsInTemplate) + .Select(uit => uit.UnitId) + .Distinct() + .ToListAsync(); + + // Вызываем метод из сервиса + var result = await unitInValueService.GetMostFrequentValueForFieldAsync(unitIds, fieldName); + return result ?? string.Empty; + }); + + foreach (var match in matches) + { + input = input.Replace(match.Value, mostFrequentValue); + } + } + + return input; + } + + private async Task ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes) + { + var shortcodeToMatches = lettersShortcodes + .Cast() + .GroupBy(m => m.Value, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in shortcodeToMatches) + { + var fullShortcode = kvp.Key; // например, "%БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ%" + var matches = kvp.Value; + var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim(); // "ЗОНА_ОТВЕТСТВЕННОСТИ" + + logger.LogDebug("Обработка {Shortcode} для unitId {UnitId}, fieldName {FieldName}", fullShortcode, unitId, fieldName); + + // Получаем значение поля через unitInValueService.GetFieldValuesAsync + var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, new List { fieldName }); + + string extractedLetters = string.Empty; + + if (fieldValues.Any()) + { + var value = fieldValues.First().Value; // Берём первое значение, если несколько + if (value != null) + { + extractedLetters = ExtractLettersOnly(value); + logger.LogDebug("Извлечены буквы: '{Letters}' из значения '{Value}'", extractedLetters, value); + } + } + + if (string.IsNullOrEmpty(extractedLetters)) + { + logger.LogDebug("Для шорткода {Shortcode} не найдено подходящее значение или из него нельзя извлечь буквы", fullShortcode); + } + + foreach (var match in matches) + { + input = input.Replace(match.Value, extractedLetters); + } + } + + return input; + } + + private static string ExtractLettersOnly(string input) + { + var result = new System.Text.StringBuilder(); + foreach (char c in input) + { + if (char.IsLetter(c)) + { + result.Append(c); + } + } + return result.ToString(); + } + } +} \ No newline at end of file diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index abf7f915..b9d59163 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -7,6 +7,7 @@ using PARR.DAL.Context; using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Implementations; using PARR.DAL.DomainServices.Interfaces; +using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.InfluxDbServices; using PARR.DAL.Services.Implementation; using PARR.DAL.Services.Implementations; @@ -43,7 +44,12 @@ namespace PARR.DAL opt.Configuration = configuration.GetConnectionString("RedisConnection"); }); + var groupedShortcodesCacheSettings = new GroupedShortcodesCacheSettings(); + configuration.GetSection(nameof(GroupedShortcodesCacheSettings)).Bind(groupedShortcodesCacheSettings); + services.AddSingleton(groupedShortcodesCacheSettings); + services.AddTransient(); + services.AddTransient(); #endregion diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs index eec6f078..af943b45 100644 --- a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs +++ b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs @@ -76,7 +76,7 @@ namespace PARR.DAL.Services.Implementations.Unit return await dataContext.UnitInValues .AsNoTracking() - .Include(uv => uv.Value) // UnitFieldValue + .Include(uv => uv.Value) .Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId)) .ToListAsync(); } @@ -85,5 +85,48 @@ namespace PARR.DAL.Services.Implementations.Unit { return dataContext.UnitInValues; } + + public async Task GetMostFrequentValueForFieldAsync(List unitIds, string fieldName) + { + if (unitIds == null || !unitIds.Any() || string.IsNullOrWhiteSpace(fieldName)) + { + logger.LogDebug("GetMostFrequentValueForFieldAsync: пустой список юнитов или имя поля. unitIds count: {Count}, fieldName: {FieldName}", unitIds?.Count ?? 0, fieldName); + return null; + } + + logger.LogDebug("Поиск наиболее частого значения для поля '{FieldName}' среди {Count} юнитов.", fieldName, unitIds.Count); + + var result = await dataContext.UnitInValues + .AsNoTracking() + .Where(uv => + unitIds.Contains(uv.UnitId) + ) + .Join( + dataContext.UnitFields, + uv => uv.FieldId, + f => f.Id, + (uv, f) => new { uv, f } + ) + .Where(x => + EF.Functions.ILike(x.f.AihitName, fieldName) + ) + .Join( + dataContext.UnitFieldValues, + x => x.uv.ValueId, + v => v.Id, + (x, v) => new { x.uv.UnitId, v.Value } + ) + .Where(x => !string.IsNullOrWhiteSpace(x.Value)) + .GroupBy(x => x.Value) + .Select(g => new { Value = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Value) + .FirstOrDefaultAsync(); + + var mostFrequentValue = result?.Value; + logger.LogDebug("Наиболее частое значение для поля '{FieldName}': {Value}", fieldName, mostFrequentValue); + return mostFrequentValue; + } } } + diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs index e4f011b7..a83dc557 100644 --- a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs +++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs @@ -5,13 +5,24 @@ namespace PARR.DAL.Services.Interfaces.Unit public interface IUnitInValueService { Task> GetByUnitIdsAsync(IEnumerable unitIds); + Task> GetByUnitIdAsync(Guid unitId); + Task> GetFieldValuesAsync(Guid unitId, IReadOnlyCollection aihitNames); /// /// Получает UnitInValue (с Value) для заданных UnitId и FieldId. /// Task> GetByUnitIdsAndFieldIdsAsync(IEnumerable unitIds, IEnumerable fieldIds); + + /// + /// Находит наиболее часто встречающееся непустое значение указанного поля среди переданных юнитов. + /// + /// Список Id юнитов для анализа. + /// Имя поля (AihitName) для поиска значения. + /// Наиболее частое значение поля или null, если не найдено. + Task GetMostFrequentValueForFieldAsync(List unitIds, string fieldName); + IQueryable Get(); } } diff --git a/PARR.DAL/Settings/GroupedShortcodesCacheSettings.cs b/PARR.DAL/Settings/GroupedShortcodesCacheSettings.cs new file mode 100644 index 00000000..948e1b25 --- /dev/null +++ b/PARR.DAL/Settings/GroupedShortcodesCacheSettings.cs @@ -0,0 +1,10 @@ +namespace PARR.DAL.Settings +{ + public class GroupedShortcodesCacheSettings + { + /// + /// Время хранения + /// + public TimeSpan ValueTtl { get; set; } = TimeSpan.FromMinutes(20); + } +} diff --git a/PARR.EsppSync/SyncService.cs b/PARR.EsppSync/SyncService.cs index f6999525..3f72c19a 100644 --- a/PARR.EsppSync/SyncService.cs +++ b/PARR.EsppSync/SyncService.cs @@ -1,8 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; 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.Services.Interfaces; using System.Reflection; @@ -14,23 +16,13 @@ namespace PARR.EsppSync private readonly ILogger> logger; private readonly IServiceProvider serviceProvider; - //private readonly ITemplateService templateService; - //private readonly IRobotConfigurationService robotConfigurationService; - //private readonly IShortcodesService shortcodesService; - public SyncService( ILogger> logger, IServiceProvider serviceProvider - //ITemplateService templateService, - //IRobotConfigurationService robotConfigurationService, - //IShortcodesService shortcodesService ) { this.logger = logger; this.serviceProvider = serviceProvider; - //this.templateService = templateService; - //this.robotConfigurationService = robotConfigurationService; - //this.shortcodesService = shortcodesService; } @@ -65,98 +57,136 @@ namespace PARR.EsppSync try { - var template = await templateService.GetTemplateByNameAsync(esppObject.TemplateName); + // Загружаем Template и TemplateForShortcodes в одном запросе + var query = templateService.Get() + .AsNoTracking() + .Include(t => t.Job) + .ThenInclude(j => j.Group) + .ThenInclude(g => g.GroupType) + .Include(t => t.Job) + .ThenInclude(j => j.Tnk) + .Include(t => t.UnitsInTemplate); + + var template = await query.FirstOrDefaultAsync(t => t.Name == esppObject.TemplateName); - //todo: существует в ЕСПП но отсутствует в ПАРР. Может его деактивировать или еще что-то сделать. Пока просто пропустим if (template == null) { logger.LogWarning($"Найден объект в ЕСПП с именем шаблона {esppObject.TemplateName} незарегистрированный в ПАРР."); - return; } + + // ✅ Построим TemplateForShortcodes из уже загруженного template + var templateForShortcodes = new TemplateForShortcodes + { + Id = template.Id, + Index = template.Index, + JobId = template.JobId, + UnitId = template.UnitId, + Job = template.Job == null ? null : new JobForShortcodes + { + Group = template.Job.Group == null ? null : new JobGroupForShortcodes + { + GroupingUnitFieldId = template.Job.Group.GroupingUnitFieldId, + GroupType = template.Job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes + { + Code = template.Job.Group.GroupType.Code + }, + GroupName = template.Job.Group.GroupName + }, + Tnk = template.Job.Tnk == null ? null : new TnkForShortcodes + { + Name = template.Job.Tnk.Name, + ShortName = template.Job.Tnk.ShortName + }, + WorkName = template.Job.WorkName, + Name = template.Job.Name + }, + UnitsInTemplate = template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes { UnitId = uit.UnitId }).ToList() ?? new List() + }; + + var dbObjectInEsppObject = converterToEsppObject.Invoke(template); + + //Проверяем наличие Shortcode в полях объекта из БД + var properties = dbObjectInEsppObject.GetType().GetProperties(); + + foreach (PropertyInfo property in properties) + { + var value = property.GetValue(dbObjectInEsppObject)?.ToString(); + + if (value != null && shortcodesService.IsAnyShortcodes(value)) + { + var processedValue = await shortcodesService.ApplyShortcodesAsync(value, templateForShortcodes); + property.SetValue(dbObjectInEsppObject, processedValue); + } + } + + bool isChanged = false; + //TODO: FIX ME Please, BRO + //bool isChanged; + + // если в БД isActive == false, то синхронизировать только по полям из IEsppObject + + if (dbObjectInEsppObject.IsActive == false) + { + logger.LogDebug($"Объект деактивирован в ПАРР. Сравниваем только обязательные поля. {esppObject.TemplateName}"); + var lightDbObj = new EsppLightObject(dbObjectInEsppObject); + var lightEsppObject = new EsppLightObject(esppObject); + + isChanged = IsChanged(lightEsppObject, lightDbObj, esppObject.TemplateName); + } else { - var dbObjectInEsppObject = converterToEsppObject.Invoke(template); + logger.LogDebug($"Объект активирован в ПАРР. Сравниваем все поля. {esppObject.TemplateName}"); + isChanged = IsChanged(esppObject, dbObjectInEsppObject, esppObject.TemplateName); + } - //Проверяем наличие Shortcode в полях объекта из БД - var properties = dbObjectInEsppObject.GetType().GetProperties(); + if (isChanged) + { + logger.LogDebug($"Есть изменения, требуется обновление. {esppObject.TemplateName}"); - foreach (PropertyInfo property in properties) + var config = robotConfigurationService.GetFromTemplateByRobotCode(esppObject.Robot, template); + + // Если предыдущий статус был Create или Ок, то ставим ему Update + // пусть даже Create завершился с ошибками, но раз он уже есть в ЕСПП, то изменим на Update, сбросим все счетчики и пусть попробует обновить и исправить все + if (config.TaskStatusCode != (int)TaskStatusEnum.Updating) { - var value = property.GetValue(dbObjectInEsppObject)?.ToString(); + SetUpdateStatus(ref template, robotConfigurationService, esppObject.Robot); - if (value != null && shortcodesService.IsAnyShortcodes(value)) - property.SetValue(dbObjectInEsppObject, await shortcodesService.ApplyShortcodesAsync(value, template.UnitId, template.JobId)); - } - - bool isChanged = false; - //TODO: FIX ME Please, BRO - //bool isChanged; - - // если в БД isActive == false, то синхронизировать только по полям из IEsppObject - - if (dbObjectInEsppObject.IsActive == false) - { - logger.LogDebug($"Объект деактивирован в ПАРР. Сравниваем только обязательные поля. {esppObject.TemplateName}"); - var lightDbObj = new EsppLightObject(dbObjectInEsppObject); - var lightEsppObject = new EsppLightObject(esppObject); - - isChanged = IsChanged(lightEsppObject, lightDbObj, esppObject.TemplateName); - } - else - { - logger.LogDebug($"Объект активирован в ПАРР. Сравниваем все поля. {esppObject.TemplateName}"); - isChanged = IsChanged(esppObject, dbObjectInEsppObject, esppObject.TemplateName); - } - - if (isChanged) - { - logger.LogDebug($"Есть изменения, требуется обновление. {esppObject.TemplateName}"); - - var config = robotConfigurationService.GetFromTemplateByRobotCode(esppObject.Robot, template); - - // Если предыдущий статус был Create или Ок, то ставим ему Update - // пусть даже Create завершился с ошибками, но раз он уже есть в ЕСПП, то изменим на Update, сбросим все счетчики и пусть попробует обновить и исправить все - if (config.TaskStatusCode != (int)TaskStatusEnum.Updating) - { - SetUpdateStatus(ref template, robotConfigurationService, esppObject.Robot); - - if (!await templateService.CommitAsync()) - logger.LogError($"Не удалось изменить запись Template {template.Name}, Robot: {esppObject.Robot}"); - else - logger.LogInformation($"Установлен принудительный статус {TaskStatusEnum.Updating}, Template {template.Name}, Robot: {esppObject.Robot}"); - } + if (!await templateService.CommitAsync()) + logger.LogError($"Не удалось изменить запись Template {template.Name}, Robot: {esppObject.Robot}"); else - { - // Если пред статус был Update, то ничего не делаем, так его и оставляем, не сбрасывам кол-во попыток и ошибок - logger.LogInformation($"Есть изменения в Template {template.Name}, но предыдущий статус TaskStatusCode: {(TaskStatusEnum)config.TaskStatusCode}. Не меняем статус, будем разбираться вручную."); - } - - #region Old logic - - //SetUpdateStatus(ref template, robotConfigurationService, esppObject.Robot); - - //if (!await templateService.CommitAsync()) - // logger.LogError($"Не удалось изменить запись Template {template.Name}, Robot: {esppObject.Robot}"); - //else - // logger.LogInformation($"Установлен принудительный статус {TaskStatusEnum.Updating}, Template {template.Name}, Robot: {esppObject.Robot}"); - #endregion - }//надо ли проверять если не изменился, но был статус Updating не понятно. Доверяем роботу пока, что после окончания работ он точно сообщит + logger.LogInformation($"Установлен принудительный статус {TaskStatusEnum.Updating}, Template {template.Name}, Robot: {esppObject.Robot}"); + } else { - logger.LogDebug($"Нет изменений, обновление не требуется. {esppObject.TemplateName}"); + // Если пред статус был Update, то ничего не делаем, так его и оставляем, не сбрасывам кол-во попыток и ошибок + logger.LogInformation($"Есть изменения в Template {template.Name}, но предыдущий статус TaskStatusCode: {(TaskStatusEnum)config.TaskStatusCode}. Не меняем статус, будем разбираться вручную."); + } - //если все поля совпали - //проверяем, какой был статус предыдущий статус в БД, если он был не Ок, то ставим ему ОК - var robotConfig = robotConfigurationService.GetFromTemplateByRobotCode(esppObject.Robot, template); - if (robotConfig.TaskStatusCode != (int)TaskStatusEnum.Ok) - { - robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Ok, robotConfig); - if (!await templateService.CommitAsync()) - logger.LogError($"Не удалось изменить запись Template {template.Name}, Robot: {esppObject.Robot}"); - else - logger.LogInformation($"Установлен принудительный статус {TaskStatusEnum.Ok}, Template {template.Name}, Robot: {esppObject.Robot}"); - } + #region Old logic + + //SetUpdateStatus(ref template, robotConfigurationService, esppObject.Robot); + + //if (!await templateService.CommitAsync()) + // logger.LogError($"Не удалось изменить запись Template {template.Name}, Robot: {esppObject.Robot}"); + //else + // logger.LogInformation($"Установлен принудительный статус {TaskStatusEnum.Updating}, Template {template.Name}, Robot: {esppObject.Robot}"); + #endregion + }//надо ли проверять если не изменился, но был статус Updating не понятно. Доверяем роботу пока, что после окончания работ он точно сообщит + else + { + logger.LogDebug($"Нет изменений, обновление не требуется. {esppObject.TemplateName}"); + + //если все поля совпали + //проверяем, какой был статус предыдущий статус в БД, если он был не Ок, то ставим ему ОК + var robotConfig = robotConfigurationService.GetFromTemplateByRobotCode(esppObject.Robot, template); + if (robotConfig.TaskStatusCode != (int)TaskStatusEnum.Ok) + { + robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Ok, robotConfig); + if (!await templateService.CommitAsync()) + logger.LogError($"Не удалось изменить запись Template {template.Name}, Robot: {esppObject.Robot}"); + else + logger.LogInformation($"Установлен принудительный статус {TaskStatusEnum.Ok}, Template {template.Name}, Robot: {esppObject.Robot}"); } } } @@ -229,5 +259,4 @@ namespace PARR.EsppSync return str.ToLower(); } } - } diff --git a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs index 37cfc4fe..aa0c088a 100644 --- a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs +++ b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs @@ -3,7 +3,8 @@ using PARR.BLL.Domain.Mq; 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; @@ -79,7 +80,35 @@ namespace PARR.TemplateGeneratorWorker return; } - var templateName = await shortcodesService.ApplyShortcodesAsync(job!.TemplateNameMask!, query.UnitId, query.JobId); + var templateForShortcodes = new TemplateForShortcodes + { + Id = Guid.Empty, // шаблон ещё не создан + Index = query.Index, + JobId = query.JobId, + UnitId = query.UnitId, + Job = job == null ? null : 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 = query.UnitsInTemplate?.Select(guid => new UnitInTemplateForShortcodes { UnitId = guid }).ToList() ?? new List() + }; + + var templateName = await shortcodesService.ApplyShortcodesAsync(job!.TemplateNameMask!, templateForShortcodes); var nextRun = await esppScheduleTransformService.GetNextDateAsync(job.GroupId, job.Group!.ReferenceDate); diff --git a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs new file mode 100644 index 00000000..17125f9d --- /dev/null +++ b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs @@ -0,0 +1,710 @@ +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.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; +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; + +internal class GroupedTemplateSynchronizer : ITemplateSynchronizer +{ + private const bool DefaultUnusedTemplateState = false; + private const bool DefaultUnusedScheduleState = false; + private const bool DefaultUsedTemplateState = false; + private const bool DefaultUsedScheduleState = false; + + private readonly ILogger logger; + private readonly IUnitFilterService unitFilterService; + private readonly IUnitInUnitService unitInUnitService; + private readonly IUnitInValueService unitInValueService; + private readonly IUnitService unitService; + private readonly MqSettings mqSettings; + private readonly IMqService mqService; + 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; + + public GroupedTemplateSynchronizer( + ILogger logger, + IUnitFilterService unitFilterService, + IUnitInUnitService unitInUnitService, + IUnitInValueService unitInValueService, + IUnitService unitService, + MqSettings mqSettings, + IMqService mqService, + ITemplateService templateService, + IJobGroupService jobGroupService, + ITemplateReuser templateReuser, + IShortcodesService shortcodesService, + IEsppScheduleTransformService esppScheduleTransformService, + IUnitRegionalEkPtkGroupService regionalEkPtkGroupService, + IUnitFieldService unitFieldService + ) + { + this.logger = logger; + this.unitFilterService = unitFilterService; + this.unitInUnitService = unitInUnitService; + this.unitInValueService = unitInValueService; + this.unitService = unitService; + this.mqSettings = mqSettings; + this.mqService = mqService; + this.templateService = templateService; + this.jobGroupService = jobGroupService; + this.templateReuser = templateReuser; + this.shortcodesService = shortcodesService; + this.esppScheduleTransformService = esppScheduleTransformService; + this.regionalEkPtkGroupService = regionalEkPtkGroupService; + this.unitFieldService = unitFieldService; + } + + public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) + { + logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); + } + + public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) + { + logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); + + // 1. Получить JobGroup и связанные Job'ы + var jobGroup = await jobGroupService.Get() + .AsNoTracking() + .Include(jg => jg.Jobs) + .ThenInclude(j => j.AutoControl) + .Include(jg => jg.Jobs) + .ThenInclude(j => j.UnitFilters) + .ThenInclude(uf => uf.RelationshipFilters) + .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); + + if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) + { + logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); + return; + } + + var jobsInGroup = jobGroup.Jobs.ToList(); + + // --- НОВАЯ ЛОГИКА: Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- + var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"); + if (workGroupField == null) + { + logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено в справочнике полей. Синхронизация прервана."); + return; + } + var workGroupFieldId = workGroupField.Id; + + var regionalGroupValueIds = regionalEkPtkGroupService.Get() + .Select(g => g.FieldValueId) + .ToList(); // Получаем список UnitFieldValue.Id + + logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", regionalGroupValueIds.Count); + + // 2. Найти Job с максимальным MaxValueRelationships + var maxJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue) + .OrderByDescending(j => j.MaxValueRelationships) + .FirstOrDefault(); + + if (maxJob == null) + { + logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); + return; + } + + // Проверяем, что UnitFilters и RelationshipFilters загружены (если используется для выбора targetJob) + if (maxJob.UnitFilters == null) + { + logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id); + } + + logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); + + // 3. Использовать фильтры maxJob для получения expectedUnitIds + var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id); + if (expectedUnitIds == null || !expectedUnitIds.Any()) + { + logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId); + return; + } + + // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId (дополнительный фильтр) + if (!jobGroup.GroupingUnitFieldId.HasValue) + { + logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); + return; + } + + var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; + + // Загрузить UnitValues для юнитов из expectedUnitIds, чтобы проверить GroupingUnitFieldId + var expectedUnitsWithGroupingField = await unitService.Get() + .AsNoTracking() + .AsSplitQuery() // Для Unit -> UnitValues + .Include(u => u.UnitValues) + .ThenInclude(uv => uv.Value) + .Where(u => expectedUnitIds.Contains(u.Id)) + .ToListAsync(); + + var unitIdsWithValidGroupingFieldSet = expectedUnitsWithGroupingField + .Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))) + .Select(u => u.Id) + .ToHashSet(); + + logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); + + if (!unitIdsWithValidGroupingFieldSet.Any()) + { + logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); + return; + } + + // --- НОВАЯ ЛОГИКА: Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- + var unitIdsWithValidWorkGroupFieldSet = expectedUnitsWithGroupingField + .Where(u => unitIdsWithValidGroupingFieldSet.Contains(u.Id) && // Убедимся, что юнит уже прошёл фильтр по GroupingFieldId + u.UnitValues.Any(uv => + uv.FieldId == workGroupFieldId && // Поле "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" + uv.Value != null && // Значение существует + regionalGroupValueIds.Contains(uv.Value.Id) // Значение в списке разрешённых + )) + .Select(u => u.Id) // Выбираем Id юнита + .ToHashSet(); // И снова в HashSet + + logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count); + + if (!unitIdsWithValidWorkGroupFieldSet.Any()) + { + logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); + return; + } + + // Обновляем список юнитов, прошедших оба фильтра + unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet; + + // 5. Получить RelationshipFilters из maxJob + var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List(); + + // --- Найти и отфильтровать UnitInUnit связи --- + logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию по GroupingUnitFieldId и 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'."); + var potentialUnitInUnitLinks = await unitInUnitService.Get() + .AsNoTracking() + .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) + .ToListAsync(); + + logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count); + + // Загрузить UnitInValue для всех ParentUnitId и ChildUnitId из potentialUnitInUnitLinks + var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet(); + var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet(); + + var parentUnitValues = await unitInValueService.Get() + .AsNoTracking() + .Include(uv => uv.Field) + .Include(uv => uv.Value) + .Where(uv => allParentIds.Contains(uv.UnitId)) + .ToListAsync(); + + var childUnitValues = await unitInValueService.Get() + .AsNoTracking() + .Include(uv => uv.Field) + .Include(uv => uv.Value) + .Where(uv => allChildIds.Contains(uv.UnitId)) + .ToListAsync(); + + // Сгруппировать значения по UnitId для быстрого доступа + var parentValuesMap = parentUnitValues + .GroupBy(uv => uv.UnitId) + .ToDictionary(g => g.Key, g => g.ToList()); + + var childValuesMap = childUnitValues + .GroupBy(uv => uv.UnitId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Применить фильтры к связям + logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count); + var filteredUnitInUnitLinks = new List(); + foreach (var link in potentialUnitInUnitLinks) + { + bool linkMatchesAllFilters = true; + + foreach (var rf in relationshipFilters) + { + var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List()); + + bool filterMatch = valuesToCheck.Any(uv => + uv.FieldId == rf.FieldId && + uv.Value != null && + uv.Value.Value != null && + uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase) + ); + + if (rf.IsInverse) + filterMatch = !filterMatch; + + if (!filterMatch) + { + linkMatchesAllFilters = false; + break; + } + } + + if (linkMatchesAllFilters) + { + filteredUnitInUnitLinks.Add(link); + } + } + + logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count); + + // --- НОВАЯ ЛОГИКА: Сгруппировать юниты из unitIdsWithValidGroupingFieldSet по связанному юниту --- + var groupedRelationships = new Dictionary>(); + foreach (var link in filteredUnitInUnitLinks) + { + var parentUnitId = link.ParentUnitId; + var childUnitId = link.ChildUnitId; + + if (unitIdsWithValidGroupingFieldSet.Contains(parentUnitId)) + { + if (!groupedRelationships.ContainsKey(childUnitId)) + { + groupedRelationships[childUnitId] = new List(); + } + groupedRelationships[childUnitId].Add(parentUnitId); + } + else if (unitIdsWithValidGroupingFieldSet.Contains(childUnitId)) + { + if (!groupedRelationships.ContainsKey(parentUnitId)) + { + groupedRelationships[parentUnitId] = new List(); + } + groupedRelationships[parentUnitId].Add(childUnitId); + } + } + + logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); + + // --- НОВАЯ ЛОГИКА: Разрешение конфликта - один юнит из unitIdsWithValidGroupingFieldSet только в одном списке значений --- + var unitToKeys = new Dictionary>(); // Карта: юнит из списка -> список ключей, где он встречается + + foreach (var kvp in groupedRelationships) + { + var key = kvp.Key; + var units = kvp.Value; + foreach (var unitId in units) + { + if (!unitToKeys.ContainsKey(unitId)) + { + unitToKeys[unitId] = new List(); + } + unitToKeys[unitId].Add(key); + } + } + + // Найти юниты, которые находятся в нескольких списках + var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList(); + + foreach (var conflictedUnitEntry in conflictedUnits) + { + var unitId = conflictedUnitEntry.Key; + var keysForUnit = conflictedUnitEntry.Value; + + Guid bestKey = keysForUnit[0]; // Инициализируем первым ключом + int maxCount = groupedRelationships[bestKey].Count; + + for (int i = 1; i < keysForUnit.Count; i++) + { + var currentKey = keysForUnit[i]; + var currentCount = groupedRelationships[currentKey].Count; + if (currentCount > maxCount) + { + bestKey = currentKey; + maxCount = currentCount; + } + } + + // Удалить юнит из списков всех ключей, кроме bestKey + foreach (var key in keysForUnit) + { + if (key != bestKey) + { + groupedRelationships[key].Remove(unitId); + logger.LogDebug("Юнит {UnitId} перемещён из группы {OldKey} в группу {BestKey} (по кол-ву).", unitId, key, bestKey); + } + } + } + + // Удаляем ключи, у которых список стал пустым после разрешения конфликтов + var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList(); + foreach (var key in keysToRemove) + { + groupedRelationships.Remove(key); + logger.LogDebug("Ключ {Key} удалён, так как его список юнитов стал пустым после разрешения конфликтов.", key); + } + + logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count); + + // 7. Разбить каждую группу и сопоставить с Job + foreach (var kvp in groupedRelationships) + { + var relationshipUnitId = kvp.Key; // Связанный юнит (не из unitIdsWithValidGroupingFieldSet) + var childUnitIds = kvp.Value; // Юниты из unitIdsWithValidGroupingFieldSet, связанные с regionalUnitId + + logger.LogDebug("Обработка связанного юнита {RegionalUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count); + + // Применяем ограничение MaxValueRelationships maxJob + if (!maxJob.MaxValueRelationships.HasValue) + { + logger.LogWarning("Job {JobId} не заполнено MaxValueRelationships.", maxJob.Id); + return; + } + int maxValueForSplitting = maxJob.MaxValueRelationships.Value; + var childUnitGroups = childUnitIds + .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) + .GroupBy(x => x.groupIndex) + .Select(g => g.Select(x => x.id).ToList()) + .ToList(); + + logger.LogDebug("Связанный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count); + + // Для каждой подгруппы: + for (int i = 0; i < childUnitGroups.Count; i++) + { + var subGroup = childUnitGroups[i]; + var subGroupSize = subGroup.Count; + + logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RegionalUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); + + // 8. Найти подходящий Job для подгруппы (логика без изменений) + Job? targetJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) + .FirstOrDefault(); + + if (targetJob == null) + { + targetJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) + .OrderBy(j => j.MaxValueRelationships!.Value) + .FirstOrDefault(); + } + + if (targetJob == null) + { + targetJob = maxJob; // maxJob уже проверен на null ранее + logger.LogDebug("Для подгруппы {Index} связанного юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, relationshipUnitId, maxJob.Id); + } + else + { + logger.LogDebug("Для подгруппы {Index} связанного юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, relationshipUnitId, targetJob.Id, targetJob.MaxValueRelationships); + } + + // 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId + var existingTemplatesForRelationship = await templateService.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i) + .ToListAsync(); + + var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault(); + + if (existingTemplateForSubGroup != null) + { + // Проверить, изменились ли юниты + var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); + var newUnitIds = subGroup.ToHashSet(); + + if (existingUnitIds.SetEquals(newUnitIds)) + { + logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) актуален.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); + } + else + { + logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); + await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, initiator); + } + } + else + { + var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, relationshipUnitId, initiator); + + if (reusableTemplate != null) + { + logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i); + + var expectedName = await GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup); + var nextRun = await GetNextRunAsync(targetJob); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = reusableTemplate.Id, + JobId = targetJob.Id, + UnitId = relationshipUnitId, + Name = expectedName, + IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, + IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + NextRun = nextRun, + Index = i, + UnitsInTemplate = subGroup + }; + + await SendTemplateUpdateMessage(updateRequest); + } + else + { + logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count); + await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator); + } + } + } + } + + // 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе (логика без изменений) + var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); + foreach (var kvp in groupedRelationships) + { + var regionalUnitId = kvp.Key; + var childUnitIds = kvp.Value; + int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; + var childUnitGroups = childUnitIds + .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) + .GroupBy(x => x.groupIndex) + .Select(g => g.Select(x => x.id).ToList()) + .ToList(); + + for (int i = 0; i < childUnitGroups.Count; i++) + { + var subGroup = childUnitGroups[i]; + var subGroupSize = subGroup.Count; + + Job? targetJobForExpectedKey = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) + .FirstOrDefault(); + + if (targetJobForExpectedKey == null) + { + targetJobForExpectedKey = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) + .OrderBy(j => j.MaxValueRelationships!.Value) + .FirstOrDefault(); + } + + if (targetJobForExpectedKey == null) targetJobForExpectedKey = maxJob; // maxJob уже проверен на null + + expectedTemplateKeys.Add((targetJobForExpectedKey.Id, regionalUnitId, i)); + } + } + + var allRegionalUnitIds = groupedRelationships.Keys.ToHashSet(); + var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); + + var allExistingTemplatesInGroup = await templateService.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .Where(t => allJobIdsInGroup.Contains(t.JobId) && allRegionalUnitIds.Contains(t.UnitId)) + .ToListAsync(); + + foreach (var existingTemplate in allExistingTemplatesInGroup) + { + var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); + 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, initiator); + } + } + + logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); + } + + public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) + { + logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); + return; + } + + // --- Вспомогательные методы --- + private async Task UpdateTemplateUnitsAsync(Template template, List newUnitIds, Job targetJob, HistoryInitiator initiator) + { + template.StatusTypeId = TemplateStatusTypeEnum.Updating; + template.DateModified = DateTimeOffset.UtcNow; + + if (!await templateService.CommitAsync(initiator)) + { + logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); + return; + } + + var expectedName = await GetNormalizedTemplateNameAsync(targetJob, template.UnitId, template.Index, newUnitIds); + var nextRun = await GetNextRunAsync(targetJob, template.NextRun); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = targetJob.Id, + UnitId = template.UnitId, + Name = expectedName, + IsActiveTemplate = template.IsActiveTemplate, + IsActiveSchedule = template.IsActiveSchedule, + LastRun = template.LastRun, + NextRun = nextRun, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + UnitsInTemplate = newUnitIds + }; + + await SendTemplateUpdateMessage(updateRequest); + } + + private async Task CreateGroupedTemplateAsync(Guid jobId, Guid regionalUnitId, List unitIds, int index, HistoryInitiator initiator) + { + logger.LogInformation("Создание нового группового шаблона для Job {JobId}, связанного юнита {RegionalUnitId}, Index {Index}, с {Count} юнитами.", jobId, regionalUnitId, index, unitIds.Count); + + var mqRequest = new TemplateGeneratorMq + { + JobId = jobId, + UnitId = regionalUnitId, // UnitId шаблона + UnitsInTemplate = unitIds, // Юниты для UnitsInTemplate + Index = index, // Индекс шаблона + HistoryInitiator = initiator + }; + + var msg = JsonSerializer.Serialize(mqRequest); + var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); + + if (!result.IsSuccess) + logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, связанного юнита {RegionalUnitId}, Index {Index}.", jobId, regionalUnitId, index); + } + + private async Task DeactivateTemplateAsync( + Template template, + Guid jobId, + HistoryInitiator initiator) + { + if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) + 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 updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + UnitId = template.UnitId, + Name = GetTemplateNameForUnused(template.Name), + IsActiveTemplate = DefaultUnusedTemplateState, + IsActiveSchedule = DefaultUnusedScheduleState, + LastRun = template.LastRun, + NextRun = template.NextRun, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Unused, + Initiator = initiator, + UnitsInTemplate = new List() + }; + + 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(); + } + + // Изменённая сигнатура: добавлен templateUnitIds + private async Task GetNormalizedTemplateNameAsync(Job targetJob, Guid unitId, int? index = null, List? templateUnitIds = null) + { + // Подготовка объекта TemplateForShortcodes для передачи в ShortcodesService + var templateForShortcodes = new TemplateForShortcodes + { + Id = Guid.Empty, // Не используется в подстановке, но нужен для структуры + Index = index, + 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 в List + UnitsInTemplate = templateUnitIds?.Select(id => new UnitInTemplateForShortcodes { UnitId = id }).ToList() ?? new List() + }; + + var rawName = await shortcodesService.ApplyShortcodesAsync(targetJob.TemplateNameMask, templateForShortcodes); + return rawName.ToUpper(); + } + + private async Task 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; + return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate); + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs new file mode 100644 index 00000000..2d1c28ff --- /dev/null +++ b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs @@ -0,0 +1,487 @@ +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.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.TransformServices; +using PARR.TemplateMatcher.Services.Interfaces; +using PARR.TemplateMatcher.Settings; +using System.Text.Json; + +namespace PARR.TemplateMatcher.Services.Implemetaions; + +internal class SimpleTemplateSynchronizer : ITemplateSynchronizer +{ + private const bool DefaultUnusedTemplateState = false; + private const bool DefaultUnusedScheduleState = false; + private const bool DefaultUsedTemplateState = false; + private const bool DefaultUsedScheduleState = false; + + private readonly ILogger logger; + private readonly IUnitFilterService unitFilterService; + private readonly MqSettings mqSettings; + private readonly IMqService mqService; + private readonly ITemplateService templateService; + private readonly IJobService jobService; + private readonly ITemplateReuser templateReuser; + private readonly IShortcodesService shortcodesService; + private readonly IEsppScheduleTransformService esppScheduleTransformService; + + public SimpleTemplateSynchronizer( + ILogger logger, + IUnitFilterService unitFilterService, + MqSettings mqSettings, + IMqService mqService, + ITemplateService templateService, + IJobService jobService, + ITemplateReuser templateReuser, + IShortcodesService shortcodesService, + IEsppScheduleTransformService esppScheduleTransformService) + { + this.logger = logger; + this.unitFilterService = unitFilterService; + this.mqSettings = mqSettings; + this.mqService = mqService; + 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; + } + + public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) + { + logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); + + var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); + logger.LogDebug("JobId {JobId}: найдено {Count} UnitId по фильтрам.", jobId, expectedUnitIds.Count); + + var job = await GetJobWithGroupAndAutoControlAsync(jobId); + if (job == null) + { + logger.LogError("Job с Id {JobId} не найден.", jobId); + return; + } + + // Проверяем, является ли Job "групповым" + bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group; + + if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue) + { + logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId); + return; // Ничего не делаем для группового Job + } + + 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 (existingTemplates.Any()) + { + logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.", + jobId, existingTemplates.Count); + + foreach (var template in existingTemplates) + { + await DeactivateTemplateAsync(template, jobId, initiator); + } + } + else + { + logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", jobId); + } + + 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, initiator); + } + + // Перечитываем шаблоны после деактивации + existingTemplates = await templateService.Get() + .AsNoTracking() + .Where(t => t.JobId == jobId) + .ToListAsync(); + + var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); + + // UnitId без шаблона → попытка переиспользования или создание + var unitIdsMissingTemplates = expectedUnitIds + .Where(unitId => !unitToTemplate.ContainsKey(unitId)) + .ToList(); + + var unitIdsToCreateFresh = new List(); + + foreach (var unitId in unitIdsMissingTemplates) + { + var reused = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); + if (reused != null) // если захват успешен + { + logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); + + var expectedName = await GetNormalizedTemplateNameAsync(job, unitId); + var nextRun = await GetNextRunAsync(job); // всегда пересчитываем для нового назначения + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = reused.Id, + JobId = jobId, + UnitId = unitId, + Name = expectedName, + IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, + IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + NextRun = nextRun, + UnitsInTemplate = new List() // Для простого шаблона + }; + + await SendTemplateUpdateMessage(updateRequest); + } + else + { + logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId); + unitIdsToCreateFresh.Add(unitId); + } + } + + // Обновление/реактивация шаблонов, оставшихся в фильтре + var templatesInFilter = existingTemplates + .Where(t => expectedUnitIds.Contains(t.UnitId)) + .ToList(); + + foreach (var template in templatesInFilter) + { + var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); + await ReactivateOrRenameTemplateAsync(template, job, expectedName, initiator); + } + + // Создание новых шаблонов + foreach (var unitId in unitIdsToCreateFresh) + { + await SendTemplateGeneratorMessageAsync(jobId, unitId, initiator); + } + + logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); + } + + public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) + { + logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); + + var existingTemplates = await templateService.Get() + .AsNoTracking() + .Where(t => t.JobId == jobId) + .ToListAsync(); + + if (!existingTemplates.Any()) return; + + var job = await GetJobWithGroupAndAutoControlAsync(jobId); + if (job == null) return; + + var currentUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); + 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) + { + targetStatus = TemplateStatusTypeEnum.Used; + targetIsActiveTemplate = template.IsActiveTemplate; + targetIsActiveSchedule = template.IsActiveSchedule; + } + else + { + targetStatus = TemplateStatusTypeEnum.Unused; + targetIsActiveTemplate = DefaultUnusedTemplateState; + targetIsActiveSchedule = DefaultUnusedScheduleState; + expectedName = GetTemplateNameForUnused(expectedName); + 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; + } + + var nextRun = await GetNextRunAsync(job, template.NextRun); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + 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 DeactivateTemplateAsync( + Template template, + Guid jobId, + HistoryInitiator initiator) + { + if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) + 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 updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + UnitId = template.UnitId, + Name = GetTemplateNameForUnused(template.Name), + IsActiveTemplate = DefaultUnusedTemplateState, + IsActiveSchedule = DefaultUnusedScheduleState, + LastRun = template.LastRun, + NextRun = template.NextRun, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Unused, + Initiator = initiator, + UnitsInTemplate = new List() // Для простого шаблона + }; + + await SendTemplateUpdateMessage(updateRequest); + return true; + } + + private async Task 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, + UnitId = template.UnitId, + Name = expectedName, + IsActiveTemplate = template.IsActiveTemplate, + IsActiveSchedule = template.IsActiveSchedule, + LastRun = template.LastRun, + NextRun = nextRun, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() // Для простого шаблона это список из одного элемента + }; + + await SendTemplateUpdateMessage(updateRequest); + return true; + } + + private async Task SendTemplateGeneratorMessageAsync( + Guid jobId, + Guid unitId, + HistoryInitiator initiator) + { + logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); + var mqRequest = new TemplateGeneratorMq + { + JobId = jobId, + UnitId = unitId, + HistoryInitiator = initiator, + UnitsInTemplate = new List() // Для простого шаблона + }; + + 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 GetJobWithGroupAndAutoControlAsync(Guid jobId) + { + return await jobService.Get() + .AsNoTracking() + .Include(j => j.Group) + .ThenInclude(j => j.GroupType) + .Include(j => j.AutoControl) + .FirstOrDefaultAsync(j => j.Id == jobId); + } + + private async Task> GetExpectedUnitIdsAsync(Guid jobId) + { + var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); + return units?.ToHashSet() ?? new HashSet(); + } + + private async Task 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() // для простого шаблона + }; + + var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, templateForShortcodes); + return rawName.ToUpper(); + } + + private async Task 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; + return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Implemetaions/TemplateReuser.cs b/PARR.TemplateMatcher/Services/Implemetaions/TemplateReuser.cs new file mode 100644 index 00000000..9ff39107 --- /dev/null +++ b/PARR.TemplateMatcher/Services/Implemetaions/TemplateReuser.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.Common.Domain; +using PARR.Constants; +using PARR.DAL.Models; +using PARR.DAL.Services.Interfaces; +using PARR.TemplateMatcher.Services.Interfaces; + +namespace PARR.TemplateMatcher.Services.Implementations; + +internal class TemplateReuser : ITemplateReuser +{ + private const int UnusedCandidateBatchSize = 10; + + private readonly ILogger logger; + private readonly ITemplateService templateService; + + public TemplateReuser( + ILogger logger, + ITemplateService templateService) + { + this.logger = logger; + this.templateService = templateService; + } + + public async Task TryReuseOneUnusedTemplateAsync( + Guid jobId, + Guid unitId, + HistoryInitiator initiator, + int maxAttempts = 3) + { + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + var unusedCandidates = await templateService.Get() + .AsNoTracking() + .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) + .OrderBy(t => t.DateModified ?? t.DateCreated) + .Take(UnusedCandidateBatchSize) + .ToListAsync(); + + if (!unusedCandidates.Any()) + { + logger.LogDebug("Нет Unused-шаблонов (попытка {Attempt}).", attempt); + return null; + } + + foreach (var candidate in unusedCandidates) + { + var originalStatus = candidate.StatusTypeId; + var originalModified = candidate.DateModified; + + try + { + candidate.StatusTypeId = TemplateStatusTypeEnum.Updating; + candidate.DateModified = DateTimeOffset.UtcNow; + + if (await templateService.CommitAsync(initiator)) + { + logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).", + candidate.Id, attempt); + return candidate; // Возвращаем захваченный шаблон + } + + // Откат при неудаче + candidate.StatusTypeId = originalStatus; + candidate.DateModified = originalModified; + } + catch (Exception ex) when ( + ex is DbUpdateException || + ex.InnerException?.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase) == true || + ex.InnerException?.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) == true) + { + logger.LogWarning(ex, "Конфликт при захвате шаблона {TemplateId} (попытка {Attempt}).", candidate.Id, attempt); + candidate.StatusTypeId = originalStatus; + candidate.DateModified = originalModified; + } + } + + if (attempt < maxAttempts) + await Task.Delay(Random.Shared.Next(5, 15) * attempt); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в попытке захвата (попытка {Attempt}).", attempt); + if (attempt == maxAttempts) throw; + } + } + + return null; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Interfaces/ITemplateReuser.cs b/PARR.TemplateMatcher/Services/Interfaces/ITemplateReuser.cs new file mode 100644 index 00000000..36167d8a --- /dev/null +++ b/PARR.TemplateMatcher/Services/Interfaces/ITemplateReuser.cs @@ -0,0 +1,14 @@ +using PARR.Common.Domain; +using PARR.DAL.Models; + +namespace PARR.TemplateMatcher.Services.Interfaces +{ + public interface ITemplateReuser + { + Task TryReuseOneUnusedTemplateAsync( + Guid jobId, + Guid unitId, + HistoryInitiator initiator, + int maxAttempts = 3); + } +} diff --git a/PARR.TemplateMatcher/Services/Interfaces/ITemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Interfaces/ITemplateSynchronizer.cs new file mode 100644 index 00000000..d65b8f2e --- /dev/null +++ b/PARR.TemplateMatcher/Services/Interfaces/ITemplateSynchronizer.cs @@ -0,0 +1,11 @@ +using PARR.Common.Domain; + +namespace PARR.TemplateMatcher.Services.Interfaces +{ + public interface ITemplateSynchronizer + { + Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator); + Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator); + Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator); + } +} diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/TemplateMatcher.cs index b299ac0f..a79609a5 100644 --- a/PARR.TemplateMatcher/TemplateMatcher.cs +++ b/PARR.TemplateMatcher/TemplateMatcher.cs @@ -1,79 +1,37 @@ 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.Contracts; -using PARR.DAL.DomainServices.Interfaces; -using PARR.DAL.Models; using PARR.DAL.Models.Job; -using PARR.DAL.Models.Unit; -using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; -using PARR.DAL.Services.Interfaces.Unit; -using PARR.DAL.TransformServices; -using PARR.TemplateMatcher.Settings; -using System.Text.Json; +using PARR.TemplateMatcher.Services.Implemetaions; +using PARR.TemplateMatcher.Services.Interfaces; namespace PARR.TemplateMatcher { internal class TemplateMatcher : ITemplateMatcher { - private const int UnusedCandidateBatchSize = 10; - private const bool DefaultUnusedTemplateState = false; - private const bool DefaultUnusedScheduleState = false; - private const bool DefaultUsedTemplateState = false; - private const bool DefaultUsedScheduleState = false; - private readonly ILogger logger; - private readonly IUnitFilterService unitFilterService; - private readonly IUnitInUnitService unitInUnitService; - private readonly IUnitInValueService unitInValueService; // Добавлено - private readonly IUnitService unitService; // Добавлено - private readonly MqSettings mqSettings; - private readonly IMqService mqService; - private readonly ITemplateService templateService; private readonly IJobService jobService; - private readonly IJobGroupService jobGroupService; - private readonly IShortcodesService shortcodesService; - private readonly IEsppScheduleTransformService esppScheduleTransformService; + private readonly IEnumerable synchronizers; // Получаем все стратегии + public TemplateMatcher( ILogger logger, - IUnitFilterService unitFilterService, - IUnitInUnitService unitInUnitService, - IUnitInValueService unitInValueService, - IUnitService unitService, - MqSettings mqSettings, - IMqService mqService, - ITemplateService templateService, IJobService jobService, - IJobGroupService jobGroupService, - IShortcodesService shortcodesService, - IEsppScheduleTransformService esppScheduleTransformService) + IEnumerable synchronizers + ) { this.logger = logger; - this.unitFilterService = unitFilterService; - this.unitInUnitService = unitInUnitService; - this.unitInValueService = unitInValueService; - this.unitService = unitService; - this.mqSettings = mqSettings; - this.mqService = mqService; - this.templateService = templateService; this.jobService = jobService; - this.jobGroupService = jobGroupService; - this.shortcodesService = shortcodesService; - this.esppScheduleTransformService = esppScheduleTransformService; + this.synchronizers = synchronizers; + } public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); - var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); - logger.LogDebug("JobId {JobId}: найдено {Count} UnitId по фильтрам.", jobId, expectedUnitIds.Count); - var job = await GetJobWithGroupAndAutoControlAsync(jobId); if (job == null) { @@ -86,843 +44,101 @@ namespace PARR.TemplateMatcher if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue) { - logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId); - return; // Ничего не делаем для группового Job + logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer.", jobId); + // Находим нужный синхронизатор + var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); + if (synchronizer != null) + { + // Так как Job групповой, вызываем синхронизацию для его JobGroup + await synchronizer.SyncTemplatesForJobGroupAsync(job.GroupId, initiator); + } + else + { + logger.LogError("GroupedTemplateSynchronizer не найден."); + } + return; // Завершаем выполнение для группового Job } else { - await SyncSimpleTemplatesAsync(job, expectedUnitIds, initiator); + logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer.", jobId); + // Находим нужный синхронизатор + var synchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); + if (synchronizer != null) + { + await synchronizer.SyncTemplatesForJobAsync(jobId, initiator); + } + else + { + logger.LogError("SimpleTemplateSynchronizer не найден."); + } + return; // Завершаем выполнение для обычного Job } - - logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); } public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); - - // 1. Получить JobGroup и связанные Job'ы - var jobGroup = await jobGroupService.Get() - .AsNoTracking() // Добавлено - .Include(jg => jg.Jobs) - .ThenInclude(j => j.AutoControl) - .Include(jg => jg.Jobs) - .ThenInclude(j => j.UnitFilters) - .ThenInclude(uf => uf.RelationshipFilters) - .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); - - if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) + var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); + if (synchronizer != null) { - logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); - return; + await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator); } - - var jobsInGroup = jobGroup.Jobs.ToList(); - - // 2. Найти Job с максимальным MaxValueRelationships - var maxJob = jobsInGroup - .Where(j => j.MaxValueRelationships.HasValue) - .OrderByDescending(j => j.MaxValueRelationships) - .FirstOrDefault(); - - if (maxJob == null) + else { - logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); - // Возможно, нужно обработать случай, когда MaxValueRelationships не установлено ни у одного Job. - // Пока просто выйдем. - return; - } - - // Проверяем, что UnitFilters и RelationshipFilters загружены - if (maxJob.UnitFilters == null) - { - logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id); - // Продолжить с пустыми фильтрами или выйти? - // Пока продолжим с пустым списком. - } - - logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); - - // 3. Использовать фильтры maxJob для получения expectedUnitIds - var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id); - if (expectedUnitIds == null || !expectedUnitIds.Any()) - { - logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId); - // Деактивировать все шаблоны для всех Job в группе? - // Пока просто выйдем. - return; - } - - // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId - if (!jobGroup.GroupingUnitFieldId.HasValue) - { - logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); - return; - } - - var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; - - // --- ИСПРАВЛЕНИЕ: Разбиваем запрос на части --- - // Загрузить UnitValues для отфильтрованных юнитов, чтобы проверить GroupingUnitFieldId - var filteredUnits = await unitService.Get() - .AsNoTracking() // Добавлено - .AsSplitQuery() // Добавлено - .Include(t => t.UnitValues) // Добавлено - .ThenInclude(t => t.Value) // Добавлено - .Where(u => expectedUnitIds.Contains(u.Id)) - .ToListAsync(); // Сначала загружаем Unit'ы - - // Затем фильтруем их UnitValues и собираем UnitId - var unitIdsWithValidGroupingFieldSet = filteredUnits - .Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))) - .Select(u => u.Id) - .ToHashSet(); // Создаем HashSet - - logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); - - if (!unitIdsWithValidGroupingFieldSet.Any()) - { - logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); - return; - } - - // --- НОВАЯ ЛОГИКА: Применение RelationshipFilters --- - // Найти связи UnitInUnit для unitIdsWithValidGroupingFieldSet - var potentialUnitInUnitLinks = await unitInUnitService.Get() - .AsNoTracking() // Добавлено - .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) - .ToListAsync(); - - logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit до применения RelationshipFilters.", potentialUnitInUnitLinks.Count); - - // Получить RelationshipFilters из maxJob - var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List(); - - if (relationshipFilters.Any()) - { - // Загрузить UnitInValue для ParentUnitId и ChildUnitId из potentialUnitInUnitLinks - var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet(); - var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet(); - - var parentUnitValues = await unitInValueService.Get() - .AsNoTracking() // Добавлено - .Include(uv => uv.Field) // Добавлено - .Include(uv => uv.Value) // Добавлено - .Where(uv => allParentIds.Contains(uv.UnitId)) - .ToListAsync(); - - var childUnitValues = await unitInValueService.Get() - .AsNoTracking() // Добавлено - .Include(uv => uv.Field) // Добавлено - .Include(uv => uv.Value) // Добавлено - .Where(uv => allChildIds.Contains(uv.UnitId)) - .ToListAsync(); - - // Сгруппировать значения по UnitId для быстрого доступа - var parentValuesMap = parentUnitValues - .GroupBy(uv => uv.UnitId) - .ToDictionary(g => g.Key, g => g.ToList()); - - var childValuesMap = childUnitValues - .GroupBy(uv => uv.UnitId) - .ToDictionary(g => g.Key, g => g.ToList()); - - // Применить фильтры к связям - var filteredUnitInUnitLinks = new List(); - foreach (var link in potentialUnitInUnitLinks) - { - bool linkMatchesAllFilters = true; - - foreach (var rf in relationshipFilters) - { - var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List()); - - bool filterMatch = valuesToCheck.Any(uv => - uv.FieldId == rf.FieldId && - uv.Value != null && - uv.Value.Value != null && - uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase) - ); - - if (rf.IsInverse) - filterMatch = !filterMatch; - - if (!filterMatch) - { - linkMatchesAllFilters = false; - break; // Не подходит под один из фильтров - } - } - - if (linkMatchesAllFilters) - { - filteredUnitInUnitLinks.Add(link); - } - } - - potentialUnitInUnitLinks = filteredUnitInUnitLinks; // Заменяем на отфильтрованные - } - - logger.LogDebug("Осталось {Count} связей UnitInUnit после применения RelationshipFilters.", potentialUnitInUnitLinks.Count); - - // 6. Сгруппировать ChildUnitId по ParentUnitId (региональный ЭК) из ОТФИЛЬТРОВАННЫХ связей - var groupedByRegional = potentialUnitInUnitLinks - .GroupBy(link => link.ParentUnitId) - .ToDictionary(g => g.Key, g => g.Select(l => l.ChildUnitId).ToList()); - - logger.LogDebug("Сформировано {Count} групп по региональным юнитам.", groupedByRegional.Count); - - // 7. Разбить каждую группу и сопоставить с Job - // Для каждого регионального юнита и его дочерних юнитов: - foreach (var kvp in groupedByRegional) - { - var regionalUnitId = kvp.Key; - var childUnitIds = kvp.Value; - - logger.LogDebug("Обработка регионального юнита {RegionalUnitId} с {Count} дочерними юнитами.", regionalUnitId, childUnitIds.Count); - - // Применяем ограничение MaxValueRelationships maxJob - int maxValueForSplitting = maxJob.MaxValueRelationships.Value; // Уже проверили, что не null - var childUnitGroups = childUnitIds - .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) - .GroupBy(x => x.groupIndex) - .Select(g => g.Select(x => x.id).ToList()) - .ToList(); - - logger.LogDebug("Региональный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", regionalUnitId, childUnitGroups.Count); - - // Для каждой подгруппы: - for (int i = 0; i < childUnitGroups.Count; i++) - { - var subGroup = childUnitGroups[i]; - var subGroupSize = subGroup.Count; - - logger.LogDebug("Обработка подгруппы {Index} регионального юнита {RegionalUnitId}, размер {Size}.", i, regionalUnitId, subGroupSize); - - // 8. Найти подходящий Job для подгруппы - // Попробовать найти Job с MaxValueRelationships, равным размеру подгруппы - var targetJob = jobsInGroup - .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) - .FirstOrDefault(); - - if (targetJob == null) - { - // Найти Job с MaxValueRelationships >= размеру подгруппы, но минимально подходящее - targetJob = jobsInGroup - .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) - .OrderBy(j => j.MaxValueRelationships.Value) - .FirstOrDefault(); - } - - if (targetJob == null) - { - // Если подходящий Job не найден, используем maxJob - targetJob = maxJob; - logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, regionalUnitId, maxJob.Id); - } - else - { - logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, regionalUnitId, targetJob.Id, targetJob.MaxValueRelationships); - } - - // 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId - var existingTemplatesForRegional = await templateService.Get() - .AsNoTracking() // Добавлено - .Include(t => t.UnitsInTemplate) - .Where(t => t.JobId == targetJob.Id && t.UnitId == regionalUnitId && t.Index == i) - .ToListAsync(); - - Template existingTemplateForSubGroup = existingTemplatesForRegional.FirstOrDefault(); - - if (existingTemplateForSubGroup != null) - { - // Проверить, изменились ли юниты - var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); - var newUnitIds = subGroup.ToHashSet(); - - if (existingUnitIds.SetEquals(newUnitIds)) - { - logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) актуален.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i); - // Возможно, нужно обновить имя или статус, если изменились фильтры или AutoControl - // Пока оставим как есть, если структура не изменилась. - } - else - { - logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i); - // Обновляем существующий шаблон - await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, initiator); - } - } - else - { - // --- ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ МЕТОД TryReuseOneUnusedTemplateAsync --- - var reusableTemplate = await TryReuseOneUnusedTemplateAsync(targetJob.Id, regionalUnitId, initiator); // передаём regionalUnitId как unitId для старого метода - - if (reusableTemplate != null) // если захват успешен - { - logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, регионального юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, regionalUnitId, i); - - // Подготовить сообщение для TemplateUpdater с новыми параметрами - var expectedName = await GetNormalizedTemplateNameAsync(targetJob, regionalUnitId, i); - var nextRun = await GetNextRunAsync(targetJob); // всегда пересчитываем для нового назначения - - var updateRequest = new TemplateUpdaterMq - { - TemplateId = reusableTemplate.Id, // ID захваченного шаблона - JobId = targetJob.Id, // Новый JobId - UnitId = regionalUnitId, // Новый UnitId (региональный) - Name = expectedName, - IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, - IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, - StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator, - NextRun = nextRun, - Index = i, // Новый Index - UnitsInTemplate = subGroup // Новые UnitsInTemplate - }; - - await SendTemplateUpdateMessage(updateRequest); - } - else - { - // Создать новый шаблон - logger.LogDebug("Создание нового шаблона для Job {JobId}, Regional {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, regionalUnitId, i, subGroup.Count); - await CreateGroupedTemplateAsync(targetJob.Id, regionalUnitId, subGroup, i, initiator); - } - } - } - } - - // 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе - // Это требует сбора всех ожидаемых (JobId, UnitId, Index) и сравнения с существующими. - // Соберем ожидаемые комбинации - var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); - foreach (var kvp in groupedByRegional) - { - var regionalUnitId = kvp.Key; - var childUnitIds = kvp.Value; - int maxValueForSplitting = maxJob.MaxValueRelationships.Value; - var childUnitGroups = childUnitIds - .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) - .GroupBy(x => x.groupIndex) - .Select(g => g.Select(x => x.id).ToList()) - .ToList(); - - for (int i = 0; i < childUnitGroups.Count; i++) - { - var subGroup = childUnitGroups[i]; - var subGroupSize = subGroup.Count; - - var targetJob = jobsInGroup - .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) - .FirstOrDefault(); - - if (targetJob == null) - { - targetJob = jobsInGroup - .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) - .OrderBy(j => j.MaxValueRelationships.Value) - .FirstOrDefault(); - } - - if (targetJob == null) targetJob = maxJob; - - // Исправлено: используем конкретные типы для кортежа - expectedTemplateKeys.Add((targetJob.Id, regionalUnitId, i)); - } - } - - // Загрузить *все* шаблоны для всех Job в группе, связанные с региональными юнитами из групп - var allRegionalUnitIds = groupedByRegional.Keys.ToHashSet(); - var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); - - var allExistingTemplatesInGroup = await templateService.Get() - .AsNoTracking() // Добавлено - .Include(t => t.UnitsInTemplate) - .Where(t => allJobIdsInGroup.Contains(t.JobId) && allRegionalUnitIds.Contains(t.UnitId)) - .ToListAsync(); - - foreach (var existingTemplate in allExistingTemplatesInGroup) - { - // Исправлено: используем конкретные типы для ключа - var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); - 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, initiator); - } - } - - logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); - } - - - private async Task SyncSimpleTemplatesAsync(Job job, HashSet expectedUnitIds, HistoryInitiator initiator) - { - logger.LogDebug("Синхронизация обычных шаблонов для JobId {JobId}", job.Id); - - var existingTemplates = await templateService.Get() - .AsNoTracking() // Добавлено - .Where(t => t.JobId == job.Id) - .ToListAsync(); - - logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", - job.Id, expectedUnitIds.Count, existingTemplates.Count); - - // Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны - if (!expectedUnitIds.Any()) - { - if (existingTemplates.Any()) - { - logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.", - job.Id, existingTemplates.Count); - - foreach (var template in existingTemplates) - { - await DeactivateTemplateAsync(template, job.Id, initiator); - } - } - else - { - logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", job.Id); - } - - logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", job.Id); - return; - } - - // Деактивация шаблонов, которые вышли из фильтра - var templatesToDeactivate = existingTemplates - .Where(t => !expectedUnitIds.Contains(t.UnitId)) - .ToList(); - - foreach (var template in templatesToDeactivate) - { - await DeactivateTemplateAsync(template, job.Id, initiator); - } - - // Перечитываем шаблоны после деактивации - existingTemplates = await templateService.Get() - .AsNoTracking() // Добавлено - .Where(t => t.JobId == job.Id) - .ToListAsync(); - - var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); - - // UnitId без шаблона → попытка переиспользования или создание - var unitIdsMissingTemplates = expectedUnitIds - .Where(unitId => !unitToTemplate.ContainsKey(unitId)) - .ToList(); - - var unitIdsToCreateFresh = new List(); - - foreach (var unitId in unitIdsMissingTemplates) - { - var reused = await TryReuseOneUnusedTemplateAsync(job.Id, unitId, initiator); - if (reused != null) - { - logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); - - var expectedName = await GetNormalizedTemplateNameAsync(job, unitId); - var nextRun = await GetNextRunAsync(job); // всегда пересчитываем для нового назначения - - var updateRequest = new TemplateUpdaterMq - { - TemplateId = reused.Id, - JobId = job.Id, - UnitId = unitId, - Name = expectedName, - IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, - IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, - StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator, - NextRun = nextRun, - UnitsInTemplate = new List() - }; - - await SendTemplateUpdateMessage(updateRequest); - } - else - { - logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId); - unitIdsToCreateFresh.Add(unitId); - } - } - - // Обновление/реактивация шаблонов, оставшихся в фильтре - var templatesInFilter = existingTemplates - .Where(t => expectedUnitIds.Contains(t.UnitId)) - .ToList(); - - foreach (var template in templatesInFilter) - { - var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); - await ReactivateOrRenameTemplateAsync(template, job, expectedName, initiator); - } - - // Создание новых шаблонов - foreach (var unitId in unitIdsToCreateFresh) - { - await SendTemplateGeneratorMessageAsync(job.Id, unitId, initiator); + logger.LogError("GroupedTemplateSynchronizer не найден."); } } - public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); - var existingTemplates = await templateService.Get() - .AsNoTracking() // Добавлено - .Where(t => t.JobId == jobId) - .ToListAsync(); - - if (!existingTemplates.Any()) return; - var job = await GetJobWithGroupAndAutoControlAsync(jobId); - if (job == null) return; - - var currentUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); - logger.LogDebug("JobId {JobId}: {Count} UnitId по текущему фильтру.", jobId, currentUnitIds.Count); - - foreach (var template in existingTemplates) + if (job == null) { - 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) - { - targetStatus = TemplateStatusTypeEnum.Used; - targetIsActiveTemplate = template.IsActiveTemplate; - targetIsActiveSchedule = template.IsActiveSchedule; - } - else - { - targetStatus = TemplateStatusTypeEnum.Unused; - targetIsActiveTemplate = DefaultUnusedTemplateState; - targetIsActiveSchedule = DefaultUnusedScheduleState; - expectedName = GetTemplateNameForUnused(expectedName); - 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; - } - - var nextRun = await GetNextRunAsync(job, template.NextRun); - - var updateRequest = new TemplateUpdaterMq - { - TemplateId = template.Id, - JobId = jobId, - 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 UpdateTemplateUnitsAsync(Template template, List newUnitIds, Job job, HistoryInitiator initiator) - { - // Обновляем шаблон как "Updating" - template.StatusTypeId = TemplateStatusTypeEnum.Updating; - template.DateModified = DateTimeOffset.UtcNow; - - if (!await templateService.CommitAsync(initiator)) - { - logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); + logger.LogError("Job с Id {JobId} не найден.", jobId); return; } - // Здесь нужно обновить UnitsInTemplate. - // Это может быть сделано через TemplateUpdaterMq, если он поддерживает передачу нового списка юнитов. - // Или напрямую в сервисе шаблонов, если логика обновления простая. - // Пока отправим сообщение в TemplateUpdater. + // Проверяем, является ли Job "групповым" + bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group; - var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); - var nextRun = await GetNextRunAsync(job, template.NextRun); - - var updateRequest = new TemplateUpdaterMq + if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue) { - TemplateId = template.Id, - JobId = job.Id, - UnitId = template.UnitId, - Name = expectedName, - IsActiveTemplate = template.IsActiveTemplate, - IsActiveSchedule = template.IsActiveSchedule, - LastRun = template.LastRun, - NextRun = nextRun, - Index = template.Index, - StatusTypeId = TemplateStatusTypeEnum.Used, // Предполагаем, что используется - Initiator = initiator, - UnitsInTemplate = newUnitIds // Передаем обновленный список юнитов - }; - - await SendTemplateUpdateMessage(updateRequest); - } - - private async Task CreateGroupedTemplateAsync(Guid jobId, Guid regionalUnitId, List unitIds, int index, HistoryInitiator initiator) - { - logger.LogInformation("Создание нового группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}, с {Count} юнитами.", jobId, regionalUnitId, index, unitIds.Count); - - // Предполагаем, что TemplateGeneratorMq может обрабатывать UnitsInTemplate и Index - var mqRequest = new TemplateGeneratorMq - { - JobId = jobId, - UnitId = regionalUnitId, // UnitId шаблона - UnitsInTemplate = unitIds, // Юниты для UnitsInTemplate - Index = index, // Индекс шаблона - HistoryInitiator = initiator - }; - - var msg = JsonSerializer.Serialize(mqRequest); - var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); - - if (!result.IsSuccess) - logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}.", jobId, regionalUnitId, index); - } - - private async Task DeactivateTemplateAsync( - Template template, - Guid jobId, - HistoryInitiator initiator) - { - if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) - 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 updateRequest = new TemplateUpdaterMq - { - TemplateId = template.Id, - JobId = jobId, - UnitId = template.UnitId, - Name = GetTemplateNameForUnused(template.Name), - IsActiveTemplate = DefaultUnusedTemplateState, - IsActiveSchedule = DefaultUnusedScheduleState, - LastRun = template.LastRun, - NextRun = template.NextRun, - Index = template.Index, - StatusTypeId = TemplateStatusTypeEnum.Unused, - Initiator = initiator, - UnitsInTemplate = new List() - }; - - await SendTemplateUpdateMessage(updateRequest); - return true; - } - - private async Task 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, - UnitId = template.UnitId, - Name = expectedName, - IsActiveTemplate = template.IsActiveTemplate, - IsActiveSchedule = template.IsActiveSchedule, - LastRun = template.LastRun, - NextRun = nextRun, - Index = template.Index, - StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator, - UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() - }; - - await SendTemplateUpdateMessage(updateRequest); - return true; - } - - private async Task SendTemplateGeneratorMessageAsync( - Guid jobId, - Guid unitId, - HistoryInitiator initiator) - { - logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); - var mqRequest = new TemplateGeneratorMq - { - JobId = jobId, - UnitId = unitId, - HistoryInitiator = initiator, - UnitsInTemplate = new List() - }; - - 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); + logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer для Update.", jobId); + // Находим нужный синхронизатор + var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); + if (synchronizer != null) + { + // Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение) + await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator); + // Или, возможно, имеет смысл вызвать полную синхронизацию для группы: + // await synchronizer.SyncTemplatesForJobGroupAsync(job.GroupId, initiator); + } + else + { + logger.LogError("GroupedTemplateSynchronizer не найден."); + } + return; // Завершаем выполнение } else { - logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.", - updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName); + logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer для Update.", jobId); + // Находим нужный синхронизатор + var synchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); + if (synchronizer != null) + { + await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator); + } + else + { + logger.LogError("SimpleTemplateSynchronizer не найден."); + } + return; // Завершаем выполнение } } - // --- ИЗМЕНЕННЫЙ МЕТОД: Теперь используется как для простых, так и для групповых шаблонов --- - private async Task TryReuseOneUnusedTemplateAsync( - Guid jobId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync) - Guid unitId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync) - HistoryInitiator initiator, - int maxAttempts = 3) - { - for (int attempt = 1; attempt <= maxAttempts; attempt++) - { - try - { - var unusedCandidates = await templateService.Get() - .AsNoTracking() // Добавлено - .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) - .OrderBy(t => t.DateModified ?? t.DateCreated) - .Take(UnusedCandidateBatchSize) - .ToListAsync(); - - if (!unusedCandidates.Any()) - { - logger.LogDebug("Нет Unused-шаблонов (попытка {Attempt}).", attempt); - return null; - } - - foreach (var candidate in unusedCandidates) - { - var originalStatus = candidate.StatusTypeId; - var originalModified = candidate.DateModified; - - try - { - candidate.StatusTypeId = TemplateStatusTypeEnum.Updating; - candidate.DateModified = DateTimeOffset.UtcNow; - - if (await templateService.CommitAsync(initiator)) - { - logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).", - candidate.Id, attempt); - return candidate; // Возвращаем захваченный шаблон - } - - // Откат при неудаче - candidate.StatusTypeId = originalStatus; - candidate.DateModified = originalModified; - } - catch (Exception ex) when ( - ex is DbUpdateException || - ex.InnerException?.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase) == true || - ex.InnerException?.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) == true) - { - logger.LogWarning(ex, "Конфликт при захвате шаблона {TemplateId} (попытка {Attempt}).", candidate.Id, attempt); - candidate.StatusTypeId = originalStatus; - candidate.DateModified = originalModified; - } - } - - if (attempt < maxAttempts) - await Task.Delay(Random.Shared.Next(5, 15) * attempt); - } - catch (Exception ex) - { - logger.LogError(ex, "Ошибка в попытке захвата (попытка {Attempt}).", attempt); - if (attempt == maxAttempts) throw; - } - } - - return null; - } - - private string GetTemplateNameForUnused(string templateName) - { - return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - } - + // --- Вспомогательные методы --- private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId) { return await jobService.Get() @@ -932,29 +148,5 @@ namespace PARR.TemplateMatcher .Include(j => j.AutoControl) .FirstOrDefaultAsync(j => j.Id == jobId); } - - private async Task> GetExpectedUnitIdsAsync(Guid jobId) - { - var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); - return units?.ToHashSet() ?? new HashSet(); - } - - private async Task GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null) - { - var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id, index); - return rawName.ToUpper(); - } - - private async Task 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; - return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); - } } } \ No newline at end of file diff --git a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs index e22ae913..0bf44b09 100644 --- a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs +++ b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using PARR.BLL; using PARR.DAL; +using PARR.TemplateMatcher.Services.Implementations; using PARR.TemplateMatcher.Services.Implemetaions; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Settings; @@ -23,6 +24,9 @@ namespace PARR.TemplateMatcher services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static IConfigurationBuilder AddTemplateMatcherConfigurations(this IConfigurationBuilder builder, IServiceCollection services) diff --git a/PARR.TemplateMatcherWorker/appsettings.json b/PARR.TemplateMatcherWorker/appsettings.json index eb2299bf..9e75ba0d 100644 --- a/PARR.TemplateMatcherWorker/appsettings.json +++ b/PARR.TemplateMatcherWorker/appsettings.json @@ -1,17 +1,17 @@ { - "ConnectionStrings": { - "DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;", - "RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs" - }, - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } - }, + "ConnectionStrings": { + "DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;", + "RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } + }, "MqSettings": { "TemplateMatcher": { "HostName": "parr-rabbitmq", @@ -31,5 +31,8 @@ "User": "template_updater_writer", "Password": "sjdhgfkJHGIUFDi14asd^12" } + }, + "GroupedShortcodeCacheSettings": { + "ValueTtl": "00:20:00" } } diff --git a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs index 7d3453d0..7b7f6e34 100644 --- a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs +++ b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs @@ -45,7 +45,9 @@ namespace PARR.TemplateTaskGenerator { JobId = jobId, UnitId = unitId, - HistoryInitiator = new HistoryInitiator { InitiatorComment = "Запрос на создание шаблона", InitiatorParrComponentId = ParrComponentsEnum.TemplateTaskGenerator } + HistoryInitiator = new HistoryInitiator { InitiatorComment = "Запрос на создание шаблона", InitiatorParrComponentId = ParrComponentsEnum.TemplateTaskGenerator }, + UnitsInTemplate = new List() + }; var msg = JsonSerializer.Serialize(mqRequest); diff --git a/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs b/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs index 346167fd..967ab9d3 100644 --- a/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs +++ b/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs @@ -4,6 +4,7 @@ using PARR.BLL.Domain.Mq; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.Extensions; +using PARR.DAL.Models; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; @@ -45,6 +46,8 @@ namespace PARR.TemplateUpdater.Services var template = await templateService.Get() .Include(t => t.RobotConfigurations) + .Include(t => t.UnitsInTemplate) + .AsSplitQuery() .FirstOrDefaultAsync(t => t.Id == query.TemplateId); if (template == null) { @@ -100,6 +103,37 @@ namespace PARR.TemplateUpdater.Services template.Index = query.Index; template.StatusTypeId = query.StatusTypeId; + // === Обработка изменения состава UnitsInTemplate === + var currentUnitIds = template.UnitsInTemplate.Select(u => u.UnitId).ToHashSet(); + var newUnitIds = query.UnitsInTemplate.ToHashSet(); + + if (!currentUnitIds.SetEquals(newUnitIds)) + { + // Удаляем старые связи + var toRemove = template.UnitsInTemplate + .Where(u => !newUnitIds.Contains(u.UnitId)) + .ToList(); + + foreach (var item in toRemove) + template.UnitsInTemplate.Remove(item); + + // Добавляем новые связи + var toAdd = newUnitIds.Except(currentUnitIds); + foreach (var unitId in toAdd) + { + template.UnitsInTemplate.Add(new UnitsInTemplate + { + TemplateId = template.Id, + UnitId = unitId, + DateCreated = DateTimeOffset.UtcNow + }); + } + + // Поскольку коллекция изменилась — шаблон считается изменённым + templateIsChanged = true; + } + // === конец обработки UnitsInTemplate === + if (templateIsChanged) { // ставим задачу на обновление шаблона