feat(core): В ShortcodesService добавлена реализация %ТЕГ% и %ТЕГ_СВЯЗИ% для работы с тегами ЭК
- Реализован %ТЕГ:ПРЕФИКС% для извлечения значения тега текущего юнита по правилу "префикс + двоеточие". - Реализован %ТЕГ_СВЯЗИ:ПРЕФИКС% для агрегации тегов связанных юнитов (UnitsInTemplate) с выбором самого популярного значения (сортировка по алфавиту при равенстве частоты). - Добавлены флаги ленивой загрузки UnitTags и UnitsInTemplateTags в ShortcodeDataRequirementsEnum. - Расширена запись TemplateForShortcode и метод PrepareTemplateDataAsync для пакетной подгрузки тегов из базы данных. - Зарегистрированы новые хендлеры в DI-контейнере и в методе GetAvailableShortcodesAsync.
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
JobGroupType = 4,
|
||||
JobTnk = 8,
|
||||
UnitName = 16,
|
||||
UnitsInTemplate = 32
|
||||
UnitsInTemplate = 32,
|
||||
UnitTags = 64,
|
||||
UnitsInTemplateTags = 128
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
Guid UnitId,
|
||||
string UnitName,
|
||||
JobForShortcode? Job,
|
||||
List<UnitInTemplateForShortcode> UnitsInTemplate
|
||||
List<UnitInTemplateForShortcode> UnitsInTemplate,
|
||||
List<string> UnitTags,
|
||||
Dictionary<Guid, List<string>> RelatedUnitTags
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,9 @@ internal class ShortcodesService : IShortcodesService
|
||||
UnitId: template.UnitId,
|
||||
UnitName: initialUnitName ?? string.Empty,
|
||||
Job: initialJob,
|
||||
UnitsInTemplate: initialUnitsInTemplate
|
||||
UnitsInTemplate: initialUnitsInTemplate,
|
||||
UnitTags: new List<string>(),
|
||||
RelatedUnitTags: new Dictionary<Guid, List<string>>()
|
||||
);
|
||||
|
||||
var result = str;
|
||||
@@ -206,12 +208,66 @@ internal class ShortcodesService : IShortcodesService
|
||||
.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
|
||||
{
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user