feat(core): В ShortcodesService добавлена реализация %ТЕГ% и %ТЕГ_СВЯЗИ% для работы с тегами ЭК
- Реализован %ТЕГ:ПРЕФИКС% для извлечения значения тега текущего юнита по правилу "префикс + двоеточие". - Реализован %ТЕГ_СВЯЗИ:ПРЕФИКС% для агрегации тегов связанных юнитов (UnitsInTemplate) с выбором самого популярного значения (сортировка по алфавиту при равенстве частоты). - Добавлены флаги ленивой загрузки UnitTags и UnitsInTemplateTags в ShortcodeDataRequirementsEnum. - Расширена запись TemplateForShortcode и метод PrepareTemplateDataAsync для пакетной подгрузки тегов из базы данных. - Зарегистрированы новые хендлеры в DI-контейнере и в методе GetAvailableShortcodesAsync.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
JobGroupType = 4,
|
JobGroupType = 4,
|
||||||
JobTnk = 8,
|
JobTnk = 8,
|
||||||
UnitName = 16,
|
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,
|
Guid UnitId,
|
||||||
string UnitName,
|
string UnitName,
|
||||||
JobForShortcode? Job,
|
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,
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user