feat(shortcodesService): шорт-код поиска родительского ЭК по атрибутам теперь работает по множеству условий

This commit is contained in:
Mikhail Kuznetsov
2026-10-28 10:56:07 +10:00
parent 236ddee5ef
commit 8c7d041a8b
2 changed files with 97 additions and 33 deletions

View File

@@ -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))

View File

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