From f3183e5f68b229d599e840a7b525d4377eeb5dc4 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Mon, 25 May 2026 16:35:37 +1000 Subject: [PATCH] =?UTF-8?q?feat(core):=20=D0=92=20ShortcodesService=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20%?= =?UTF-8?q?=D0=A2=D0=95=D0=93%=20=D0=B8=20%=D0=A2=D0=95=D0=93=5F=D0=A1?= =?UTF-8?q?=D0=92=D0=AF=D0=97=D0=98%=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D1=82=D0=B5=D0=B3=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=AD=D0=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализован %ТЕГ:ПРЕФИКС% для извлечения значения тега текущего юнита по правилу "префикс + двоеточие". - Реализован %ТЕГ_СВЯЗИ:ПРЕФИКС% для агрегации тегов связанных юнитов (UnitsInTemplate) с выбором самого популярного значения (сортировка по алфавиту при равенстве частоты). - Добавлены флаги ленивой загрузки UnitTags и UnitsInTemplateTags в ShortcodeDataRequirementsEnum. - Расширена запись TemplateForShortcode и метод PrepareTemplateDataAsync для пакетной подгрузки тегов из базы данных. - Зарегистрированы новые хендлеры в DI-контейнере и в методе GetAvailableShortcodesAsync. --- PARR.Core/DependencyInjection.cs | 2 + .../Enums/ShortcodeDataRequirementsEnum.cs | 4 +- .../RelatedUnitTagShortcodeHandler.cs | 98 +++++++++++++++++++ .../Handlers/TagShortcodeHandler.cs | 79 +++++++++++++++ .../Shortcodes/Models/TemplateForShortcode.cs | 4 +- .../Services/Shortcodes/ShortcodesService.cs | 68 ++++++++++++- 6 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 PARR.Core/Services/Shortcodes/Handlers/RelatedUnitTagShortcodeHandler.cs create mode 100644 PARR.Core/Services/Shortcodes/Handlers/TagShortcodeHandler.cs diff --git a/PARR.Core/DependencyInjection.cs b/PARR.Core/DependencyInjection.cs index 2d5a2249..7a6fd864 100644 --- a/PARR.Core/DependencyInjection.cs +++ b/PARR.Core/DependencyInjection.cs @@ -91,6 +91,8 @@ namespace PARR.Core services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); #endregion diff --git a/PARR.Core/Services/Shortcodes/Enums/ShortcodeDataRequirementsEnum.cs b/PARR.Core/Services/Shortcodes/Enums/ShortcodeDataRequirementsEnum.cs index d8690428..efe5d23a 100644 --- a/PARR.Core/Services/Shortcodes/Enums/ShortcodeDataRequirementsEnum.cs +++ b/PARR.Core/Services/Shortcodes/Enums/ShortcodeDataRequirementsEnum.cs @@ -9,6 +9,8 @@ JobGroupType = 4, JobTnk = 8, UnitName = 16, - UnitsInTemplate = 32 + UnitsInTemplate = 32, + UnitTags = 64, + UnitsInTemplateTags = 128 } } diff --git a/PARR.Core/Services/Shortcodes/Handlers/RelatedUnitTagShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/RelatedUnitTagShortcodeHandler.cs new file mode 100644 index 00000000..3eb43e5a --- /dev/null +++ b/PARR.Core/Services/Shortcodes/Handlers/RelatedUnitTagShortcodeHandler.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Logging; +using PARR.Core.Services.Shortcodes.Enums; +using PARR.Core.Services.Shortcodes.Models; +using System.Text.RegularExpressions; + +namespace PARR.Core.Services.Shortcodes.Handlers; + +internal class RelatedUnitTagShortcodeHandler : IShortcodeHandler +{ + private readonly ILogger logger; + + public int Order => 75; + + private static readonly Regex _pattern = new(@"%ТЕГ_СВЯЗИ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.UnitsInTemplateTags; + + public RelatedUnitTagShortcodeHandler(ILogger logger) + { + this.logger = logger; + } + + public Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + { + var matches = Pattern.Matches(input); + if (matches.Count == 0) return Task.FromResult(input); + + var uniqueTags = matches.Cast() + .Select(m => m.Groups[1].Value.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var tagName in uniqueTags) + { + resolved[$"%ТЕГ_СВЯЗИ:{tagName}%"] = GetMostPopularTagValue(data.RelatedUnitTags, tagName, caller); + } + + var result = input; + foreach (Match m in matches) + { + if (resolved.TryGetValue(m.Value, out var val)) + { + result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase); + } + } + + return Task.FromResult(result); + } + + private string GetMostPopularTagValue(Dictionary>? relatedTags, string tagName, string caller) + { + if (relatedTags == null || relatedTags.Count == 0) + { + return string.Empty; + } + + var prefix = tagName + ":"; + var matchedValues = new List(); + + foreach (var tagList in relatedTags.Values) + { + foreach (var tag in tagList) + { + string? matchedValue = null; + + if (tag.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + matchedValue = tag.Substring(prefix.Length); + } + else if (tag.Equals(tagName, StringComparison.OrdinalIgnoreCase)) + { + matchedValue = tag; + } + + if (!string.IsNullOrEmpty(matchedValue)) + { + matchedValues.Add(matchedValue); + } + } + } + + if (matchedValues.Count == 0) + { + return string.Empty; + } + + var mostPopular = matchedValues + .GroupBy(v => v, StringComparer.OrdinalIgnoreCase) + .Select(g => new { Value = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Value, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault()!; + + return mostPopular.Value; + } +} \ No newline at end of file diff --git a/PARR.Core/Services/Shortcodes/Handlers/TagShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/TagShortcodeHandler.cs new file mode 100644 index 00000000..1e42235c --- /dev/null +++ b/PARR.Core/Services/Shortcodes/Handlers/TagShortcodeHandler.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Logging; +using PARR.Core.Services.Shortcodes.Enums; +using PARR.Core.Services.Shortcodes.Models; +using System.Text.RegularExpressions; + +namespace PARR.Core.Services.Shortcodes.Handlers; + +internal class TagShortcodeHandler : IShortcodeHandler +{ + private readonly ILogger logger; + + public int Order => 70; + + private static readonly Regex _pattern = new(@"%ТЕГ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.UnitTags; + + public TagShortcodeHandler(ILogger logger) + { + this.logger = logger; + } + + public Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + { + var matches = Pattern.Matches(input); + if (matches.Count == 0) return Task.FromResult(input); + + var uniqueTags = matches.Cast() + .Select(m => m.Groups[1].Value.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var tagName in uniqueTags) + { + resolved[$"%ТЕГ:{tagName}%"] = GetTagValue(data.UnitTags, tagName, caller); + } + + var result = input; + foreach (Match m in matches) + { + if (resolved.TryGetValue(m.Value, out var val)) + { + result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase); + } + } + + return Task.FromResult(result); + } + + private string GetTagValue(List? unitTags, string tagName, string caller) + { + if (unitTags == null || unitTags.Count == 0) + { + return string.Empty; + } + + var prefix = tagName + ":"; + // Ищем тег, который начинается с префикса и двоеточия (например, "БД_в_ЗО_РГ:") + var matchedTag = unitTags.FirstOrDefault(t => t.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + + if (matchedTag != null) + { + // Возвращаем часть строки после двоеточия + return matchedTag.Substring(prefix.Length); + } + + // Резервный вариант: если тег совпадает полностью без двоеточия + var exactTag = unitTags.FirstOrDefault(t => t.Equals(tagName, StringComparison.OrdinalIgnoreCase)); + if (exactTag != null) + { + return exactTag; + } + + logger.LogDebug("[{Caller}] Тег с префиксом '{Prefix}' не найден в UnitTags.", caller, prefix); + return string.Empty; + } +} \ No newline at end of file diff --git a/PARR.Core/Services/Shortcodes/Models/TemplateForShortcode.cs b/PARR.Core/Services/Shortcodes/Models/TemplateForShortcode.cs index f4a0b49a..bdf8de61 100644 --- a/PARR.Core/Services/Shortcodes/Models/TemplateForShortcode.cs +++ b/PARR.Core/Services/Shortcodes/Models/TemplateForShortcode.cs @@ -7,6 +7,8 @@ Guid UnitId, string UnitName, JobForShortcode? Job, - List UnitsInTemplate + List UnitsInTemplate, + List UnitTags, + Dictionary> RelatedUnitTags ); } diff --git a/PARR.Core/Services/Shortcodes/ShortcodesService.cs b/PARR.Core/Services/Shortcodes/ShortcodesService.cs index 1507f660..ac5f2d86 100644 --- a/PARR.Core/Services/Shortcodes/ShortcodesService.cs +++ b/PARR.Core/Services/Shortcodes/ShortcodesService.cs @@ -81,7 +81,9 @@ internal class ShortcodesService : IShortcodesService UnitId: template.UnitId, UnitName: initialUnitName ?? string.Empty, Job: initialJob, - UnitsInTemplate: initialUnitsInTemplate + UnitsInTemplate: initialUnitsInTemplate, + UnitTags: new List(), + RelatedUnitTags: new Dictionary>() ); var result = str; @@ -206,12 +208,66 @@ internal class ShortcodesService : IShortcodesService .ToList() ?? new List(); } - // 4. Возвращаем обновленный рекорд + + // 5. Проверка UnitTags + var unitTags = currentData.UnitTags; + if ((unitTags == null || unitTags.Count == 0) && + requirements.HasFlag(ShortcodeDataRequirementsEnum.UnitTags)) + { + logger.LogWarning( + "[{Caller}] Шаблон {TemplateId} требует теги (UnitTags). Данные догружены автоматически.", + caller, template.Id); + + unitTags = await unitService.Get() + .AsNoTracking() + .Where(u => u.Id == template.UnitId) + .SelectMany(u => u.UnitValues + .Where(uv => uv.Field != null && + uv.Field.AihitName == "ПАРР тег" && + uv.Value != null && + uv.Value.Value != null) + .Select(uv => uv.Value!.Value!)) + .ToListAsync() + .ConfigureAwait(false); + } + + // 6. Проверка RelatedUnitTags + var relatedUnitTags = currentData.RelatedUnitTags; + var relatedUnitIds = template.UnitsInTemplate?.Select(uit => uit.UnitId).Distinct().ToList() ?? new List(); + + if ((relatedUnitTags == null || relatedUnitTags.Count == 0) && + requirements.HasFlag(ShortcodeDataRequirementsEnum.UnitsInTemplateTags) && + relatedUnitIds.Count > 0) + { + logger.LogWarning( + "[{Caller}] Шаблон {TemplateId} требует теги связанных ЭК (UnitsInTemplateTags). Данные догружены автоматически.", + caller, template.Id); + + var rawTags = await unitService.Get() + .AsNoTracking() + .Where(u => relatedUnitIds.Contains(u.Id)) + .SelectMany(u => u.UnitValues + .Where(uv => uv.Field != null && + uv.Field.AihitName == "ПАРР тег" && + uv.Value != null && + uv.Value.Value != null) + .Select(uv => new { UnitId = u.Id, Tag = uv.Value!.Value! })) + .ToListAsync() + .ConfigureAwait(false); + + relatedUnitTags = rawTags + .GroupBy(x => x.UnitId) + .ToDictionary(g => g.Key, g => g.Select(x => x.Tag).ToList()); + } + + // 7. Возвращаем обновленный рекорд return currentData with { UnitName = unitName ?? string.Empty, Job = jobData, - UnitsInTemplate = unitsInTemplate ?? currentData.UnitsInTemplate + UnitsInTemplate = unitsInTemplate ?? currentData.UnitsInTemplate, + UnitTags = unitTags ?? currentData.UnitTags, + RelatedUnitTags = relatedUnitTags ?? currentData.RelatedUnitTags }; } @@ -272,7 +328,8 @@ internal class ShortcodesService : IShortcodesService new ShortcodeInfoDto { Shortcode = "%ТНК%", Description = "Полное наименование ТНК", Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%ТНК-КРАТКО%", Description = "Краткое наименование ТНК", Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%ТИКТАК%", Description = "Текущее время в формате Unix timestamp", Type = ShortcodeTypeEnum.Standard }, - new ShortcodeInfoDto { Shortcode = "%ИД%", Description = "Уникальный идентификатор шаблона (GUID без дефисов)", Type = ShortcodeTypeEnum.Standard } + new ShortcodeInfoDto { Shortcode = "%ИД%", Description = "Уникальный идентификатор шаблона (GUID без дефисов)", Type = ShortcodeTypeEnum.Standard }, + new ShortcodeInfoDto { Shortcode = "%ТЕГ:ИМЯ_ТЕГА%", Description = "Значение тега ЭК после двоеточия (например, для 'БД_в_ЗО_РГ:ЕРП' вернет 'ЕРП')", Type = ShortcodeTypeEnum.Standard } }); result.AddRange(new[] @@ -280,7 +337,8 @@ internal class ShortcodesService : IShortcodesService new ShortcodeInfoDto { Shortcode = "%ИНДЕКС%", Description = "Порядковый индекс шаблона для групповых работ", Type = ShortcodeTypeEnum.GroupValue }, new ShortcodeInfoDto { Shortcode = "%МАКС:ИМЯ АТРИБУТА%", Description = "Наиболее частое значение поля в группе", Type = ShortcodeTypeEnum.GroupValue }, new ShortcodeInfoDto { Shortcode = "%ГР_ПОЛЕ-ПН%", Description = "Нумерованный список связанных ЭК со значениями", Type = ShortcodeTypeEnum.GroupValue }, - new ShortcodeInfoDto { Shortcode = "%СВЯЗЬ:ИМЯ АТРИБУТА%", Description = "Первое значение поля из связанных ЭК", Type = ShortcodeTypeEnum.GroupValue } + new ShortcodeInfoDto { Shortcode = "%СВЯЗЬ:ИМЯ АТРИБУТА%", Description = "Первое значение поля из связанных ЭК", Type = ShortcodeTypeEnum.GroupValue }, + new ShortcodeInfoDto { Shortcode = "%ТЕГ_СВЯЗИ:ИМЯ_ТЕГА%", Description = "Самое популярное значение тега среди связанных ЭК (по алфавиту при равенстве частот)", Type = ShortcodeTypeEnum.GroupValue} }); result.Add(new ShortcodeInfoDto