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

@@ -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;
}
}