diff --git a/PARR.API/Controllers/V1/RobotTaskController.cs b/PARR.API/Controllers/V1/RobotTaskController.cs index 8f9af29f..4293d917 100644 --- a/PARR.API/Controllers/V1/RobotTaskController.cs +++ b/PARR.API/Controllers/V1/RobotTaskController.cs @@ -73,7 +73,7 @@ namespace PARR.API.Controllers.V1 await robotConfigurationService.FindUnfulfilledTaskAndSetRobotErrorStatusAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime); - var query = robotConfigurationService.Get() + var query = robotConfigurationService.Get().AsSingleQuery() .Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode); switch (robotCode) @@ -108,6 +108,7 @@ namespace PARR.API.Controllers.V1 case RobotsEnum.ScheduleOrder: //расписание query = query + .AsSingleQuery() .Include(t => t.Template) .ThenInclude(t => t!.Unit) .ThenInclude(t => t!.UnitValues) diff --git a/PARR.Core/DependencyInjection.cs b/PARR.Core/DependencyInjection.cs index 49e9f42c..078c0ea2 100644 --- a/PARR.Core/DependencyInjection.cs +++ b/PARR.Core/DependencyInjection.cs @@ -79,7 +79,7 @@ namespace PARR.Core services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/PARR.Core/Services/Shortcodes/Enums/ShortcodeDataRequirementsEnum.cs b/PARR.Core/Services/Shortcodes/Enums/ShortcodeDataRequirementsEnum.cs new file mode 100644 index 00000000..d8690428 --- /dev/null +++ b/PARR.Core/Services/Shortcodes/Enums/ShortcodeDataRequirementsEnum.cs @@ -0,0 +1,14 @@ +namespace PARR.Core.Services.Shortcodes.Enums +{ + [Flags] + public enum ShortcodeDataRequirementsEnum + { + None = 0, + Job = 1, + JobGroup = 2, + JobGroupType = 4, + JobTnk = 8, + UnitName = 16, + UnitsInTemplate = 32 + } +} diff --git a/PARR.Core/Services/Shortcodes/Handlers/ConstantsShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/ConstantsShortcodeHandler.cs index 2499025c..cb3611d9 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/ConstantsShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/ConstantsShortcodeHandler.cs @@ -1,4 +1,5 @@ -using PARR.Core.Services.Shortcodes.Models; +using PARR.Core.Services.Shortcodes.Enums; +using PARR.Core.Services.Shortcodes.Models; using PARR.Domain.Settings; using System.Text.RegularExpressions; @@ -8,11 +9,15 @@ internal class ConstantsShortcodeHandler : IShortcodeHandler { private readonly SettingsFromDb settings; public int Order => 5; - public Regex Pattern => new(@"%[А-ЯA-Z0-9_\-]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex _pattern = new(@"%[А-ЯA-Z0-9_\-]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None; public ConstantsShortcodeHandler(SettingsFromDb settings) => this.settings = settings; - public Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { var result = input; foreach (var constant in settings.TemplateNameConstantPartsList) diff --git a/PARR.Core/Services/Shortcodes/Handlers/FieldShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/FieldShortcodeHandler.cs index 830627ce..72c2d4e2 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/FieldShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/FieldShortcodeHandler.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.Shortcodes.Enums; using PARR.Core.Services.Shortcodes.Models; using System.Text.RegularExpressions; @@ -10,7 +11,11 @@ internal class FieldShortcodeHandler : IShortcodeHandler private readonly IUnitInValueRepository unitInValueRepository; private readonly ILogger logger; public int Order => 90; // Всегда последним - public Regex Pattern => new(@"%[^%]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex _pattern = new(@"%[^%]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None; public FieldShortcodeHandler(IUnitInValueRepository unitInValueRepository, ILogger logger) { @@ -18,7 +23,7 @@ internal class FieldShortcodeHandler : IShortcodeHandler this.logger = logger; } - public async Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public async Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { var matches = Pattern.Matches(input); if (matches.Count == 0) return input; @@ -30,7 +35,7 @@ internal class FieldShortcodeHandler : IShortcodeHandler if (fieldNames.Count == 0) return input; - var fieldValues = await unitInValueRepository.GetFieldValuesAsync(data.UnitId, fieldNames, ct).ConfigureAwait(false); + var fieldValues = await unitInValueRepository.GetFieldValuesAsync(template.UnitId, fieldNames, ct).ConfigureAwait(false); var valuesMap = fieldValues .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase) @@ -49,7 +54,7 @@ internal class FieldShortcodeHandler : IShortcodeHandler } else { - logger.LogDebug("[{Caller}] Поле '{FieldName}' не найдено для UnitId={UnitId}. Шорткод пропущен.", caller, fieldName, data.UnitId); + logger.LogDebug("[{Caller}] Поле '{FieldName}' не найдено для UnitId={UnitId}. Шорткод пропущен.", caller, fieldName, template.UnitId); } } diff --git a/PARR.Core/Services/Shortcodes/Handlers/GroupedFieldShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/GroupedFieldShortcodeHandler.cs index 4830fb17..7fecb589 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/GroupedFieldShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/GroupedFieldShortcodeHandler.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.Shortcodes.Enums; using PARR.Core.Services.Shortcodes.Models; using System.Text.RegularExpressions; @@ -15,7 +16,14 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler private readonly ILogger logger; public int Order => 30; - public Regex Pattern => new(@"%ГР_ПОЛЕ-ПН%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex _pattern = new(@"%ГР_ПОЛЕ-ПН%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => + ShortcodeDataRequirementsEnum.Job | + ShortcodeDataRequirementsEnum.JobGroup | + ShortcodeDataRequirementsEnum.UnitsInTemplate; public GroupedFieldShortcodeHandler( IUnitRepository unitRepo, @@ -29,32 +37,22 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler this.logger = logger; } - public async Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public async Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { if (!input.Contains("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase)) return input; - if (data.Job?.Group == null) + if (template.Job?.Group == null) { logger.LogWarning("[{Caller}] Job не содержит Group, необходимый для %ГР_ПОЛЕ-ПН%. Шорткод пропущен.", caller); return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase); } - var unitsList = data.UnitsInTemplate; + var unitsList = template.UnitsInTemplate; if (unitsList.Count == 0) { - logger.LogWarning("[{Caller}] Template {TemplateId} не содержит UnitsInTemplate. Данные догружаются из БД.", caller, data.Id); - var fullTemplate = await templateRepo.Get() - .AsNoTracking() - .Include(t => t.UnitsInTemplate) - .FirstOrDefaultAsync(t => t.Id == data.Id, ct); - - unitsList = fullTemplate?.UnitsInTemplate - ?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId)) - .ToList() ?? new List(); - } - - if (unitsList.Count == 0) + // Данные должны быть загружены оркестратором. Если пусто — значит в БД действительно нет связей. return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase); + } var unitIds = unitsList.Select(u => u.UnitId).Distinct().ToList(); var fieldValueIds = unitsList.Select(u => u.UnitFieldValueId).Distinct().ToList(); diff --git a/PARR.Core/Services/Shortcodes/Handlers/IShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/IShortcodeHandler.cs index 47fc7d2e..3b27a8c8 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/IShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/IShortcodeHandler.cs @@ -1,4 +1,5 @@ -using PARR.Core.Services.Shortcodes.Models; +using PARR.Core.Services.Shortcodes.Enums; +using PARR.Core.Services.Shortcodes.Models; using System.Text.RegularExpressions; namespace PARR.Core.Services.Shortcodes.Handlers @@ -16,6 +17,11 @@ namespace PARR.Core.Services.Shortcodes.Handlers /// Regex Pattern { get; } + /// + /// Декларация данных, необходимых хендлеру для работы. + /// + ShortcodeDataRequirementsEnum Requirements { get; } + /// /// Заменяет все найденные шорткоды в строке на вычисленные значения. /// diff --git a/PARR.Core/Services/Shortcodes/Handlers/IndexShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/IndexShortcodeHandler.cs index a49581c2..695b0ac7 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/IndexShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/IndexShortcodeHandler.cs @@ -1,4 +1,5 @@ -using PARR.Core.Services.Shortcodes.Models; +using PARR.Core.Services.Shortcodes.Enums; +using PARR.Core.Services.Shortcodes.Models; using System.Text.RegularExpressions; namespace PARR.Core.Services.Shortcodes.Handlers; @@ -6,13 +7,18 @@ namespace PARR.Core.Services.Shortcodes.Handlers; internal class IndexShortcodeHandler : IShortcodeHandler { public int Order => 15; - public Regex Pattern => new(@"%ИНДЕКС%", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + private static readonly Regex _pattern = new(@"%ИНДЕКС%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None; + + public Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { - if (data.Index.HasValue && input.Contains("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) + if (template.Index.HasValue && input.Contains("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult(input.Replace("%ИНДЕКС%", data.Index.Value.ToString(), StringComparison.OrdinalIgnoreCase)); + return Task.FromResult(input.Replace("%ИНДЕКС%", template.Index.Value.ToString(), StringComparison.OrdinalIgnoreCase)); } return Task.FromResult(input); } diff --git a/PARR.Core/Services/Shortcodes/Handlers/LettersShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/LettersShortcodeHandler.cs index c659a464..30f1ac64 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/LettersShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/LettersShortcodeHandler.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.Shortcodes.Enums; using PARR.Core.Services.Shortcodes.Models; using System.Text.RegularExpressions; @@ -11,7 +12,11 @@ internal class LettersShortcodeHandler : IShortcodeHandler private readonly ILogger logger; public int Order => 60; - public Regex Pattern => new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex _pattern = new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None; public LettersShortcodeHandler(IUnitInValueRepository unitInValueRepo, ILogger logger) { @@ -19,7 +24,7 @@ internal class LettersShortcodeHandler : IShortcodeHandler this.logger = logger; } - public async Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public async Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { var matches = Pattern.Matches(input); if (matches.Count == 0) return input; @@ -32,7 +37,7 @@ internal class LettersShortcodeHandler : IShortcodeHandler var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var field in uniqueFields) { - var values = await unitInValueRepo.GetFieldValuesAsync(data.UnitId, new List { field }, ct); + var values = await unitInValueRepo.GetFieldValuesAsync(template.UnitId, new List { field }, ct); var rawValue = values.FirstOrDefault().Value; var letters = string.IsNullOrEmpty(rawValue) ? string.Empty : new string(rawValue.Where(char.IsLetter).ToArray()); resolved[$"%БУКВЫ:{field}%"] = letters; diff --git a/PARR.Core/Services/Shortcodes/Handlers/MaxShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/MaxShortcodeHandler.cs index 8af7cd07..c7133c10 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/MaxShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/MaxShortcodeHandler.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.Shortcodes.Enums; using PARR.Core.Services.Shortcodes.Models; using PARR.Domain.Cache.Models; using System.Text.RegularExpressions; @@ -14,7 +15,13 @@ internal class MaxShortcodeHandler : IShortcodeHandler private readonly ILogger logger; public int Order => 40; - public Regex Pattern => new(@"%МАКС:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex _pattern = new(@"%МАКС:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => + ShortcodeDataRequirementsEnum.Job | + ShortcodeDataRequirementsEnum.JobGroup | + ShortcodeDataRequirementsEnum.UnitsInTemplate; public MaxShortcodeHandler( IUnitInValueRepository unitInValueRepo, @@ -26,11 +33,12 @@ internal class MaxShortcodeHandler : IShortcodeHandler this.logger = logger; } - public async Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public async Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { var matches = Pattern.Matches(input); if (matches.Count == 0) return input; + // Извлекаем уникальные имена полей, чтобы не ходить в кэш/БД дважды для одинаковых шорткодов var uniqueFields = matches.Cast() .Select(m => m.Value.Trim('%').Split(':', 2)[1].Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) @@ -39,7 +47,7 @@ internal class MaxShortcodeHandler : IShortcodeHandler var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var field in uniqueFields) { - resolved[$"%МАКС:{field}%"] = await GetMaxValueAsync(data, field, caller, ct); + resolved[$"%МАКС:{field}%"] = await GetMaxValueAsync(template, field, caller, ct); } var result = input; @@ -48,21 +56,30 @@ internal class MaxShortcodeHandler : IShortcodeHandler if (resolved.TryGetValue(m.Value, out var val)) result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase); } + return result; } - private async Task GetMaxValueAsync(TemplateForShortcode data, string fieldName, string caller, CancellationToken ct = default) + private async Task GetMaxValueAsync(TemplateForShortcode template, string fieldName, string caller, CancellationToken ct) { - if (data.Job?.Group?.Id == Guid.Empty || data.UnitId == Guid.Empty) return "Нет данных"; + // Guard: данные должны быть подготовлены оркестратором согласно Requirements + if (template.Job?.Group == null || template.UnitId == Guid.Empty) + return "Нет данных"; + + if (template.UnitsInTemplate.Count == 0) + { + logger.LogDebug("[{Caller}] %МАКС:{Field}% пропущен: UnitsInTemplate пуст для шаблона {TemplateId}.", caller, fieldName, template.Id); + return "Нет данных"; + } var cacheKey = cacheService.GetKey( - new[] { "grouped", "shortcode", $"{data.Job.Group.Id:N}", $"{data.UnitId:N}" }, + new[] { "grouped", "shortcode", $"{template.Job.Group.Id:N}", $"{template.UnitId:N}" }, new[] { $"%МАКС:{fieldName}%" }); var cached = await cacheService.GetCachedDataAsync(cacheKey); if (cached != null) return cached.Data.Value; - var unitIds = data.UnitsInTemplate.Select(u => u.UnitId).Distinct().ToList(); + var unitIds = template.UnitsInTemplate.Select(u => u.UnitId).Distinct().ToList(); var value = await unitInValueRepo.GetMostFrequentValueForFieldAsync(unitIds, fieldName, ct); if (!string.IsNullOrEmpty(value)) diff --git a/PARR.Core/Services/Shortcodes/Handlers/RelShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/RelationshipUnitShortcodeHandler.cs similarity index 63% rename from PARR.Core/Services/Shortcodes/Handlers/RelShortcodeHandler.cs rename to PARR.Core/Services/Shortcodes/Handlers/RelationshipUnitShortcodeHandler.cs index ace5c072..df4cd1a9 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/RelShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/RelationshipUnitShortcodeHandler.cs @@ -1,32 +1,33 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.Shortcodes.Enums; using PARR.Core.Services.Shortcodes.Models; using System.Text.RegularExpressions; namespace PARR.Core.Services.Shortcodes.Handlers; -internal class RelShortcodeHandler : IShortcodeHandler +internal class RelationshipUnitShortcodeHandler : IShortcodeHandler { private readonly IUnitInValueRepository unitInValueRepo; - private readonly ITemplateRepository templateRepo; - private readonly ILogger logger; + private readonly ILogger logger; public int Order => 50; - public Regex Pattern => new(@"%СВЯЗЬ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public RelShortcodeHandler( + private static readonly Regex _pattern = new(@"%СВЯЗЬ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.UnitsInTemplate; + + public RelationshipUnitShortcodeHandler( IUnitInValueRepository unitInValueRepo, - ITemplateRepository templateRepo, - ILogger logger) + ILogger logger) { this.unitInValueRepo = unitInValueRepo; - this.templateRepo = templateRepo; this.logger = logger; } - public async Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public async Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { var matches = Pattern.Matches(input); if (matches.Count == 0) return input; @@ -39,7 +40,7 @@ internal class RelShortcodeHandler : IShortcodeHandler var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var field in uniqueFields) { - resolved[$"%СВЯЗЬ:{field}%"] = await GetRelValueAsync(data, field, caller, ct); + resolved[$"%СВЯЗЬ:{field}%"] = await GetRelValueAsync(template, field, caller, ct); } var result = input; @@ -51,24 +52,16 @@ internal class RelShortcodeHandler : IShortcodeHandler return result; } - private async Task GetRelValueAsync(TemplateForShortcode data, string fieldName, string caller, CancellationToken ct) + private async Task GetRelValueAsync(TemplateForShortcode template, string fieldName, string caller, CancellationToken ct) { - var unitsList = data.UnitsInTemplate; - if (unitsList.Count == 0) + // Оркестратор гарантирует наличие данных согласно Requirements + if (template.UnitsInTemplate.Count == 0) { - var fullTemplate = await templateRepo.Get() - .AsNoTracking() - .Include(t => t.UnitsInTemplate) - .FirstOrDefaultAsync(t => t.Id == data.Id, ct); - - unitsList = fullTemplate?.UnitsInTemplate - ?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId)) - .ToList() ?? new List(); + logger.LogDebug("[{Caller}] %СВЯЗЬ:{Field}% пропущен: UnitsInTemplate пуст.", caller, fieldName); + return string.Empty; } - if (unitsList.Count == 0) return string.Empty; - - var unitIds = unitsList.Select(u => u.UnitId).Distinct().ToList(); + var unitIds = template.UnitsInTemplate.Select(u => u.UnitId).Distinct().ToList(); // Пакетный запрос вместо N+1 var allValues = await unitInValueRepo.Get() @@ -86,7 +79,10 @@ internal class RelShortcodeHandler : IShortcodeHandler if (distinctSorted.Count == 0) return string.Empty; if (distinctSorted.Count > 1) - logger.LogWarning("[{Caller}] %СВЯЗЬ:{Field}% нашёл {Count} значений. Используется первое: '{First}'", caller, fieldName, distinctSorted.Count, distinctSorted[0]); + { + logger.LogWarning("[{Caller}] %СВЯЗЬ:{Field}% нашёл {Count} значений. Используется первое: '{First}'", + caller, fieldName, distinctSorted.Count, distinctSorted[0]); + } return distinctSorted[0]!; } diff --git a/PARR.Core/Services/Shortcodes/Handlers/RelationshipsShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/RelationshipsShortcodeHandler.cs index 33938cff..3d43f332 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/RelationshipsShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/RelationshipsShortcodeHandler.cs @@ -1,4 +1,5 @@ -using PARR.Core.Services.Shortcodes.Models; +using PARR.Core.Services.Shortcodes.Enums; +using PARR.Core.Services.Shortcodes.Models; using PARR.Core.Services.UnitFilterService; using System.Text.RegularExpressions; @@ -9,19 +10,23 @@ internal class RelationshipsShortcodeHandler : IShortcodeHandler private readonly IUnitFilterService unitFilterService; public int Order => 70; - public Regex Pattern => new(@"%СВЯЗИ(-ПН)?%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex _pattern = new(@"%СВЯЗИ(-ПН)?%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.Job; public RelationshipsShortcodeHandler(IUnitFilterService unitFilterService) => this.unitFilterService = unitFilterService; - public async Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public async Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { - if (data.Job == null) return input; + if (template.Job == null) return input; var hasPlain = input.Contains("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase); var hasNumbered = input.Contains("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase); if (!hasPlain && !hasNumbered) return input; - var relatedNames = await unitFilterService.GetRelatedUnitNamesAsync(data.JobId, data.UnitId, ct); + var relatedNames = await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId, ct); var result = input; if (hasPlain) diff --git a/PARR.Core/Services/Shortcodes/Handlers/StandardShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/StandardShortcodeHandler.cs index 769b3fcb..1580ae27 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/StandardShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/StandardShortcodeHandler.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using PARR.Core.Services.Shortcodes.Enums; using PARR.Core.Services.Shortcodes.Models; using System.Text.RegularExpressions; @@ -8,23 +9,31 @@ internal class StandardShortcodeHandler : IShortcodeHandler { private readonly ILogger logger; public int Order => 10; - public Regex Pattern => new(@"%(?:ЭК|ТИКТАК|РАБОТА|ГРУППА_РАБОТ|ТНК|ТНК-КРАТКО|ИД)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex _pattern = new(@"%(?:ЭК|ТИКТАК|РАБОТА|ГРУППА_РАБОТ|ТНК|ТНК-КРАТКО|ИД)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public Regex Pattern => _pattern; + + public ShortcodeDataRequirementsEnum Requirements => + ShortcodeDataRequirementsEnum.Job | + ShortcodeDataRequirementsEnum.JobGroup | + ShortcodeDataRequirementsEnum.JobTnk | + ShortcodeDataRequirementsEnum.UnitName; public StandardShortcodeHandler(ILogger logger) => this.logger = logger; - public Task ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default) + public Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) { var result = input; - result = Replace(result, "%ЭК%", data.UnitName); + result = Replace(result, "%ЭК%", template.UnitName); result = Replace(result, "%ТИКТАК%", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); - result = Replace(result, "%ИД%", data.Id.ToString("N")); + result = Replace(result, "%ИД%", template.Id.ToString("N")); - if (data.Job != null) + if (template.Job != null) { - result = Replace(result, "%РАБОТА%", data.Job.WorkName); - result = Replace(result, "%ГРУППА_РАБОТ%", data.Job.Group?.GroupName); - result = Replace(result, "%ТНК%", data.Job.Tnk?.Name); - result = Replace(result, "%ТНК-КРАТКО%", data.Job.Tnk?.ShortName); + result = Replace(result, "%РАБОТА%", template.Job.WorkName); + result = Replace(result, "%ГРУППА_РАБОТ%", template.Job.Group?.GroupName); + result = Replace(result, "%ТНК%", template.Job.Tnk?.Name); + result = Replace(result, "%ТНК-КРАТКО%", template.Job.Tnk?.ShortName); } return Task.FromResult(result); } diff --git a/PARR.Core/Services/Shortcodes/ShortcodesService.cs b/PARR.Core/Services/Shortcodes/ShortcodesService.cs index 9fc105b8..1507f660 100644 --- a/PARR.Core/Services/Shortcodes/ShortcodesService.cs +++ b/PARR.Core/Services/Shortcodes/ShortcodesService.cs @@ -1,8 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces; +using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.Shortcodes.Enums; using PARR.Core.Services.Shortcodes.Handlers; using PARR.Core.Services.Shortcodes.Models; using PARR.Domain.Cache.Models; @@ -12,7 +14,6 @@ using PARR.Domain.Entities.Job; using PARR.Domain.Enums; using PARR.Domain.Settings; using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; namespace PARR.Core.Services.Shortcodes; @@ -23,12 +24,6 @@ internal class ShortcodesService : IShortcodesService "Рекомендуется обновить запрос шаблона с Include(t => t.Unit)."; private const int MaxIterations = 3; - private static readonly Regex GeneralShortcodeRegex = new(@"%[^%]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly HashSet SupportedStandardShortcodes = new(StringComparer.OrdinalIgnoreCase) - { - "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%", "%ИД%" - }; private readonly ILogger logger; private readonly IEnumerable handlers; @@ -36,7 +31,8 @@ internal class ShortcodesService : IShortcodesService private readonly SettingsFromDb settingsFromDb; private readonly IJobRepository jobService; private readonly IUnitRepository unitService; - private readonly IUnitFieldRepository unitFieldService; + private readonly IUnitFieldRepository unitFieldRepo; + private readonly ITemplateRepository templateRepo; public ShortcodesService( ILogger logger, @@ -45,7 +41,9 @@ internal class ShortcodesService : IShortcodesService SettingsFromDb settingsFromDb, IJobRepository jobService, IUnitRepository unitService, - IUnitFieldRepository unitFieldService) + IUnitFieldRepository unitFieldService, + ITemplateRepository templateRepo + ) { this.logger = logger; this.handlers = handlers.OrderBy(h => h.Order).ToList(); @@ -53,7 +51,8 @@ internal class ShortcodesService : IShortcodesService this.settingsFromDb = settingsFromDb; this.jobService = jobService; this.unitService = unitService; - this.unitFieldService = unitFieldService; + this.unitFieldRepo = unitFieldService; + this.templateRepo = templateRepo; } public async Task ApplyShortcodesAsync(string str, Template template, [CallerMemberName] string? caller = null) @@ -61,39 +60,69 @@ internal class ShortcodesService : IShortcodesService if (string.IsNullOrEmpty(str)) return str; var callerName = caller ?? "Unknown"; + // Кэш результата (проверяем один раз на входе) var cacheKey = cacheService.GetKey(new[] { "shortcodes", "result", template.Id.ToString("N") }, new[] { str }); var cachedResult = await cacheService.GetCachedDataAsync(cacheKey).ConfigureAwait(false); if (cachedResult != null) return cachedResult.Data.Result; - var shortcodes = GeneralShortcodeRegex.Matches(str).Select(m => m.Value).ToList(); - var data = await PrepareTemplateDataAsync(template, shortcodes, callerName).ConfigureAwait(false); + // Начальный контекст + var initialUnitName = template.Unit?.Name; + + var initialJob = template.Job != null ? MapJobForShortcodes(template.Job) : null; + + var initialUnitsInTemplate = template.UnitsInTemplate?.Select(uit => + new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId)).ToList() + ?? new List(); + + var data = new TemplateForShortcode( + Id: template.Id, + Index: template.Index, + JobId: template.JobId, + UnitId: template.UnitId, + UnitName: initialUnitName ?? string.Empty, + Job: initialJob, + UnitsInTemplate: initialUnitsInTemplate + ); var result = str; var iteration = 0; while (iteration < MaxIterations) { - var changed = false; + // 1. Находим хендлеры, которые сработают для ТЕКУЩЕЙ версии строки + var matchingHandlers = handlers + .Where(h => h.Pattern.IsMatch(result)) + .ToList(); - foreach (var handler in handlers) + if (matchingHandlers.Count == 0) break; // Нечего заменять + + // 2. Агрегируем требования только активных хендлеров + var currentRequirements = matchingHandlers + .Aggregate(ShortcodeDataRequirementsEnum.None, (acc, h) => acc | h.Requirements); + + // 3. ОБНОВЛЯЕМ ДАННЫЕ (Ленивая загрузка) + // Передаем текущий 'data', чтобы метод знал, что уже загружено, и не бил в БД повторно + data = await PrepareTemplateDataAsync(template, currentRequirements, data, callerName).ConfigureAwait(false); + + // 4. Выполняем замену + var previousResult = result; + foreach (var handler in matchingHandlers) { - if (handler.Pattern.IsMatch(result)) - { - var before = result; - result = await handler.ResolveAsync(result, data, callerName).ConfigureAwait(false); - if (result != before) changed = true; - } + result = await handler.ResolveAsync(result, data, callerName, CancellationToken.None).ConfigureAwait(false); } - if (!changed) break; + // 5. Если строка не изменилась — стабилизация достигнута + if (result == previousResult) break; + iteration++; } if (iteration == MaxIterations && result != str) { - logger.LogWarning("[{Caller}] Достигнут лимит итераций для шаблона {TemplateId}. Возможна циклическая зависимость шорткодов.", callerName, template.Id); + logger.LogWarning("[{Caller}] Достигнут лимит итераций для шаблона {TemplateId}. Возможна циклическая зависимость.", callerName, template.Id); } + // Сохранение в кэш if (!string.IsNullOrEmpty(result) && result != str && template.Id != Guid.Empty) { await cacheService.SetCachedDataAsync(cacheKey, new ShortcodesResult @@ -109,80 +138,87 @@ internal class ShortcodesService : IShortcodesService private async Task PrepareTemplateDataAsync( Template template, - List shortcodes, + ShortcodeDataRequirementsEnum requirements, + TemplateForShortcode currentData, string caller) { - // 1. Загружаем имя юнита, если его нет - var unitName = template.Unit?.Name ?? await GetUnitNameAsyncWithWarning(template, shortcodes, caller).ConfigureAwait(false); - - // 2. Формируем базовые данные - var data = new TemplateForShortcode( - Id: template.Id, - Index: template.Index, - JobId: template.JobId, - UnitId: template.UnitId, - UnitName: unitName, - Job: null, - UnitsInTemplate: template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId)).ToList() ?? new List() - ); - - // 3. Проверяем, нужны ли вообще данные о работе - if (!NeedsJobForShortcodes(shortcodes)) + // 1. Проверка UnitName + var unitName = currentData.UnitName; + if (string.IsNullOrEmpty(unitName) && requirements.HasFlag(ShortcodeDataRequirementsEnum.UnitName)) { - return data with { Job = new JobForShortcode(null, null, string.Empty, string.Empty) }; + // Логирование и загрузка только если нужно и еще не загружено + logger.LogWarning(MissingUnitNameWarningMsg, caller, template.Id); + var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == template.UnitId).ConfigureAwait(false); + unitName = unit?.Name ?? throw new InvalidOperationException($"Unit {template.UnitId} не найден."); } - JobForShortcode jobData; + // 2. Проверка Job и навигационных свойств + JobForShortcode? jobData = currentData.Job; + bool needLoadJob = false; - // 4. Проверяем полноту контекста Job - if (!IsJobContextComplete(template.Job)) + if (requirements.HasFlag(ShortcodeDataRequirementsEnum.Job)) { - // === Graceful Degradation + Structured Logging === - - var missingParts = new List(); - if (template.Job == null) missingParts.Add("Job"); + if (jobData == null) + { + needLoadJob = true; + } else { - if (template.Job.Group == null) missingParts.Add("Group"); - if (template.Job.Group?.GroupType == null) missingParts.Add("Group.GroupType"); - if (template.Job.Tnk == null) missingParts.Add("Tnk"); + // Job загружен, но хватает ли вложенных свойств? + // Так как LoadFullJobAsync грузит всё сразу, если jobData != null, значит там есть всё. + // Но на всякий случай проверим флаги, если в будущем загрузка станет частичной. + if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobGroup) && jobData.Group == null) needLoadJob = true; + if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobTnk) && jobData.Tnk == null) needLoadJob = true; } + } + if (needLoadJob) + { + var missing = new List(); + if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobGroup)) missing.Add("Group"); + if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobTnk)) missing.Add("Tnk"); + + // Логируем только если реально пошли в БД logger.LogWarning( - "[{Caller}] Шаблон {TemplateId} передан без необходимых Include для шорткодов. Отсутствуют: {MissingIncludes}. " + - "Данные будут догружены автоматически. Для оптимизации добавьте .Include() в запрос.", - caller, template.Id, string.Join(", ", missingParts)); + "[{Caller}] Шаблон {TemplateId} требует догрузки данных ({Missing}). Данные догружены.", + caller, template.Id, string.Join(", ", missing)); - // Делаем ОДИН пакетный запрос за всем недостающим jobData = await LoadFullJobAsync(template.JobId, caller).ConfigureAwait(false); } - else + + // 3. Проверка UnitsInTemplate + var unitsInTemplate = currentData.UnitsInTemplate; + if ((unitsInTemplate == null || unitsInTemplate.Count == 0) && + requirements.HasFlag(ShortcodeDataRequirementsEnum.UnitsInTemplate)) { - // Данные полные, используем их без запросов к БД - jobData = MapJobForShortcodes(template.Job); + logger.LogWarning( + "[{Caller}] Шаблон {TemplateId} передан без UnitsInTemplate. Данные догружены автоматически.", + caller, template.Id); + + var fullTemplate = await templateRepo.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .FirstOrDefaultAsync(t => t.Id == template.Id) + .ConfigureAwait(false); + + unitsInTemplate = fullTemplate?.UnitsInTemplate + ?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId)) + .ToList() ?? new List(); } - return data with { Job = jobData }; + // 4. Возвращаем обновленный рекорд + return currentData with + { + UnitName = unitName ?? string.Empty, + Job = jobData, + UnitsInTemplate = unitsInTemplate ?? currentData.UnitsInTemplate + }; } - /// - /// Контракт полноты данных Job для движка шорткодов. - /// Если появятся новые зависимости (например, Job.AutoControl), добавляем проверку сюда. - /// - private static bool IsJobContextComplete(Job? job) - { - if (job == null) return false; - - // Базовые зависимости, необходимые для корректной работы стандартных и групповых шорткодов, - // а также для безопасного разрешения динамических цепочек. - return job.Group != null && - job.Group.GroupType != null && - job.Tnk != null; - } private async Task LoadFullJobAsync(Guid jobId, string caller) { - var job = await jobService.Get().AsNoTracking() + var job = await jobService.Get().AsNoTracking().AsSingleQuery() .Include(j => j.Group).ThenInclude(g => g!.GroupType) .Include(j => j.Tnk) .FirstOrDefaultAsync(j => j.Id == jobId) @@ -194,33 +230,6 @@ internal class ShortcodesService : IShortcodesService return MapJobForShortcodes(job); } - private static bool NeedsJobForShortcodes(List shortcodes) - { - return shortcodes.Any(sc => - sc != "%ЭК%" && - (SupportedStandardShortcodes.Contains(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))); - } - - - private async Task GetUnitNameAsyncWithWarning(Template template, List shortcodes, string caller) - { - if (shortcodes.Any(sc => sc.Equals("%ЭК%", StringComparison.OrdinalIgnoreCase))) - { - logger.LogWarning(MissingUnitNameWarningMsg, caller, template.Id); - } - - var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == template.UnitId).ConfigureAwait(false); - if (unit == null || string.IsNullOrEmpty(unit.Name)) - throw new InvalidOperationException($"Unit {template.UnitId} не найден или не содержит Name."); - - return unit.Name; - } private JobForShortcode MapJobForShortcodes(Job job) { @@ -287,7 +296,7 @@ internal class ShortcodesService : IShortcodesService new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", Description = "Связанные ЭК с нумерацией", Type = ShortcodeTypeEnum.Relationship } }); - var fieldNames = await unitFieldService.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync().ConfigureAwait(false); + var fieldNames = await unitFieldRepo.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync().ConfigureAwait(false); foreach (var fieldName in fieldNames.OrderBy(n => n.AihitName)) { result.Add(new ShortcodeInfoDto diff --git a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs index 50226b2a..13b54697 100644 --- a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs +++ b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs @@ -87,7 +87,7 @@ namespace PARR.TemplateGeneratorWorker var tempTemplateForShortcodes = new Template { Id = Guid.Empty, // ещё не создан - Name = "", // не используется + Name = string.Empty, // не используется JobId = query.JobId, UnitId = query.UnitId, Index = query.Index,