feat(core): Рефакторинг ShortcodesService, реализован паттерн стратегия
This commit is contained in:
@@ -7,6 +7,7 @@ using PARR.Core.Services.MatchingStatusService;
|
|||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
using PARR.Core.Services.NextRunServices.Subservices;
|
using PARR.Core.Services.NextRunServices.Subservices;
|
||||||
using PARR.Core.Services.Shortcodes;
|
using PARR.Core.Services.Shortcodes;
|
||||||
|
using PARR.Core.Services.Shortcodes.Handlers;
|
||||||
using PARR.Core.Services.TaskServices.Handlers;
|
using PARR.Core.Services.TaskServices.Handlers;
|
||||||
using PARR.Core.Services.TaskServices.Handlers.Factory;
|
using PARR.Core.Services.TaskServices.Handlers.Factory;
|
||||||
using PARR.Core.Services.TaskServices.Implementations;
|
using PARR.Core.Services.TaskServices.Implementations;
|
||||||
@@ -71,7 +72,21 @@ namespace PARR.Core
|
|||||||
|
|
||||||
#region Services
|
#region Services
|
||||||
|
|
||||||
services.AddTransient<IShortcodesService, ShortcodesService>();
|
#region Shortсodes
|
||||||
|
|
||||||
|
services.AddScoped<IShortcodeHandler, ConstantsShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, StandardShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, IndexShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, GroupedFieldShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, MaxShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, RelShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, LettersShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, RelationshipsShortcodeHandler>();
|
||||||
|
services.AddScoped<IShortcodeHandler, FieldShortcodeHandler>();
|
||||||
|
|
||||||
|
services.AddScoped<IShortcodesService, ShortcodesService>();
|
||||||
|
#endregion
|
||||||
|
|
||||||
services.AddTransient<IMatchingStatusService, MatchingStatusService>();
|
services.AddTransient<IMatchingStatusService, MatchingStatusService>();
|
||||||
|
|
||||||
//services.AddScoped<IUserService, UserService>();
|
//services.AddScoped<IUserService, UserService>();
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ namespace PARR.Core.Repositories.Interfaces.Unit
|
|||||||
{
|
{
|
||||||
public interface IUnitInValueRepository
|
public interface IUnitInValueRepository
|
||||||
{
|
{
|
||||||
Task<List<UnitInValue>> GetByUnitIdsAsync(IEnumerable<Guid> unitIds);
|
Task<List<UnitInValue>> GetByUnitIdsAsync(IEnumerable<Guid> unitIds, CancellationToken ct = default);
|
||||||
|
|
||||||
Task<List<UnitInValue>> GetByUnitIdAsync(Guid unitId);
|
Task<List<UnitInValue>> GetByUnitIdAsync(Guid unitId, CancellationToken ct = default);
|
||||||
|
|
||||||
Task<List<(string FieldName, string? Value)>> GetFieldValuesAsync(Guid unitId, IReadOnlyCollection<string> aihitNames);
|
Task<List<(string FieldName, string? Value)>> GetFieldValuesAsync(Guid unitId, IReadOnlyCollection<string> aihitNames, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает UnitInValue (с Value) для заданных UnitId и FieldId.
|
/// Получает UnitInValue (с Value) для заданных UnitId и FieldId.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds);
|
Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Находит наиболее часто встречающееся непустое значение указанного поля среди переданных юнитов.
|
/// Находит наиболее часто встречающееся непустое значение указанного поля среди переданных юнитов.
|
||||||
@@ -21,7 +21,7 @@ namespace PARR.Core.Repositories.Interfaces.Unit
|
|||||||
/// <param name="unitIds">Список Id юнитов для анализа.</param>
|
/// <param name="unitIds">Список Id юнитов для анализа.</param>
|
||||||
/// <param name="fieldName">Имя поля (AihitName) для поиска значения.</param>
|
/// <param name="fieldName">Имя поля (AihitName) для поиска значения.</param>
|
||||||
/// <returns>Наиболее частое значение поля или null, если не найдено.</returns>
|
/// <returns>Наиболее частое значение поля или null, если не найдено.</returns>
|
||||||
Task<string?> GetMostFrequentValueForFieldAsync(List<Guid> unitIds, string fieldName);
|
Task<string?> GetMostFrequentValueForFieldAsync(List<Guid> unitIds, string fieldName, CancellationToken ct = default);
|
||||||
|
|
||||||
IQueryable<UnitInValue> Get();
|
IQueryable<UnitInValue> Get();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using PARR.Domain.Settings;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class ConstantsShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly SettingsFromDb settings;
|
||||||
|
public int Order => 5;
|
||||||
|
public Regex Pattern => new(@"%[А-ЯA-Z0-9_\-]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public ConstantsShortcodeHandler(SettingsFromDb settings) => this.settings = settings;
|
||||||
|
|
||||||
|
public Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = input;
|
||||||
|
foreach (var constant in settings.TemplateNameConstantPartsList)
|
||||||
|
{
|
||||||
|
var shortcode = $"%{constant.Name}%";
|
||||||
|
if (result.Contains(shortcode, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result = result.Replace(shortcode, constant.Value ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class FieldShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly IUnitInValueRepository unitInValueRepository;
|
||||||
|
private readonly ILogger<FieldShortcodeHandler> logger;
|
||||||
|
public int Order => 90; // Всегда последним
|
||||||
|
public Regex Pattern => new(@"%[^%]+%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public FieldShortcodeHandler(IUnitInValueRepository unitInValueRepository, ILogger<FieldShortcodeHandler> logger)
|
||||||
|
{
|
||||||
|
this.unitInValueRepository = unitInValueRepository;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var matches = Pattern.Matches(input);
|
||||||
|
if (matches.Count == 0) return input;
|
||||||
|
|
||||||
|
var fieldNames = matches.Cast<Match>()
|
||||||
|
.Select(m => m.Value.Trim('%').ToUpperInvariant())
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (fieldNames.Count == 0) return input;
|
||||||
|
|
||||||
|
var fieldValues = await unitInValueRepository.GetFieldValuesAsync(data.UnitId, fieldNames, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var valuesMap = fieldValues
|
||||||
|
.GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => string.Join(", ", g.Select(v => v.Value ?? "null")),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var result = input;
|
||||||
|
foreach (var match in matches.Cast<Match>())
|
||||||
|
{
|
||||||
|
var fieldName = match.Value.Trim('%').ToUpperInvariant();
|
||||||
|
if (valuesMap.TryGetValue(fieldName, out var combinedValue))
|
||||||
|
{
|
||||||
|
result = result.Replace(match.Value, combinedValue, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogDebug("[{Caller}] Поле '{FieldName}' не найдено для UnitId={UnitId}. Шорткод пропущен.", caller, fieldName, data.UnitId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class GroupedFieldShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly IUnitRepository unitRepo;
|
||||||
|
private readonly IUnitFieldValueRepository fieldValueRepo;
|
||||||
|
private readonly ITemplateRepository templateRepo;
|
||||||
|
private readonly ILogger<GroupedFieldShortcodeHandler> logger;
|
||||||
|
|
||||||
|
public int Order => 30;
|
||||||
|
public Regex Pattern => new(@"%ГР_ПОЛЕ-ПН%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public GroupedFieldShortcodeHandler(
|
||||||
|
IUnitRepository unitRepo,
|
||||||
|
IUnitFieldValueRepository fieldValueRepo,
|
||||||
|
ITemplateRepository templateRepo,
|
||||||
|
ILogger<GroupedFieldShortcodeHandler> logger)
|
||||||
|
{
|
||||||
|
this.unitRepo = unitRepo;
|
||||||
|
this.fieldValueRepo = fieldValueRepo;
|
||||||
|
this.templateRepo = templateRepo;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!input.Contains("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase)) return input;
|
||||||
|
|
||||||
|
if (data.Job?.Group == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("[{Caller}] Job не содержит Group, необходимый для %ГР_ПОЛЕ-ПН%. Шорткод пропущен.", caller);
|
||||||
|
return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var unitsList = data.UnitsInTemplate;
|
||||||
|
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);
|
||||||
|
|
||||||
|
var unitIds = unitsList.Select(u => u.UnitId).Distinct().ToList();
|
||||||
|
var fieldValueIds = unitsList.Select(u => u.UnitFieldValueId).Distinct().ToList();
|
||||||
|
|
||||||
|
var unitNames = await unitRepo.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(u => unitIds.Contains(u.Id))
|
||||||
|
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), ct);
|
||||||
|
|
||||||
|
var fieldValueStrings = await fieldValueRepo.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(fv => fieldValueIds.Contains(fv.Id))
|
||||||
|
.ToDictionaryAsync(fv => fv.Id, fv => fv.Value ?? string.Empty, ct);
|
||||||
|
|
||||||
|
var lines = unitsList.Select((uit, i) =>
|
||||||
|
{
|
||||||
|
var uName = unitNames.GetValueOrDefault(uit.UnitId, $"(UnitId={uit.UnitId})");
|
||||||
|
var fVal = fieldValueStrings.GetValueOrDefault(uit.UnitFieldValueId, string.Empty);
|
||||||
|
return $"{i + 1}. {uName} ({fVal})";
|
||||||
|
});
|
||||||
|
|
||||||
|
return input.Replace("%ГР_ПОЛЕ-ПН%", string.Join("\n", lines), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
PARR.Core/Services/Shortcodes/Handlers/IShortcodeHandler.cs
Normal file
24
PARR.Core/Services/Shortcodes/Handlers/IShortcodeHandler.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers
|
||||||
|
{
|
||||||
|
public interface IShortcodeHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Порядок выполнения в пайплайне.
|
||||||
|
/// Меньшее значение = раньше выполнение. Важно для зависимых шорткодов.
|
||||||
|
/// </summary>
|
||||||
|
int Order { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регулярное выражение для поиска шорткодов, которые обрабатывает этот хендлер.
|
||||||
|
/// </summary>
|
||||||
|
Regex Pattern { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заменяет все найденные шорткоды в строке на вычисленные значения.
|
||||||
|
/// </summary>
|
||||||
|
Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class IndexShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (data.Index.HasValue && input.Contains("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Task.FromResult(input.Replace("%ИНДЕКС%", data.Index.Value.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
return Task.FromResult(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class LettersShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly IUnitInValueRepository unitInValueRepo;
|
||||||
|
private readonly ILogger<LettersShortcodeHandler> logger;
|
||||||
|
|
||||||
|
public int Order => 60;
|
||||||
|
public Regex Pattern => new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public LettersShortcodeHandler(IUnitInValueRepository unitInValueRepo, ILogger<LettersShortcodeHandler> logger)
|
||||||
|
{
|
||||||
|
this.unitInValueRepo = unitInValueRepo;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var matches = Pattern.Matches(input);
|
||||||
|
if (matches.Count == 0) return input;
|
||||||
|
|
||||||
|
var uniqueFields = matches.Cast<Match>()
|
||||||
|
.Select(m => m.Value.Trim('%').Split(':', 2)[1].Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var field in uniqueFields)
|
||||||
|
{
|
||||||
|
var values = await unitInValueRepo.GetFieldValuesAsync(data.UnitId, new List<string> { field }, ct);
|
||||||
|
var rawValue = values.FirstOrDefault().Value;
|
||||||
|
var letters = string.IsNullOrEmpty(rawValue) ? string.Empty : new string(rawValue.Where(char.IsLetter).ToArray());
|
||||||
|
resolved[$"%БУКВЫ:{field}%"] = letters;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = input;
|
||||||
|
foreach (Match m in matches)
|
||||||
|
{
|
||||||
|
if (resolved.TryGetValue(m.Value, out var val))
|
||||||
|
result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Common.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using PARR.Domain.Cache.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class MaxShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly IUnitInValueRepository unitInValueRepo;
|
||||||
|
private readonly IRedisCacheService cacheService;
|
||||||
|
private readonly ILogger<MaxShortcodeHandler> logger;
|
||||||
|
|
||||||
|
public int Order => 40;
|
||||||
|
public Regex Pattern => new(@"%МАКС:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public MaxShortcodeHandler(
|
||||||
|
IUnitInValueRepository unitInValueRepo,
|
||||||
|
IRedisCacheService cacheService,
|
||||||
|
ILogger<MaxShortcodeHandler> logger)
|
||||||
|
{
|
||||||
|
this.unitInValueRepo = unitInValueRepo;
|
||||||
|
this.cacheService = cacheService;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var matches = Pattern.Matches(input);
|
||||||
|
if (matches.Count == 0) return input;
|
||||||
|
|
||||||
|
var uniqueFields = matches.Cast<Match>()
|
||||||
|
.Select(m => m.Value.Trim('%').Split(':', 2)[1].Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var field in uniqueFields)
|
||||||
|
{
|
||||||
|
resolved[$"%МАКС:{field}%"] = await GetMaxValueAsync(data, field, caller, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = input;
|
||||||
|
foreach (Match m in matches)
|
||||||
|
{
|
||||||
|
if (resolved.TryGetValue(m.Value, out var val))
|
||||||
|
result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetMaxValueAsync(TemplateForShortcode data, string fieldName, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (data.Job?.Group?.Id == Guid.Empty || data.UnitId == Guid.Empty) return "Нет данных";
|
||||||
|
|
||||||
|
var cacheKey = cacheService.GetKey(
|
||||||
|
new[] { "grouped", "shortcode", $"{data.Job.Group.Id:N}", $"{data.UnitId:N}" },
|
||||||
|
new[] { $"%МАКС:{fieldName}%" });
|
||||||
|
|
||||||
|
var cached = await cacheService.GetCachedDataAsync<GroupedShortcode>(cacheKey);
|
||||||
|
if (cached != null) return cached.Data.Value;
|
||||||
|
|
||||||
|
var unitIds = data.UnitsInTemplate.Select(u => u.UnitId).Distinct().ToList();
|
||||||
|
var value = await unitInValueRepo.GetMostFrequentValueForFieldAsync(unitIds, fieldName, ct);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
await cacheService.SetCachedDataAsync(cacheKey, new GroupedShortcode
|
||||||
|
{
|
||||||
|
Data = new GroupedShortcodeDto { Value = value },
|
||||||
|
Timestamp = DateTimeOffset.UtcNow,
|
||||||
|
Source = nameof(MaxShortcodeHandler)
|
||||||
|
}, TimeSpan.FromHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return value ?? "Нет данных";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class RelShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly IUnitInValueRepository unitInValueRepo;
|
||||||
|
private readonly ITemplateRepository templateRepo;
|
||||||
|
private readonly ILogger<RelShortcodeHandler> logger;
|
||||||
|
|
||||||
|
public int Order => 50;
|
||||||
|
public Regex Pattern => new(@"%СВЯЗЬ:([^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public RelShortcodeHandler(
|
||||||
|
IUnitInValueRepository unitInValueRepo,
|
||||||
|
ITemplateRepository templateRepo,
|
||||||
|
ILogger<RelShortcodeHandler> logger)
|
||||||
|
{
|
||||||
|
this.unitInValueRepo = unitInValueRepo;
|
||||||
|
this.templateRepo = templateRepo;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var matches = Pattern.Matches(input);
|
||||||
|
if (matches.Count == 0) return input;
|
||||||
|
|
||||||
|
var uniqueFields = matches.Cast<Match>()
|
||||||
|
.Select(m => m.Value.Trim('%').Split(':', 2)[1].Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var resolved = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var field in uniqueFields)
|
||||||
|
{
|
||||||
|
resolved[$"%СВЯЗЬ:{field}%"] = await GetRelValueAsync(data, field, caller, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = input;
|
||||||
|
foreach (Match m in matches)
|
||||||
|
{
|
||||||
|
if (resolved.TryGetValue(m.Value, out var val))
|
||||||
|
result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetRelValueAsync(TemplateForShortcode data, string fieldName, string caller, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var unitsList = data.UnitsInTemplate;
|
||||||
|
if (unitsList.Count == 0)
|
||||||
|
{
|
||||||
|
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 string.Empty;
|
||||||
|
|
||||||
|
var unitIds = unitsList.Select(u => u.UnitId).Distinct().ToList();
|
||||||
|
|
||||||
|
// Пакетный запрос вместо N+1
|
||||||
|
var allValues = await unitInValueRepo.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(uv => uv.Field)
|
||||||
|
.Include(uv => uv.Value)
|
||||||
|
.Where(uv => unitIds.Contains(uv.UnitId) &&
|
||||||
|
uv.Field!.AihitName == fieldName &&
|
||||||
|
uv.Value != null &&
|
||||||
|
!string.IsNullOrEmpty(uv.Value.Value))
|
||||||
|
.Select(uv => uv.Value!.Value)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var distinctSorted = allValues.Distinct().OrderBy(v => v, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
if (distinctSorted.Count == 0) return string.Empty;
|
||||||
|
|
||||||
|
if (distinctSorted.Count > 1)
|
||||||
|
logger.LogWarning("[{Caller}] %СВЯЗЬ:{Field}% нашёл {Count} значений. Используется первое: '{First}'", caller, fieldName, distinctSorted.Count, distinctSorted[0]);
|
||||||
|
|
||||||
|
return distinctSorted[0]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using PARR.Core.Services.UnitFilterService;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class RelationshipsShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly IUnitFilterService unitFilterService;
|
||||||
|
|
||||||
|
public int Order => 70;
|
||||||
|
public Regex Pattern => new(@"%СВЯЗИ(-ПН)?%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public RelationshipsShortcodeHandler(IUnitFilterService unitFilterService) => this.unitFilterService = unitFilterService;
|
||||||
|
|
||||||
|
public async Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (data.Job == null) return input;
|
||||||
|
|
||||||
|
var hasPlain = input.Contains("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var hasNumbered = input.Contains("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!hasPlain && !hasNumbered) return input;
|
||||||
|
|
||||||
|
var relatedNames = await unitFilterService.GetRelatedUnitNamesAsync(data.JobId, data.UnitId, ct);
|
||||||
|
var result = input;
|
||||||
|
|
||||||
|
if (hasPlain)
|
||||||
|
result = result.Replace("%СВЯЗИ%", string.Join("\n", relatedNames), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (hasNumbered)
|
||||||
|
{
|
||||||
|
var numbered = relatedNames.Select((n, i) => $"{i + 1}. {n}");
|
||||||
|
result = result.Replace("%СВЯЗИ-ПН%", string.Join("\n", numbered), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Services.Shortcodes.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Handlers;
|
||||||
|
|
||||||
|
internal class StandardShortcodeHandler : IShortcodeHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger<StandardShortcodeHandler> logger;
|
||||||
|
public int Order => 10;
|
||||||
|
public Regex Pattern => new(@"%(?:ЭК|ТИКТАК|РАБОТА|ГРУППА_РАБОТ|ТНК|ТНК-КРАТКО|ИД)%", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public StandardShortcodeHandler(ILogger<StandardShortcodeHandler> logger) => this.logger = logger;
|
||||||
|
|
||||||
|
public Task<string> ResolveAsync(string input, TemplateForShortcode data, string caller, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = input;
|
||||||
|
result = Replace(result, "%ЭК%", data.UnitName);
|
||||||
|
result = Replace(result, "%ТИКТАК%", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
|
||||||
|
result = Replace(result, "%ИД%", data.Id.ToString("N"));
|
||||||
|
|
||||||
|
if (data.Job != null)
|
||||||
|
{
|
||||||
|
result = Replace(result, "%РАБОТА%", data.Job.WorkName);
|
||||||
|
result = Replace(result, "%ГРУППА_РАБОТ%", data.Job.Group?.GroupName);
|
||||||
|
result = Replace(result, "%ТНК%", data.Job.Tnk?.Name);
|
||||||
|
result = Replace(result, "%ТНК-КРАТКО%", data.Job.Tnk?.ShortName);
|
||||||
|
}
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Replace(string input, string shortcode, string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value)) return input;
|
||||||
|
return input.Replace(shortcode, value, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
PARR.Core/Services/Shortcodes/Models/JobForShortcode.cs
Normal file
9
PARR.Core/Services/Shortcodes/Models/JobForShortcode.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Core.Services.Shortcodes.Models
|
||||||
|
{
|
||||||
|
public record class JobForShortcode(
|
||||||
|
JobGroupForShortcode? Group,
|
||||||
|
TnkForShortcode? Tnk,
|
||||||
|
string WorkName,
|
||||||
|
string Name
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Core.Services.Shortcodes.Models
|
||||||
|
{
|
||||||
|
public record class JobGroupForShortcode(
|
||||||
|
Guid Id,
|
||||||
|
Guid? GroupingUnitFieldId,
|
||||||
|
JobGroupTypeForShortcodes? GroupType,
|
||||||
|
string GroupName
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.Shortcodes.Models
|
||||||
|
{
|
||||||
|
public record class JobGroupTypeForShortcodes(
|
||||||
|
JobGroupTypesEnum Code
|
||||||
|
);
|
||||||
|
}
|
||||||
12
PARR.Core/Services/Shortcodes/Models/TemplateForShortcode.cs
Normal file
12
PARR.Core/Services/Shortcodes/Models/TemplateForShortcode.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace PARR.Core.Services.Shortcodes.Models
|
||||||
|
{
|
||||||
|
public record class TemplateForShortcode(
|
||||||
|
Guid Id,
|
||||||
|
int? Index,
|
||||||
|
Guid JobId,
|
||||||
|
Guid UnitId,
|
||||||
|
string UnitName,
|
||||||
|
JobForShortcode? Job,
|
||||||
|
List<UnitInTemplateForShortcode> UnitsInTemplate
|
||||||
|
);
|
||||||
|
}
|
||||||
7
PARR.Core/Services/Shortcodes/Models/TnkForShortcode.cs
Normal file
7
PARR.Core/Services/Shortcodes/Models/TnkForShortcode.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PARR.Core.Services.Shortcodes.Models
|
||||||
|
{
|
||||||
|
public record class TnkForShortcode(
|
||||||
|
string Name,
|
||||||
|
string ShortName
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PARR.Core.Services.Shortcodes.Models
|
||||||
|
{
|
||||||
|
public record class UnitInTemplateForShortcode(
|
||||||
|
Guid UnitId,
|
||||||
|
Guid UnitFieldValueId
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -20,26 +20,27 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
this.dataContext = dataContext;
|
this.dataContext = dataContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<UnitInValue>> GetByUnitIdAsync(Guid unitId)
|
public async Task<List<UnitInValue>> GetByUnitIdAsync(Guid unitId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await dataContext.UnitInValues.AsNoTracking()
|
return await dataContext.UnitInValues.AsNoTracking()
|
||||||
.Include(t => t.Value)
|
.Include(t => t.Value)
|
||||||
.Where(uv => uv.UnitId == unitId)
|
.Where(uv => uv.UnitId == unitId)
|
||||||
.ToListAsync();
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<UnitInValue>> GetByUnitIdsAsync(IEnumerable<Guid> unitIds)
|
public async Task<List<UnitInValue>> GetByUnitIdsAsync(IEnumerable<Guid> unitIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await dataContext.UnitInValues.AsNoTracking()
|
return await dataContext.UnitInValues.AsNoTracking()
|
||||||
.Include(t => t.Value)
|
.Include(t => t.Value)
|
||||||
.Where(uv => unitIds.Contains(uv.UnitId))
|
.Where(uv => unitIds.Contains(uv.UnitId))
|
||||||
.ToListAsync();
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<(string FieldName, string? Value)>> GetFieldValuesAsync(
|
public async Task<List<(string FieldName, string? Value)>> GetFieldValuesAsync(
|
||||||
Guid unitId,
|
Guid unitId,
|
||||||
IReadOnlyCollection<string> aihitNames)
|
IReadOnlyCollection<string> aihitNames
|
||||||
|
, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (aihitNames == null || aihitNames.Count == 0)
|
if (aihitNames == null || aihitNames.Count == 0)
|
||||||
return new List<(string, string?)>();
|
return new List<(string, string?)>();
|
||||||
@@ -52,7 +53,7 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
&& uiv.Value != null
|
&& uiv.Value != null
|
||||||
&& aihitNames.Contains(uiv.Field.AihitName.ToUpper()))
|
&& aihitNames.Contains(uiv.Field.AihitName.ToUpper()))
|
||||||
.Select(uiv => new { Key = uiv.Field!.AihitName.ToUpper(), Value = uiv.Value!.Value })
|
.Select(uiv => new { Key = uiv.Field!.AihitName.ToUpper(), Value = uiv.Value!.Value })
|
||||||
.ToListAsync();
|
.ToListAsync(ct);
|
||||||
|
|
||||||
// Журналируем если не нашли поля
|
// Журналируем если не нашли поля
|
||||||
var foundFieldNames = keyValuePairs.Select(kvp => kvp.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var foundFieldNames = keyValuePairs.Select(kvp => kvp.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -69,7 +70,7 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds)
|
public async Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var unitIdSet = unitIds.ToHashSet();
|
var unitIdSet = unitIds.ToHashSet();
|
||||||
var fieldIdSet = fieldIds.ToHashSet();
|
var fieldIdSet = fieldIds.ToHashSet();
|
||||||
@@ -78,7 +79,7 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(uv => uv.Value)
|
.Include(uv => uv.Value)
|
||||||
.Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId))
|
.Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId))
|
||||||
.ToListAsync();
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IQueryable<UnitInValue> Get()
|
public IQueryable<UnitInValue> Get()
|
||||||
@@ -86,7 +87,7 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
return dataContext.UnitInValues;
|
return dataContext.UnitInValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> GetMostFrequentValueForFieldAsync(List<Guid> unitIds, string fieldName)
|
public async Task<string?> GetMostFrequentValueForFieldAsync(List<Guid> unitIds, string fieldName, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (unitIds == null || !unitIds.Any() || string.IsNullOrWhiteSpace(fieldName))
|
if (unitIds == null || !unitIds.Any() || string.IsNullOrWhiteSpace(fieldName))
|
||||||
{
|
{
|
||||||
@@ -121,7 +122,7 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
.Select(g => new { Value = g.Key, Count = g.Count() })
|
.Select(g => new { Value = g.Key, Count = g.Count() })
|
||||||
.OrderByDescending(x => x.Count)
|
.OrderByDescending(x => x.Count)
|
||||||
.ThenBy(x => x.Value)
|
.ThenBy(x => x.Value)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
var mostFrequentValue = result?.Value;
|
var mostFrequentValue = result?.Value;
|
||||||
logger.LogDebug("Наиболее частое значение для поля '{FieldName}': {Value}", fieldName, mostFrequentValue);
|
logger.LogDebug("Наиболее частое значение для поля '{FieldName}': {Value}", fieldName, mostFrequentValue);
|
||||||
@@ -131,16 +132,6 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
|
|
||||||
public IQueryable<Guid> GetMatchingTargetIds(Guid fieldId, string valueMask)
|
public IQueryable<Guid> GetMatchingTargetIds(Guid fieldId, string valueMask)
|
||||||
{
|
{
|
||||||
//TODO: Вынесено из UnitFilterService
|
|
||||||
|
|
||||||
//var matchingTargetIds = await unitInValueService.Get()
|
|
||||||
// .AsNoTracking()
|
|
||||||
// .Where(uiv => uiv.FieldId == relFilter.FieldId)
|
|
||||||
// .Where(uiv => EF.Functions.ILike(uiv.Value.Value, dbValueMask))
|
|
||||||
// .Select(uiv => uiv.UnitId)
|
|
||||||
// .Distinct()
|
|
||||||
// .ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Get()
|
return Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(uiv => uiv.FieldId == fieldId)
|
.Where(uiv => uiv.FieldId == fieldId)
|
||||||
@@ -152,4 +143,3 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user