feat(core): ShortcodeService в классы реализации шорткодов добавлены требования по загрузке таблиц
This commit is contained in:
@@ -73,7 +73,7 @@ namespace PARR.API.Controllers.V1
|
|||||||
await robotConfigurationService.FindUnfulfilledTaskAndSetRobotErrorStatusAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime);
|
await robotConfigurationService.FindUnfulfilledTaskAndSetRobotErrorStatusAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime);
|
||||||
|
|
||||||
|
|
||||||
var query = robotConfigurationService.Get()
|
var query = robotConfigurationService.Get().AsSingleQuery()
|
||||||
.Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode);
|
.Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode);
|
||||||
|
|
||||||
switch (robotCode)
|
switch (robotCode)
|
||||||
@@ -108,6 +108,7 @@ namespace PARR.API.Controllers.V1
|
|||||||
case RobotsEnum.ScheduleOrder:
|
case RobotsEnum.ScheduleOrder:
|
||||||
//расписание
|
//расписание
|
||||||
query = query
|
query = query
|
||||||
|
.AsSingleQuery()
|
||||||
.Include(t => t.Template)
|
.Include(t => t.Template)
|
||||||
.ThenInclude(t => t!.Unit)
|
.ThenInclude(t => t!.Unit)
|
||||||
.ThenInclude(t => t!.UnitValues)
|
.ThenInclude(t => t!.UnitValues)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ namespace PARR.Core
|
|||||||
services.AddScoped<IShortcodeHandler, IndexShortcodeHandler>();
|
services.AddScoped<IShortcodeHandler, IndexShortcodeHandler>();
|
||||||
services.AddScoped<IShortcodeHandler, GroupedFieldShortcodeHandler>();
|
services.AddScoped<IShortcodeHandler, GroupedFieldShortcodeHandler>();
|
||||||
services.AddScoped<IShortcodeHandler, MaxShortcodeHandler>();
|
services.AddScoped<IShortcodeHandler, MaxShortcodeHandler>();
|
||||||
services.AddScoped<IShortcodeHandler, RelShortcodeHandler>();
|
services.AddScoped<IShortcodeHandler, RelationshipUnitShortcodeHandler>();
|
||||||
services.AddScoped<IShortcodeHandler, LettersShortcodeHandler>();
|
services.AddScoped<IShortcodeHandler, LettersShortcodeHandler>();
|
||||||
services.AddScoped<IShortcodeHandler, RelationshipsShortcodeHandler>();
|
services.AddScoped<IShortcodeHandler, RelationshipsShortcodeHandler>();
|
||||||
services.AddScoped<IShortcodeHandler, FieldShortcodeHandler>();
|
services.AddScoped<IShortcodeHandler, FieldShortcodeHandler>();
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace PARR.Core.Services.Shortcodes.Enums
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
public enum ShortcodeDataRequirementsEnum
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Job = 1,
|
||||||
|
JobGroup = 2,
|
||||||
|
JobGroupType = 4,
|
||||||
|
JobTnk = 8,
|
||||||
|
UnitName = 16,
|
||||||
|
UnitsInTemplate = 32
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -8,11 +9,15 @@ internal class ConstantsShortcodeHandler : IShortcodeHandler
|
|||||||
{
|
{
|
||||||
private readonly SettingsFromDb settings;
|
private readonly SettingsFromDb settings;
|
||||||
public int Order => 5;
|
public int Order => 5;
|
||||||
public Regex Pattern => new(@"%[А-ЯA-Z0-9_\-]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
private static readonly Regex _pattern = new(@"%[А-ЯA-Z0-9_\-]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None;
|
||||||
|
|
||||||
public ConstantsShortcodeHandler(SettingsFromDb settings) => this.settings = settings;
|
public ConstantsShortcodeHandler(SettingsFromDb settings) => this.settings = settings;
|
||||||
|
|
||||||
public Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = input;
|
var result = input;
|
||||||
foreach (var constant in settings.TemplateNameConstantPartsList)
|
foreach (var constant in settings.TemplateNameConstantPartsList)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -10,7 +11,11 @@ internal class FieldShortcodeHandler : IShortcodeHandler
|
|||||||
private readonly IUnitInValueRepository unitInValueRepository;
|
private readonly IUnitInValueRepository unitInValueRepository;
|
||||||
private readonly ILogger<FieldShortcodeHandler> logger;
|
private readonly ILogger<FieldShortcodeHandler> logger;
|
||||||
public int Order => 90; // Всегда последним
|
public int Order => 90; // Всегда последним
|
||||||
public Regex Pattern => new(@"%[^%]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
private static readonly Regex _pattern = new(@"%[^%]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None;
|
||||||
|
|
||||||
public FieldShortcodeHandler(IUnitInValueRepository unitInValueRepository, ILogger<FieldShortcodeHandler> logger)
|
public FieldShortcodeHandler(IUnitInValueRepository unitInValueRepository, ILogger<FieldShortcodeHandler> logger)
|
||||||
{
|
{
|
||||||
@@ -18,7 +23,7 @@ internal class FieldShortcodeHandler : IShortcodeHandler
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var matches = Pattern.Matches(input);
|
var matches = Pattern.Matches(input);
|
||||||
if (matches.Count == 0) return input;
|
if (matches.Count == 0) return input;
|
||||||
@@ -30,7 +35,7 @@ internal class FieldShortcodeHandler : IShortcodeHandler
|
|||||||
|
|
||||||
if (fieldNames.Count == 0) return input;
|
if (fieldNames.Count == 0) return input;
|
||||||
|
|
||||||
var fieldValues = await unitInValueRepository.GetFieldValuesAsync(data.UnitId, fieldNames, ct).ConfigureAwait(false);
|
var fieldValues = await unitInValueRepository.GetFieldValuesAsync(template.UnitId, fieldNames, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
var valuesMap = fieldValues
|
var valuesMap = fieldValues
|
||||||
.GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -49,7 +54,7 @@ internal class FieldShortcodeHandler : IShortcodeHandler
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("[{Caller}] Поле '{FieldName}' не найдено для UnitId={UnitId}. Шорткод пропущен.", caller, fieldName, data.UnitId);
|
logger.LogDebug("[{Caller}] Поле '{FieldName}' не найдено для UnitId={UnitId}. Шорткод пропущен.", caller, fieldName, template.UnitId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -15,7 +16,14 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler
|
|||||||
private readonly ILogger<GroupedFieldShortcodeHandler> logger;
|
private readonly ILogger<GroupedFieldShortcodeHandler> logger;
|
||||||
|
|
||||||
public int Order => 30;
|
public int Order => 30;
|
||||||
public Regex Pattern => new(@"%ГР_ПОЛЕ-ПН%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
private static readonly Regex _pattern = new(@"%ГР_ПОЛЕ-ПН%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements =>
|
||||||
|
ShortcodeDataRequirementsEnum.Job |
|
||||||
|
ShortcodeDataRequirementsEnum.JobGroup |
|
||||||
|
ShortcodeDataRequirementsEnum.UnitsInTemplate;
|
||||||
|
|
||||||
public GroupedFieldShortcodeHandler(
|
public GroupedFieldShortcodeHandler(
|
||||||
IUnitRepository unitRepo,
|
IUnitRepository unitRepo,
|
||||||
@@ -29,32 +37,22 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!input.Contains("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase)) return input;
|
if (!input.Contains("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase)) return input;
|
||||||
|
|
||||||
if (data.Job?.Group == null)
|
if (template.Job?.Group == null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("[{Caller}] Job не содержит Group, необходимый для %ГР_ПОЛЕ-ПН%. Шорткод пропущен.", caller);
|
logger.LogWarning("[{Caller}] Job не содержит Group, необходимый для %ГР_ПОЛЕ-ПН%. Шорткод пропущен.", caller);
|
||||||
return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase);
|
return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
var unitsList = data.UnitsInTemplate;
|
var unitsList = template.UnitsInTemplate;
|
||||||
if (unitsList.Count == 0)
|
if (unitsList.Count == 0)
|
||||||
{
|
{
|
||||||
logger.LogWarning("[{Caller}] Template {TemplateId} не содержит UnitsInTemplate. Данные догружаются из БД.", caller, data.Id);
|
// Данные должны быть загружены оркестратором. Если пусто — значит в БД действительно нет связей.
|
||||||
var fullTemplate = await templateRepo.Get()
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(t => t.UnitsInTemplate)
|
|
||||||
.FirstOrDefaultAsync(t => t.Id == data.Id, ct);
|
|
||||||
|
|
||||||
unitsList = fullTemplate?.UnitsInTemplate
|
|
||||||
?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId))
|
|
||||||
.ToList() ?? new List<UnitInTemplateForShortcode>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unitsList.Count == 0)
|
|
||||||
return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase);
|
return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
var unitIds = unitsList.Select(u => u.UnitId).Distinct().ToList();
|
var unitIds = unitsList.Select(u => u.UnitId).Distinct().ToList();
|
||||||
var fieldValueIds = unitsList.Select(u => u.UnitFieldValueId).Distinct().ToList();
|
var fieldValueIds = unitsList.Select(u => u.UnitFieldValueId).Distinct().ToList();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace PARR.Core.Services.Shortcodes.Handlers
|
namespace PARR.Core.Services.Shortcodes.Handlers
|
||||||
@@ -16,6 +17,11 @@ namespace PARR.Core.Services.Shortcodes.Handlers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Regex Pattern { get; }
|
Regex Pattern { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Декларация данных, необходимых хендлеру для работы.
|
||||||
|
/// </summary>
|
||||||
|
ShortcodeDataRequirementsEnum Requirements { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Заменяет все найденные шорткоды в строке на вычисленные значения.
|
/// Заменяет все найденные шорткоды в строке на вычисленные значения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace PARR.Core.Services.Shortcodes.Handlers;
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
@@ -6,13 +7,18 @@ namespace PARR.Core.Services.Shortcodes.Handlers;
|
|||||||
internal class IndexShortcodeHandler : IShortcodeHandler
|
internal class IndexShortcodeHandler : IShortcodeHandler
|
||||||
{
|
{
|
||||||
public int Order => 15;
|
public int Order => 15;
|
||||||
public Regex Pattern => new(@"%ИНДЕКС%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
public Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
private static readonly Regex _pattern = new(@"%ИНДЕКС%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None;
|
||||||
|
|
||||||
|
public Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (data.Index.HasValue && input.Contains("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))
|
if (template.Index.HasValue && input.Contains("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return Task.FromResult(input.Replace("%ИНДЕКС%", data.Index.Value.ToString(), StringComparison.OrdinalIgnoreCase));
|
return Task.FromResult(input.Replace("%ИНДЕКС%", template.Index.Value.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
return Task.FromResult(input);
|
return Task.FromResult(input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -11,7 +12,11 @@ internal class LettersShortcodeHandler : IShortcodeHandler
|
|||||||
private readonly ILogger<LettersShortcodeHandler> logger;
|
private readonly ILogger<LettersShortcodeHandler> logger;
|
||||||
|
|
||||||
public int Order => 60;
|
public int Order => 60;
|
||||||
public Regex Pattern => new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
private static readonly Regex _pattern = new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.None;
|
||||||
|
|
||||||
public LettersShortcodeHandler(IUnitInValueRepository unitInValueRepo, ILogger<LettersShortcodeHandler> logger)
|
public LettersShortcodeHandler(IUnitInValueRepository unitInValueRepo, ILogger<LettersShortcodeHandler> logger)
|
||||||
{
|
{
|
||||||
@@ -19,7 +24,7 @@ internal class LettersShortcodeHandler : IShortcodeHandler
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var matches = Pattern.Matches(input);
|
var matches = Pattern.Matches(input);
|
||||||
if (matches.Count == 0) return input;
|
if (matches.Count == 0) return input;
|
||||||
@@ -32,7 +37,7 @@ internal class LettersShortcodeHandler : IShortcodeHandler
|
|||||||
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var field in uniqueFields)
|
foreach (var field in uniqueFields)
|
||||||
{
|
{
|
||||||
var values = await unitInValueRepo.GetFieldValuesAsync(data.UnitId, new List<string> { field }, ct);
|
var values = await unitInValueRepo.GetFieldValuesAsync(template.UnitId, new List<string> { field }, ct);
|
||||||
var rawValue = values.FirstOrDefault().Value;
|
var rawValue = values.FirstOrDefault().Value;
|
||||||
var letters = string.IsNullOrEmpty(rawValue) ? string.Empty : new string(rawValue.Where(char.IsLetter).ToArray());
|
var letters = string.IsNullOrEmpty(rawValue) ? string.Empty : new string(rawValue.Where(char.IsLetter).ToArray());
|
||||||
resolved[$"%БУКВЫ:{field}%"] = letters;
|
resolved[$"%БУКВЫ:{field}%"] = letters;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Common.Interfaces;
|
using PARR.Core.Common.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using PARR.Domain.Cache.Models;
|
using PARR.Domain.Cache.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -14,7 +15,13 @@ internal class MaxShortcodeHandler : IShortcodeHandler
|
|||||||
private readonly ILogger<MaxShortcodeHandler> logger;
|
private readonly ILogger<MaxShortcodeHandler> logger;
|
||||||
|
|
||||||
public int Order => 40;
|
public int Order => 40;
|
||||||
public Regex Pattern => new(@"%МАКС:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex _pattern = new(@"%МАКС:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements =>
|
||||||
|
ShortcodeDataRequirementsEnum.Job |
|
||||||
|
ShortcodeDataRequirementsEnum.JobGroup |
|
||||||
|
ShortcodeDataRequirementsEnum.UnitsInTemplate;
|
||||||
|
|
||||||
public MaxShortcodeHandler(
|
public MaxShortcodeHandler(
|
||||||
IUnitInValueRepository unitInValueRepo,
|
IUnitInValueRepository unitInValueRepo,
|
||||||
@@ -26,11 +33,12 @@ internal class MaxShortcodeHandler : IShortcodeHandler
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var matches = Pattern.Matches(input);
|
var matches = Pattern.Matches(input);
|
||||||
if (matches.Count == 0) return input;
|
if (matches.Count == 0) return input;
|
||||||
|
|
||||||
|
// Извлекаем уникальные имена полей, чтобы не ходить в кэш/БД дважды для одинаковых шорткодов
|
||||||
var uniqueFields = matches.Cast<Match>()
|
var uniqueFields = matches.Cast<Match>()
|
||||||
.Select(m => m.Value.Trim('%').Split(':', 2)[1].Trim())
|
.Select(m => m.Value.Trim('%').Split(':', 2)[1].Trim())
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -39,7 +47,7 @@ internal class MaxShortcodeHandler : IShortcodeHandler
|
|||||||
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var field in uniqueFields)
|
foreach (var field in uniqueFields)
|
||||||
{
|
{
|
||||||
resolved[$"%МАКС:{field}%"] = await GetMaxValueAsync(data, field, caller, ct);
|
resolved[$"%МАКС:{field}%"] = await GetMaxValueAsync(template, field, caller, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = input;
|
var result = input;
|
||||||
@@ -48,21 +56,30 @@ internal class MaxShortcodeHandler : IShortcodeHandler
|
|||||||
if (resolved.TryGetValue(m.Value, out var val))
|
if (resolved.TryGetValue(m.Value, out var val))
|
||||||
result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase);
|
result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GetMaxValueAsync(TemplateForShortcode data, string fieldName, string caller, CancellationToken ct = default)
|
private async Task<string> GetMaxValueAsync(TemplateForShortcode template, string fieldName, string caller, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (data.Job?.Group?.Id == Guid.Empty || data.UnitId == Guid.Empty) return "Нет данных";
|
// Guard: данные должны быть подготовлены оркестратором согласно Requirements
|
||||||
|
if (template.Job?.Group == null || template.UnitId == Guid.Empty)
|
||||||
|
return "Нет данных";
|
||||||
|
|
||||||
|
if (template.UnitsInTemplate.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogDebug("[{Caller}] %МАКС:{Field}% пропущен: UnitsInTemplate пуст для шаблона {TemplateId}.", caller, fieldName, template.Id);
|
||||||
|
return "Нет данных";
|
||||||
|
}
|
||||||
|
|
||||||
var cacheKey = cacheService.GetKey(
|
var cacheKey = cacheService.GetKey(
|
||||||
new[] { "grouped", "shortcode", $"{data.Job.Group.Id:N}", $"{data.UnitId:N}" },
|
new[] { "grouped", "shortcode", $"{template.Job.Group.Id:N}", $"{template.UnitId:N}" },
|
||||||
new[] { $"%МАКС:{fieldName}%" });
|
new[] { $"%МАКС:{fieldName}%" });
|
||||||
|
|
||||||
var cached = await cacheService.GetCachedDataAsync<GroupedShortcode>(cacheKey);
|
var cached = await cacheService.GetCachedDataAsync<GroupedShortcode>(cacheKey);
|
||||||
if (cached != null) return cached.Data.Value;
|
if (cached != null) return cached.Data.Value;
|
||||||
|
|
||||||
var unitIds = data.UnitsInTemplate.Select(u => u.UnitId).Distinct().ToList();
|
var unitIds = template.UnitsInTemplate.Select(u => u.UnitId).Distinct().ToList();
|
||||||
var value = await unitInValueRepo.GetMostFrequentValueForFieldAsync(unitIds, fieldName, ct);
|
var value = await unitInValueRepo.GetMostFrequentValueForFieldAsync(unitIds, fieldName, ct);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(value))
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace PARR.Core.Services.Shortcodes.Handlers;
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
internal class RelShortcodeHandler : IShortcodeHandler
|
internal class RelationshipUnitShortcodeHandler : IShortcodeHandler
|
||||||
{
|
{
|
||||||
private readonly IUnitInValueRepository unitInValueRepo;
|
private readonly IUnitInValueRepository unitInValueRepo;
|
||||||
private readonly ITemplateRepository templateRepo;
|
private readonly ILogger<RelationshipUnitShortcodeHandler> logger;
|
||||||
private readonly ILogger<RelShortcodeHandler> logger;
|
|
||||||
|
|
||||||
public int Order => 50;
|
public int Order => 50;
|
||||||
public Regex Pattern => new(@"%СВЯЗЬ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
public RelShortcodeHandler(
|
private static readonly Regex _pattern = new(@"%СВЯЗЬ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.UnitsInTemplate;
|
||||||
|
|
||||||
|
public RelationshipUnitShortcodeHandler(
|
||||||
IUnitInValueRepository unitInValueRepo,
|
IUnitInValueRepository unitInValueRepo,
|
||||||
ITemplateRepository templateRepo,
|
ILogger<RelationshipUnitShortcodeHandler> logger)
|
||||||
ILogger<RelShortcodeHandler> logger)
|
|
||||||
{
|
{
|
||||||
this.unitInValueRepo = unitInValueRepo;
|
this.unitInValueRepo = unitInValueRepo;
|
||||||
this.templateRepo = templateRepo;
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var matches = Pattern.Matches(input);
|
var matches = Pattern.Matches(input);
|
||||||
if (matches.Count == 0) return input;
|
if (matches.Count == 0) return input;
|
||||||
@@ -39,7 +40,7 @@ internal class RelShortcodeHandler : IShortcodeHandler
|
|||||||
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var field in uniqueFields)
|
foreach (var field in uniqueFields)
|
||||||
{
|
{
|
||||||
resolved[$"%СВЯЗЬ:{field}%"] = await GetRelValueAsync(data, field, caller, ct);
|
resolved[$"%СВЯЗЬ:{field}%"] = await GetRelValueAsync(template, field, caller, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = input;
|
var result = input;
|
||||||
@@ -51,24 +52,16 @@ internal class RelShortcodeHandler : IShortcodeHandler
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GetRelValueAsync(TemplateForShortcode data, string fieldName, string caller, CancellationToken ct)
|
private async Task<string> GetRelValueAsync(TemplateForShortcode template, string fieldName, string caller, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var unitsList = data.UnitsInTemplate;
|
// Оркестратор гарантирует наличие данных согласно Requirements
|
||||||
if (unitsList.Count == 0)
|
if (template.UnitsInTemplate.Count == 0)
|
||||||
{
|
{
|
||||||
var fullTemplate = await templateRepo.Get()
|
logger.LogDebug("[{Caller}] %СВЯЗЬ:{Field}% пропущен: UnitsInTemplate пуст.", caller, fieldName);
|
||||||
.AsNoTracking()
|
return string.Empty;
|
||||||
.Include(t => t.UnitsInTemplate)
|
|
||||||
.FirstOrDefaultAsync(t => t.Id == data.Id, ct);
|
|
||||||
|
|
||||||
unitsList = fullTemplate?.UnitsInTemplate
|
|
||||||
?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId))
|
|
||||||
.ToList() ?? new List<UnitInTemplateForShortcode>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unitsList.Count == 0) return string.Empty;
|
var unitIds = template.UnitsInTemplate.Select(u => u.UnitId).Distinct().ToList();
|
||||||
|
|
||||||
var unitIds = unitsList.Select(u => u.UnitId).Distinct().ToList();
|
|
||||||
|
|
||||||
// Пакетный запрос вместо N+1
|
// Пакетный запрос вместо N+1
|
||||||
var allValues = await unitInValueRepo.Get()
|
var allValues = await unitInValueRepo.Get()
|
||||||
@@ -86,7 +79,10 @@ internal class RelShortcodeHandler : IShortcodeHandler
|
|||||||
if (distinctSorted.Count == 0) return string.Empty;
|
if (distinctSorted.Count == 0) return string.Empty;
|
||||||
|
|
||||||
if (distinctSorted.Count > 1)
|
if (distinctSorted.Count > 1)
|
||||||
logger.LogWarning("[{Caller}] %СВЯЗЬ:{Field}% нашёл {Count} значений. Используется первое: '{First}'", caller, fieldName, distinctSorted.Count, distinctSorted[0]);
|
{
|
||||||
|
logger.LogWarning("[{Caller}] %СВЯЗЬ:{Field}% нашёл {Count} значений. Используется первое: '{First}'",
|
||||||
|
caller, fieldName, distinctSorted.Count, distinctSorted[0]);
|
||||||
|
}
|
||||||
|
|
||||||
return distinctSorted[0]!;
|
return distinctSorted[0]!;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using PARR.Core.Services.UnitFilterService;
|
using PARR.Core.Services.UnitFilterService;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -9,19 +10,23 @@ internal class RelationshipsShortcodeHandler : IShortcodeHandler
|
|||||||
private readonly IUnitFilterService unitFilterService;
|
private readonly IUnitFilterService unitFilterService;
|
||||||
|
|
||||||
public int Order => 70;
|
public int Order => 70;
|
||||||
public Regex Pattern => new(@"%СВЯЗИ(-ПН)?%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
private static readonly Regex _pattern = new(@"%СВЯЗИ(-ПН)?%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements => ShortcodeDataRequirementsEnum.Job;
|
||||||
|
|
||||||
public RelationshipsShortcodeHandler(IUnitFilterService unitFilterService) => this.unitFilterService = unitFilterService;
|
public RelationshipsShortcodeHandler(IUnitFilterService unitFilterService) => this.unitFilterService = unitFilterService;
|
||||||
|
|
||||||
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (data.Job == null) return input;
|
if (template.Job == null) return input;
|
||||||
|
|
||||||
var hasPlain = input.Contains("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase);
|
var hasPlain = input.Contains("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase);
|
||||||
var hasNumbered = input.Contains("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase);
|
var hasNumbered = input.Contains("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase);
|
||||||
if (!hasPlain && !hasNumbered) return input;
|
if (!hasPlain && !hasNumbered) return input;
|
||||||
|
|
||||||
var relatedNames = await unitFilterService.GetRelatedUnitNamesAsync(data.JobId, data.UnitId, ct);
|
var relatedNames = await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId, ct);
|
||||||
var result = input;
|
var result = input;
|
||||||
|
|
||||||
if (hasPlain)
|
if (hasPlain)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -8,23 +9,31 @@ internal class StandardShortcodeHandler : IShortcodeHandler
|
|||||||
{
|
{
|
||||||
private readonly ILogger<StandardShortcodeHandler> logger;
|
private readonly ILogger<StandardShortcodeHandler> logger;
|
||||||
public int Order => 10;
|
public int Order => 10;
|
||||||
public Regex Pattern => new(@"%(?:ЭК|ТИКТАК|РАБОТА|ГРУППА_РАБОТ|ТНК|ТНК-КРАТКО|ИД)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
private static readonly Regex _pattern = new(@"%(?:ЭК|ТИКТАК|РАБОТА|ГРУППА_РАБОТ|ТНК|ТНК-КРАТКО|ИД)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
public Regex Pattern => _pattern;
|
||||||
|
|
||||||
|
public ShortcodeDataRequirementsEnum Requirements =>
|
||||||
|
ShortcodeDataRequirementsEnum.Job |
|
||||||
|
ShortcodeDataRequirementsEnum.JobGroup |
|
||||||
|
ShortcodeDataRequirementsEnum.JobTnk |
|
||||||
|
ShortcodeDataRequirementsEnum.UnitName;
|
||||||
|
|
||||||
public StandardShortcodeHandler(ILogger<StandardShortcodeHandler> logger) => this.logger = logger;
|
public StandardShortcodeHandler(ILogger<StandardShortcodeHandler> logger) => this.logger = logger;
|
||||||
|
|
||||||
public Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
public Task<string> ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = input;
|
var result = input;
|
||||||
result = Replace(result, "%ЭК%", data.UnitName);
|
result = Replace(result, "%ЭК%", template.UnitName);
|
||||||
result = Replace(result, "%ТИКТАК%", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
|
result = Replace(result, "%ТИКТАК%", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
|
||||||
result = Replace(result, "%ИД%", data.Id.ToString("N"));
|
result = Replace(result, "%ИД%", template.Id.ToString("N"));
|
||||||
|
|
||||||
if (data.Job != null)
|
if (template.Job != null)
|
||||||
{
|
{
|
||||||
result = Replace(result, "%РАБОТА%", data.Job.WorkName);
|
result = Replace(result, "%РАБОТА%", template.Job.WorkName);
|
||||||
result = Replace(result, "%ГРУППА_РАБОТ%", data.Job.Group?.GroupName);
|
result = Replace(result, "%ГРУППА_РАБОТ%", template.Job.Group?.GroupName);
|
||||||
result = Replace(result, "%ТНК%", data.Job.Tnk?.Name);
|
result = Replace(result, "%ТНК%", template.Job.Tnk?.Name);
|
||||||
result = Replace(result, "%ТНК-КРАТКО%", data.Job.Tnk?.ShortName);
|
result = Replace(result, "%ТНК-КРАТКО%", template.Job.Tnk?.ShortName);
|
||||||
}
|
}
|
||||||
return Task.FromResult(result);
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Common.Interfaces;
|
using PARR.Core.Common.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces.Job;
|
using PARR.Core.Repositories.Interfaces.Job;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Enums;
|
||||||
using PARR.Core.Services.Shortcodes.Handlers;
|
using PARR.Core.Services.Shortcodes.Handlers;
|
||||||
using PARR.Core.Services.Shortcodes.Models;
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
using PARR.Domain.Cache.Models;
|
using PARR.Domain.Cache.Models;
|
||||||
@@ -12,7 +14,6 @@ using PARR.Domain.Entities.Job;
|
|||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace PARR.Core.Services.Shortcodes;
|
namespace PARR.Core.Services.Shortcodes;
|
||||||
|
|
||||||
@@ -23,12 +24,6 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
"Рекомендуется обновить запрос шаблона с Include(t => t.Unit).";
|
"Рекомендуется обновить запрос шаблона с Include(t => t.Unit).";
|
||||||
|
|
||||||
private const int MaxIterations = 3;
|
private const int MaxIterations = 3;
|
||||||
private static readonly Regex GeneralShortcodeRegex = new(@"%[^%]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
private static readonly HashSet<string> SupportedStandardShortcodes = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
"%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%", "%ИД%"
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly ILogger<ShortcodesService> logger;
|
private readonly ILogger<ShortcodesService> logger;
|
||||||
private readonly IEnumerable<IShortcodeHandler> handlers;
|
private readonly IEnumerable<IShortcodeHandler> handlers;
|
||||||
@@ -36,7 +31,8 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
private readonly SettingsFromDb settingsFromDb;
|
private readonly SettingsFromDb settingsFromDb;
|
||||||
private readonly IJobRepository jobService;
|
private readonly IJobRepository jobService;
|
||||||
private readonly IUnitRepository unitService;
|
private readonly IUnitRepository unitService;
|
||||||
private readonly IUnitFieldRepository unitFieldService;
|
private readonly IUnitFieldRepository unitFieldRepo;
|
||||||
|
private readonly ITemplateRepository templateRepo;
|
||||||
|
|
||||||
public ShortcodesService(
|
public ShortcodesService(
|
||||||
ILogger<ShortcodesService> logger,
|
ILogger<ShortcodesService> logger,
|
||||||
@@ -45,7 +41,9 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
SettingsFromDb settingsFromDb,
|
SettingsFromDb settingsFromDb,
|
||||||
IJobRepository jobService,
|
IJobRepository jobService,
|
||||||
IUnitRepository unitService,
|
IUnitRepository unitService,
|
||||||
IUnitFieldRepository unitFieldService)
|
IUnitFieldRepository unitFieldService,
|
||||||
|
ITemplateRepository templateRepo
|
||||||
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.handlers = handlers.OrderBy(h => h.Order).ToList();
|
this.handlers = handlers.OrderBy(h => h.Order).ToList();
|
||||||
@@ -53,7 +51,8 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
this.settingsFromDb = settingsFromDb;
|
this.settingsFromDb = settingsFromDb;
|
||||||
this.jobService = jobService;
|
this.jobService = jobService;
|
||||||
this.unitService = unitService;
|
this.unitService = unitService;
|
||||||
this.unitFieldService = unitFieldService;
|
this.unitFieldRepo = unitFieldService;
|
||||||
|
this.templateRepo = templateRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ApplyShortcodesAsync(string str, Template template, [CallerMemberName] string? caller = null)
|
public async Task<string> ApplyShortcodesAsync(string str, Template template, [CallerMemberName] string? caller = null)
|
||||||
@@ -61,39 +60,69 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
if (string.IsNullOrEmpty(str)) return str;
|
if (string.IsNullOrEmpty(str)) return str;
|
||||||
var callerName = caller ?? "Unknown";
|
var callerName = caller ?? "Unknown";
|
||||||
|
|
||||||
|
// Кэш результата (проверяем один раз на входе)
|
||||||
var cacheKey = cacheService.GetKey(new[] { "shortcodes", "result", template.Id.ToString("N") }, new[] { str });
|
var cacheKey = cacheService.GetKey(new[] { "shortcodes", "result", template.Id.ToString("N") }, new[] { str });
|
||||||
var cachedResult = await cacheService.GetCachedDataAsync<ShortcodesResult>(cacheKey).ConfigureAwait(false);
|
var cachedResult = await cacheService.GetCachedDataAsync<ShortcodesResult>(cacheKey).ConfigureAwait(false);
|
||||||
if (cachedResult != null) return cachedResult.Data.Result;
|
if (cachedResult != null) return cachedResult.Data.Result;
|
||||||
|
|
||||||
var shortcodes = GeneralShortcodeRegex.Matches(str).Select(m => m.Value).ToList();
|
// Начальный контекст
|
||||||
var data = await PrepareTemplateDataAsync(template, shortcodes, callerName).ConfigureAwait(false);
|
var initialUnitName = template.Unit?.Name;
|
||||||
|
|
||||||
|
var initialJob = template.Job != null ? MapJobForShortcodes(template.Job) : null;
|
||||||
|
|
||||||
|
var initialUnitsInTemplate = template.UnitsInTemplate?.Select(uit =>
|
||||||
|
new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId)).ToList()
|
||||||
|
?? new List<UnitInTemplateForShortcode>();
|
||||||
|
|
||||||
|
var data = new TemplateForShortcode(
|
||||||
|
Id: template.Id,
|
||||||
|
Index: template.Index,
|
||||||
|
JobId: template.JobId,
|
||||||
|
UnitId: template.UnitId,
|
||||||
|
UnitName: initialUnitName ?? string.Empty,
|
||||||
|
Job: initialJob,
|
||||||
|
UnitsInTemplate: initialUnitsInTemplate
|
||||||
|
);
|
||||||
|
|
||||||
var result = str;
|
var result = str;
|
||||||
var iteration = 0;
|
var iteration = 0;
|
||||||
|
|
||||||
while (iteration < MaxIterations)
|
while (iteration < MaxIterations)
|
||||||
{
|
{
|
||||||
var changed = false;
|
// 1. Находим хендлеры, которые сработают для ТЕКУЩЕЙ версии строки
|
||||||
|
var matchingHandlers = handlers
|
||||||
|
.Where(h => h.Pattern.IsMatch(result))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
foreach (var handler in handlers)
|
if (matchingHandlers.Count == 0) break; // Нечего заменять
|
||||||
|
|
||||||
|
// 2. Агрегируем требования только активных хендлеров
|
||||||
|
var currentRequirements = matchingHandlers
|
||||||
|
.Aggregate(ShortcodeDataRequirementsEnum.None, (acc, h) => acc | h.Requirements);
|
||||||
|
|
||||||
|
// 3. ОБНОВЛЯЕМ ДАННЫЕ (Ленивая загрузка)
|
||||||
|
// Передаем текущий 'data', чтобы метод знал, что уже загружено, и не бил в БД повторно
|
||||||
|
data = await PrepareTemplateDataAsync(template, currentRequirements, data, callerName).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 4. Выполняем замену
|
||||||
|
var previousResult = result;
|
||||||
|
foreach (var handler in matchingHandlers)
|
||||||
{
|
{
|
||||||
if (handler.Pattern.IsMatch(result))
|
result = await handler.ResolveAsync(result, data, callerName, CancellationToken.None).ConfigureAwait(false);
|
||||||
{
|
|
||||||
var before = result;
|
|
||||||
result = await handler.ResolveAsync(result, data, callerName).ConfigureAwait(false);
|
|
||||||
if (result != before) changed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changed) break;
|
// 5. Если строка не изменилась — стабилизация достигнута
|
||||||
|
if (result == previousResult) break;
|
||||||
|
|
||||||
iteration++;
|
iteration++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iteration == MaxIterations && result != str)
|
if (iteration == MaxIterations && result != str)
|
||||||
{
|
{
|
||||||
logger.LogWarning("[{Caller}] Достигнут лимит итераций для шаблона {TemplateId}. Возможна циклическая зависимость шорткодов.", callerName, template.Id);
|
logger.LogWarning("[{Caller}] Достигнут лимит итераций для шаблона {TemplateId}. Возможна циклическая зависимость.", callerName, template.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сохранение в кэш
|
||||||
if (!string.IsNullOrEmpty(result) && result != str && template.Id != Guid.Empty)
|
if (!string.IsNullOrEmpty(result) && result != str && template.Id != Guid.Empty)
|
||||||
{
|
{
|
||||||
await cacheService.SetCachedDataAsync(cacheKey, new ShortcodesResult
|
await cacheService.SetCachedDataAsync(cacheKey, new ShortcodesResult
|
||||||
@@ -109,80 +138,87 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
|
|
||||||
private async Task<TemplateForShortcode> PrepareTemplateDataAsync(
|
private async Task<TemplateForShortcode> PrepareTemplateDataAsync(
|
||||||
Template template,
|
Template template,
|
||||||
List<string> shortcodes,
|
ShortcodeDataRequirementsEnum requirements,
|
||||||
|
TemplateForShortcode currentData,
|
||||||
string caller)
|
string caller)
|
||||||
{
|
{
|
||||||
// 1. Загружаем имя юнита, если его нет
|
// 1. Проверка UnitName
|
||||||
var unitName = template.Unit?.Name ?? await GetUnitNameAsyncWithWarning(template, shortcodes, caller).ConfigureAwait(false);
|
var unitName = currentData.UnitName;
|
||||||
|
if (string.IsNullOrEmpty(unitName) && requirements.HasFlag(ShortcodeDataRequirementsEnum.UnitName))
|
||||||
// 2. Формируем базовые данные
|
|
||||||
var data = new TemplateForShortcode(
|
|
||||||
Id: template.Id,
|
|
||||||
Index: template.Index,
|
|
||||||
JobId: template.JobId,
|
|
||||||
UnitId: template.UnitId,
|
|
||||||
UnitName: unitName,
|
|
||||||
Job: null,
|
|
||||||
UnitsInTemplate: template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId)).ToList() ?? new List<UnitInTemplateForShortcode>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Проверяем, нужны ли вообще данные о работе
|
|
||||||
if (!NeedsJobForShortcodes(shortcodes))
|
|
||||||
{
|
{
|
||||||
return data with { Job = new JobForShortcode(null, null, string.Empty, string.Empty) };
|
// Логирование и загрузка только если нужно и еще не загружено
|
||||||
|
logger.LogWarning(MissingUnitNameWarningMsg, caller, template.Id);
|
||||||
|
var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == template.UnitId).ConfigureAwait(false);
|
||||||
|
unitName = unit?.Name ?? throw new InvalidOperationException($"Unit {template.UnitId} не найден.");
|
||||||
}
|
}
|
||||||
|
|
||||||
JobForShortcode jobData;
|
// 2. Проверка Job и навигационных свойств
|
||||||
|
JobForShortcode? jobData = currentData.Job;
|
||||||
|
bool needLoadJob = false;
|
||||||
|
|
||||||
// 4. Проверяем полноту контекста Job
|
if (requirements.HasFlag(ShortcodeDataRequirementsEnum.Job))
|
||||||
if (!IsJobContextComplete(template.Job))
|
|
||||||
{
|
{
|
||||||
// === Graceful Degradation + Structured Logging ===
|
if (jobData == null)
|
||||||
|
{
|
||||||
var missingParts = new List<string>();
|
needLoadJob = true;
|
||||||
if (template.Job == null) missingParts.Add("Job");
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (template.Job.Group == null) missingParts.Add("Group");
|
// Job загружен, но хватает ли вложенных свойств?
|
||||||
if (template.Job.Group?.GroupType == null) missingParts.Add("Group.GroupType");
|
// Так как LoadFullJobAsync грузит всё сразу, если jobData != null, значит там есть всё.
|
||||||
if (template.Job.Tnk == null) missingParts.Add("Tnk");
|
// Но на всякий случай проверим флаги, если в будущем загрузка станет частичной.
|
||||||
|
if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobGroup) && jobData.Group == null) needLoadJob = true;
|
||||||
|
if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobTnk) && jobData.Tnk == null) needLoadJob = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needLoadJob)
|
||||||
|
{
|
||||||
|
var missing = new List<string>();
|
||||||
|
if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobGroup)) missing.Add("Group");
|
||||||
|
if (requirements.HasFlag(ShortcodeDataRequirementsEnum.JobTnk)) missing.Add("Tnk");
|
||||||
|
|
||||||
|
// Логируем только если реально пошли в БД
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
"[{Caller}] Шаблон {TemplateId} передан без необходимых Include для шорткодов. Отсутствуют: {MissingIncludes}. " +
|
"[{Caller}] Шаблон {TemplateId} требует догрузки данных ({Missing}). Данные догружены.",
|
||||||
"Данные будут догружены автоматически. Для оптимизации добавьте .Include() в запрос.",
|
caller, template.Id, string.Join(", ", missing));
|
||||||
caller, template.Id, string.Join(", ", missingParts));
|
|
||||||
|
|
||||||
// Делаем ОДИН пакетный запрос за всем недостающим
|
|
||||||
jobData = await LoadFullJobAsync(template.JobId, caller).ConfigureAwait(false);
|
jobData = await LoadFullJobAsync(template.JobId, caller).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// 3. Проверка UnitsInTemplate
|
||||||
|
var unitsInTemplate = currentData.UnitsInTemplate;
|
||||||
|
if ((unitsInTemplate == null || unitsInTemplate.Count == 0) &&
|
||||||
|
requirements.HasFlag(ShortcodeDataRequirementsEnum.UnitsInTemplate))
|
||||||
{
|
{
|
||||||
// Данные полные, используем их без запросов к БД
|
logger.LogWarning(
|
||||||
jobData = MapJobForShortcodes(template.Job);
|
"[{Caller}] Шаблон {TemplateId} передан без UnitsInTemplate. Данные догружены автоматически.",
|
||||||
|
caller, template.Id);
|
||||||
|
|
||||||
|
var fullTemplate = await templateRepo.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.UnitsInTemplate)
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == template.Id)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
unitsInTemplate = fullTemplate?.UnitsInTemplate
|
||||||
|
?.Select(uit => new UnitInTemplateForShortcode(uit.UnitId, uit.UnitFieldValueId))
|
||||||
|
.ToList() ?? new List<UnitInTemplateForShortcode>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return data with { Job = jobData };
|
// 4. Возвращаем обновленный рекорд
|
||||||
|
return currentData with
|
||||||
|
{
|
||||||
|
UnitName = unitName ?? string.Empty,
|
||||||
|
Job = jobData,
|
||||||
|
UnitsInTemplate = unitsInTemplate ?? currentData.UnitsInTemplate
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Контракт полноты данных Job для движка шорткодов.
|
|
||||||
/// Если появятся новые зависимости (например, Job.AutoControl), добавляем проверку сюда.
|
|
||||||
/// </summary>
|
|
||||||
private static bool IsJobContextComplete(Job? job)
|
|
||||||
{
|
|
||||||
if (job == null) return false;
|
|
||||||
|
|
||||||
// Базовые зависимости, необходимые для корректной работы стандартных и групповых шорткодов,
|
|
||||||
// а также для безопасного разрешения динамических цепочек.
|
|
||||||
return job.Group != null &&
|
|
||||||
job.Group.GroupType != null &&
|
|
||||||
job.Tnk != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<JobForShortcode> LoadFullJobAsync(Guid jobId, string caller)
|
private async Task<JobForShortcode> LoadFullJobAsync(Guid jobId, string caller)
|
||||||
{
|
{
|
||||||
var job = await jobService.Get().AsNoTracking()
|
var job = await jobService.Get().AsNoTracking().AsSingleQuery()
|
||||||
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
|
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
|
||||||
.Include(j => j.Tnk)
|
.Include(j => j.Tnk)
|
||||||
.FirstOrDefaultAsync(j => j.Id == jobId)
|
.FirstOrDefaultAsync(j => j.Id == jobId)
|
||||||
@@ -194,33 +230,6 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
return MapJobForShortcodes(job);
|
return MapJobForShortcodes(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool NeedsJobForShortcodes(List<string> shortcodes)
|
|
||||||
{
|
|
||||||
return shortcodes.Any(sc =>
|
|
||||||
sc != "%ЭК%" &&
|
|
||||||
(SupportedStandardShortcodes.Contains(sc) ||
|
|
||||||
sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
sc.StartsWith("%СВЯЗЬ:", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
sc.Equals("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
sc.Equals("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase)));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<string> GetUnitNameAsyncWithWarning(Template template, List<string> shortcodes, string caller)
|
|
||||||
{
|
|
||||||
if (shortcodes.Any(sc => sc.Equals("%ЭК%", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
logger.LogWarning(MissingUnitNameWarningMsg, caller, template.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.");
|
|
||||||
|
|
||||||
return unit.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JobForShortcode MapJobForShortcodes(Job job)
|
private JobForShortcode MapJobForShortcodes(Job job)
|
||||||
{
|
{
|
||||||
@@ -287,7 +296,7 @@ internal class ShortcodesService : IShortcodesService
|
|||||||
new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", Description = "Связанные ЭК с нумерацией", Type = ShortcodeTypeEnum.Relationship }
|
new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", Description = "Связанные ЭК с нумерацией", Type = ShortcodeTypeEnum.Relationship }
|
||||||
});
|
});
|
||||||
|
|
||||||
var fieldNames = await unitFieldService.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync().ConfigureAwait(false);
|
var fieldNames = await unitFieldRepo.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync().ConfigureAwait(false);
|
||||||
foreach (var fieldName in fieldNames.OrderBy(n => n.AihitName))
|
foreach (var fieldName in fieldNames.OrderBy(n => n.AihitName))
|
||||||
{
|
{
|
||||||
result.Add(new ShortcodeInfoDto
|
result.Add(new ShortcodeInfoDto
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace PARR.TemplateGeneratorWorker
|
|||||||
var tempTemplateForShortcodes = new Template
|
var tempTemplateForShortcodes = new Template
|
||||||
{
|
{
|
||||||
Id = Guid.Empty, // ещё не создан
|
Id = Guid.Empty, // ещё не создан
|
||||||
Name = "", // не используется
|
Name = string.Empty, // не используется
|
||||||
JobId = query.JobId,
|
JobId = query.JobId,
|
||||||
UnitId = query.UnitId,
|
UnitId = query.UnitId,
|
||||||
Index = query.Index,
|
Index = query.Index,
|
||||||
|
|||||||
Reference in New Issue
Block a user