From be1599aa250e91b210af651c2c33e1baeb2caa60 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Mon, 20 Apr 2026 12:02:46 +1000 Subject: [PATCH] =?UTF-8?q?feat(dal):=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=202=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85?= =?UTF-8?q?=20=D1=88=D0=BE=D1=80=D1=82=D0=BA=D0=BE=D0=B4=D0=B0=20%=D0=98?= =?UTF-8?q?=D0=94%=20=D0=B8=20%=D0=A1=D0=92=D0=AF=D0=97=D0=AC:=D0=98=D0=9C?= =?UTF-8?q?=D0=AF=20=D0=90=D0=A2=D0=A0=D0=98=D0=91=D0=A3=D0=A2=D0=90%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shortcodes/ShortcodesService.cs | 152 ++++++++++++++++-- PARR.DAL/PARR.DAL.csproj | 1 + .../PARR.TemplateMatcher.csproj | 1 + .../GroupedTemplateSynchronizer.cs | 16 +- .../Settings/TemplateSettings.cs | 2 +- 5 files changed, 150 insertions(+), 22 deletions(-) diff --git a/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs b/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs index ffe9101c..53e8223a 100644 --- a/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs +++ b/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs @@ -40,10 +40,11 @@ namespace PARR.DAL.DomainServices.Shortcodes private static readonly Regex GeneralShortcodeRegex = new(@"%[^%\s]+%", RegexOptions.Compiled); private static readonly Regex MaxShortcodeRegex = new(@"%МАКС:([а-яА-Яa-zA-Z0-9_]+)%", RegexOptions.Compiled); private static readonly Regex LettersShortcodeRegex = new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled); + private static readonly Regex RelShortcodeRegex = new(@"%СВЯЗЬ:([а-яА-Яa-zA-Z0-9_]+)%", RegexOptions.Compiled); private static readonly HashSet SupportedStandardShortcodes = new(StringComparer.OrdinalIgnoreCase) { - "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%" + "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%", "%ИД%" }; private readonly ILogger logger; @@ -194,6 +195,7 @@ namespace PARR.DAL.DomainServices.Shortcodes (sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) || (sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) || (sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) || + (sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) || ((sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)) && template.Job.Tnk == null) ).ToList(); @@ -216,6 +218,7 @@ namespace PARR.DAL.DomainServices.Shortcodes sc != "%ЭК%" && // %ЭК% не требует Job (SupportedStandardShortcodes.Contains(sc) || sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || + sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase) || @@ -230,7 +233,8 @@ namespace PARR.DAL.DomainServices.Shortcodes sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase) || sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || - sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) + sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) || + sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase)) && job?.Group == null) { missing.Add(".Include(j => j.Group)"); @@ -239,7 +243,8 @@ namespace PARR.DAL.DomainServices.Shortcodes if (shortcodes.Any(sc => sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || - sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) + sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) || + sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase)) && job?.Group?.GroupType == null) { missing.Add(".ThenInclude(g => g.GroupType)"); @@ -296,7 +301,8 @@ namespace PARR.DAL.DomainServices.Shortcodes if (shortcodes.Any(sc => sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || - sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))) + sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) || + sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase))) { query = query .Include(j => j.Group) @@ -444,14 +450,24 @@ namespace PARR.DAL.DomainServices.Shortcodes } } - // 4. Transform: %БУКВЫ:... + // 4. СВЯЗЬ:ИМЯ_АТРИБУТА (только для групповых работ) + if (data.Job?.Group?.GroupType?.Code == JobGroupTypesEnum.Group) + { + var svyazShortcodes = RelShortcodeRegex.Matches(result); + if (svyazShortcodes.Count > 0) + { + result = await ReplaceRelShortcodesAsync(data.Id, data.UnitId, data.UnitsInTemplate, result, svyazShortcodes, caller).ConfigureAwait(false); + } + } + + // 5. Transform: %БУКВЫ:... var lettersShortcodes = LettersShortcodeRegex.Matches(result); if (lettersShortcodes.Count > 0) { result = await ReplaceLettersShortcodesAsync(data.UnitId, result, lettersShortcodes, caller).ConfigureAwait(false); } - // 5. %СВЯЗИ% и %СВЯЗИ-ПН% + // 6. %СВЯЗИ% и %СВЯЗИ-ПН% List? relatedUnitNames = null; if (shortcodesInMask.Any(m => string.Equals(m, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase))) { @@ -471,7 +487,14 @@ namespace PARR.DAL.DomainServices.Shortcodes } } - // 6. Поля + // 7. ИД шаблона + if (shortcodesInMask.Any(m => string.Equals(m, "%ИД%", StringComparison.OrdinalIgnoreCase))) + { + var templateIdWithoutDashes = data.Id.ToString("N"); + result = ReplaceSingleShortcode(result, "%ИД%", templateIdWithoutDashes); + } + + // 8. Поля var fieldShortcodes = GetShortCodes(result); if (fieldShortcodes.Count > 0) result = await ReplaceFieldValues(data.UnitId, result, fieldShortcodes).ConfigureAwait(false); @@ -528,8 +551,11 @@ namespace PARR.DAL.DomainServices.Shortcodes Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%ТИКТАК%", Description = "Текущее время в формате Unix timestamp (секунды с 1970-01-01 UTC)", - Type = ShortcodeTypeEnum.Standard - } + Type = ShortcodeTypeEnum.Standard}, + new ShortcodeInfoDto { + Shortcode = "%ИД%", + Description = "Уникальный идентификатор шаблона (GUID без дефисов)", + Type = ShortcodeTypeEnum.Standard} }); // 3. Групповые шорткоды @@ -542,8 +568,12 @@ namespace PARR.DAL.DomainServices.Shortcodes Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК%", Type = ShortcodeTypeEnum.GroupValue }, new ShortcodeInfoDto { Shortcode = "%ГР_ПОЛЕ-ПН%", - Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.", + Description = "Нумерованный список связанных ЭК с основноым ЭК шаблона: 1. ЭК-123 (Значение1, Значение2). Использует имя поля группировки для вывода значений", Type = ShortcodeTypeEnum.GroupValue }, + new ShortcodeInfoDto { + Shortcode = "%СВЯЗЬ:ИМЯ АТРИБУТА%", + Description = "Извлекает значения поля с именем ИМЯ АТРИБУТА из связанных ЭК, сортирует по алфавиту и берёт первое. Пример: %СВЯЗЬ:ЗОНА_ОТВЕТСТВЕННОСТИ%", + Type = ShortcodeTypeEnum.GroupValue} }); // 4. Трансформирующие шорткоды @@ -819,6 +849,108 @@ namespace PARR.DAL.DomainServices.Shortcodes return effectiveUnitIds; } + private async Task ReplaceRelShortcodesAsync( + Guid templateId, + Guid templateUnitId, + List unitsInTemplate, + string input, + MatchCollection relShortcodes, + string caller) + { + var shortcodeToMatches = relShortcodes + .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("[{Caller}] Обработка {Shortcode} для шаблона с UnitId {UnitId}, fieldName {FieldName}", caller, templateUnitId, fieldName); + + List unitIds; + bool wasLoadedFromDb = false; + + if (unitsInTemplate.Count > 0) + { + unitIds = unitsInTemplate.Select(uit => uit.UnitId).ToList(); + } + else + { + logger.LogWarning( + "[{Caller}] Шаблон с UnitId {UnitId} не содержит UnitsInTemplate. Данные будут догружены из БД. " + + "Рекомендуется обновить запрос с Include(t => t.UnitsInTemplate).", + caller, templateUnitId); + + var fullTemplate = await templateService.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .FirstOrDefaultAsync(t => t.Id == templateId) + .ConfigureAwait(false); + + unitIds = fullTemplate?.UnitsInTemplate?.Select(uit => uit.UnitId).ToList() ?? new List(); + wasLoadedFromDb = true; + } + + if (unitIds.Count == 0) + { + logger.LogDebug("[{Caller}] Нет UnitsInTemplate для шаблона {TemplateId}. Шорткод {Shortcode} пропущен.", caller, templateUnitId, fullShortcode); + foreach (var match in matches) + input = input.Replace(match.Value, ""); + continue; + } + + var allFieldValues = new List<(string FieldName, string? Value)>(); + + foreach (var unitId in unitIds) + { + var values = await unitInValueService.GetFieldValuesAsync(unitId, new List { fieldName }).ConfigureAwait(false); + allFieldValues.AddRange(values.Select(v => (v.FieldName, v.Value))); + } + + if (allFieldValues == null || !allFieldValues.Any()) + { + logger.LogDebug("[{Caller}] Для шорткода {Shortcode} не найдено значений поля {FieldName} среди UnitsInTemplate.", caller, fullShortcode, fieldName); + foreach (var match in matches) + input = input.Replace(match.Value, ""); + continue; + } + + var distinctSortedValues = allFieldValues + .Where(fv => !string.IsNullOrEmpty(fv.Value)) + .Select(fv => fv.Value!) + .Distinct() + .OrderBy(v => v, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (distinctSortedValues.Count == 0) + { + logger.LogDebug("[{Caller}] Для шорткода {Shortcode} все значения поля {FieldName} пустые.", caller, fullShortcode, fieldName); + foreach (var match in matches) + input = input.Replace(match.Value, ""); + continue; + } + + if (distinctSortedValues.Count > 1) + { + logger.LogWarning( + "[{Caller}] Шорткод {Shortcode} нашёл {Count} различных значений поля {FieldName}: [{Values}]. Используется первое: '{FirstValue}'", + caller, fullShortcode, distinctSortedValues.Count, fieldName, + string.Join(", ", distinctSortedValues), + distinctSortedValues[0]); + } + + var firstValue = distinctSortedValues[0]; + + foreach (var match in matches) + input = input.Replace(match.Value, firstValue); + } + + return input; + } + private async Task ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes, string caller) { var shortcodeToMatches = lettersShortcodes diff --git a/PARR.DAL/PARR.DAL.csproj b/PARR.DAL/PARR.DAL.csproj index 234b2959..f7b981c5 100644 --- a/PARR.DAL/PARR.DAL.csproj +++ b/PARR.DAL/PARR.DAL.csproj @@ -7,6 +7,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj b/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj index e8ef522a..6f3fde69 100644 --- a/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj +++ b/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj @@ -9,6 +9,7 @@ + diff --git a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs index 79e81c58..004e27f5 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces.RabbitServices; using PARR.DAL.DomainServices.Interfaces; -using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.DomainServices.UnitFilterService; using PARR.DAL.Models; using PARR.DAL.Models.Job; @@ -38,9 +37,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly IMatchingStatusService matchingStatusService; - private readonly IShortcodesService shortcodesService; - //private readonly INextRunService nextRunService; public GroupedTemplateSynchronizer( ILogger logger, @@ -57,9 +54,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, ITemplateUpdaterMqSender templateUpdaterMqSender, - IMatchingStatusService matchingStatusService, - IShortcodesService shortcodesService - //INextRunService nextRunService + IMatchingStatusService matchingStatusService ) { this.logger = logger; @@ -77,8 +72,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; this.matchingStatusService = matchingStatusService; - this.shortcodesService = shortcodesService; - //this.nextRunService = nextRunService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) @@ -248,7 +241,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { // dto.Id - это ParentUnitId, связанные - ChildUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate // relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона - relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); // <-- Исправлено + relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); } else { @@ -298,7 +291,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .ToDictionary(uv => uv.UnitId, uv => uv.Value.Value); // 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов) - var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); + var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); foreach (var kvpOuter in reverseMapping) { @@ -318,7 +311,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer // 8. Цикл по внутренним группам UnitsInTemplate foreach (var innerGroup in innerGroupedUnitsInTemplate) { - var groupingValueName = innerGroup.Key; + var groupingValueName = innerGroup.Key; + var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count); diff --git a/PARR.TemplateMatcher/Settings/TemplateSettings.cs b/PARR.TemplateMatcher/Settings/TemplateSettings.cs index 154b1c7a..c48d3b8b 100644 --- a/PARR.TemplateMatcher/Settings/TemplateSettings.cs +++ b/PARR.TemplateMatcher/Settings/TemplateSettings.cs @@ -2,6 +2,6 @@ { internal class TemplateSettings { - public string UnusedTemplateNameMask { get; set; } = "%П-1%_НЕАКТУАЛЬНЫЕ_%ЭК%_%ТИКТАК%"; + public string UnusedTemplateNameMask { get; set; } = "%П-1%_НЕАКТУАЛЬНЫЕ_%ИД%"; } }