feat(dal): Добавлены 2 новых шорткода %ИД% и %СВЯЗЬ:ИМЯ АТРИБУТА%

This commit is contained in:
Mikhail Kuznetsov
2026-04-20 12:02:46 +10:00
parent 6d792f04b1
commit be1599aa25
5 changed files with 150 additions and 22 deletions

View File

@@ -40,10 +40,11 @@ namespace PARR.DAL.DomainServices.Shortcodes
private static readonly Regex GeneralShortcodeRegex = new(@"%[^%\s]+%", RegexOptions.Compiled);
private static readonly Regex MaxShortcodeRegex = new(@"%МАКС:([а-яА-Яa-zA-Z0-9_]+)%", RegexOptions.Compiled);
private static readonly Regex LettersShortcodeRegex = new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled);
private static readonly Regex RelShortcodeRegex = new(@"%СВЯЗЬ:([а-яА-Яa-zA-Z0-9_]+)%", RegexOptions.Compiled);
private static readonly HashSet<string> SupportedStandardShortcodes = new(StringComparer.OrdinalIgnoreCase)
{
"%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%"
"%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%", "%ИД%"
};
private readonly ILogger<ShortcodesService> logger;
@@ -194,6 +195,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
(sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) ||
(sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) ||
(sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) ||
(sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) ||
((sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)) && template.Job.Tnk == null)
).ToList();
@@ -216,6 +218,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
sc != "%ЭК%" && // %ЭК% не требует Job
(SupportedStandardShortcodes.Contains(sc) ||
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase) ||
@@ -230,7 +233,8 @@ namespace PARR.DAL.DomainServices.Shortcodes
sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase))
&& job?.Group == null)
{
missing.Add(".Include(j => j.Group)");
@@ -239,7 +243,8 @@ namespace PARR.DAL.DomainServices.Shortcodes
if (shortcodes.Any(sc =>
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase))
&& job?.Group?.GroupType == null)
{
missing.Add(".ThenInclude(g => g.GroupType)");
@@ -296,7 +301,8 @@ namespace PARR.DAL.DomainServices.Shortcodes
if (shortcodes.Any(sc =>
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)))
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase)))
{
query = query
.Include(j => j.Group)
@@ -444,14 +450,24 @@ namespace PARR.DAL.DomainServices.Shortcodes
}
}
// 4. Transform: %БУКВЫ:...
// 4. СВЯЗЬ:ИМЯ_АТРИБУТА (только для групповых работ)
if (data.Job?.Group?.GroupType?.Code == JobGroupTypesEnum.Group)
{
var svyazShortcodes = RelShortcodeRegex.Matches(result);
if (svyazShortcodes.Count > 0)
{
result = await ReplaceRelShortcodesAsync(data.Id, data.UnitId, data.UnitsInTemplate, result, svyazShortcodes, caller).ConfigureAwait(false);
}
}
// 5. Transform: %БУКВЫ:...
var lettersShortcodes = LettersShortcodeRegex.Matches(result);
if (lettersShortcodes.Count > 0)
{
result = await ReplaceLettersShortcodesAsync(data.UnitId, result, lettersShortcodes, caller).ConfigureAwait(false);
}
// 5. %СВЯЗИ% и %СВЯЗИ-ПН%
// 6. %СВЯЗИ% и %СВЯЗИ-ПН%
List<string>? relatedUnitNames = null;
if (shortcodesInMask.Any(m => string.Equals(m, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase)))
{
@@ -471,7 +487,14 @@ namespace PARR.DAL.DomainServices.Shortcodes
}
}
// 6. Поля
// 7. ИД шаблона
if (shortcodesInMask.Any(m => string.Equals(m, "%ИД%", StringComparison.OrdinalIgnoreCase)))
{
var templateIdWithoutDashes = data.Id.ToString("N");
result = ReplaceSingleShortcode(result, "%ИД%", templateIdWithoutDashes);
}
// 8. Поля
var fieldShortcodes = GetShortCodes(result);
if (fieldShortcodes.Count > 0)
result = await ReplaceFieldValues(data.UnitId, result, fieldShortcodes).ConfigureAwait(false);
@@ -528,8 +551,11 @@ namespace PARR.DAL.DomainServices.Shortcodes
Type = ShortcodeTypeEnum.Standard },
new ShortcodeInfoDto { Shortcode = "%ТИКТАК%",
Description = "Текущее время в формате Unix timestamp (секунды с 1970-01-01 UTC)",
Type = ShortcodeTypeEnum.Standard
}
Type = ShortcodeTypeEnum.Standard},
new ShortcodeInfoDto {
Shortcode = "%ИД%",
Description = "Уникальный идентификатор шаблона (GUID без дефисов)",
Type = ShortcodeTypeEnum.Standard}
});
// 3. Групповые шорткоды
@@ -542,8 +568,12 @@ namespace PARR.DAL.DomainServices.Shortcodes
Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗАК%",
Type = ShortcodeTypeEnum.GroupValue },
new ShortcodeInfoDto { Shortcode = "%ГРОЛЕ-ПН%",
Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.",
Description = "Нумерованный список связанных ЭК с основноым ЭК шаблона: 1. ЭК-123 (Значение1, Значение2). Использует имя поля группировки для вывода значений",
Type = ShortcodeTypeEnum.GroupValue },
new ShortcodeInfoDto {
Shortcode = "%СВЯЗЬ:ИМЯ АТРИБУТА%",
Description = "Извлекает значения поля с именем ИМЯ АТРИБУТА из связанных ЭК, сортирует по алфавиту и берёт первое. Пример: %СВЯЗЬ:ЗОНА_ОТВЕТСТВЕННОСТИ%",
Type = ShortcodeTypeEnum.GroupValue}
});
// 4. Трансформирующие шорткоды
@@ -819,6 +849,108 @@ namespace PARR.DAL.DomainServices.Shortcodes
return effectiveUnitIds;
}
private async Task<string> ReplaceRelShortcodesAsync(
Guid templateId,
Guid templateUnitId,
List<UnitInTemplateForShortcodes> unitsInTemplate,
string input,
MatchCollection relShortcodes,
string caller)
{
var shortcodeToMatches = relShortcodes
.Cast<Match>()
.GroupBy(m => m.Value, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
foreach (var kvp in shortcodeToMatches)
{
var fullShortcode = kvp.Key;
var matches = kvp.Value;
var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim();
logger.LogDebug("[{Caller}] Обработка {Shortcode} для шаблона с UnitId {UnitId}, fieldName {FieldName}", caller, templateUnitId, fieldName);
List<Guid> unitIds;
bool wasLoadedFromDb = false;
if (unitsInTemplate.Count > 0)
{
unitIds = unitsInTemplate.Select(uit => uit.UnitId).ToList();
}
else
{
logger.LogWarning(
"[{Caller}] Шаблон с UnitId {UnitId} не содержит UnitsInTemplate. Данные будут догружены из БД. " +
"Рекомендуется обновить запрос с Include(t => t.UnitsInTemplate).",
caller, templateUnitId);
var fullTemplate = await templateService.Get()
.AsNoTracking()
.Include(t => t.UnitsInTemplate)
.FirstOrDefaultAsync(t => t.Id == templateId)
.ConfigureAwait(false);
unitIds = fullTemplate?.UnitsInTemplate?.Select(uit => uit.UnitId).ToList() ?? new List<Guid>();
wasLoadedFromDb = true;
}
if (unitIds.Count == 0)
{
logger.LogDebug("[{Caller}] Нет UnitsInTemplate для шаблона {TemplateId}. Шорткод {Shortcode} пропущен.", caller, templateUnitId, fullShortcode);
foreach (var match in matches)
input = input.Replace(match.Value, "");
continue;
}
var allFieldValues = new List<(string FieldName, string? Value)>();
foreach (var unitId in unitIds)
{
var values = await unitInValueService.GetFieldValuesAsync(unitId, new List<string> { fieldName }).ConfigureAwait(false);
allFieldValues.AddRange(values.Select(v => (v.FieldName, v.Value)));
}
if (allFieldValues == null || !allFieldValues.Any())
{
logger.LogDebug("[{Caller}] Для шорткода {Shortcode} не найдено значений поля {FieldName} среди UnitsInTemplate.", caller, fullShortcode, fieldName);
foreach (var match in matches)
input = input.Replace(match.Value, "");
continue;
}
var distinctSortedValues = allFieldValues
.Where(fv => !string.IsNullOrEmpty(fv.Value))
.Select(fv => fv.Value!)
.Distinct()
.OrderBy(v => v, StringComparer.OrdinalIgnoreCase)
.ToList();
if (distinctSortedValues.Count == 0)
{
logger.LogDebug("[{Caller}] Для шорткода {Shortcode} все значения поля {FieldName} пустые.", caller, fullShortcode, fieldName);
foreach (var match in matches)
input = input.Replace(match.Value, "");
continue;
}
if (distinctSortedValues.Count > 1)
{
logger.LogWarning(
"[{Caller}] Шорткод {Shortcode} нашёл {Count} различных значений поля {FieldName}: [{Values}]. Используется первое: '{FirstValue}'",
caller, fullShortcode, distinctSortedValues.Count, fieldName,
string.Join(", ", distinctSortedValues),
distinctSortedValues[0]);
}
var firstValue = distinctSortedValues[0];
foreach (var match in matches)
input = input.Replace(match.Value, firstValue);
}
return input;
}
private async Task<string> ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes, string caller)
{
var shortcodeToMatches = lettersShortcodes

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.20">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\PARR.Core\PARR.Core.csproj" />
<ProjectReference Include="..\PARR.DAL\PARR.DAL.csproj" />
<ProjectReference Include="..\PARR.Domain\PARR.Domain.csproj" />
<ProjectReference Include="..\PARR.Infrastructure\PARR.Infrastructure.csproj" />
</ItemGroup>

View File

@@ -2,7 +2,6 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
@@ -38,9 +37,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
private readonly IShortcodesService shortcodesService;
//private readonly INextRunService nextRunService;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
@@ -57,9 +54,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService,
IShortcodesService shortcodesService
//INextRunService nextRunService
IMatchingStatusService matchingStatusService
)
{
this.logger = logger;
@@ -77,8 +72,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
this.shortcodesService = shortcodesService;
//this.nextRunService = nextRunService;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
@@ -248,7 +241,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
// dto.Id - это ParentUnitId, связанные - ChildUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate
// relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона
relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); // <-- Исправлено
relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList();
}
else
{
@@ -298,7 +291,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.ToDictionary(uv => uv.UnitId, uv => uv.Value.Value);
// 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов)
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvpOuter in reverseMapping)
{
@@ -318,7 +311,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
// 8. Цикл по внутренним группам UnitsInTemplate
foreach (var innerGroup in innerGroupedUnitsInTemplate)
{
var groupingValueName = innerGroup.Key;
var groupingValueName = innerGroup.Key;
var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля
logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count);

View File

@@ -2,6 +2,6 @@
{
internal class TemplateSettings
{
public string UnusedTemplateNameMask { get; set; } = "%П-1%_НЕАКТУАЛЬНЫЕ_%ЭК%_%ТИКТАК%";
public string UnusedTemplateNameMask { get; set; } = "%П-1%_НЕАКТУАЛЬНЫЕ_%ИД%";
}
}