feat(core): В ShortcodesService добавлена реализация %ТЕГ% и %ТЕГ_СВЯЗИ% для работы с тегами ЭК

- Реализован %ТЕГ:ПРЕФИКС% для извлечения значения тега текущего юнита по правилу "префикс + двоеточие".
- Реализован %ТЕГ_СВЯЗИ:ПРЕФИКС% для агрегации тегов связанных юнитов (UnitsInTemplate) с выбором самого популярного значения (сортировка по алфавиту при равенстве частоты).
- Добавлены флаги ленивой загрузки UnitTags и UnitsInTemplateTags в ShortcodeDataRequirementsEnum.
- Расширена запись TemplateForShortcode и метод PrepareTemplateDataAsync для пакетной подгрузки тегов из базы данных.
- Зарегистрированы новые хендлеры в DI-контейнере и в методе GetAvailableShortcodesAsync.
This commit is contained in:
Mikhail Kuznetsov
2026-05-25 16:35:37 +10:00
parent 146e8f6f9a
commit f3183e5f68
6 changed files with 248 additions and 7 deletions

View File

@@ -91,6 +91,8 @@ namespace PARR.Core
services.AddScoped<IShortcodeHandler, LettersShortcodeHandler>(); services.AddScoped<IShortcodeHandler, LettersShortcodeHandler>();
services.AddScoped<IShortcodeHandler, RelationshipsShortcodeHandler>(); services.AddScoped<IShortcodeHandler, RelationshipsShortcodeHandler>();
services.AddScoped<IShortcodeHandler, FieldShortcodeHandler>(); services.AddScoped<IShortcodeHandler, FieldShortcodeHandler>();
services.AddScoped<IShortcodeHandler, TagShortcodeHandler>();
services.AddScoped<IShortcodeHandler, RelatedUnitTagShortcodeHandler>();
services.AddScoped<IShortcodesService, ShortcodesService>(); services.AddScoped<IShortcodesService, ShortcodesService>();
#endregion #endregion

View File

@@ -9,6 +9,8 @@
JobGroupType = 4, JobGroupType = 4,
JobTnk = 8, JobTnk = 8,
UnitName = 16, UnitName = 16,
UnitsInTemplate = 32 UnitsInTemplate = 32,
UnitTags = 64,
UnitsInTemplateTags = 128
} }
} }

View File

@@ -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<RelatedUnitTagShortcodeHandler> 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<RelatedUnitTagShortcodeHandler> logger)
{
this.logger = logger;
}
public Task<string> 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<Match>()
.Select(m => m.Groups[1].Value.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var resolved = new Dictionary<string, string>(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<Guid, List<string>>? relatedTags, string tagName, string caller)
{
if (relatedTags == null || relatedTags.Count == 0)
{
return string.Empty;
}
var prefix = tagName + ":";
var matchedValues = new List<string>();
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;
}
}

View File

@@ -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<TagShortcodeHandler> 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<TagShortcodeHandler> logger)
{
this.logger = logger;
}
public Task<string> 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<Match>()
.Select(m => m.Groups[1].Value.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var resolved = new Dictionary<string, string>(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<string>? 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;
}
}

View File

@@ -7,6 +7,8 @@
Guid UnitId, Guid UnitId,
string UnitName, string UnitName,
JobForShortcode? Job, JobForShortcode? Job,
List<UnitInTemplateForShortcode> UnitsInTemplate List<UnitInTemplateForShortcode> UnitsInTemplate,
List<string> UnitTags,
Dictionary<Guid, List<string>> RelatedUnitTags
); );
} }

View File

@@ -81,7 +81,9 @@ internal class ShortcodesService : IShortcodesService
UnitId: template.UnitId, UnitId: template.UnitId,
UnitName: initialUnitName ?? string.Empty, UnitName: initialUnitName ?? string.Empty,
Job: initialJob, Job: initialJob,
UnitsInTemplate: initialUnitsInTemplate UnitsInTemplate: initialUnitsInTemplate,
UnitTags: new List<string>(),
RelatedUnitTags: new Dictionary<Guid, List<string>>()
); );
var result = str; var result = str;
@@ -206,12 +208,66 @@ internal class ShortcodesService : IShortcodesService
.ToList() ?? new List<UnitInTemplateForShortcode>(); .ToList() ?? new List<UnitInTemplateForShortcode>();
} }
// 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<Guid>();
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 return currentData with
{ {
UnitName = unitName ?? string.Empty, UnitName = unitName ?? string.Empty,
Job = jobData, 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 = "Краткое наименование ТНК", Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%ТНК-КРАТКО%", Description = "Краткое наименование ТНК", Type = ShortcodeTypeEnum.Standard },
new ShortcodeInfoDto { Shortcode = "%ТИКТАК%", Description = "Текущее время в формате Unix timestamp", 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[] 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 },
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 result.Add(new ShortcodeInfoDto