diff --git a/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs
index 471d29ad..c9ea9353 100644
--- a/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs
+++ b/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs
@@ -6,17 +6,19 @@ using System.Text.RegularExpressions;
namespace PARR.Core.Services.Shortcodes.Handlers;
///
-/// Шорткод %РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:ИМЯ_АТРИБУТА_ФИЛЬТРА:МАСКА_ЗНАЧЕНИЯ:ИМЯ_АТРИБУТА_РЕЗУЛЬТАТА%
-/// Возвращает значение атрибута первого ближайшего родителя, у которого значение атрибута-фильтра подходит под маску
+/// Шорткод %РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:АТРИБУТ1:МАСКА1&АТРИБУТ2:МАСКА2>>АТРИБУТ_РЕЗУЛЬТАТА%
+/// Возвращает значение атрибута первого ближайшего родителя, удовлетворяющего всем условиям фильтрации.
+/// При неверном синтаксисе выбрасывает InvalidOperationException.
///
internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler
{
- private readonly ILogger logger;
+ private readonly ILogger _logger;
public int Order => 56;
+ // Паттерн: захватываем все содержимое между ключевым словом и конечным %
private static readonly Regex _pattern = new(
- @"%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:([^:]+):([^:]+):([^%]+)%",
+ @"%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:(?[^%]+)%",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public Regex Pattern => _pattern;
@@ -25,7 +27,7 @@ internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler
public ParentUnitByFieldValueShortcodeHandler(ILogger logger)
{
- this.logger = logger;
+ _logger = logger;
}
public Task 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()
- .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()
+ .Select(m => m.Groups["payload"].Value.Trim())
+ .Distinct()
.ToList();
- var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var resolvedValues = new Dictionary();
- 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))
diff --git a/PARR.DAL/Repositories/Base/BaseRepository.cs b/PARR.DAL/Repositories/Base/BaseRepository.cs
index 229b1c23..d5737e36 100644
--- a/PARR.DAL/Repositories/Base/BaseRepository.cs
+++ b/PARR.DAL/Repositories/Base/BaseRepository.cs
@@ -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;