diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index 82086c65..54d05f8f 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -1,6 +1,4 @@ -using RabbitMQ.Client.Exceptions; - -namespace PARR.API.Contracts.V1 +namespace PARR.API.Contracts.V1 { // https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/ @@ -514,6 +512,11 @@ namespace PARR.API.Contracts.V1 public const string getParamDate = "{date}"; } + public static class Shortcode + { + public const string GetAll = Base + "/shortcodes/"; + } + } } diff --git a/PARR.API/Contracts/V1/Responses/ShortcodeResponse.cs b/PARR.API/Contracts/V1/Responses/ShortcodeResponse.cs new file mode 100644 index 00000000..45a4abdf --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/ShortcodeResponse.cs @@ -0,0 +1,15 @@ +using PARR.Constants; + +namespace PARR.API.Contracts.V1.Responses +{ + public class ShortcodeResponse : ShortcodeBaseResponse + { + } + + public class ShortcodeBaseResponse + { + public required string Shortcode { get; set; } + public required string Description { get; set; } + public required ShortcodeTypeEnum Type { get; set; } + } +} diff --git a/PARR.API/Controllers/V1/RobotTaskController.cs b/PARR.API/Controllers/V1/RobotTaskController.cs index 148e5926..609a0dd1 100644 --- a/PARR.API/Controllers/V1/RobotTaskController.cs +++ b/PARR.API/Controllers/V1/RobotTaskController.cs @@ -165,17 +165,17 @@ namespace PARR.API.Controllers.V1 var robotTaskTemplateResponse = mapper.Map(task); //TODO Вынести в отдельный метод ShortcodesService - if (shortcodesService.isAnyShortcodes(robotTaskTemplateResponse.FullDescription)) + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.FullDescription)) robotTaskTemplateResponse.FullDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.FullDescription, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.isAnyShortcodes(robotTaskTemplateResponse.ShortDescription)) + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.ShortDescription)) robotTaskTemplateResponse.ShortDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.ShortDescription, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.isAnyShortcodes(robotTaskTemplateResponse.Solution)) + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.Solution)) robotTaskTemplateResponse.Solution = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.Solution, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.isAnyShortcodes(robotTaskTemplateResponse.TnkName)) + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.TnkName)) robotTaskTemplateResponse.TnkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.TnkName, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.isAnyShortcodes(robotTaskTemplateResponse.WorkName)) + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.WorkName)) robotTaskTemplateResponse.WorkName = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkName, task!.Template!.UnitId, task!.Template!.JobId); - if (shortcodesService.isAnyShortcodes(robotTaskTemplateResponse.WorkGroup)) + if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.WorkGroup)) robotTaskTemplateResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.WorkGroup, task!.Template!.UnitId, task!.Template!.JobId); return Ok(new Response(robotTaskTemplateResponse, true)); @@ -185,7 +185,7 @@ namespace PARR.API.Controllers.V1 //RobotTaskScheduleResponse var robotTaskScheduleResponse = mapper.Map(task); - if (shortcodesService.isAnyShortcodes(robotTaskScheduleResponse.WorkGroup)) + if (shortcodesService.IsAnyShortcodes(robotTaskScheduleResponse.WorkGroup)) robotTaskScheduleResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.WorkGroup, task!.Template!.UnitId, task!.Template!.JobId); return Ok(new Response(robotTaskScheduleResponse, true)); diff --git a/PARR.API/Controllers/V1/ShortcodeController.cs b/PARR.API/Controllers/V1/ShortcodeController.cs new file mode 100644 index 00000000..ef6d2c50 --- /dev/null +++ b/PARR.API/Controllers/V1/ShortcodeController.cs @@ -0,0 +1,53 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PARR.API.Contracts.V1; +using PARR.API.Contracts.V1.Responses; +using PARR.API.Contracts.V1.Responses.Base; +using PARR.API.Controllers.V1.Base; +using PARR.Constants; +using PARR.DAL.DomainServices.Interfaces; + +namespace PARR.API.Controllers.V1 +{ + /// + /// Переменные составляющие полей объектов платформы автоматизации регламентных работ + /// + [Authorize(Roles = ParrRoles.Administrator.Role)] + public class ShortcodeController : BaseApiController + { + private readonly ILogger logger; + private readonly IShortcodesService shortcodesService; + private readonly IMapper mapper; + + public ShortcodeController( + ILogger logger, + IShortcodesService shortcodesService, + IMapper mapper + ) + { + this.logger = logger; + this.shortcodesService = shortcodesService; + this.mapper = mapper; + } + + + /// + /// Получить список всех переменных составляющих + /// + /// + [HttpGet(ApiRoutes.Shortcode.GetAll)] + public async Task GetAll() + { + + var shortcodes = await shortcodesService.GetAvailableShortcodesAsync(); + + if (!shortcodes.Any()) + return NoContent(); + + var response = mapper.Map>(shortcodes); + + return Ok(new Response>(response, true)); + } + } +} diff --git a/PARR.API/MappingProfiles/DomainToResponseProfile.cs b/PARR.API/MappingProfiles/DomainToResponseProfile.cs index 18b6ff4e..143cc196 100644 --- a/PARR.API/MappingProfiles/DomainToResponseProfile.cs +++ b/PARR.API/MappingProfiles/DomainToResponseProfile.cs @@ -414,6 +414,7 @@ namespace PARR.API.MappingProfiles #endregion + CreateMap(); } } diff --git a/PARR.Constants/ShortcodeTypeEnum.cs b/PARR.Constants/ShortcodeTypeEnum.cs new file mode 100644 index 00000000..0d534fd8 --- /dev/null +++ b/PARR.Constants/ShortcodeTypeEnum.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace PARR.Constants +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum ShortcodeTypeEnum + { + Static, + Standart, + Relationship, + FieldValue + } +} diff --git a/PARR.DAL/DomainModels/ShortcodeInfoDto.cs b/PARR.DAL/DomainModels/ShortcodeInfoDto.cs new file mode 100644 index 00000000..56caa8b6 --- /dev/null +++ b/PARR.DAL/DomainModels/ShortcodeInfoDto.cs @@ -0,0 +1,11 @@ +using PARR.Constants; + +namespace PARR.DAL.DomainModels +{ + public class ShortcodeInfoDto + { + public required string Shortcode { get; set; } + public required string Description { get; set; } + public ShortcodeTypeEnum Type { get; set; } + } +} diff --git a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs b/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs index dc8ad1ae..27b5dd23 100644 --- a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs +++ b/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs @@ -1,6 +1,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using PARR.Constants; using PARR.DAL.Contracts; +using PARR.DAL.DomainModels; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.Models.Job; using PARR.DAL.Models.Unit; @@ -16,13 +18,15 @@ namespace PARR.DAL.DomainServices.Implementations private static readonly HashSet SupportedShortcodes = new(StringComparer.OrdinalIgnoreCase) { - "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%СВЯЗИ%", "%ТНК-КРАТКО%" + "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%СВЯЗИ%", "%ТНК-КРАТКО%", "%СВЯЗИ-ПН%" }; private readonly ILogger logger; private readonly SettingsFromDb settingsFromDb; private readonly IJobService jobService; private readonly IUnitService unitService; + private readonly IUnitInValueService unitInValueService; + private readonly IUnitFieldService unitFieldService; private readonly IUnitFilterService unitFilterService; public ShortcodesService( @@ -30,19 +34,23 @@ namespace PARR.DAL.DomainServices.Implementations SettingsFromDb settingsFromDb, IJobService jobService, IUnitService unitService, - IUnitFilterService unitFilterService + IUnitFilterService unitFilterService, + IUnitInValueService unitInValueService, + IUnitFieldService unitFieldService ) { this.logger = logger; this.settingsFromDb = settingsFromDb; this.jobService = jobService; this.unitService = unitService; + this.unitInValueService = unitInValueService; + this.unitFieldService = unitFieldService; this.unitFilterService = unitFilterService; } public async Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId) { - var nameConstants = settingsFromDb.TemplateNameConstantPartsList; + logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', unitId={UnitId}, jobId={JobId}", str, unitId, jobId); var job = await jobService .Get().AsNoTracking() @@ -64,60 +72,174 @@ namespace PARR.DAL.DomainServices.Implementations 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. Стандартные шорткоды (%ЭК%, %РАБОТА% и т.д.) - if (shortcodesInMask.Any(m => SupportedShortcodes.Contains(m.Value))) + // 2. Стандартные шорткоды — с поддержкой вложенных (%РАБОТА% → "Мониторинг | %ЭК%") + const int MaxStandardIterations = 3; + var iteration = 0; + + while (iteration < MaxStandardIterations) { + // Ищем ТОЛЬКО поддерживаемые шорткоды в текущей строке + var remainingShortcodes = GetShortCodes(resultName) + .Select(m => m.Value) + .Where(s => SupportedShortcodes.Contains(s)) + .ToList(); + + if (!remainingShortcodes.Any()) + break; + + // Делаем замену + var oldResult = resultName; resultName = ReplaceStandardShortcodes(job, unit, resultName); + iteration++; + + // Защита от "бесполезных" итераций (строка не изменилась) + if (resultName == oldResult) + { + logger.LogWarning("Замена стандартных шорткодов не изменила строку на итерации {Iteration}. Останов.", iteration); + break; + } } - // 3. %СВЯЗИ% — отдельная обработка + if (iteration >= MaxStandardIterations) + { + logger.LogWarning( + "Достигнуто максимальное число итераций ({Max}) при замене стандартных шорткодов. Текущий результат: {Result}", + MaxStandardIterations, resultName); + } + + // 3. %СВЯЗИ% или %СВЯЗИ-ПН% + List? relatedUnitNames = null; + + // 3.1. %СВЯЗИ% if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase))) { - var relatedUnitNames = await unitFilterService.GetRelatedUnitNamesAsync(jobId, unitId); + relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(jobId, unitId); var linksText = string.Join("\n", relatedUnitNames); resultName = Regex.Replace(resultName, "%СВЯЗИ%", linksText, RegexOptions.IgnoreCase); } - // 4. Поля (оставшиеся %FIELD_NAME%) + // 3.2. %СВЯЗИ-ПН% + if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase))) + { + relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(jobId, unitId); + var linksText = string.Join("\n", relatedUnitNames.Select((name, i) => $"{i + 1}. {name}")); + resultName = Regex.Replace(resultName, "%СВЯЗИ-ПН%", linksText, RegexOptions.IgnoreCase); + } + + // 4. Поля (оставшиеся %FIELD_NAME%) - выполняем запрос ТОЛЬКО если после предыдущих замен остались необработанные шорткоды shortcodesInMask = GetShortCodes(resultName); if (shortcodesInMask.Count > 0) - { resultName = await ReplaceFieldValues(unitId, resultName, shortcodesInMask); - } + + logger.LogDebug("Подстановка завершена. Результат: '{Result}'", resultName); return resultName; } - public bool isAnyShortcodes(string str) + public bool IsAnyShortcodes(string str) { return Regex.IsMatch(str, shortcodePattern); } + private static List GetShortCodes(string resultName) + { + var shortcodesInMask = Regex.Matches(resultName, shortcodePattern).ToList(); + return shortcodesInMask; + } + + public async Task> GetAvailableShortcodesAsync() + { + var result = new List(); + + // 1. Статические константы — из settingsFromDb + foreach (var constant in settingsFromDb.TemplateNameConstantPartsList) + { + result.Add(new ShortcodeInfoDto + { + Shortcode = $"%{constant.Name}%", + Description = $"Константа: {constant.Value ?? "(пусто)"}", + Type = ShortcodeTypeEnum.Static + }); + } + + // 2. Стандартные шорткоды + result.AddRange(new[] + { + 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 }, + new ShortcodeInfoDto { Shortcode = "%ТНК-КРАТКО%", + Description = "Краткое наименование ТНК", + Type = ShortcodeTypeEnum.Standart } + }); + + // 3. Связи + result.AddRange(new[] + { + new ShortcodeInfoDto { Shortcode = "%СВЯЗИ%", + Description = "Связанные ЭК (по одному на строку), выбираются только при настроенном фильтре по полям в связанных ЭК", + Type = ShortcodeTypeEnum.Relationship }, + new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", + Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК", + Type = ShortcodeTypeEnum.Relationship } + }); + + // 4. Все доступные поля из UnitField + var fieldNames = await unitFieldService.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync(); + foreach (var fieldName in fieldNames.OrderBy(n => n.AihitName)) + { + result.Add(new ShortcodeInfoDto + { + Shortcode = $"%{fieldName.AihitName}%", + Description = $"Атрибут: {fieldName.DisplayName ?? fieldName.AihitName}", + Type = ShortcodeTypeEnum.FieldValue + }); + } + return result; + } + + private async Task ReplaceFieldValues(Guid unitId, string resultName, List shortcodesInMask) { - var unitWithFields = await unitService.Get() - .AsNoTracking() - .Include(u => u.UnitValues) - .ThenInclude(uv => uv.Field) - .Include(u => u.UnitValues) - .ThenInclude(uv => uv.Value) - .FirstOrDefaultAsync(t => t.Id == unitId); + // 1. Извлекаем имена полей из шорткодов: %IP_АДРЕС% → "IP_АДРЕС" + var requiredFieldNames = shortcodesInMask + .Select(m => m.Value.Trim('%').ToUpper()) + .ToList(); - foreach (var item in shortcodesInMask) + if (requiredFieldNames.Count == 0) + return resultName; + + // 2. Получаем только нужные значения + var fieldValueMap = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames); + + // 3. Подставляем значения + foreach (var match in shortcodesInMask) { - var fieldName = item.Value.Replace("%", "").ToUpper(); - - var value = unitWithFields!.UnitValues!.FirstOrDefault(t => t.Field!.AihitName!.ToUpper() == fieldName!); - - if (value != null) - resultName = resultName.Replace(item.Value, value!.Value!.Value); + var fieldName = match.Value.Trim('%').ToUpper(); + if (fieldValueMap.TryGetValue(fieldName, out var fieldValue)) + resultName = resultName.Replace(match.Value, fieldValue); + else + { + logger.LogWarning("Поле '{FieldName}' не найдено для unitId={UnitId} при подстановке шорткода '{Shortcode}'", + fieldName, unitId, match.Value); + } } return resultName; @@ -144,12 +266,5 @@ namespace PARR.DAL.DomainServices.Implementations return resultName; } - - - private static List GetShortCodes(string resultName) - { - var shortcodesInMask = Regex.Matches(resultName, shortcodePattern).ToList(); - return shortcodesInMask; - } } } diff --git a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs b/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs index f604ee73..cca7e67f 100644 --- a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs +++ b/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs @@ -1,9 +1,13 @@ -namespace PARR.DAL.DomainServices.Interfaces +using PARR.DAL.DomainModels; + +namespace PARR.DAL.DomainServices.Interfaces { public interface IShortcodesService { Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId); - bool isAnyShortcodes(string str); + bool IsAnyShortcodes(string str); + + Task> GetAvailableShortcodesAsync(); } } diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs index 25f2b41b..aa818669 100644 --- a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs +++ b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs @@ -31,9 +31,40 @@ namespace PARR.DAL.Services.Implementations.Unit public async Task> GetByUnitIdsAsync(IEnumerable unitIds) { return await dataContext.UnitInValues.AsNoTracking() - .Include(t=>t.Value) + .Include(t => t.Value) .Where(uv => unitIds.Contains(uv.UnitId)) .ToListAsync(); } + + + public async Task> GetFieldValuesAsync(Guid unitId, IReadOnlyCollection aihitNames) + { + if (aihitNames == null || aihitNames.Count == 0) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var result = await dataContext.UnitInValues + .AsNoTracking() + .AsSplitQuery() + .Where(uiv => uiv.UnitId == unitId + && uiv.Field != null + && uiv.Value != null + && aihitNames.Contains(uiv.Field.AihitName.ToUpper())) + .ToDictionaryAsync( + uiv => uiv.Field!.AihitName.ToUpper(), + uiv => uiv.Value!.Value ?? "", + StringComparer.OrdinalIgnoreCase); + + // Журналируем если не нашли поля + var missing = aihitNames + .Where(name => !result.ContainsKey(name.ToUpper())) + .ToList(); + if (missing.Any()) + { + logger.LogDebug("UnitInValueService: поля не найдены для unitId={UnitId}: {Fields}", + unitId, string.Join(", ", missing)); + } + + return result; + } } } diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs index 3e8ac488..ff33d3ee 100644 --- a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs +++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs @@ -1,9 +1,4 @@ using PARR.DAL.Models.Unit; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PARR.DAL.Services.Interfaces.Unit { @@ -11,5 +6,6 @@ namespace PARR.DAL.Services.Interfaces.Unit { Task> GetByUnitIdsAsync(IEnumerable unitIds); Task> GetByUnitIdAsync(Guid unitId); + Task> GetFieldValuesAsync(Guid unitId, IReadOnlyCollection aihitNames); } } diff --git a/PARR.EsppSync/SyncService.cs b/PARR.EsppSync/SyncService.cs index 16351797..f6999525 100644 --- a/PARR.EsppSync/SyncService.cs +++ b/PARR.EsppSync/SyncService.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PARR.Constants; using PARR.DAL.Contracts; @@ -86,7 +85,7 @@ namespace PARR.EsppSync { var value = property.GetValue(dbObjectInEsppObject)?.ToString(); - if (value != null && shortcodesService.isAnyShortcodes(value)) + if (value != null && shortcodesService.IsAnyShortcodes(value)) property.SetValue(dbObjectInEsppObject, await shortcodesService.ApplyShortcodesAsync(value, template.UnitId, template.JobId)); }