@@ -0,0 +1,470 @@
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.DomainServices.Shortcodes.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 IGroupedShortcodesCacheService groupedShortcodesCacheService ;
private readonly IUnitFilterService unitFilterService ;
public ShortcodesService (
ILogger < ShortcodesService > logger ,
SettingsFromDb settingsFromDb ,
IJobService jobService ,
IUnitService unitService ,
IUnitFilterService unitFilterService ,
IUnitInValueService unitInValueService ,
IUnitFieldService unitFieldService ,
ITemplateService templateService ,
IGroupedShortcodesCacheService groupedShortcodesCacheService
)
{
this . logger = logger ;
this . settingsFromDb = settingsFromDb ;
this . jobService = jobService ;
this . unitService = unitService ;
this . unitInValueService = unitInValueService ;
this . unitFieldService = unitFieldService ;
this . templateService = templateService ;
this . groupedShortcodesCacheService = groupedShortcodesCacheService ;
this . unitFilterService = unitFilterService ;
}
public async Task < string > ApplyShortcodesAsync ( string str , TemplateForShortcodes template )
{
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 ) ; // ✅ Исправлено: job.GroupId
}
}
// 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 ( ) ;
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 ) )
) ;
}
var lines = unitsInTemplate
. Select ( ( uit , indexInList ) = >
{
var unitInList = units . FirstOrDefault ( u = > u . Id = = uit . UnitId ) ;
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 ;
}
public bool IsAnyShortcodes ( string str )
{
return Regex . IsMatch ( str , shortcodePattern ) ;
}
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 = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (З на че ние 1, З на че ние 2). Использует GroupingUnitFieldId из JobGroup." ,
Type = ShortcodeTypeEnum . Relationship } ,
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 < 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 )
{
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}" ,
fullShortcode , jobGroupId , unitId ) ;
var mostFrequentValue = await groupedShortcodesCacheService . GetAggregatedValueAsync (
unitId ,
fullShortcode ,
async ( ) = >
{
// Логика вычисления, если кэш пуст
var unitIds = await jobService . Get ( )
. Where ( j = > j . GroupId = = jobGroupId )
. Join (
templateService . Get ( )
. Where ( t = > t . StatusTypeId = = TemplateStatusTypeEnum . Used )
. Include ( t = > t . UnitsInTemplate ) ,
job = > job . Id ,
template = > template . JobId ,
( job , template ) = > template
)
. SelectMany ( template = > template . UnitsInTemplate )
. Select ( uit = > uit . UnitId )
. Distinct ( )
. ToListAsync ( ) ;
// Вызываем метод из сервиса
var result = await unitInValueService . GetMostFrequentValueForFieldAsync ( unitIds , fieldName ) ;
return result ? ? string . Empty ;
} ) ;
foreach ( var match in matches )
{
input = input . Replace ( match . Value , mostFrequentValue ) ;
}
}
return input ;
}
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 ( ) ;
}
}
}