2025-12-23 18:27:31 +10:00
using Microsoft.EntityFrameworkCore ;
using Microsoft.Extensions.Logging ;
using PARR.Constants ;
2026-01-15 15:01:56 +10:00
using PARR.DAL.Cache.Services.Base ;
2025-12-23 18:27:31 +10:00
using PARR.DAL.Contracts ;
using PARR.DAL.DomainModels ;
using PARR.DAL.DomainServices.Shortcodes.Models ;
2026-01-21 18:25:09 +10:00
using PARR.DAL.DomainServices.UnitFilterService ;
2026-01-13 14:36:07 +10:00
using PARR.DAL.Models ;
2026-01-16 18:26:06 +10:00
using PARR.DAL.Models.Job ;
2025-12-23 18:27:31 +10:00
using PARR.DAL.Services.Interfaces ;
using PARR.DAL.Services.Interfaces.Job ;
using PARR.DAL.Services.Interfaces.Unit ;
2026-01-16 18:26:06 +10:00
using System.Runtime.CompilerServices ;
2025-12-23 18:27:31 +10:00
using System.Text.RegularExpressions ;
namespace PARR.DAL.DomainServices.Shortcodes
{
2026-01-16 18:26:06 +10:00
/// <summary>
/// Сервис для подстановки шорткодов в строке на основе данных шаблона.
/// </summary>
2025-12-23 18:27:31 +10:00
internal class ShortcodesService : IShortcodesService
{
2026-01-16 18:26:06 +10:00
private const string MissingJobWarningMsg =
"[{Caller}] Шаблон {TemplateId} не содержит Job в переданных данных. Данные будут догружены из БД. " +
"Рекомендуется обновить запрос шаблона с Include(j => j.Job)." ;
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 < string > SupportedStandardShortcodes = new ( StringComparer . OrdinalIgnoreCase )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
"%ЭК%" , "%ГР У ППА _Р А БО Т %" , "%РАБОТА%" , "%Т Н К %" , "%Т Н К -К Р А Т К О %" , "%ТИКТАК%"
2025-12-23 18:27:31 +10:00
} ;
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 ;
2025-12-29 19:21:51 +10:00
private readonly IRedisCacheService cacheService ;
2025-12-23 18:27:31 +10:00
private readonly IUnitFilterService unitFilterService ;
2026-01-16 18:26:06 +10:00
// Используем record struct вместо class
private record class TemplateForShortcodes (
Guid Id ,
int? Index ,
Guid JobId ,
Guid UnitId ,
JobForShortcodes ? Job ,
List < UnitInTemplateForShortcodes > 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
) ;
2025-12-23 18:27:31 +10:00
public ShortcodesService (
ILogger < ShortcodesService > logger ,
SettingsFromDb settingsFromDb ,
IJobService jobService ,
IUnitService unitService ,
IUnitFilterService unitFilterService ,
IUnitInValueService unitInValueService ,
IUnitFieldService unitFieldService ,
ITemplateService templateService ,
2025-12-29 19:21:51 +10:00
IRedisCacheService cacheService
2025-12-23 18:27:31 +10:00
)
{
this . logger = logger ;
this . settingsFromDb = settingsFromDb ;
this . jobService = jobService ;
this . unitService = unitService ;
this . unitInValueService = unitInValueService ;
this . unitFieldService = unitFieldService ;
this . templateService = templateService ;
2025-12-29 19:21:51 +10:00
this . cacheService = cacheService ;
2025-12-23 18:27:31 +10:00
this . unitFilterService = unitFilterService ;
}
2026-01-16 18:26:06 +10:00
/// <summary>
/// Подставляет шорткоды в строке на основе данных шаблона.
/// </summary>
/// <param name="str">Строка с шорткодами</param>
/// <param name="template">Шаблон, из которого берутся данные</param>
/// <returns>Строка с подставленными значениями</returns>
public async Task < string > ApplyShortcodesAsync ( string str , Template template , [ CallerMemberName ] string? caller = null )
{
if ( string . IsNullOrEmpty ( str ) ) return str ;
if ( template = = null ) throw new ArgumentNullException ( nameof ( template ) ) ;
var callerName = caller ? ? "Unknown" ;
logger . LogDebug ( "[{Caller}] Начата подстановка шорткодов. Вход: '{Input}', templateId={TemplateId}" , callerName , str , template . Id ) ;
var result = str ;
var iteration = 0 ;
while ( iteration < MaxIterations )
{
var shortcodes = GetShortCodes ( result ) . Select ( m = > m . Value ) . ToList ( ) ;
if ( ! shortcodes . Any ( ) ) break ;
// Подготавливаем данные с догрузкой при необходимости
var data = await PrepareTemplateDataAsync ( template , shortcodes , callerName ) . ConfigureAwait ( false ) ;
var oldResult = result ;
// Подстановка всех типов шорткодов
result = await ApplyAllShortcodesOnceAsync ( result , data , shortcodes , callerName ) . ConfigureAwait ( false ) ;
iteration + + ;
if ( result = = oldResult ) break ;
}
logger . LogDebug ( "[{Caller}] Подстановка завершена. Результат: '{Result}'" , callerName , result ) ;
return result ;
}
private async Task < TemplateForShortcodes > PrepareTemplateDataAsync ( Template template , List < string > shortcodes , string caller )
2026-01-13 14:36:07 +10:00
{
2026-01-16 18:26:06 +10:00
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 < UnitInTemplateForShortcodes > ( )
) ;
// Загружаем 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 ( ) )
2026-01-13 14:36:07 +10:00
{
2026-01-16 18:26:06 +10:00
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 ( ) ;
logger . LogWarning ( MissingIncludesWarningMsg , caller , template . Id , string . Join ( ", " , problematicShortcodes ) , string . Join ( " " , missingIncludes ) ) ;
var jobData = await LoadJobForShortcodesAsync ( template . JobId , shortcodes , caller ) . ConfigureAwait ( false ) ;
return data with { Job = jobData } ;
}
else
{
var jobData = MapJobForShortcodes ( template . Job ) ;
return data with { Job = jobData } ;
}
}
2026-01-13 14:36:07 +10:00
}
2026-01-16 18:26:06 +10:00
private static bool NeedsJobForShortcodes ( List < string > 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 ) ) ) ;
}
2026-01-13 14:36:07 +10:00
2026-01-16 18:26:06 +10:00
private static List < string > GetMissingIncludesForShortcodes ( List < string > shortcodes , Job ? job )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
var missing = new List < string > ( ) ;
if ( shortcodes . Any ( sc = >
sc . Equals ( "%ГР У ППА _Р А БО Т %" , StringComparison . OrdinalIgnoreCase ) | |
sc . StartsWith ( "%М А К С :" , StringComparison . OrdinalIgnoreCase ) | |
sc . Equals ( "%ГР _ПО ЛЕ -ПН%" , StringComparison . OrdinalIgnoreCase ) | |
sc . Equals ( "%ИНДЕКС%" , StringComparison . OrdinalIgnoreCase ) )
& & job ? . Group = = null )
2026-01-13 14:36:07 +10:00
{
2026-01-16 18:26:06 +10:00
missing . Add ( ".Include(j => j.Group)" ) ;
2026-01-13 14:36:07 +10:00
}
2026-01-16 18:26:06 +10:00
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)" ) ;
}
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
if ( shortcodes . Any ( sc = >
sc . Equals ( "%Т Н К %" , StringComparison . OrdinalIgnoreCase ) | |
sc . Equals ( "%Т Н К -К Р А Т К О %" , StringComparison . OrdinalIgnoreCase ) )
& & job ? . Tnk = = null )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
missing . Add ( ".Include(j => j.Tnk)" ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
return missing ;
}
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
private async Task < string > GetUnitNameAsyncWithWarning ( Template template , List < string > shortcodes , string caller )
{
if ( shortcodes . Any ( sc = > sc . Equals ( "%ЭК%" , StringComparison . OrdinalIgnoreCase ) ) )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
logger . LogWarning ( MissingUnitNameWarningMsg , caller , template . Id ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
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." ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
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 < JobForShortcodes > LoadJobForShortcodesAsync ( Guid jobId , List < string > 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 ) ) )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
query = query
. Include ( j = > j . Group )
. ThenInclude ( g = > g ! . GroupType ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
// Для стандартных шорткодов тоже может понадобиться Group.GroupName
if ( shortcodes . Any ( sc = > sc . Equals ( "%ГР У ППА _Р А БО Т %" , StringComparison . OrdinalIgnoreCase ) ) )
{
query = query
. Include ( j = > j . Group ) ;
}
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
// Для Tnk
if ( shortcodes . Any ( sc = >
sc . Equals ( "%Т Н К %" , StringComparison . OrdinalIgnoreCase ) | |
sc . Equals ( "%Т Н К -К Р А Т К О %" , StringComparison . OrdinalIgnoreCase ) ) )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
query = query
. Include ( j = > j . Tnk ) ;
}
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
var job = await query . FirstOrDefaultAsync ( j = > j . Id = = jobId ) . ConfigureAwait ( false ) ;
if ( job = = null )
throw new InvalidOperationException ( $"Job {jobId} не найден." ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
return MapJobForShortcodes ( job ) ;
}
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
private async Task < string > ApplyAllShortcodesOnceAsync ( string str , TemplateForShortcodes data , List < string > 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 ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
// 2. Стандартные
var hasStandardShortcodes = shortcodesInMask . Any ( m = > SupportedStandardShortcodes . Contains ( m ) ) ;
if ( hasStandardShortcodes )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
var unitName = await GetUnitNameForTemplateAsync ( data . UnitId ) . ConfigureAwait ( false ) ;
result = ReplaceStandardShortcodes ( data . Job , unitName , result ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
// 3. GroupJob: %ИНДЕКС%, %М А К С :..., %ГР _ПО ЛЕ -ПН%
if ( shortcodesInMask . Any ( m = > string . Equals ( m , "%ИНДЕКС%" , StringComparison . OrdinalIgnoreCase ) ) )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
result = ReplaceSingleShortcode ( result , "%ИНДЕКС%" , data . Index ? . ToString ( ) ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
if ( data . Job ? . Group ? . GroupType ? . Code = = JobGroupTypesEnum . Group )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
var maxShortcodes = MaxShortcodeRegex . Matches ( result ) ;
if ( maxShortcodes . Count > 0 )
{
result = await ReplaceMaxShortcodesAsync ( data . Job . Group . Id , data . UnitId , result , maxShortcodes , caller ) . ConfigureAwait ( false ) ;
}
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
if ( shortcodesInMask . Any ( m = > string . Equals ( m , "%ГР _ПО ЛЕ -ПН%" , StringComparison . OrdinalIgnoreCase ) ) )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
if ( data . Job ? . Group = = null )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
logger . LogWarning ( "[{Caller}] Job не содержит Group, необходимый для %ГР _ПО ЛЕ -ПН%. Шорткод пропущен." , caller ) ;
2025-12-23 18:27:31 +10:00
}
else
{
2026-01-16 18:26:06 +10:00
List < Guid > unitIds ;
bool wasLoadedFromDb = false ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
if ( data . UnitsInTemplate . Count = = 0 )
{
logger . LogWarning (
"[{Caller}] Template {TemplateId} не содержит UnitsInTemplate. Данные будут догружены из БД. " +
"Рекомендуется обновить запрос с Include(t => t.UnitsInTemplate)." ,
caller , data . Id ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
var fullTemplate = await templateService . Get ( )
. AsNoTracking ( )
. Include ( t = > t . UnitsInTemplate )
. FirstOrDefaultAsync ( t = > t . Id = = data . Id )
. ConfigureAwait ( false ) ;
2026-01-12 15:28:28 +10:00
2026-01-16 18:26:06 +10:00
unitIds = fullTemplate ? . UnitsInTemplate ? . Select ( uit = > uit . UnitId ) . ToList ( ) ? ? new List < Guid > ( ) ;
wasLoadedFromDb = true ;
}
else
{
unitIds = data . UnitsInTemplate . Select ( uit = > uit . UnitId ) . ToList ( ) ;
}
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
if ( unitIds . Count = = 0 )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Нет UnitsInTemplate для шаблона {TemplateId}. %ГР _ПО ЛЕ -ПН% заменён на пустую строку." , caller , data . Id ) ;
result = Regex . Replace ( result , "%ГР _ПО ЛЕ -ПН%" , "" , RegexOptions . IgnoreCase ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
else
{
var units = await unitService . Get ( )
. AsNoTracking ( )
. Where ( u = > unitIds . Contains ( u . Id ) )
. ToDictionaryAsync ( u = > u . Id , u = > u ) . ConfigureAwait ( false ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
var groupingFieldId = data . Job . Group . GroupingUnitFieldId ;
Dictionary < Guid , string > valuesByUnit = new ( ) ;
2026-01-12 15:28:28 +10:00
2026-01-16 18:26:06 +10:00
if ( groupingFieldId . HasValue )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
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 ) ;
}
2025-12-23 18:27:31 +10:00
}
}
2026-01-16 18:26:06 +10:00
// 4. Transform: %БУКВЫ:...
var lettersShortcodes = LettersShortcodeRegex . Matches ( result ) ;
if ( lettersShortcodes . Count > 0 )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
result = await ReplaceLettersShortcodesAsync ( data . UnitId , result , lettersShortcodes , caller ) . ConfigureAwait ( false ) ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
// 5. %СВЯЗИ% и %СВЯЗИ-ПН%
List < string > ? relatedUnitNames = null ;
if ( shortcodesInMask . Any ( m = > string . Equals ( m , "%СВЯЗИ%" , StringComparison . OrdinalIgnoreCase ) ) )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
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 ) ;
}
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
// 6. Поля
var fieldShortcodes = GetShortCodes ( result ) ;
if ( fieldShortcodes . Count > 0 )
result = await ReplaceFieldValues ( data . UnitId , result , fieldShortcodes ) . ConfigureAwait ( false ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
return result ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
private async Task < string > GetUnitNameForTemplateAsync ( Guid unitId )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
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 ;
2025-12-23 18:27:31 +10:00
}
private static List < Match > GetShortCodes ( string resultName )
{
2026-01-16 18:26:06 +10:00
var shortcodesInMask = GeneralShortcodeRegex . Matches ( resultName ) . ToList ( ) ;
2025-12-23 18:27:31 +10:00
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 = "Наименование ЭК(Код поиска)" ,
2026-01-16 18:26:06 +10:00
Type = ShortcodeTypeEnum . Standard } ,
2025-12-23 18:27:31 +10:00
new ShortcodeInfoDto { Shortcode = "%ГР У ППА _Р А БО Т %" ,
Description = "Наименование группы работ" ,
2026-01-16 18:26:06 +10:00
Type = ShortcodeTypeEnum . Standard } ,
2025-12-23 18:27:31 +10:00
new ShortcodeInfoDto { Shortcode = "%РАБОТА%" ,
Description = "Наименование работы в А С У ЕСПП" ,
2026-01-16 18:26:06 +10:00
Type = ShortcodeTypeEnum . Standard } ,
2025-12-23 18:27:31 +10:00
new ShortcodeInfoDto { Shortcode = "%Т Н К %" ,
Description = "Полное наименование Т Н К " ,
2026-01-16 18:26:06 +10:00
Type = ShortcodeTypeEnum . Standard } ,
2025-12-23 18:27:31 +10:00
new ShortcodeInfoDto { Shortcode = "%Т Н К -К Р А Т К О %" ,
Description = "Краткое наименование Т Н К " ,
2026-01-16 18:26:06 +10:00
Type = ShortcodeTypeEnum . Standard } ,
new ShortcodeInfoDto { Shortcode = "%ТИКТАК%" ,
Description = "Текущее время в формате Unix timestamp (секунды с 1970-01-01 UTC)" ,
Type = ShortcodeTypeEnum . Standard
}
} ) ;
// 3. Групповые шорткоды
result . AddRange ( new [ ]
{
2025-12-23 18:27:31 +10:00
new ShortcodeInfoDto { Shortcode = "%ИНДЕКС%" ,
Description = "Порядковый индекс шаблона для групповых работ" ,
2026-01-16 18:26:06 +10:00
Type = ShortcodeTypeEnum . GroupValue } ,
2025-12-23 18:27:31 +10:00
new ShortcodeInfoDto { Shortcode = "%М А К С :ИМЯ АТРИБУТА%" ,
Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %М А К С :Р А БО ЧА Я_ГР _О Т В _З А _ЭК %" ,
2026-01-16 18:26:06 +10:00
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
2025-12-23 18:27:31 +10:00
} ) ;
2026-01-16 18:26:06 +10:00
// 5. Связи
2025-12-23 18:27:31 +10:00
result . AddRange ( new [ ]
{
new ShortcodeInfoDto { Shortcode = "%СВЯЗИ%" ,
Description = "Связанные ЭК (по одному на строку), выбираются только при настроенном фильтре по полям в связанных ЭК" ,
Type = ShortcodeTypeEnum . Relationship } ,
new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%" ,
Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК" ,
2025-12-29 19:21:51 +10:00
Type = ShortcodeTypeEnum . Relationship } ,
2025-12-23 18:27:31 +10:00
} ) ;
2026-01-16 18:26:06 +10:00
// 6. В с е доступные поля из UnitField
var fieldNames = await unitFieldService . Get ( ) . AsNoTracking ( ) . Select ( t = > new { t . AihitName , t . DisplayName } ) . ToListAsync ( ) . ConfigureAwait ( false ) ;
2025-12-23 18:27:31 +10:00
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 ;
2026-01-16 18:26:06 +10:00
var fieldValues = await unitInValueService . GetFieldValuesAsync ( unitId , requiredFieldNames ) . ConfigureAwait ( false ) ;
2025-12-23 18:27:31 +10:00
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 ;
}
2026-01-16 18:26:06 +10:00
private static string ReplaceStandardShortcodes ( JobForShortcodes ? job , string unitName , string input )
2025-12-23 18:27:31 +10:00
{
2026-01-16 18:26:06 +10:00
var result = input ;
// %ЭК% — заменяем, только если есть в строке
if ( result . Contains ( "%ЭК%" , StringComparison . OrdinalIgnoreCase ) )
{
result = ReplaceSingleShortcode ( result , "%ЭК%" , unitName ) ;
}
// %ТИКТАК% — не зависит от Job или Unit
if ( result . Contains ( "%ТИКТАК%" , StringComparison . OrdinalIgnoreCase ) )
{
2026-01-20 14:23:02 +10:00
result = ReplaceSingleShortcode ( result , "%ТИКТАК%" , DateTimeOffset . UtcNow . ToUnixTimeMilliseconds ( ) . ToString ( ) ) ;
2026-01-16 18:26:06 +10:00
}
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 ) ;
2025-12-23 18:27:31 +10:00
}
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 ;
}
2026-01-16 18:26:06 +10:00
private async Task < string > ReplaceMaxShortcodesAsync ( Guid jobGroupId , Guid unitId , string input , MatchCollection maxShortcodes , string caller )
2025-12-23 18:27:31 +10:00
{
2026-01-12 11:06:47 +10:00
if ( jobGroupId = = Guid . Empty )
{
2026-01-16 18:26:06 +10:00
logger . LogWarning ( "[{Caller}] jobGroupId не задан. Пропускаем обработку %М А К С :...%" , caller ) ;
2026-01-12 11:06:47 +10:00
return input ;
}
if ( unitId = = Guid . Empty )
{
2026-01-16 18:26:06 +10:00
logger . LogWarning ( "[{Caller}] unitId не задан. Пропускаем обработку %М А К С :...%" , caller ) ;
2026-01-12 11:06:47 +10:00
return input ;
}
2025-12-23 18:27:31 +10:00
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 ;
2025-12-24 11:25:25 +10:00
var fieldName = fullShortcode . Trim ( '%' ) . Split ( ':' , 2 ) [ 1 ] . Trim ( ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}, fieldName {FieldName}" ,
caller , fullShortcode , jobGroupId , unitId , fieldName ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
var mostFrequentValue = await GetMaxShortCodeFromCacheOrDbAsync ( jobGroupId , unitId , fullShortcode , fieldName , caller ) . ConfigureAwait ( false ) ;
2025-12-24 11:25:25 +10:00
2025-12-29 19:21:51 +10:00
foreach ( var match in matches )
input = input . Replace ( match . Value , mostFrequentValue ) ;
}
2025-12-24 11:25:25 +10:00
2025-12-29 19:21:51 +10:00
return input ;
}
2025-12-24 11:25:25 +10:00
2026-01-16 18:26:06 +10:00
private async Task < string > GetMaxShortCodeFromCacheOrDbAsync ( Guid jobGroupId , Guid unitId , string fullShortcode , string fieldName , string caller )
2025-12-29 19:21:51 +10:00
{
2026-01-12 11:06:47 +10:00
if ( jobGroupId = = Guid . Empty )
{
2026-01-16 18:26:06 +10:00
logger . LogWarning ( "[{Caller}] jobGroupId не задан. Пропускаем обработку шорткода {Shortcode}" , caller , fullShortcode ) ;
return NoContent ;
2026-01-12 11:06:47 +10:00
}
if ( unitId = = Guid . Empty )
{
2026-01-16 18:26:06 +10:00
logger . LogWarning ( "[{Caller}] unitId не задан. Пропускаем обработку шорткода {Shortcode}" , caller , fullShortcode ) ;
return NoContent ;
2026-01-12 11:06:47 +10:00
}
2025-12-29 19:21:51 +10:00
var cacheKey = $"gr_shcd_{jobGroupId:N}_{unitId:N}_{ComputeHash(fullShortcode)}" ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
var cachedData = await cacheService . GetCachedDataAsync < CachedGroupedShortCode > ( cacheKey ) . ConfigureAwait ( false ) ;
2025-12-29 19:21:51 +10:00
if ( cachedData ! = null )
{
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Кэш попал для GroupedShortCode '{Name}': {Value}" , caller , fullShortcode , cachedData . Value ) ;
2025-12-29 19:21:51 +10:00
return cachedData . Value ;
}
2025-12-24 11:25:25 +10:00
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Кэш промахнут для GroupedShortCode '{Name}'. Запрашиваем из БД." , caller , fullShortcode ) ;
2025-12-29 19:21:51 +10:00
// Загружаем юниты из БД
var effectiveUnitIds = await jobService . Get ( )
. Where ( j = > j . GroupId = = jobGroupId )
. Join (
templateService . Get ( )
2026-01-13 14:36:07 +10:00
. Where ( t = > t . StatusTypeId = = TemplateStatusTypeEnum . Used & & t . UnitId = = unitId )
2025-12-29 19:21:51 +10:00
. Include ( t = > t . UnitsInTemplate ) ,
job = > job . Id ,
template = > template . JobId ,
( job , template ) = > template
)
. SelectMany ( template = > template . UnitsInTemplate )
. Select ( uit = > uit . UnitId )
. Distinct ( )
2026-01-16 18:26:06 +10:00
. ToListAsync ( ) . ConfigureAwait ( false ) ;
2025-12-29 19:21:51 +10:00
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] EffectiveUnitIds: [{Ids}], Count: {Count}" , caller , string . Join ( ", " , effectiveUnitIds ) , effectiveUnitIds . Count ) ;
2025-12-29 19:21:51 +10:00
// Вызываем метод из сервиса
2026-01-16 18:26:06 +10:00
var mostFrequentValue = await unitInValueService . GetMostFrequentValueForFieldAsync ( effectiveUnitIds , fieldName ) . ConfigureAwait ( false ) ;
2025-12-29 19:21:51 +10:00
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Результат GetMostFrequentValueForFieldAsync: {Result}, для поля {FieldName}, unitIds: [{Ids}]" , caller , mostFrequentValue , fieldName , string . Join ( ", " , effectiveUnitIds ) ) ;
2025-12-29 19:21:51 +10:00
// Н е кэшируем пустые значения
if ( ! string . IsNullOrEmpty ( mostFrequentValue ) )
{
var toCache = new CachedGroupedShortCode
2025-12-23 18:27:31 +10:00
{
2025-12-29 19:21:51 +10:00
Value = mostFrequentValue ,
Timestamp = DateTimeOffset . UtcNow ,
2026-01-16 18:26:06 +10:00
Source = typeof ( ShortcodesService ) . Name ,
2025-12-29 19:21:51 +10:00
Version = 1
} ;
2026-01-16 18:26:06 +10:00
await cacheService . SetCachedDataAsync ( cacheKey , toCache , TimeSpan . FromHours ( 1 ) ) . ConfigureAwait ( false ) ;
2025-12-29 19:21:51 +10:00
return mostFrequentValue ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
return NoContent ;
2025-12-23 18:27:31 +10:00
}
2026-01-16 18:26:06 +10:00
private async Task < string > ReplaceLettersShortcodesAsync ( Guid unitId , string input , MatchCollection lettersShortcodes , string caller )
2025-12-23 18:27:31 +10:00
{
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 )
{
2026-01-16 18:26:06 +10:00
var fullShortcode = kvp . Key ;
2025-12-23 18:27:31 +10:00
var matches = kvp . Value ;
2026-01-16 18:26:06 +10:00
var fieldName = fullShortcode . Trim ( '%' ) . Split ( ':' , 2 ) [ 1 ] . Trim ( ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Обработка {Shortcode} для unitId {UnitId}, fieldName {FieldName}" , caller , fullShortcode , unitId , fieldName ) ;
2025-12-23 18:27:31 +10:00
2026-01-16 18:26:06 +10:00
var fieldValues = await unitInValueService . GetFieldValuesAsync ( unitId , new List < string > { fieldName } ) . ConfigureAwait ( false ) ;
2025-12-23 18:27:31 +10:00
string extractedLetters = string . Empty ;
if ( fieldValues . Any ( ) )
{
2026-01-16 18:26:06 +10:00
var value = fieldValues . First ( ) . Value ;
2025-12-23 18:27:31 +10:00
if ( value ! = null )
{
extractedLetters = ExtractLettersOnly ( value ) ;
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Извлечены буквы: '{Letters}' из значения '{Value}'" , caller , extractedLetters , value ) ;
2025-12-23 18:27:31 +10:00
}
}
if ( string . IsNullOrEmpty ( extractedLetters ) )
{
2026-01-16 18:26:06 +10:00
logger . LogDebug ( "[{Caller}] Для шорткода {Shortcode} не найдено подходящее значение или из него нельзя извлечь буквы" , caller , fullShortcode ) ;
2025-12-23 18:27:31 +10:00
}
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 ( ) ;
}
2025-12-29 19:21:51 +10:00
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 ) ;
}
2025-12-23 18:27:31 +10:00
}
}