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 GeneralShortcodeRegex = new(@"%[^%\s]+%", RegexOptions.Compiled);
private static readonly Regex MaxShortcodeRegex = new(@"%МАКС:([а-яА-Яa-zA-Z0-9_]+)%", 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 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 static readonly HashSet<string> SupportedStandardShortcodes = new(StringComparer.OrdinalIgnoreCase)
{ {
"%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%" "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%", "%ИД%"
}; };
private readonly ILogger<ShortcodesService> logger; private readonly ILogger<ShortcodesService> logger;
@@ -194,6 +195,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
(sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) || (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.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) ((sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)) && template.Job.Tnk == null)
).ToList(); ).ToList();
@@ -216,6 +218,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
sc != "%ЭК%" && // %ЭК% не требует Job sc != "%ЭК%" && // %ЭК% не требует Job
(SupportedStandardShortcodes.Contains(sc) || (SupportedStandardShortcodes.Contains(sc) ||
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", 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.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase))
&& job?.Group == null) && job?.Group == null)
{ {
missing.Add(".Include(j => j.Group)"); missing.Add(".Include(j => j.Group)");
@@ -239,7 +243,8 @@ namespace PARR.DAL.DomainServices.Shortcodes
if (shortcodes.Any(sc => if (shortcodes.Any(sc =>
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase))
&& job?.Group?.GroupType == null) && job?.Group?.GroupType == null)
{ {
missing.Add(".ThenInclude(g => g.GroupType)"); missing.Add(".ThenInclude(g => g.GroupType)");
@@ -296,7 +301,8 @@ namespace PARR.DAL.DomainServices.Shortcodes
if (shortcodes.Any(sc => if (shortcodes.Any(sc =>
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ГРОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))) sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase)))
{ {
query = query query = query
.Include(j => j.Group) .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); var lettersShortcodes = LettersShortcodeRegex.Matches(result);
if (lettersShortcodes.Count > 0) if (lettersShortcodes.Count > 0)
{ {
result = await ReplaceLettersShortcodesAsync(data.UnitId, result, lettersShortcodes, caller).ConfigureAwait(false); result = await ReplaceLettersShortcodesAsync(data.UnitId, result, lettersShortcodes, caller).ConfigureAwait(false);
} }
// 5. %СВЯЗИ% и %СВЯЗИ-ПН% // 6. %СВЯЗИ% и %СВЯЗИ-ПН%
List<string>? relatedUnitNames = null; List<string>? relatedUnitNames = null;
if (shortcodesInMask.Any(m => string.Equals(m, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase))) 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); var fieldShortcodes = GetShortCodes(result);
if (fieldShortcodes.Count > 0) if (fieldShortcodes.Count > 0)
result = await ReplaceFieldValues(data.UnitId, result, fieldShortcodes).ConfigureAwait(false); result = await ReplaceFieldValues(data.UnitId, result, fieldShortcodes).ConfigureAwait(false);
@@ -528,8 +551,11 @@ namespace PARR.DAL.DomainServices.Shortcodes
Type = ShortcodeTypeEnum.Standard }, Type = ShortcodeTypeEnum.Standard },
new ShortcodeInfoDto { Shortcode = "%ТИКТАК%", new ShortcodeInfoDto { Shortcode = "%ТИКТАК%",
Description = "Текущее время в формате Unix timestamp (секунды с 1970-01-01 UTC)", Description = "Текущее время в формате Unix timestamp (секунды с 1970-01-01 UTC)",
Type = ShortcodeTypeEnum.Standard Type = ShortcodeTypeEnum.Standard},
} new ShortcodeInfoDto {
Shortcode = "%ИД%",
Description = "Уникальный идентификатор шаблона (GUID без дефисов)",
Type = ShortcodeTypeEnum.Standard}
}); });
// 3. Групповые шорткоды // 3. Групповые шорткоды
@@ -542,8 +568,12 @@ namespace PARR.DAL.DomainServices.Shortcodes
Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗАК%", Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗАК%",
Type = ShortcodeTypeEnum.GroupValue }, Type = ShortcodeTypeEnum.GroupValue },
new ShortcodeInfoDto { Shortcode = "%ГРОЛЕ-ПН%", new ShortcodeInfoDto { Shortcode = "%ГРОЛЕ-ПН%",
Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.", Description = "Нумерованный список связанных ЭК с основноым ЭК шаблона: 1. ЭК-123 (Значение1, Значение2). Использует имя поля группировки для вывода значений",
Type = ShortcodeTypeEnum.GroupValue }, Type = ShortcodeTypeEnum.GroupValue },
new ShortcodeInfoDto {
Shortcode = "%СВЯЗЬ:ИМЯ АТРИБУТА%",
Description = "Извлекает значения поля с именем ИМЯ АТРИБУТА из связанных ЭК, сортирует по алфавиту и берёт первое. Пример: %СВЯЗЬ:ЗОНА_ОТВЕТСТВЕННОСТИ%",
Type = ShortcodeTypeEnum.GroupValue}
}); });
// 4. Трансформирующие шорткоды // 4. Трансформирующие шорткоды
@@ -819,6 +849,108 @@ namespace PARR.DAL.DomainServices.Shortcodes
return effectiveUnitIds; 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) private async Task<string> ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes, string caller)
{ {
var shortcodeToMatches = lettersShortcodes var shortcodeToMatches = lettersShortcodes

View File

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

View File

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

View File

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

View File

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