Files
parr_api/PARR.DAL/DomainServices/Shortcodes/ShortcodesService.cs

584 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Constants;
using PARR.DAL.Cache.Services.Base;
using PARR.DAL.Contracts;
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.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
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 static readonly HashSet<string> SupportedShortcodes = new(StringComparer.OrdinalIgnoreCase)
{
"%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%СВЯЗИ%", "%ТНК-КРАТКО%", "%СВЯЗИ-ПН%", "%ИНДЕКС%"
};
private readonly ILogger<ShortcodesService> logger;
private readonly SettingsFromDb settingsFromDb;
private readonly IJobService jobService;
private readonly IUnitService unitService;
private readonly IUnitInValueService unitInValueService;
private readonly IUnitFieldService unitFieldService;
private readonly ITemplateService templateService;
private readonly IRedisCacheService cacheService;
private readonly IUnitFilterService unitFilterService;
public ShortcodesService(
ILogger<ShortcodesService> logger,
SettingsFromDb settingsFromDb,
IJobService jobService,
IUnitService unitService,
IUnitFilterService unitFilterService,
IUnitInValueService unitInValueService,
IUnitFieldService unitFieldService,
ITemplateService templateService,
IRedisCacheService cacheService
)
{
this.logger = logger;
this.settingsFromDb = settingsFromDb;
this.jobService = jobService;
this.unitService = unitService;
this.unitInValueService = unitInValueService;
this.unitFieldService = unitFieldService;
this.templateService = templateService;
this.cacheService = cacheService;
this.unitFilterService = unitFilterService;
}
public Task<string> ApplyShortcodesAsync(string str, Template template)
{
// Построим 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<UnitInTemplateForShortcodes>()
};
return ApplyShortcodesAsync(str, templateForShortcodes);
}
//todo: сделать private в перспективе
public async Task<string> 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 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, template.Index);
iteration++;
if (resultName == oldResult)
{
logger.LogDebug("Замена стандартных шорткодов не изменила строку на итерации {Iteration}. Останов.", iteration);
break;
}
}
if (iteration >= MaxStandardIterations)
{
logger.LogWarning(
"Достигнуто максимальное число итераций ({Max}) при замене стандартных шорткодов. Текущий результат: {Result}",
MaxStandardIterations, resultName);
}
// 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);
}
else
{
var unitIds = unitsInTemplate.Select(uit => uit.UnitId).ToList();
var units = await unitService.Get()
.AsNoTracking()
.Where(u => unitIds.Contains(u.Id))
.ToListAsync();
// Словарь: UnitId -> Unit (для быстрого поиска)
var unitDict = units.ToDictionary(u => u.Id, u => u);
var groupingFieldId = job.Group?.GroupingUnitFieldId;
Dictionary<Guid, string> 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();
valuesByUnit = fieldValues
.GroupBy(x => x.UnitId)
.ToDictionary(
g => g.Key,
g => string.Join(", ", g.Select(v => v.Value).OrderBy(v => v))
);
}
// Сортируем UnitsInTemplate по имени юнита
var sortedUnitsInTemplate = unitsInTemplate
.OrderBy(uit => unitDict.TryGetValue(uit.UnitId, out var unit) ? unit.Name : $"(UnitId={uit.UnitId})")
.ToList();
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();
var resultText = string.Join("\n", lines);
resultName = Regex.Replace(resultName, "%ГРОЛЕ-ПН%", resultText, RegexOptions.IgnoreCase);
}
}
// 3. %СВЯЗИ% или %СВЯЗИ-ПН% (если всё ещё зависят от jobId/unitId)
List<string>? relatedUnitNames = null;
if (shortcodesInMask.Any(m => string.Equals(m.Value, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase)))
{
relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId);
var linksText = string.Join("\n", relatedUnitNames);
resultName = Regex.Replace(resultName, "%СВЯЗИ%", linksText, 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);
}
// 4. Поля (оставшиеся %FIELD_NAME%)
shortcodesInMask = GetShortCodes(resultName);
if (shortcodesInMask.Count > 0)
resultName = await ReplaceFieldValues(template.UnitId, resultName, shortcodesInMask);
logger.LogDebug("Подстановка завершена. Результат: '{Result}'", resultName);
return resultName;
}
private bool IsAnyShortcodes(string str)
{
return Regex.IsMatch(str, shortcodePattern) ||
Regex.IsMatch(str, maxShortcodePattern) ||
Regex.IsMatch(str, lettersShortcodePattern);
}
private static List<Match> GetShortCodes(string resultName)
{
var shortcodesInMask = Regex.Matches(resultName, shortcodePattern).ToList();
return shortcodesInMask;
}
public async Task<List<ShortcodeInfoDto>> GetAvailableShortcodesAsync()
{
var result = new List<ShortcodeInfoDto>();
// 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 },
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 },
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();
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<string> ReplaceFieldValues(Guid unitId, string resultName, List<Match> shortcodesInMask)
{
var requiredFieldNames = shortcodesInMask
.Select(m => m.Value.Trim('%').ToUpperInvariant())
.ToList();
if (requiredFieldNames.Count == 0)
return resultName;
var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames);
var fieldValuesMap = fieldValues
.GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => g.Select(x => x.Value).ToList(),
StringComparer.OrdinalIgnoreCase);
foreach (var match in shortcodesInMask)
{
var fieldName = match.Value.Trim('%').ToUpperInvariant();
if (fieldValuesMap.TryGetValue(fieldName, out var values))
{
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(JobForShortcodes job, Unit unit, string input, int? index = null)
{
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);
}
private static string ReplaceConstants(List<BLL.Domain.TemplateNameConstantPart> nameConstants, string resultName)
{
foreach (var item in nameConstants)
resultName = resultName.Replace($"%{item.Name}%", item.Value);
return resultName;
}
private async Task<string> ReplaceMaxShortcodesAsync(Guid jobGroupId, Guid unitId, string input, MatchCollection maxShortcodes)
{
if (jobGroupId == Guid.Empty)
{
logger.LogWarning("jobGroupId не задан. Пропускаем обработку %МАКС:...%");
return input;
}
if (unitId == Guid.Empty)
{
logger.LogWarning("unitId не задан. Пропускаем обработку %МАКС:...%");
return input;
}
var shortcodeToMatches = maxShortcodes
.Cast<Match>()
.GroupBy(m => m.Value, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
foreach (var kvp in shortcodeToMatches)
{
var fullShortcode = kvp.Key;
var matches = kvp.Value;
var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim();
logger.LogDebug("Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}, fieldName {FieldName}",
fullShortcode, jobGroupId, unitId, fieldName);
var mostFrequentValue = await GetMaxShortCodeFromCacheOrDbAsync(jobGroupId, unitId, fullShortcode, fieldName);
foreach (var match in matches)
input = input.Replace(match.Value, mostFrequentValue);
}
return input;
}
private async Task<string> GetMaxShortCodeFromCacheOrDbAsync(Guid jobGroupId, Guid unitId, string fullShortcode, string fieldName)
{
if (jobGroupId == Guid.Empty)
{
logger.LogWarning("jobGroupId не задан. Пропускаем обработку шорткода {Shortcode}", fullShortcode);
return "Нет данных";
}
if (unitId == Guid.Empty)
{
logger.LogWarning("unitId не задан. Пропускаем обработку шорткода {Shortcode}", fullShortcode);
return "Нет данных";
}
var cacheKey = $"gr_shcd_{jobGroupId:N}_{unitId:N}_{ComputeHash(fullShortcode)}";
var cachedData = await cacheService.GetCachedDataAsync<CachedGroupedShortCode>(cacheKey);
if (cachedData != null)
{
logger.LogDebug("Кэш попал для GroupedShortCode '{Name}': {Value}", fullShortcode, cachedData.Value);
return cachedData.Value;
}
logger.LogDebug("Кэш промахнут для GroupedShortCode '{Name}'. Запрашиваем из БД.", fullShortcode);
// Загружаем юниты из БД
var effectiveUnitIds = await jobService.Get()
.Where(j => j.GroupId == jobGroupId)
.Join(
templateService.Get()
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used && t.UnitId == unitId)
.Include(t => t.UnitsInTemplate),
job => job.Id,
template => template.JobId,
(job, template) => template
)
.SelectMany(template => template.UnitsInTemplate)
.Select(uit => uit.UnitId)
.Distinct()
.ToListAsync();
logger.LogDebug("EffectiveUnitIds: [{Ids}], Count: {Count}", string.Join(", ", effectiveUnitIds), effectiveUnitIds.Count);
// Вызываем метод из сервиса
var mostFrequentValue = await unitInValueService.GetMostFrequentValueForFieldAsync(effectiveUnitIds, fieldName);
logger.LogDebug("Результат GetMostFrequentValueForFieldAsync: {Result}, для поля {FieldName}, unitIds: [{Ids}]", mostFrequentValue, fieldName, string.Join(", ", effectiveUnitIds));
// Не кэшируем пустые значения
if (!string.IsNullOrEmpty(mostFrequentValue))
{
var toCache = new CachedGroupedShortCode
{
Value = mostFrequentValue,
Timestamp = DateTimeOffset.UtcNow,
Source = GetType().Name,
Version = 1
};
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1));
return mostFrequentValue;
}
return "Нет данных";
}
private async Task<string> ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes)
{
var shortcodeToMatches = lettersShortcodes
.Cast<Match>()
.GroupBy(m => m.Value, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
foreach (var kvp in shortcodeToMatches)
{
var fullShortcode = kvp.Key; // например, "%БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ%"
var matches = kvp.Value;
var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim(); // "ЗОНА_ОТВЕТСТВЕННОСТИ"
logger.LogDebug("Обработка {Shortcode} для unitId {UnitId}, fieldName {FieldName}", fullShortcode, unitId, fieldName);
// Получаем значение поля через unitInValueService.GetFieldValuesAsync
var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, new List<string> { fieldName });
string extractedLetters = string.Empty;
if (fieldValues.Any())
{
var value = fieldValues.First().Value; // Берём первое значение, если несколько
if (value != null)
{
extractedLetters = ExtractLettersOnly(value);
logger.LogDebug("Извлечены буквы: '{Letters}' из значения '{Value}'", extractedLetters, value);
}
}
if (string.IsNullOrEmpty(extractedLetters))
{
logger.LogDebug("Для шорткода {Shortcode} не найдено подходящее значение или из него нельзя извлечь буквы", fullShortcode);
}
foreach (var match in matches)
{
input = input.Replace(match.Value, extractedLetters);
}
}
return input;
}
private static string ExtractLettersOnly(string input)
{
var result = new System.Text.StringBuilder();
foreach (char c in input)
{
if (char.IsLetter(c))
{
result.Append(c);
}
}
return result.ToString();
}
private static string ComputeHash(string input)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(hashedBytes).Replace('+', '-').Replace('/', '_').Substring(0, 16);
}
}
}