From d91877453a27ebceb1cbeddd15f2d7e38ce1fb64 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Fri, 16 Jan 2026 18:26:06 +1000 Subject: [PATCH] =?UTF-8?q?feat(dal,=20templateMatcher):=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20Shortcode?= =?UTF-8?q?s=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=BD=D1=83=20=D1=81=D0=B0=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D1=82=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B4=D0=BE=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=91=D0=94,=20=20=D0=B2=20Template=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B8=D1=81=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D1=80=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=B2=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20TemplateMatcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Contracts/V1/ApiRoutes.cs | 4 + .../V1/Requests/ShortcodeApplyRequest.cs | 8 + .../V1/Responses/ShortcodeApplyResponse.cs | 7 + .../V1/ShortcodeApplyController.cs | 60 ++ PARR.Constants/ShortcodeTypeEnum.cs | 6 +- .../Implementations/UnitFilterService.cs | 2 +- .../Shortcodes/IShortcodesService.cs | 8 +- .../Shortcodes/Models/JobForShortcodes.cs | 10 - .../Models/JobGroupForShortcodes.cs | 10 - .../Models/JobGroupTypeForShortcodes.cs | 9 - .../Models/TemplateForShortcodes.cs | 13 - .../Shortcodes/Models/TnkForShortcodes.cs | 8 - .../Models/UnitInTemplateForShortcodes.cs | 7 - .../Shortcodes/ShortcodesService.cs | 756 ++++++++++++------ PARR.DAL/ParrDalInstaller.cs | 1 - .../Implementations/TemplateService.cs | 45 +- .../Services/Interfaces/ITemplateService.cs | 4 +- PARR.EsppSync/SyncService.cs | 44 +- .../TemplateGenerator.cs | 50 +- .../GroupedTemplateSynchronizer.cs | 109 ++- .../SimpleTemplateSynchronizer.cs | 33 +- .../Implemetaions/TemplateDeactivator.cs | 16 +- .../Implemetaions/TemplateNameNormalizer.cs | 41 +- .../Services/Implemetaions/TemplateReuser.cs | 75 +- .../Interfaces/ITemplateNameNormalizer.cs | 5 +- 25 files changed, 822 insertions(+), 509 deletions(-) create mode 100644 PARR.API/Contracts/V1/Requests/ShortcodeApplyRequest.cs create mode 100644 PARR.API/Contracts/V1/Responses/ShortcodeApplyResponse.cs create mode 100644 PARR.API/Controllers/V1/ShortcodeApplyController.cs delete mode 100644 PARR.DAL/DomainServices/Shortcodes/Models/JobForShortcodes.cs delete mode 100644 PARR.DAL/DomainServices/Shortcodes/Models/JobGroupForShortcodes.cs delete mode 100644 PARR.DAL/DomainServices/Shortcodes/Models/JobGroupTypeForShortcodes.cs delete mode 100644 PARR.DAL/DomainServices/Shortcodes/Models/TemplateForShortcodes.cs delete mode 100644 PARR.DAL/DomainServices/Shortcodes/Models/TnkForShortcodes.cs delete mode 100644 PARR.DAL/DomainServices/Shortcodes/Models/UnitInTemplateForShortcodes.cs diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index f254f7ab..3d97c61d 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -521,6 +521,10 @@ public const string GetAll = Base + "/shortcodes/"; } + public static class ShortcodeApply + { + public const string Apply = Base + "/shortcodes/apply"; + } public static class RegionalEkPtkGroup { public const string GetAll = Base + "/regional-ek-ptk-groups/"; diff --git a/PARR.API/Contracts/V1/Requests/ShortcodeApplyRequest.cs b/PARR.API/Contracts/V1/Requests/ShortcodeApplyRequest.cs new file mode 100644 index 00000000..a9431923 --- /dev/null +++ b/PARR.API/Contracts/V1/Requests/ShortcodeApplyRequest.cs @@ -0,0 +1,8 @@ +namespace PARR.API.Contracts.V1.Requests +{ + public class ShortcodeApplyRequest + { + public Guid TemplateId { get; set; } + public required string Content { get; set; } + } +} diff --git a/PARR.API/Contracts/V1/Responses/ShortcodeApplyResponse.cs b/PARR.API/Contracts/V1/Responses/ShortcodeApplyResponse.cs new file mode 100644 index 00000000..a02d922b --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/ShortcodeApplyResponse.cs @@ -0,0 +1,7 @@ +namespace PARR.API.Contracts.V1.Responses +{ + public class ShortcodeApplyResponse + { + public required string Content { get; set; } + } +} diff --git a/PARR.API/Controllers/V1/ShortcodeApplyController.cs b/PARR.API/Controllers/V1/ShortcodeApplyController.cs new file mode 100644 index 00000000..cdf209cc --- /dev/null +++ b/PARR.API/Controllers/V1/ShortcodeApplyController.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using PARR.API.Contracts.V1; +using PARR.API.Contracts.V1.Requests; +using PARR.API.Contracts.V1.Responses; +using PARR.API.Contracts.V1.Responses.Base; +using PARR.API.Controllers.V1.Base; +using PARR.DAL.DomainServices.Shortcodes; +using PARR.DAL.Services.Interfaces; + +namespace PARR.API.Controllers.V1 +{ + public class ShortcodeApplyController : BaseApiController + { + private readonly ILogger logger; + private readonly IShortcodesService shortcodesService; + private readonly ITemplateService templateService; + + public ShortcodeApplyController( + ILogger logger, + IShortcodesService shortcodesService, + ITemplateService templateService + ) + { + this.logger = logger; + this.shortcodesService = shortcodesService; + this.templateService = templateService; + } + + + /// + /// Получить список всех переменных составляющих + /// + /// + [HttpPost(ApiRoutes.ShortcodeApply.Apply)] + public async Task Apply([FromBody] ShortcodeApplyRequest request) + { + var template = await templateService.Get() + .Include(t => t.Unit) + .Include(t => t.UnitsInTemplate) + .Include(t => t.Unit) + .Include(t => t.Job) + .ThenInclude(t => t!.Group) + .ThenInclude(t => t!.GroupType) + .Include(t => t.Job) + .ThenInclude(t => t!.Tnk) + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Id == request.TemplateId); + + if (template == null) + return BadRequest(new Response(false, new List { new ErrorModel { FieldName = nameof(request.TemplateId), Message = $"Шаблон имеющий Id = {request.TemplateId} не найден" } })); + + var aplyingShortCodes = await shortcodesService.ApplyShortcodesAsync(request.Content, template); + + var response = new ShortcodeApplyResponse { Content = aplyingShortCodes }; + + return Ok(new Response(response, true)); + } + } +} diff --git a/PARR.Constants/ShortcodeTypeEnum.cs b/PARR.Constants/ShortcodeTypeEnum.cs index 0d534fd8..255a7f7f 100644 --- a/PARR.Constants/ShortcodeTypeEnum.cs +++ b/PARR.Constants/ShortcodeTypeEnum.cs @@ -6,8 +6,10 @@ namespace PARR.Constants public enum ShortcodeTypeEnum { Static, - Standart, + Standard, Relationship, - FieldValue + FieldValue, + GroupValue, + Transform } } diff --git a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs index 9048bc13..633c4a6e 100644 --- a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs @@ -13,7 +13,7 @@ namespace PARR.DAL.DomainServices.Implementations internal class UnitFilterService : IUnitFilterService { #if DEBUG - private readonly Guid targetUnitId = Guid.Parse("6c019569-ecba-4d9a-9cdf-cdc1dea93925"); + private readonly Guid targetUnitId = Guid.Parse("87fc4c36-1ea8-4163-983f-1605fee1de99"); #endif private readonly ILogger logger; diff --git a/PARR.DAL/DomainServices/Shortcodes/IShortcodesService.cs b/PARR.DAL/DomainServices/Shortcodes/IShortcodesService.cs index 0e95bdd8..00a7d071 100644 --- a/PARR.DAL/DomainServices/Shortcodes/IShortcodesService.cs +++ b/PARR.DAL/DomainServices/Shortcodes/IShortcodesService.cs @@ -1,16 +1,12 @@ using PARR.DAL.DomainModels; -using PARR.DAL.DomainServices.Shortcodes.Models; using PARR.DAL.Models; +using System.Runtime.CompilerServices; namespace PARR.DAL.DomainServices.Shortcodes { public interface IShortcodesService { - Task ApplyShortcodesAsync(string str, TemplateForShortcodes template); - - Task ApplyShortcodesAsync(string str, Template template); - - // bool IsAnyShortcodes(string str); + Task ApplyShortcodesAsync(string str, Template template, [CallerMemberName] string? caller = null); Task> GetAvailableShortcodesAsync(); } diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/JobForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/JobForShortcodes.cs deleted file mode 100644 index 0dab9e42..00000000 --- a/PARR.DAL/DomainServices/Shortcodes/Models/JobForShortcodes.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace PARR.DAL.DomainServices.Shortcodes.Models -{ - public class JobForShortcodes - { - public JobGroupForShortcodes? Group { get; set; } - public TnkForShortcodes? Tnk { get; set; } - public string WorkName { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - } -} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupForShortcodes.cs deleted file mode 100644 index 1105892c..00000000 --- a/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupForShortcodes.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace PARR.DAL.DomainServices.Shortcodes.Models -{ - public class JobGroupForShortcodes - { - public Guid Id { get; set; } - public Guid? GroupingUnitFieldId { get; set; } - public JobGroupTypeForShortcodes? GroupType { get; set; } - public string GroupName { get; set; } = string.Empty; - } -} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupTypeForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupTypeForShortcodes.cs deleted file mode 100644 index f1596b7c..00000000 --- a/PARR.DAL/DomainServices/Shortcodes/Models/JobGroupTypeForShortcodes.cs +++ /dev/null @@ -1,9 +0,0 @@ -using PARR.DAL.Contracts; - -namespace PARR.DAL.DomainServices.Shortcodes.Models -{ - public class JobGroupTypeForShortcodes - { - public JobGroupTypesEnum Code { get; set; } - } -} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/TemplateForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/TemplateForShortcodes.cs deleted file mode 100644 index 76a5119b..00000000 --- a/PARR.DAL/DomainServices/Shortcodes/Models/TemplateForShortcodes.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace PARR.DAL.DomainServices.Shortcodes.Models -{ - public class TemplateForShortcodes - { - public Guid Id { get; set; } - public int? Index { get; set; } - public Guid JobId { get; set; } - public Guid UnitId { get; set; } - - public JobForShortcodes? Job { get; set; } - public List UnitsInTemplate { get; set; } = new(); - } -} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/TnkForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/TnkForShortcodes.cs deleted file mode 100644 index 1e284b63..00000000 --- a/PARR.DAL/DomainServices/Shortcodes/Models/TnkForShortcodes.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PARR.DAL.DomainServices.Shortcodes.Models -{ - public class TnkForShortcodes - { - public string Name { get; set; } = string.Empty; - public string ShortName { get; set; } = string.Empty; - } -} diff --git a/PARR.DAL/DomainServices/Shortcodes/Models/UnitInTemplateForShortcodes.cs b/PARR.DAL/DomainServices/Shortcodes/Models/UnitInTemplateForShortcodes.cs deleted file mode 100644 index 985aa973..00000000 --- a/PARR.DAL/DomainServices/Shortcodes/Models/UnitInTemplateForShortcodes.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PARR.DAL.DomainServices.Shortcodes.Models -{ - public class UnitInTemplateForShortcodes - { - public Guid UnitId { get; set; } - } -} diff --git a/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs b/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs index 2cff538a..094de983 100644 --- a/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs +++ b/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs @@ -7,23 +7,42 @@ using PARR.DAL.DomainModels; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes.Models; using PARR.DAL.Models; -using PARR.DAL.Models.Unit; +using PARR.DAL.Models.Job; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; namespace PARR.DAL.DomainServices.Shortcodes { + /// + /// Сервис для подстановки шорткодов в строке на основе данных шаблона. + /// internal class ShortcodesService : IShortcodesService { - private const string shortcodePattern = "%[^%\\s]+%"; - private const string maxShortcodePattern = @"%МАКС:([а-яА-Яa-zA-Z0-9_]+)%"; - private const string lettersShortcodePattern = @"%БУКВЫ:([^%]+)%"; + private const string MissingJobWarningMsg = + "[{Caller}] Шаблон {TemplateId} не содержит Job в переданных данных. Данные будут догружены из БД. " + + "Рекомендуется обновить запрос шаблона с Include(j => j.Job)."; - private static readonly HashSet SupportedShortcodes = new(StringComparer.OrdinalIgnoreCase) + private const string MissingIncludesWarningMsg = + "[{Caller}] Шаблон {TemplateId} содержит Job, но не хватает данных для шорткодов: {Shortcodes}. " + + "Отсутствуют Include: {MissingIncludes}. Данные будут догружены из БД. " + + "Рекомендуется обновить запрос шаблона."; + + private const string MissingUnitNameWarningMsg = + "[{Caller}] Шаблон {TemplateId} не содержит Unit.Name в переданных данных. Данные будут догружены из БД. " + + "Рекомендуется обновить запрос шаблона с Include(t => t.Unit)."; + + private const int MaxIterations = 3; + private const string NoContent = "Нет данных"; + 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 HashSet SupportedStandardShortcodes = new(StringComparer.OrdinalIgnoreCase) { - "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%СВЯЗИ%", "%ТНК-КРАТКО%", "%СВЯЗИ-ПН%", "%ИНДЕКС%" + "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%" }; private readonly ILogger logger; @@ -36,6 +55,43 @@ namespace PARR.DAL.DomainServices.Shortcodes private readonly IRedisCacheService cacheService; private readonly IUnitFilterService unitFilterService; + // Используем record struct вместо class + private record class TemplateForShortcodes( + Guid Id, + int? Index, + Guid JobId, + Guid UnitId, + JobForShortcodes? Job, + List UnitsInTemplate + ); + + private record class JobForShortcodes( + JobGroupForShortcodes? Group, + TnkForShortcodes? Tnk, + string WorkName, + string Name + ); + + private record class JobGroupForShortcodes( + Guid Id, + Guid? GroupingUnitFieldId, + JobGroupTypeForShortcodes? GroupType, + string GroupName + ); + + private record class JobGroupTypeForShortcodes( + JobGroupTypesEnum Code + ); + + private record class TnkForShortcodes( + string Name, + string ShortName + ); + + private record class UnitInTemplateForShortcodes( + Guid UnitId + ); + public ShortcodesService( ILogger logger, SettingsFromDb settingsFromDb, @@ -59,229 +115,380 @@ namespace PARR.DAL.DomainServices.Shortcodes this.unitFilterService = unitFilterService; } - public Task ApplyShortcodesAsync(string str, Template template) + /// + /// Подставляет шорткоды в строке на основе данных шаблона. + /// + /// Строка с шорткодами + /// Шаблон, из которого берутся данные + /// Строка с подставленными значениями + public async Task ApplyShortcodesAsync(string str, Template template, [CallerMemberName] string? caller = null) { - // Построим TemplateForShortcodes из уже загруженного template - var templateForShortcodes = new TemplateForShortcodes - { - Id = template.Id, - Index = template.Index, - JobId = template.JobId, - UnitId = template.UnitId, - Job = template.Job == null ? null : new JobForShortcodes - { - Group = template.Job.Group == null ? null : new JobGroupForShortcodes - { - Id = template.Job.GroupId, - GroupingUnitFieldId = template.Job.Group.GroupingUnitFieldId, - GroupType = template.Job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes - { - Code = template.Job.Group.GroupType.Code - }, - GroupName = template.Job.Group.GroupName - }, - Tnk = template.Job.Tnk == null ? null : new TnkForShortcodes - { - Name = template.Job.Tnk.Name, - ShortName = template.Job.Tnk.ShortName - }, - WorkName = template.Job.WorkName, - Name = template.Job.Name - }, - UnitsInTemplate = template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes { UnitId = uit.UnitId }).ToList() ?? new List() - }; + if (string.IsNullOrEmpty(str)) return str; + if (template == null) throw new ArgumentNullException(nameof(template)); - return ApplyShortcodesAsync(str, templateForShortcodes); - } + var callerName = caller ?? "Unknown"; + logger.LogDebug("[{Caller}] Начата подстановка шорткодов. Вход: '{Input}', templateId={TemplateId}", callerName, str, template.Id); - //todo: сделать private в перспективе - public async Task ApplyShortcodesAsync(string str, TemplateForShortcodes template) - { - if (!IsAnyShortcodes(str)) - { - logger.LogDebug("Строка не содержит шорткодов: {str}", str); - return str; - } - - logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', templateId={TemplateId}, index={Index}", str, template.Id, template.Index); - - var job = template.Job; - if (job == null) - { - logger.LogError("Шаблон {TemplateId} не содержит Job. Подстановка прервана.", template.Id); - return str; - } - - var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == template.UnitId); - - if (unit == null || string.IsNullOrEmpty(str)) - { - logger.LogError("Переданы некорректные данные для подстановки динамических записей"); - return str; - } - - var resultName = str; - var shortcodesInMask = GetShortCodes(resultName); - - // 1. Статические константы - var nameConstants = settingsFromDb.TemplateNameConstantPartsList; - if (shortcodesInMask.Any(m => nameConstants.Any(c => $"%{c.Name}%".Equals(m.Value, StringComparison.OrdinalIgnoreCase)))) - { - resultName = ReplaceConstants(nameConstants, resultName); - } - - // 2. Стандартные шорткоды — с поддержкой вложенных (%РАБОТА% → "Мониторинг | %ЭК%") - const int MaxStandardIterations = 3; + var result = str; var iteration = 0; - while (iteration < MaxStandardIterations) + while (iteration < MaxIterations) { - var remainingShortcodes = GetShortCodes(resultName) - .Select(m => m.Value) - .Where(s => SupportedShortcodes.Contains(s)) - .ToList(); + var shortcodes = GetShortCodes(result).Select(m => m.Value).ToList(); + if (!shortcodes.Any()) break; - if (!remainingShortcodes.Any()) - break; + // Подготавливаем данные с догрузкой при необходимости + var data = await PrepareTemplateDataAsync(template, shortcodes, callerName).ConfigureAwait(false); + + var oldResult = result; + + // Подстановка всех типов шорткодов + result = await ApplyAllShortcodesOnceAsync(result, data, shortcodes, callerName).ConfigureAwait(false); - var oldResult = resultName; - resultName = ReplaceStandardShortcodes(job, unit, resultName, template.Index); iteration++; - if (resultName == oldResult) + if (result == oldResult) break; + } + + logger.LogDebug("[{Caller}] Подстановка завершена. Результат: '{Result}'", callerName, result); + return result; + } + + private async Task PrepareTemplateDataAsync(Template template, List shortcodes, string caller) + { + var data = new TemplateForShortcodes( + Id: template.Id, + Index: template.Index, + JobId: template.JobId, + UnitId: template.UnitId, + Job: null, + UnitsInTemplate: template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes(uit.UnitId)).ToList() ?? new List() + ); + + // Загружаем Unit.Name + var unitName = template.Unit?.Name ?? await GetUnitNameAsyncWithWarning(template, shortcodes, caller).ConfigureAwait(false); + + // Проверяем, нужны ли данные Job + if (!NeedsJobForShortcodes(shortcodes)) + { + // Всё равно создаём пустой JobForShortcodes, чтобы не было null + data = data with { Job = new JobForShortcodes(null, null, string.Empty, string.Empty) }; + return data; + } + + if (template.Job == null) + { + logger.LogWarning(MissingJobWarningMsg, caller, template.Id); + var jobData = await LoadJobForShortcodesAsync(template.JobId, shortcodes, caller).ConfigureAwait(false); + return data with { Job = jobData }; + } + else + { + var missingIncludes = GetMissingIncludesForShortcodes(shortcodes, template.Job); + if (missingIncludes.Any()) { - logger.LogDebug("Замена стандартных шорткодов не изменила строку на итерации {Iteration}. Останов.", iteration); - break; - } - } + var problematicShortcodes = shortcodes.Where(sc => + (sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase) && template.Job.Group == 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) || sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)) && template.Job.Tnk == null) + ).ToList(); - if (iteration >= MaxStandardIterations) - { - logger.LogWarning( - "Достигнуто максимальное число итераций ({Max}) при замене стандартных шорткодов. Текущий результат: {Result}", - MaxStandardIterations, resultName); - } + logger.LogWarning(MissingIncludesWarningMsg, caller, template.Id, string.Join(", ", problematicShortcodes), string.Join(" ", missingIncludes)); - // 2.5. %МАКС:FIELD% — только для групповых job (JobGroup.Type == Group) - if (job.Group != null && job.Group.GroupType != null && job.Group.GroupType!.Code == JobGroupTypesEnum.Group) - { - var maxShortcodes = Regex.Matches(resultName, maxShortcodePattern); - if (maxShortcodes.Count > 0) - { - resultName = await ReplaceMaxShortcodesAsync(job.Group.Id, template.UnitId, resultName, maxShortcodes); - } - } - - // 2.6. %БУКВЫ:FIELD% — извлекает только буквы из значения поля - var lettersShortcodes = Regex.Matches(resultName, lettersShortcodePattern); - if (lettersShortcodes.Count > 0) - { - resultName = await ReplaceLettersShortcodesAsync(template.UnitId, resultName, lettersShortcodes); - } - - // 2.7. %ГР_ПОЛЕ-ПН% — нумерованный список UnitsInTemplate с GroupingUnitFieldId, отсортированный по имени юнита - if (shortcodesInMask.Any(m => string.Equals(m.Value, "%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase))) - { - var unitsInTemplate = template.UnitsInTemplate; - if (unitsInTemplate == null || !unitsInTemplate.Any()) - { - logger.LogDebug("Шаблон {TemplateId} не содержит UnitsInTemplate. %ГР_ПОЛЕ-ПН% заменён на пустую строку.", template.Id); - resultName = Regex.Replace(resultName, "%ГР_ПОЛЕ-ПН%", "", RegexOptions.IgnoreCase); + var jobData = await LoadJobForShortcodesAsync(template.JobId, shortcodes, caller).ConfigureAwait(false); + return data with { Job = jobData }; } else { - var unitIds = unitsInTemplate.Select(uit => uit.UnitId).ToList(); + var jobData = MapJobForShortcodes(template.Job); + return data with { Job = jobData }; + } + } + } - var units = await unitService.Get() - .AsNoTracking() - .Where(u => unitIds.Contains(u.Id)) - .ToListAsync(); + private static bool NeedsJobForShortcodes(List shortcodes) + { + return shortcodes.Any(sc => + sc != "%ЭК%" && // %ЭК% не требует Job + (SupportedStandardShortcodes.Contains(sc) || + sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase))); + } - // Словарь: UnitId -> Unit (для быстрого поиска) - var unitDict = units.ToDictionary(u => u.Id, u => u); + private static List GetMissingIncludesForShortcodes(List shortcodes, Job? job) + { + var missing = new List(); - var groupingFieldId = job.Group?.GroupingUnitFieldId; - Dictionary valuesByUnit = new(); + if (shortcodes.Any(sc => + sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase) || + sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) + && job?.Group == null) + { + missing.Add(".Include(j => j.Group)"); + } - if (groupingFieldId.HasValue) - { - var fieldValues = await unitInValueService.Get() - .AsNoTracking() - .Include(uv => uv.Value) - .Where(uv => - uv.FieldId == groupingFieldId.Value && - unitIds.Contains(uv.UnitId) && - uv.Value != null && - !string.IsNullOrWhiteSpace(uv.Value.Value)) - .Select(uv => new { uv.UnitId, Value = uv.Value!.Value }) - .ToListAsync(); + if (shortcodes.Any(sc => + sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)) + && job?.Group?.GroupType == null) + { + missing.Add(".ThenInclude(g => g.GroupType)"); + } - valuesByUnit = fieldValues - .GroupBy(x => x.UnitId) - .ToDictionary( - g => g.Key, - g => string.Join(", ", g.Select(v => v.Value).OrderBy(v => v)) - ); - } + if (shortcodes.Any(sc => + sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)) + && job?.Tnk == null) + { + missing.Add(".Include(j => j.Tnk)"); + } - // Сортируем UnitsInTemplate по имени юнита - var sortedUnitsInTemplate = unitsInTemplate - .OrderBy(uit => unitDict.TryGetValue(uit.UnitId, out var unit) ? unit.Name : $"(UnitId={uit.UnitId})") - .ToList(); + return missing; + } - var lines = sortedUnitsInTemplate - .Select((uit, indexInList) => - { - var unitInList = unitDict.TryGetValue(uit.UnitId, out var unit) ? unit : null; - var unitName = unitInList?.Name ?? $"(UnitId={uit.UnitId})"; - var valuesStr = valuesByUnit.TryGetValue(uit.UnitId, out var vals) ? vals : ""; - return $"{indexInList + 1}. {unitName} ({valuesStr})"; - }) - .ToList(); + 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 resultText = string.Join("\n", lines); - resultName = Regex.Replace(resultName, "%ГР_ПОЛЕ-ПН%", resultText, RegexOptions.IgnoreCase); + 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 JobForShortcodes MapJobForShortcodes(Job job) + { + return new JobForShortcodes( + Group: job.Group == null ? null : new JobGroupForShortcodes( + Id: job.Group.Id, + GroupingUnitFieldId: job.Group.GroupingUnitFieldId, + GroupName: job.Group.GroupName ?? string.Empty, + GroupType: job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes(job.Group.GroupType.Code) + ), + Tnk: job.Tnk == null ? null : new TnkForShortcodes( + Name: job.Tnk.Name ?? string.Empty, + ShortName: job.Tnk.ShortName ?? string.Empty + ), + WorkName: job.WorkName ?? string.Empty, + Name: job.Name ?? string.Empty + ); + } + + private async Task LoadJobForShortcodesAsync(Guid jobId, List shortcodes, string caller) + { + var query = jobService.Get().AsNoTracking(); + + // Всегда загружаем Group и GroupType, если нужны групповые шорткоды + if (shortcodes.Any(sc => + sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))) + { + query = query + .Include(j => j.Group) + .ThenInclude(g => g!.GroupType); + } + + // Для стандартных шорткодов тоже может понадобиться Group.GroupName + if (shortcodes.Any(sc => sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase))) + { + query = query + .Include(j => j.Group); + } + + // Для Tnk + if (shortcodes.Any(sc => + sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) || + sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase))) + { + query = query + .Include(j => j.Tnk); + } + + var job = await query.FirstOrDefaultAsync(j => j.Id == jobId).ConfigureAwait(false); + if (job == null) + throw new InvalidOperationException($"Job {jobId} не найден."); + + return MapJobForShortcodes(job); + } + + private async Task ApplyAllShortcodesOnceAsync(string str, TemplateForShortcodes data, List shortcodesInMask, string caller) + { + var result = str; + + // 1. Константы + var nameConstants = settingsFromDb.TemplateNameConstantPartsList; + if (shortcodesInMask.Any(m => nameConstants.Any(c => $"%{c.Name}%".Equals(m, StringComparison.OrdinalIgnoreCase)))) + { + result = ReplaceConstants(nameConstants, result); + } + + // 2. Стандартные + var hasStandardShortcodes = shortcodesInMask.Any(m => SupportedStandardShortcodes.Contains(m)); + if (hasStandardShortcodes) + { + var unitName = await GetUnitNameForTemplateAsync(data.UnitId).ConfigureAwait(false); + result = ReplaceStandardShortcodes(data.Job, unitName, result); + } + + // 3. GroupJob: %ИНДЕКС%, %МАКС:..., %ГР_ПОЛЕ-ПН% + if (shortcodesInMask.Any(m => string.Equals(m, "%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))) + { + result = ReplaceSingleShortcode(result, "%ИНДЕКС%", data.Index?.ToString()); + } + + if (data.Job?.Group?.GroupType?.Code == JobGroupTypesEnum.Group) + { + var maxShortcodes = MaxShortcodeRegex.Matches(result); + if (maxShortcodes.Count > 0) + { + result = await ReplaceMaxShortcodesAsync(data.Job.Group.Id, data.UnitId, result, maxShortcodes, caller).ConfigureAwait(false); } } - // 3. %СВЯЗИ% или %СВЯЗИ-ПН% (если всё ещё зависят от jobId/unitId) + if (shortcodesInMask.Any(m => string.Equals(m, "%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase))) + { + if (data.Job?.Group == null) + { + logger.LogWarning("[{Caller}] Job не содержит Group, необходимый для %ГР_ПОЛЕ-ПН%. Шорткод пропущен.", caller); + } + else + { + List unitIds; + bool wasLoadedFromDb = false; + + if (data.UnitsInTemplate.Count == 0) + { + logger.LogWarning( + "[{Caller}] Template {TemplateId} не содержит UnitsInTemplate. Данные будут догружены из БД. " + + "Рекомендуется обновить запрос с Include(t => t.UnitsInTemplate).", + caller, data.Id); + + var fullTemplate = await templateService.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .FirstOrDefaultAsync(t => t.Id == data.Id) + .ConfigureAwait(false); + + unitIds = fullTemplate?.UnitsInTemplate?.Select(uit => uit.UnitId).ToList() ?? new List(); + wasLoadedFromDb = true; + } + else + { + unitIds = data.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); + } + + if (unitIds.Count == 0) + { + logger.LogDebug("[{Caller}] Нет UnitsInTemplate для шаблона {TemplateId}. %ГР_ПОЛЕ-ПН% заменён на пустую строку.", caller, data.Id); + result = Regex.Replace(result, "%ГР_ПОЛЕ-ПН%", "", RegexOptions.IgnoreCase); + } + else + { + var units = await unitService.Get() + .AsNoTracking() + .Where(u => unitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u).ConfigureAwait(false); + + var groupingFieldId = data.Job.Group.GroupingUnitFieldId; + Dictionary valuesByUnit = new(); + + if (groupingFieldId.HasValue) + { + var fieldValues = await unitInValueService.Get() + .AsNoTracking() + .Include(uv => uv.Value) + .Where(uv => + uv.FieldId == groupingFieldId.Value && + unitIds.Contains(uv.UnitId) && + uv.Value != null && + !string.IsNullOrWhiteSpace(uv.Value.Value)) + .Select(uv => new { uv.UnitId, Value = uv.Value!.Value }) + .ToListAsync().ConfigureAwait(false); + + valuesByUnit = fieldValues + .GroupBy(x => x.UnitId) + .ToDictionary(g => g.Key, g => string.Join(", ", g.OrderBy(v => v.Value).Select(v => v.Value))); + } + + var actualUnitsInTemplate = unitIds.Select(id => new UnitInTemplateForShortcodes(id)).ToList(); + + var sortedUnitsInTemplate = actualUnitsInTemplate + .OrderBy(uit => units.TryGetValue(uit.UnitId, out var u) ? u.Name : uit.UnitId.ToString()) + .ToList(); + + var lines = sortedUnitsInTemplate + .Select((uit, i) => + { + var unitName = units.TryGetValue(uit.UnitId, out var u) ? u.Name : $"(UnitId={uit.UnitId})"; + var valuesStr = valuesByUnit.TryGetValue(uit.UnitId, out var vals) ? vals : ""; + return $"{i + 1}. {unitName} ({valuesStr})"; + }); + + result = Regex.Replace(result, "%ГР_ПОЛЕ-ПН%", string.Join("\n", lines), RegexOptions.IgnoreCase); + } + } + } + + // 4. Transform: %БУКВЫ:... + var lettersShortcodes = LettersShortcodeRegex.Matches(result); + if (lettersShortcodes.Count > 0) + { + result = await ReplaceLettersShortcodesAsync(data.UnitId, result, lettersShortcodes, caller).ConfigureAwait(false); + } + + // 5. %СВЯЗИ% и %СВЯЗИ-ПН% List? relatedUnitNames = null; - - if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase))) + if (shortcodesInMask.Any(m => string.Equals(m, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase))) { - relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId); - var linksText = string.Join("\n", relatedUnitNames); - resultName = Regex.Replace(resultName, "%СВЯЗИ%", linksText, RegexOptions.IgnoreCase); + if (data.Job != null) + { + relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(data.JobId, data.UnitId).ConfigureAwait(false); + result = Regex.Replace(result, "%СВЯЗИ%", string.Join("\n", relatedUnitNames), RegexOptions.IgnoreCase); + } + } + if (shortcodesInMask.Any(m => string.Equals(m, "%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase))) + { + if (data.Job != null) + { + relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(data.JobId, data.UnitId).ConfigureAwait(false); + var numbered = relatedUnitNames.Select((name, i) => $"{i + 1}. {name}"); + result = Regex.Replace(result, "%СВЯЗИ-ПН%", string.Join("\n", numbered), RegexOptions.IgnoreCase); + } } - if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase))) - { - relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId); - var linksText = string.Join("\n", relatedUnitNames.Select((name, i) => $"{i + 1}. {name}")); - resultName = Regex.Replace(resultName, "%СВЯЗИ-ПН%", linksText, RegexOptions.IgnoreCase); - } + // 6. Поля + var fieldShortcodes = GetShortCodes(result); + if (fieldShortcodes.Count > 0) + result = await ReplaceFieldValues(data.UnitId, result, fieldShortcodes).ConfigureAwait(false); - // 4. Поля (оставшиеся %FIELD_NAME%) - shortcodesInMask = GetShortCodes(resultName); - if (shortcodesInMask.Count > 0) - resultName = await ReplaceFieldValues(template.UnitId, resultName, shortcodesInMask); - - logger.LogDebug("Подстановка завершена. Результат: '{Result}'", resultName); - - return resultName; + return result; } - private bool IsAnyShortcodes(string str) + private async Task GetUnitNameForTemplateAsync(Guid unitId) { - return Regex.IsMatch(str, shortcodePattern) || - Regex.IsMatch(str, maxShortcodePattern) || - Regex.IsMatch(str, lettersShortcodePattern); + var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == unitId).ConfigureAwait(false); + if (unit == null || string.IsNullOrEmpty(unit.Name)) + throw new InvalidOperationException($"Unit {unitId} не найден или не содержит Name."); + return unit.Name; } private static List GetShortCodes(string resultName) { - var shortcodesInMask = Regex.Matches(resultName, shortcodePattern).ToList(); + var shortcodesInMask = GeneralShortcodeRegex.Matches(resultName).ToList(); return shortcodesInMask; } @@ -305,32 +512,48 @@ namespace PARR.DAL.DomainServices.Shortcodes { new ShortcodeInfoDto { Shortcode = "%ЭК%", Description = "Наименование ЭК(Код поиска)", - Type = ShortcodeTypeEnum.Standart }, + Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%ГРУППА_РАБОТ%", Description = "Наименование группы работ", - Type = ShortcodeTypeEnum.Standart }, + Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%РАБОТА%", Description = "Наименование работы в АСУ ЕСПП", - Type = ShortcodeTypeEnum.Standart }, + Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%ТНК%", Description = "Полное наименование ТНК", - Type = ShortcodeTypeEnum.Standart }, + Type = ShortcodeTypeEnum.Standard }, new ShortcodeInfoDto { Shortcode = "%ТНК-КРАТКО%", Description = "Краткое наименование ТНК", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto { Shortcode = "%ИНДЕКС%", - Description = "Порядковый индекс шаблона для групповых работ", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto { Shortcode = "%МАКС:ИМЯ АТРИБУТА%", - Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК%", - Type = ShortcodeTypeEnum.Standart }, - new ShortcodeInfoDto{ Shortcode = "%БУКВЫ:ИМЯ АТРИБУТА%", - Description = "Извлекает только буквы из значения поля. Пример: %БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ% → ПРИВ", - Type = ShortcodeTypeEnum.Standart + Type = ShortcodeTypeEnum.Standard }, + new ShortcodeInfoDto { Shortcode = "%ТИКТАК%", + Description = "Текущее время в формате Unix timestamp (секунды с 1970-01-01 UTC)", + Type = ShortcodeTypeEnum.Standard } }); - // 3. Связи + // 3. Групповые шорткоды + result.AddRange(new[] + { + new ShortcodeInfoDto { Shortcode = "%ИНДЕКС%", + Description = "Порядковый индекс шаблона для групповых работ", + Type = ShortcodeTypeEnum.GroupValue }, + new ShortcodeInfoDto { Shortcode = "%МАКС:ИМЯ АТРИБУТА%", + Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК%", + Type = ShortcodeTypeEnum.GroupValue }, + new ShortcodeInfoDto { Shortcode = "%ГР_ПОЛЕ-ПН%", + Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.", + Type = ShortcodeTypeEnum.GroupValue }, + }); + + // 4. Трансформирующие шорткоды + result.Add(new ShortcodeInfoDto + { + Shortcode = "%БУКВЫ:ИМЯ АТРИБУТА%", + Description = "Извлекает только буквы из значения поля. Пример: %БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ% → ПРИВ", + Type = ShortcodeTypeEnum.Transform + }); + + // 5. Связи result.AddRange(new[] { new ShortcodeInfoDto { Shortcode = "%СВЯЗИ%", @@ -339,13 +562,10 @@ namespace PARR.DAL.DomainServices.Shortcodes new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК", Type = ShortcodeTypeEnum.Relationship }, - new ShortcodeInfoDto { Shortcode = "%ГР_ПОЛЕ-ПН%", - Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.", - Type = ShortcodeTypeEnum.Relationship }, }); - // 4. Все доступные поля из UnitField - var fieldNames = await unitFieldService.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync(); + // 6. Все доступные поля из UnitField + var fieldNames = await unitFieldService.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 @@ -367,7 +587,7 @@ namespace PARR.DAL.DomainServices.Shortcodes if (requiredFieldNames.Count == 0) return resultName; - var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames); + var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames).ConfigureAwait(false); var fieldValuesMap = fieldValues .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase) @@ -395,15 +615,56 @@ namespace PARR.DAL.DomainServices.Shortcodes return resultName; } - private static string ReplaceStandardShortcodes(JobForShortcodes job, Unit unit, string input, int? index = null) + private static string ReplaceStandardShortcodes(JobForShortcodes? job, string unitName, string input) { - return input - .Replace("%ЭК%", unit.Name, StringComparison.OrdinalIgnoreCase) - .Replace("%ГРУППА_РАБОТ%", job.Group?.GroupName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("%РАБОТА%", job.WorkName, StringComparison.OrdinalIgnoreCase) - .Replace("%ТНК%", job.Tnk?.Name ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("%ТНК-КРАТКО%", job.Tnk?.ShortName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("%ИНДЕКС%", index?.ToString() ?? "", StringComparison.OrdinalIgnoreCase); + var result = input; + + // %ЭК% — заменяем, только если есть в строке + if (result.Contains("%ЭК%", StringComparison.OrdinalIgnoreCase)) + { + result = ReplaceSingleShortcode(result, "%ЭК%", unitName); + } + + // %ТИКТАК% — не зависит от Job или Unit + if (result.Contains("%ТИКТАК%", StringComparison.OrdinalIgnoreCase)) + { + result = ReplaceSingleShortcode(result, "%ТИКТАК%", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()); + } + + if (job != null) + { + if (result.Contains("%РАБОТА%", StringComparison.OrdinalIgnoreCase)) + { + result = ReplaceSingleShortcode(result, "%РАБОТА%", job.WorkName); + } + + if (result.Contains("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase)) + { + result = ReplaceSingleShortcode(result, "%ГРУППА_РАБОТ%", job.Group?.GroupName); + } + + if (result.Contains("%ТНК%", StringComparison.OrdinalIgnoreCase)) + { + result = ReplaceSingleShortcode(result, "%ТНК%", job.Tnk?.Name); + } + + if (result.Contains("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)) + { + result = ReplaceSingleShortcode(result, "%ТНК-КРАТКО%", job.Tnk?.ShortName); + } + } + + return result; + } + + private static string ReplaceSingleShortcode(string input, string shortcode, string? replacement) + { + if (replacement == null) + { + throw new InvalidOperationException($"Шорткод '{shortcode}' требует непустое значение, но подставляемое значение равно null."); + } + + return input.Replace(shortcode, replacement, StringComparison.OrdinalIgnoreCase); } private static string ReplaceConstants(List nameConstants, string resultName) @@ -414,17 +675,17 @@ namespace PARR.DAL.DomainServices.Shortcodes return resultName; } - private async Task ReplaceMaxShortcodesAsync(Guid jobGroupId, Guid unitId, string input, MatchCollection maxShortcodes) + private async Task ReplaceMaxShortcodesAsync(Guid jobGroupId, Guid unitId, string input, MatchCollection maxShortcodes, string caller) { if (jobGroupId == Guid.Empty) { - logger.LogWarning("jobGroupId не задан. Пропускаем обработку %МАКС:...%"); + logger.LogWarning("[{Caller}] jobGroupId не задан. Пропускаем обработку %МАКС:...%", caller); return input; } if (unitId == Guid.Empty) { - logger.LogWarning("unitId не задан. Пропускаем обработку %МАКС:...%"); + logger.LogWarning("[{Caller}] unitId не задан. Пропускаем обработку %МАКС:...%", caller); return input; } @@ -439,10 +700,10 @@ namespace PARR.DAL.DomainServices.Shortcodes var matches = kvp.Value; var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim(); - logger.LogDebug("Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}, fieldName {FieldName}", - fullShortcode, jobGroupId, unitId, fieldName); + logger.LogDebug("[{Caller}] Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}, fieldName {FieldName}", + caller, fullShortcode, jobGroupId, unitId, fieldName); - var mostFrequentValue = await GetMaxShortCodeFromCacheOrDbAsync(jobGroupId, unitId, fullShortcode, fieldName); + var mostFrequentValue = await GetMaxShortCodeFromCacheOrDbAsync(jobGroupId, unitId, fullShortcode, fieldName, caller).ConfigureAwait(false); foreach (var match in matches) input = input.Replace(match.Value, mostFrequentValue); @@ -451,30 +712,30 @@ namespace PARR.DAL.DomainServices.Shortcodes return input; } - private async Task GetMaxShortCodeFromCacheOrDbAsync(Guid jobGroupId, Guid unitId, string fullShortcode, string fieldName) + private async Task GetMaxShortCodeFromCacheOrDbAsync(Guid jobGroupId, Guid unitId, string fullShortcode, string fieldName, string caller) { if (jobGroupId == Guid.Empty) { - logger.LogWarning("jobGroupId не задан. Пропускаем обработку шорткода {Shortcode}", fullShortcode); - return "Нет данных"; + logger.LogWarning("[{Caller}] jobGroupId не задан. Пропускаем обработку шорткода {Shortcode}", caller, fullShortcode); + return NoContent; } if (unitId == Guid.Empty) { - logger.LogWarning("unitId не задан. Пропускаем обработку шорткода {Shortcode}", fullShortcode); - return "Нет данных"; + logger.LogWarning("[{Caller}] unitId не задан. Пропускаем обработку шорткода {Shortcode}", caller, fullShortcode); + return NoContent; } var cacheKey = $"gr_shcd_{jobGroupId:N}_{unitId:N}_{ComputeHash(fullShortcode)}"; - var cachedData = await cacheService.GetCachedDataAsync(cacheKey); + var cachedData = await cacheService.GetCachedDataAsync(cacheKey).ConfigureAwait(false); if (cachedData != null) { - logger.LogDebug("Кэш попал для GroupedShortCode '{Name}': {Value}", fullShortcode, cachedData.Value); + logger.LogDebug("[{Caller}] Кэш попал для GroupedShortCode '{Name}': {Value}", caller, fullShortcode, cachedData.Value); return cachedData.Value; } - logger.LogDebug("Кэш промахнут для GroupedShortCode '{Name}'. Запрашиваем из БД.", fullShortcode); + logger.LogDebug("[{Caller}] Кэш промахнут для GroupedShortCode '{Name}'. Запрашиваем из БД.", caller, fullShortcode); // Загружаем юниты из БД var effectiveUnitIds = await jobService.Get() @@ -490,14 +751,14 @@ namespace PARR.DAL.DomainServices.Shortcodes .SelectMany(template => template.UnitsInTemplate) .Select(uit => uit.UnitId) .Distinct() - .ToListAsync(); + .ToListAsync().ConfigureAwait(false); - logger.LogDebug("EffectiveUnitIds: [{Ids}], Count: {Count}", string.Join(", ", effectiveUnitIds), effectiveUnitIds.Count); + logger.LogDebug("[{Caller}] EffectiveUnitIds: [{Ids}], Count: {Count}", caller, string.Join(", ", effectiveUnitIds), effectiveUnitIds.Count); // Вызываем метод из сервиса - var mostFrequentValue = await unitInValueService.GetMostFrequentValueForFieldAsync(effectiveUnitIds, fieldName); + var mostFrequentValue = await unitInValueService.GetMostFrequentValueForFieldAsync(effectiveUnitIds, fieldName).ConfigureAwait(false); - logger.LogDebug("Результат GetMostFrequentValueForFieldAsync: {Result}, для поля {FieldName}, unitIds: [{Ids}]", mostFrequentValue, fieldName, string.Join(", ", effectiveUnitIds)); + logger.LogDebug("[{Caller}] Результат GetMostFrequentValueForFieldAsync: {Result}, для поля {FieldName}, unitIds: [{Ids}]", caller, mostFrequentValue, fieldName, string.Join(", ", effectiveUnitIds)); // Не кэшируем пустые значения if (!string.IsNullOrEmpty(mostFrequentValue)) @@ -506,18 +767,18 @@ namespace PARR.DAL.DomainServices.Shortcodes { Value = mostFrequentValue, Timestamp = DateTimeOffset.UtcNow, - Source = GetType().Name, + Source = typeof(ShortcodesService).Name, Version = 1 }; - await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1)); + await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1)).ConfigureAwait(false); return mostFrequentValue; } - return "Нет данных"; + return NoContent; } - private async Task ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes) + private async Task ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes, string caller) { var shortcodeToMatches = lettersShortcodes .Cast() @@ -526,30 +787,29 @@ namespace PARR.DAL.DomainServices.Shortcodes foreach (var kvp in shortcodeToMatches) { - var fullShortcode = kvp.Key; // например, "%БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ%" + var fullShortcode = kvp.Key; var matches = kvp.Value; - var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim(); // "ЗОНА_ОТВЕТСТВЕННОСТИ" + var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim(); - logger.LogDebug("Обработка {Shortcode} для unitId {UnitId}, fieldName {FieldName}", fullShortcode, unitId, fieldName); + logger.LogDebug("[{Caller}] Обработка {Shortcode} для unitId {UnitId}, fieldName {FieldName}", caller, fullShortcode, unitId, fieldName); - // Получаем значение поля через unitInValueService.GetFieldValuesAsync - var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, new List { fieldName }); + var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, new List { fieldName }).ConfigureAwait(false); string extractedLetters = string.Empty; if (fieldValues.Any()) { - var value = fieldValues.First().Value; // Берём первое значение, если несколько + var value = fieldValues.First().Value; if (value != null) { extractedLetters = ExtractLettersOnly(value); - logger.LogDebug("Извлечены буквы: '{Letters}' из значения '{Value}'", extractedLetters, value); + logger.LogDebug("[{Caller}] Извлечены буквы: '{Letters}' из значения '{Value}'", caller, extractedLetters, value); } } if (string.IsNullOrEmpty(extractedLetters)) { - logger.LogDebug("Для шорткода {Shortcode} не найдено подходящее значение или из него нельзя извлечь буквы", fullShortcode); + logger.LogDebug("[{Caller}] Для шорткода {Shortcode} не найдено подходящее значение или из него нельзя извлечь буквы", caller, fullShortcode); } foreach (var match in matches) diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 8f10279b..8db373a5 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using PARR.DAL.Cache.Services; using PARR.DAL.Cache.Services.Base; using PARR.DAL.Configurations.DbSettings; using PARR.DAL.Context; diff --git a/PARR.DAL/Services/Implementations/TemplateService.cs b/PARR.DAL/Services/Implementations/TemplateService.cs index cde95af6..d101f53e 100644 --- a/PARR.DAL/Services/Implementations/TemplateService.cs +++ b/PARR.DAL/Services/Implementations/TemplateService.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Npgsql; +using PARR.Common.Domain; using PARR.Constants; using PARR.DAL.Context; using PARR.DAL.Contracts; @@ -46,7 +48,7 @@ namespace PARR.DAL.Services.Implementations .ThenInclude(p => p!.Process) .Include(t => t.Job) .ThenInclude(t => t!.Group) - .ThenInclude(t=>t.GroupType); + .ThenInclude(t => t!.GroupType); } public override Task CreateAsync(Template obj) @@ -82,5 +84,46 @@ namespace PARR.DAL.Services.Implementations return base.CreateAsync(obj); } + + + public async Task ReserveUnusedTemplateAsync(Guid newUnitId, HistoryInitiator initiator) + { + var sql = @" + UPDATE ""Templates"" + SET ""StatusTypeId"" = @NewStatus, + ""DateModified"" = @DateModified, + ""InitiatorIp"" = @InitiatorIp, + ""InitiatorParrComponentId"" = @InitiatorComponent, + ""InitiatorComment"" = @InitiatorComment + WHERE ""Id"" = ( + SELECT ""Id"" + FROM ""Templates"" + WHERE ""StatusTypeId"" = @OldStatus + AND ""UnitId"" != @NewUnitId + ORDER BY ""DateCreated"" ASC + LIMIT 1 + ) + RETURNING ""Id"";"; + + var parameters = new[] + { + new NpgsqlParameter("@NewStatus", (int)TemplateStatusTypeEnum.Updating), + new NpgsqlParameter("@DateModified", DateTimeOffset.UtcNow), + new NpgsqlParameter("@InitiatorIp", initiator.InitiatorIp ?? (object)DBNull.Value), + new NpgsqlParameter("@InitiatorComponent", + initiator.InitiatorParrComponentId.HasValue + ? (object)(int)initiator.InitiatorParrComponentId.Value + : DBNull.Value), + new NpgsqlParameter("@InitiatorComment", initiator.InitiatorComment ?? (object)DBNull.Value), + new NpgsqlParameter("@OldStatus", (int)TemplateStatusTypeEnum.Unused), + new NpgsqlParameter("@NewUnitId", newUnitId) + }; + + var result = await dataContext.Database + .SqlQueryRaw(sql, parameters) + .ToListAsync(); + + return result.FirstOrDefault(); + } } } diff --git a/PARR.DAL/Services/Interfaces/ITemplateService.cs b/PARR.DAL/Services/Interfaces/ITemplateService.cs index 0297e5c1..3b5d72b5 100644 --- a/PARR.DAL/Services/Interfaces/ITemplateService.cs +++ b/PARR.DAL/Services/Interfaces/ITemplateService.cs @@ -1,4 +1,4 @@ -using PARR.DAL.Contracts; +using PARR.Common.Domain; using PARR.DAL.Models; using PARR.DAL.Services.Interfaces.Base; @@ -8,6 +8,8 @@ namespace PARR.DAL.Services.Interfaces { Task GetTemplateByNameAsync(string name); + Task ReserveUnusedTemplateAsync(Guid newUnitId, HistoryInitiator initiator); + IQueryable