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; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; using System.Text.RegularExpressions; namespace PARR.DAL.DomainServices.Implementations { internal class ShortcodesService : IShortcodesService { private const string shortcodePattern = "%[^%\\s]+%"; 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( ILogger logger, SettingsFromDb settingsFromDb, IJobService jobService, IUnitService unitService, 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) { logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', unitId={UnitId}, jobId={JobId}", str, unitId, jobId); var job = await jobService .Get().AsNoTracking() .Include(j => j.Tnk) .Include(j => j.Group) .Include(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .FirstOrDefaultAsync(j => j.Id == jobId); var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == unitId); if (job == null || 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 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; } } 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))) { relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(jobId, unitId); var linksText = string.Join("\n", relatedUnitNames); resultName = Regex.Replace(resultName, "%СВЯЗИ%", linksText, RegexOptions.IgnoreCase); } // 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) { 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) { // 1. Извлекаем имена полей из шорткодов var requiredFieldNames = shortcodesInMask .Select(m => m.Value.Trim('%').ToUpperInvariant()) .ToList(); if (requiredFieldNames.Count == 0) return resultName; // 2. Получаем значения (может быть дубль) var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames); // 3. Группируем по FieldName → список значений var fieldValuesMap = fieldValues .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase) .ToDictionary( g => g.Key, g => g.Select(x => x.Value).ToList(), // список значений (может быть null) StringComparer.OrdinalIgnoreCase); // 4. Объединяем значения через запятую (null → "null") и подставляем foreach (var match in shortcodesInMask) { var fieldName = match.Value.Trim('%').ToUpperInvariant(); if (fieldValuesMap.TryGetValue(fieldName, out var values)) { // Объединяем все значения через запятую, null заменяем на строку "null" var combinedValue = string.Join(", ", values.Select(v => v ?? "null")); resultName = resultName.Replace(match.Value, combinedValue); } else { logger.LogWarning("Поле '{FieldName}' не найдено для unitId={UnitId} при подстановке шорткода '{Shortcode}'", fieldName, unitId, match.Value); } } return resultName; } private static string ReplaceStandardShortcodes(Job job, Unit unit, 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); } private static string ReplaceConstants(List nameConstants, string resultName) { foreach (var item in nameConstants) { resultName = resultName.Replace($"%{item.Name}%", item.Value); } return resultName; } } }