feat(shortcodesService): шорт-код поиска родительского ЭК по атрибутам теперь работает по множеству условий
This commit is contained in:
@@ -6,17 +6,19 @@ using System.Text.RegularExpressions;
|
||||
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Шорткод %РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:ИМЯ_АТРИБУТА_ФИЛЬТРА:МАСКА_ЗНАЧЕНИЯ:ИМЯ_АТРИБУТА_РЕЗУЛЬТАТА%
|
||||
/// Возвращает значение атрибута первого ближайшего родителя, у которого значение атрибута-фильтра подходит под маску
|
||||
/// Шорткод %РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:АТРИБУТ1:МАСКА1&АТРИБУТ2:МАСКА2>>АТРИБУТ_РЕЗУЛЬТАТА%
|
||||
/// Возвращает значение атрибута первого ближайшего родителя, удовлетворяющего всем условиям фильтрации.
|
||||
/// При неверном синтаксисе выбрасывает InvalidOperationException.
|
||||
/// </summary>
|
||||
internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler
|
||||
{
|
||||
private readonly ILogger<ParentUnitByFieldValueShortcodeHandler> logger;
|
||||
private readonly ILogger<ParentUnitByFieldValueShortcodeHandler> _logger;
|
||||
|
||||
public int Order => 56;
|
||||
|
||||
// Паттерн: захватываем все содержимое между ключевым словом и конечным %
|
||||
private static readonly Regex _pattern = new(
|
||||
@"%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:([^:]+):([^:]+):([^%]+)%",
|
||||
@"%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:(?<payload>[^%]+)%",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public Regex Pattern => _pattern;
|
||||
@@ -25,7 +27,7 @@ internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler
|
||||
|
||||
public ParentUnitByFieldValueShortcodeHandler(ILogger<ParentUnitByFieldValueShortcodeHandler> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||
@@ -33,67 +35,130 @@ internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler
|
||||
var matches = Pattern.Matches(input);
|
||||
if (matches.Count == 0) return Task.FromResult(input);
|
||||
|
||||
var uniqueRequests = matches.Cast<Match>()
|
||||
.Select(m => new
|
||||
{
|
||||
FullMatch = m.Value,
|
||||
FilterField = m.Groups[1].Value.Trim(),
|
||||
Mask = m.Groups[2].Value.Trim(),
|
||||
ResultField = m.Groups[3].Value.Trim()
|
||||
})
|
||||
.GroupBy(x => new { x.FilterField, x.Mask, x.ResultField })
|
||||
.Select(g => g.First())
|
||||
var uniquePayloads = matches.Cast<Match>()
|
||||
.Select(m => m.Groups["payload"].Value.Trim())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var resolvedValues = new Dictionary<string, string>();
|
||||
|
||||
foreach (var request in uniqueRequests)
|
||||
foreach (var payload in uniquePayloads)
|
||||
{
|
||||
resolved[request.FullMatch] = GetFieldValue(template, request.FilterField, request.Mask, request.ResultField, caller);
|
||||
// Здесь может быть выброшено исключение при неверном синтаксисе
|
||||
var result = ProcessPayload(template, payload, caller);
|
||||
resolvedValues[payload] = result;
|
||||
}
|
||||
|
||||
var result = input;
|
||||
var output = input;
|
||||
foreach (Match m in matches)
|
||||
{
|
||||
if (resolved.TryGetValue(m.Value, out var val))
|
||||
result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase);
|
||||
var payload = m.Groups["payload"].Value.Trim();
|
||||
if (resolvedValues.TryGetValue(payload, out var val))
|
||||
{
|
||||
output = output.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
return Task.FromResult(output);
|
||||
}
|
||||
|
||||
private string GetFieldValue(TemplateForShortcode template, string filterField, string mask, string resultField, string caller)
|
||||
private string ProcessPayload(TemplateForShortcode template, string payload, string caller)
|
||||
{
|
||||
// 1. Проверка наличия разделителя '>>'
|
||||
var separatorIndex = payload.IndexOf(">>", StringComparison.Ordinal);
|
||||
|
||||
if (separatorIndex <= 0 || separatorIndex >= payload.Length - 2)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Неверный синтаксис шорткода '%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ...'. " +
|
||||
$"Отсутствует или расположен неправильно разделитель '>>' в выражении: '{payload}'. " +
|
||||
$"Ожидаемый формат: АТРИБУТ:МАСКА>>АТРИБУТ_РЕЗУЛЬТАТА");
|
||||
}
|
||||
|
||||
string conditionsStr = payload.Substring(0, separatorIndex).Trim();
|
||||
string resultFieldName = payload.Substring(separatorIndex + 2).Trim();
|
||||
|
||||
// 2. Валидация имени результирующего поля
|
||||
if (string.IsNullOrEmpty(resultFieldName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Неверный синтаксис шорткода '%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ...'. " +
|
||||
$"Имя результирующего атрибута после '>>' не может быть пустым. Выражение: '{payload}'");
|
||||
}
|
||||
|
||||
// 3. Парсинг условий
|
||||
var conditions = ParseConditions(conditionsStr);
|
||||
|
||||
if (conditions.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Неверный синтаксис шорткода '%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ...'. " +
|
||||
$"Не найдено ни одного валидного условия фильтрации перед '>>'. Выражение: '{payload}'");
|
||||
}
|
||||
|
||||
// 4. Поиск подходящего родителя (Бизнес-логика: если не нашли - это НЕ ошибка синтаксиса, а отсутствие данных)
|
||||
if (template.ParentUnits == null || template.ParentUnits.Count == 0)
|
||||
{
|
||||
logger.LogDebug("[{Caller}] Родителей нет. Возвращаю 'Нет данных'.", caller);
|
||||
_logger.LogDebug("[{Caller}] Список родителей пуст для шаблона.", caller);
|
||||
return "Нет данных";
|
||||
}
|
||||
|
||||
var matchingParent = template.ParentUnits.FirstOrDefault(p =>
|
||||
{
|
||||
if (!p.FieldValues.TryGetValue(filterField, out var currentValue))
|
||||
return false;
|
||||
foreach (var cond in conditions)
|
||||
{
|
||||
if (!p.FieldValues.TryGetValue(cond.AttributeName, out var currentValue))
|
||||
return false;
|
||||
|
||||
return MatchesMask(currentValue, mask);
|
||||
if (!MatchesMask(currentValue, cond.Mask))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matchingParent == null)
|
||||
{
|
||||
logger.LogDebug("[{Caller}] Не найден родитель с '{Filter}' по маске '{Mask}'. Возвращаю 'Нет данных'.",
|
||||
caller, filterField, mask);
|
||||
_logger.LogDebug("[{Caller}] Не найден родитель, подходящий под условия: {Payload}", caller, payload);
|
||||
return "Нет данных";
|
||||
}
|
||||
|
||||
if (matchingParent.FieldValues.TryGetValue(resultField, out var value))
|
||||
// 5. Извлечение значения
|
||||
if (matchingParent.FieldValues.TryGetValue(resultFieldName, out var finalValue))
|
||||
{
|
||||
return value;
|
||||
return finalValue;
|
||||
}
|
||||
|
||||
logger.LogDebug("[{Caller}] Родитель найден, но атрибут '{Field}' отсутствует. Возвращаю 'Нет данных'.", caller, resultField);
|
||||
_logger.LogDebug("[{Caller}] Родитель найден, но атрибут '{Field}' отсутствует у него.", caller, resultFieldName);
|
||||
return "Нет данных";
|
||||
}
|
||||
|
||||
private List<(string AttributeName, string Mask)> ParseConditions(string raw)
|
||||
{
|
||||
var list = new List<(string, string)>();
|
||||
if (string.IsNullOrEmpty(raw)) return list;
|
||||
|
||||
var pairs = raw.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var parts = pair.Split(':', 2);
|
||||
|
||||
// Строгая проверка формата пары "Ключ:Значение"
|
||||
if (parts.Length != 2 ||
|
||||
string.IsNullOrWhiteSpace(parts[0]) ||
|
||||
string.IsNullOrWhiteSpace(parts[1]))
|
||||
{
|
||||
// Если условие битое, считаем весь запрос синтаксически неверным
|
||||
throw new InvalidOperationException(
|
||||
$"Неверный формат условия фильтрации: '{pair}'. Ожидается 'АТРИБУТ:МАСКА'.");
|
||||
}
|
||||
|
||||
list.Add((parts[0].Trim(), parts[1].Trim()));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static bool MatchesMask(string value, string mask)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(mask))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Base;
|
||||
using PARR.DAL.Context;
|
||||
|
||||
Reference in New Issue
Block a user