feat(dal): уточнена логика получение имени связанных ЭК, переписано кэширование МАКС:FIELD, в синхронизацию и контролер заданий роботов добавлены include

This commit is contained in:
Mikhail Kuznetsov
2025-12-29 19:21:51 +10:00
parent d5d84c1853
commit acf232ecdf
24 changed files with 598 additions and 558 deletions

View File

@@ -2,11 +2,21 @@
"ConnectionStrings": { "ConnectionStrings": {
"AihitConnection": "Data Source=10.248.19.97; Initial Catalog=mao2;User ID=awhit-ipp-parr;pwd=ET3h$9y1LH#D;TrustServerCertificate=true;" "AihitConnection": "Data Source=10.248.19.97; Initial Catalog=mao2;User ID=awhit-ipp-parr;pwd=ET3h$9y1LH#D;TrustServerCertificate=true;"
}, },
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": { "Serilog": {
"MinimumLevel": { "MinimumLevel": {
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -5,7 +5,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -13,7 +15,9 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
} }
} }
}, },

View File

@@ -128,13 +128,17 @@ namespace PARR.API.Controllers.V1
.ThenInclude(t => t!.Group) .ThenInclude(t => t!.Group)
.ThenInclude(t => t.ScheduleExcludeTypeCalendar) .ThenInclude(t => t.ScheduleExcludeTypeCalendar)
.Include(t => t.Template) .Include(t => t.Template)
.ThenInclude(t => t.UnitsInTemplate); .ThenInclude(t => t!.Job)
.ThenInclude(t=>t!.Tnk)
.Include(t => t.Template)
.ThenInclude(t => t!.UnitsInTemplate);
//выбираем только записи с созданными шаблонами (у которых статус 20 или 30), а только потом у них ищем расписания //выбираем только записи с созданными шаблонами (у которых статус 20 или 30), а только потом у них ищем расписания
var createdTemplates = robotConfigurationService.Get() var createdTemplates = robotConfigurationService.Get()
.Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && (t.TaskStatusCode == (int)TaskStatusEnum.Ok)) .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && (t.TaskStatusCode == (int)TaskStatusEnum.Ok))
.Select(t => t.TemplateId); .Select(t => t.TemplateId);
query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && createdTemplates.Contains(t.TemplateId)); query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && createdTemplates.Contains(t.TemplateId));
// query = query.Where(t => t.RobotCode == (int)RobotsEnum.ScheduleOrder && t.TemplateId==Guid.Parse("7cb7c3be-506d-40e0-a63f-4554edb52459"));
break; break;
default: default:

View File

@@ -1,95 +0,0 @@
using Microsoft.Extensions.Logging;
using PARR.DAL.CacheServices;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.Settings;
using System.Security.Cryptography;
using System.Text;
namespace PARR.DAL.DomainServices.Implementations
{
public class GroupedShortcodesCacheService : IGroupedShortcodesCacheService
{
private readonly IRedisCacheService cacheService;
private readonly GroupedShortcodesCacheSettings settings;
private readonly ILogger<GroupedShortcodesCacheService> logger;
public GroupedShortcodesCacheService(
IRedisCacheService cacheService,
GroupedShortcodesCacheSettings settings,
ILogger<GroupedShortcodesCacheService> logger)
{
this.cacheService = cacheService;
this.settings = settings;
this.logger = logger;
}
public async Task<string> GetAggregatedValueAsync(
Guid jobGroupId,
Guid unitId,
string shortcode,
Func<Task<string>> computeIfMissing)
{
if (string.IsNullOrEmpty(shortcode))
throw new ArgumentException("Ключ шорткода должен быть указан.", nameof(shortcode));
var cacheKey = GetCacheKey(jobGroupId, unitId, shortcode);
try
{
var cachedValue = await cacheService.GetCachedDataAsync<string>(cacheKey);
if (cachedValue != null)
{
logger.LogDebug(
"Попадание в кэш для шорткода '{ShortcodeKey}': unit={UnitId} → '{Value}'",
shortcode, unitId, cachedValue);
return cachedValue;
}
logger.LogDebug(
"Промах кэша для шорткода '{ShortcodeKey}': unit={UnitId}. Вычисление...",
shortcode, unitId);
var computedValue = await computeIfMissing();
await cacheService.SetCachedDataAsync(cacheKey, computedValue, settings.ValueTtl);
logger.LogDebug(
"Вычислено и сохранено значение для '{ShortcodeKey}': unit={UnitId} → '{Value}' (срок хранения={Ttl})",
shortcode, unitId, computedValue, settings.ValueTtl);
return computedValue;
}
catch (Exception ex)
{
logger.LogWarning(
ex,
"Ошибка при получении или вычислении значения для шорткода '{ShortcodeKey}' (unit={UnitId}). Возвращена пустая строка.",
shortcode, unitId);
return string.Empty;
}
}
private static string GetCacheKey(Guid jobGroupId, Guid unitId, string shortcodeKey)
{
var safeKey = shortcodeKey
.Trim()
.Replace(":", "_")
.Replace(" ", "_")
.Replace(".", "_")
.Replace("%", "")
.Replace("[", "_")
.Replace("]", "_")
.Replace("/", "_")
.Replace("\\", "_");
// ✅ SHA256 от safeKey
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(safeKey));
var hashHex = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
// ✅ Новый формат ключа
return $"gr_shcd_{jobGroupId:N}{unitId:N}_{hashHex}";
}
}
}

View File

@@ -398,36 +398,61 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count()); logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count());
foreach (var rf in filter.RelationshipFilters) // ✅ Получить все связи для юнита
{ var parentLinks = await unitInUnitService.GetByChildIdAsync(unitId);
logger.LogDebug("Обработка RelationshipFilter (UnitFilterId={UnitFilterId}): IsParent={IsParent}, FieldId={FieldId}, ValueMask={ValueMask}", var childLinks = await unitInUnitService.GetByParentIdAsync(unitId);
rf.UnitFilterId, rf.IsParent, rf.FieldId, rf.ValueMask);
var valueMask = rf.ValueMask?.Trim(); // ✅ Собрать все UnitId, участвующие в связях
if (string.IsNullOrEmpty(valueMask)) continue; var allRelatedUnitIds = parentLinks
.Select(l => l.ParentUnitId)
List<UnitInUnit> relevantLinks; .Concat(childLinks.Select(l => l.ChildUnitId))
if (rf.IsParent)
relevantLinks = await unitInUnitService.GetByChildIdAsync(unitId);
else
relevantLinks = await unitInUnitService.GetByParentIdAsync(unitId);
var unitIdsToCheck = rf.IsParent
? relevantLinks.Select(l => l.ParentUnitId).ToList()
: relevantLinks.Select(l => l.ChildUnitId).ToList();
if (!unitIdsToCheck.Any()) continue;
var unitValues = await unitInValueService.GetByUnitIdsAsync(unitIdsToCheck);
var matchingUnitIds = unitValues
.Where(uv => uv.FieldId == rf.FieldId
&& uv.Value?.Value != null
&& uv.Value.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
.Select(uv => uv.UnitId)
.Distinct() .Distinct()
.ToList(); .ToList();
if (!allRelatedUnitIds.Any()) continue;
// ✅ Получить значения для всех связанных юнитов
var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds);
// ✅ Сгруппировать значения по UnitId
var valuesByUnit = allUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
// ✅ Найти UnitId, которые проходят все RelationshipFilters
var matchingUnitIds = new HashSet<Guid>();
foreach (var relatedUnitId in allRelatedUnitIds)
{
// ✅ Изменяем на `All` — все фильтры должны подходить
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
{
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
var matchingValues = values
.Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null)
.ToList();
if (!matchingValues.Any()) // Нет значений по полю
{
// ✅ Если нет значений, и IsInverse = false → не подходит
// ✅ Если нет значений, и IsInverse = true → подходит
return rf.IsInverse;
}
var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));
// ✅ Учитываем IsInverse
if (rf.IsInverse)
hasMatch = !hasMatch;
return hasMatch;
});
if (passesAllFilters)
matchingUnitIds.Add(relatedUnitId);
}
if (matchingUnitIds.Any()) if (matchingUnitIds.Any())
{ {
var names = await unitService.Get().AsNoTracking() var names = await unitService.Get().AsNoTracking()
@@ -437,7 +462,6 @@ namespace PARR.DAL.DomainServices.Implementations
result.UnionWith(names); result.UnionWith(names);
} }
} }
}
return result.ToList(); return result.ToList();
} }
@@ -450,21 +474,25 @@ namespace PARR.DAL.DomainServices.Implementations
if (string.IsNullOrEmpty(valueMask)) if (string.IsNullOrEmpty(valueMask))
return query; return query;
logger.LogDebug("Применяем FieldFilterDto: FieldId={FieldId}, ValueMask={ValueMask}", fieldFilter.FieldId, valueMask); logger.LogDebug("Применяем FieldFilterDto: FieldId={FieldId}, ValueMask={ValueMask}, IsInverse={IsInverse}", fieldFilter.FieldId, valueMask, fieldFilter.IsInverse);
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
return query.AsEnumerable().Where(dto => return query.AsEnumerable().Where(dto =>
dto.Values.Any(v => {
var hasMatch = dto.Values.Any(v =>
v.FieldId == fieldFilter.FieldId && v.FieldId == fieldFilter.FieldId &&
v.Value != null && v.Value != null &&
(isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) : (isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) : isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) : isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) :
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase)) v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
) );
).AsQueryable();
// ✅ Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр
return fieldFilter.IsInverse ? !hasMatch : hasMatch;
}).AsQueryable();
} }
private IQueryable<UnitDto> ApplyRelationshipFilterToQuery(IQueryable<UnitDto> query, JobRelationshipFilter relFilter) private IQueryable<UnitDto> ApplyRelationshipFilterToQuery(IQueryable<UnitDto> query, JobRelationshipFilter relFilter)
@@ -640,13 +668,12 @@ namespace PARR.DAL.DomainServices.Implementations
// Есть фильтры по полям → считаем только связанные юниты, подходящие под фильтр // Есть фильтры по полям → считаем только связанные юниты, подходящие под фильтр
foreach (var link in links) foreach (var link in links)
{ {
bool hasMatch = link.Values.Any(v => // ✅ Изменяем на `All` — все фильтры должны подходить
activeFilters.Any(f => bool hasMatch = activeFilters.All(f =>
v.FieldId == f.FieldId && {
v.Value != null && var value = link.Values.FirstOrDefault(v => v.FieldId == f.FieldId);
v.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase) return value != null && value.Value != null && value.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase);
) });
);
if (hasMatch) if (hasMatch)
matchingCount++; matchingCount++;

View File

@@ -1,11 +0,0 @@
namespace PARR.DAL.DomainServices.Interfaces
{
public interface IGroupedShortcodesCacheService
{
Task<string> GetAggregatedValueAsync(
Guid jobGroupId,
Guid unitId,
string shortcodeKey,
Func<Task<string>> computeIfMissing);
}
}

View File

@@ -0,0 +1,10 @@
namespace PARR.DAL.DomainServices.Shortcodes.Models
{
internal class CachedGroupedShortCode
{
public string Value { get; set; } = string.Empty;
public DateTimeOffset Timestamp { get; set; }
public string? Source { get; set; } = "ShortcodesService";
public int Version { get; set; } = 1;
}
}

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Constants; using PARR.Constants;
using PARR.DAL.CacheServices;
using PARR.DAL.Contracts; using PARR.DAL.Contracts;
using PARR.DAL.DomainModels; using PARR.DAL.DomainModels;
using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Interfaces;
@@ -31,7 +32,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
private readonly IUnitInValueService unitInValueService; private readonly IUnitInValueService unitInValueService;
private readonly IUnitFieldService unitFieldService; private readonly IUnitFieldService unitFieldService;
private readonly ITemplateService templateService; private readonly ITemplateService templateService;
private readonly IGroupedShortcodesCacheService groupedShortcodesCacheService; private readonly IRedisCacheService cacheService;
private readonly IUnitFilterService unitFilterService; private readonly IUnitFilterService unitFilterService;
public ShortcodesService( public ShortcodesService(
@@ -43,7 +44,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
IUnitInValueService unitInValueService, IUnitInValueService unitInValueService,
IUnitFieldService unitFieldService, IUnitFieldService unitFieldService,
ITemplateService templateService, ITemplateService templateService,
IGroupedShortcodesCacheService groupedShortcodesCacheService IRedisCacheService cacheService
) )
{ {
this.logger = logger; this.logger = logger;
@@ -53,7 +54,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
this.unitInValueService = unitInValueService; this.unitInValueService = unitInValueService;
this.unitFieldService = unitFieldService; this.unitFieldService = unitFieldService;
this.templateService = templateService; this.templateService = templateService;
this.groupedShortcodesCacheService = groupedShortcodesCacheService; this.cacheService = cacheService;
this.unitFilterService = unitFilterService; this.unitFilterService = unitFilterService;
} }
@@ -124,8 +125,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
var maxShortcodes = Regex.Matches(resultName, maxShortcodePattern); var maxShortcodes = Regex.Matches(resultName, maxShortcodePattern);
if (maxShortcodes.Count > 0) if (maxShortcodes.Count > 0)
{ {
var unitIds = template.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); resultName = await ReplaceMaxShortcodesAsync(job.Group.Id, template.UnitId, resultName, maxShortcodes);
resultName = await ReplaceMaxShortcodesAsync(job.Group.Id, template.UnitId, unitIds, resultName, maxShortcodes);
} }
} }
@@ -273,9 +273,6 @@ namespace PARR.DAL.DomainServices.Shortcodes
new ShortcodeInfoDto { Shortcode = "%МАКС:ИМЯ АТРИБУТА%", new ShortcodeInfoDto { Shortcode = "%МАКС:ИМЯ АТРИБУТА%",
Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗАК%", Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗАК%",
Type = ShortcodeTypeEnum.Standart }, Type = ShortcodeTypeEnum.Standart },
new ShortcodeInfoDto { Shortcode = "%ГРОЛЕ-ПН%",
Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.",
Type = ShortcodeTypeEnum.Relationship },
new ShortcodeInfoDto{ Shortcode = "%БУКВЫ:ИМЯ АТРИБУТА%", new ShortcodeInfoDto{ Shortcode = "%БУКВЫ:ИМЯ АТРИБУТА%",
Description = "Извлекает только буквы из значения поля. Пример: %БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ% → ПРИВ", Description = "Извлекает только буквы из значения поля. Пример: %БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ% → ПРИВ",
Type = ShortcodeTypeEnum.Standart Type = ShortcodeTypeEnum.Standart
@@ -290,7 +287,10 @@ namespace PARR.DAL.DomainServices.Shortcodes
Type = ShortcodeTypeEnum.Relationship }, Type = ShortcodeTypeEnum.Relationship },
new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%", new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%",
Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК", Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК",
Type = ShortcodeTypeEnum.Relationship } Type = ShortcodeTypeEnum.Relationship },
new ShortcodeInfoDto { Shortcode = "%ГРОЛЕ-ПН%",
Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.",
Type = ShortcodeTypeEnum.Relationship },
}); });
// 4. Все доступные поля из UnitField // 4. Все доступные поля из UnitField
@@ -363,7 +363,7 @@ namespace PARR.DAL.DomainServices.Shortcodes
return resultName; return resultName;
} }
private async Task<string> ReplaceMaxShortcodesAsync(Guid jobGroupId, Guid unitId, List<Guid> unitIds, string input, MatchCollection maxShortcodes) private async Task<string> ReplaceMaxShortcodesAsync(Guid jobGroupId, Guid unitId, string input, MatchCollection maxShortcodes)
{ {
var shortcodeToMatches = maxShortcodes var shortcodeToMatches = maxShortcodes
.Cast<Match>() .Cast<Match>()
@@ -379,20 +379,34 @@ namespace PARR.DAL.DomainServices.Shortcodes
logger.LogDebug("Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}, fieldName {FieldName}", logger.LogDebug("Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}, fieldName {FieldName}",
fullShortcode, jobGroupId, unitId, fieldName); fullShortcode, jobGroupId, unitId, fieldName);
var mostFrequentValue = await groupedShortcodesCacheService.GetAggregatedValueAsync( var mostFrequentValue = await GetMaxShortCodeFromCacheOrDbAsync(jobGroupId, unitId, fullShortcode, fieldName);
jobGroupId,
unitId,
fullShortcode,
async () =>
{
logger.LogDebug("Кэш промахнут для {Shortcode}, начинаем вычисление.", fullShortcode);
// ✅ Пытаемся загрузить UnitsInTemplate из БД foreach (var match in matches)
var dbUnitIds = await jobService.Get() input = input.Replace(match.Value, mostFrequentValue);
}
return input;
}
private async Task<string> GetMaxShortCodeFromCacheOrDbAsync(Guid jobGroupId, Guid unitId, string fullShortcode, string fieldName)
{
var cacheKey = $"gr_shcd_{jobGroupId:N}_{unitId:N}_{ComputeHash(fullShortcode)}";
var cachedData = await cacheService.GetCachedDataAsync<CachedGroupedShortCode>(cacheKey);
if (cachedData != null)
{
logger.LogDebug("Кэш попал для GroupedShortCode '{Name}': {Value}", fullShortcode, cachedData.Value);
return cachedData.Value;
}
logger.LogDebug("Кэш промахнут для GroupedShortCode '{Name}'. Запрашиваем из БД.", fullShortcode);
// Загружаем юниты из БД
var effectiveUnitIds = await jobService.Get()
.Where(j => j.GroupId == jobGroupId) .Where(j => j.GroupId == jobGroupId)
.Join( .Join(
templateService.Get() templateService.Get()
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used) .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used && t.UnitId == unitId)
.Include(t => t.UnitsInTemplate), .Include(t => t.UnitsInTemplate),
job => job.Id, job => job.Id,
template => template.JobId, template => template.JobId,
@@ -403,30 +417,29 @@ namespace PARR.DAL.DomainServices.Shortcodes
.Distinct() .Distinct()
.ToListAsync(); .ToListAsync();
logger.LogDebug("Загружено {Count} unitIds из БД для JobGroup {JobGroupId}", dbUnitIds.Count, jobGroupId);
// ✅ Если в БД нет UnitsInTemplate — используем переданные unitIds
var effectiveUnitIds = dbUnitIds.Any() ? dbUnitIds : unitIds;
logger.LogDebug("EffectiveUnitIds: [{Ids}], Count: {Count}", string.Join(", ", effectiveUnitIds), effectiveUnitIds.Count); logger.LogDebug("EffectiveUnitIds: [{Ids}], Count: {Count}", string.Join(", ", effectiveUnitIds), effectiveUnitIds.Count);
// Вызываем метод из сервиса // Вызываем метод из сервиса
var result = await unitInValueService.GetMostFrequentValueForFieldAsync(effectiveUnitIds, fieldName); var mostFrequentValue = await unitInValueService.GetMostFrequentValueForFieldAsync(effectiveUnitIds, fieldName);
logger.LogDebug("Результат GetMostFrequentValueForFieldAsync: {Result}, для поля {FieldName}, unitIds: [{Ids}]", result, fieldName, string.Join(", ", effectiveUnitIds)); logger.LogDebug("Результат GetMostFrequentValueForFieldAsync: {Result}, для поля {FieldName}, unitIds: [{Ids}]", mostFrequentValue, fieldName, string.Join(", ", effectiveUnitIds));
return result ?? string.Empty; // Не кэшируем пустые значения
}); if (!string.IsNullOrEmpty(mostFrequentValue))
logger.LogDebug("Итоговое значение для {Shortcode}: {Value}", fullShortcode, mostFrequentValue);
foreach (var match in matches)
{ {
input = input.Replace(match.Value, mostFrequentValue); var toCache = new CachedGroupedShortCode
} {
Value = mostFrequentValue,
Timestamp = DateTimeOffset.UtcNow,
Source = GetType().Name,
Version = 1
};
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1));
return mostFrequentValue;
} }
return input; return "Нет данных";
} }
private async Task<string> ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes) private async Task<string> ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes)
@@ -485,5 +498,12 @@ namespace PARR.DAL.DomainServices.Shortcodes
} }
return result.ToString(); return result.ToString();
} }
private static string ComputeHash(string input)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(hashedBytes).Replace('+', '-').Replace('/', '_').Substring(0, 16);
}
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -60,6 +60,13 @@ namespace PARR.EsppSync
// Загружаем Template и TemplateForShortcodes в одном запросе // Загружаем Template и TemplateForShortcodes в одном запросе
var query = templateService.Get() var query = templateService.Get()
.AsNoTracking() .AsNoTracking()
.Include(h => h.Unit)
.ThenInclude(t => t!.UnitValues)
.ThenInclude(t => t.Value)
.Include(h => h.Unit)
.ThenInclude(t => t!.UnitValues)
.ThenInclude(t => t.Field)
.Include(t => t.RobotConfigurations)
.Include(t => t.Job) .Include(t => t.Job)
.ThenInclude(j => j.Group) .ThenInclude(j => j.Group)
.ThenInclude(g => g.GroupType) .ThenInclude(g => g.GroupType)
@@ -71,6 +78,8 @@ namespace PARR.EsppSync
.ThenInclude(t => t.ScheduleExcludeTypeCalendar) .ThenInclude(t => t.ScheduleExcludeTypeCalendar)
.Include(t => t.Job) .Include(t => t.Job)
.ThenInclude(j => j.Tnk) .ThenInclude(j => j.Tnk)
.ThenInclude(s => s!.Subprocess)
.ThenInclude(p => p!.Process)
.Include(t => t.UnitsInTemplate); .Include(t => t.UnitsInTemplate);
var template = await query.FirstOrDefaultAsync(t => t.Name == esppObject.TemplateName); var template = await query.FirstOrDefaultAsync(t => t.Name == esppObject.TemplateName);

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -3,11 +3,21 @@
"DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;", "DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;",
"RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs" "RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs"
}, },
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": { "Serilog": {
"MinimumLevel": { "MinimumLevel": {
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -6,7 +6,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -14,6 +16,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }

View File

@@ -5,7 +5,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": { "Serilog": {
@@ -13,6 +15,8 @@
"Default": "Information", "Default": "Information",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }