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

@@ -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,44 +398,68 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count());
foreach (var rf in filter.RelationshipFilters)
// ✅ Получить все связи для юнита
var parentLinks = await unitInUnitService.GetByChildIdAsync(unitId);
var childLinks = await unitInUnitService.GetByParentIdAsync(unitId);
// ✅ Собрать все UnitId, участвующие в связях
var allRelatedUnitIds = parentLinks
.Select(l => l.ParentUnitId)
.Concat(childLinks.Select(l => l.ChildUnitId))
.Distinct()
.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)
{
logger.LogDebug("Обработка RelationshipFilter (UnitFilterId={UnitFilterId}): IsParent={IsParent}, FieldId={FieldId}, ValueMask={ValueMask}",
rf.UnitFilterId, rf.IsParent, rf.FieldId, rf.ValueMask);
var valueMask = rf.ValueMask?.Trim();
if (string.IsNullOrEmpty(valueMask)) continue;
List<UnitInUnit> relevantLinks;
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()
.ToList();
if (matchingUnitIds.Any())
// ✅ Изменяем на `All` — все фильтры должны подходить
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
{
var names = await unitService.Get().AsNoTracking()
.Where(u => matchingUnitIds.Contains(u.Id))
.Select(u => u.Name)
.ToListAsync();
result.UnionWith(names);
}
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())
{
var names = await unitService.Get().AsNoTracking()
.Where(u => matchingUnitIds.Contains(u.Id))
.Select(u => u.Name)
.ToListAsync();
result.UnionWith(names);
}
}
@@ -450,21 +474,25 @@ namespace PARR.DAL.DomainServices.Implementations
if (string.IsNullOrEmpty(valueMask))
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 isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
return query.AsEnumerable().Where(dto =>
dto.Values.Any(v =>
{
var hasMatch = dto.Values.Any(v =>
v.FieldId == fieldFilter.FieldId &&
v.Value != null &&
(isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), 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)
@@ -640,13 +668,12 @@ namespace PARR.DAL.DomainServices.Implementations
// Есть фильтры по полям → считаем только связанные юниты, подходящие под фильтр
foreach (var link in links)
{
bool hasMatch = link.Values.Any(v =>
activeFilters.Any(f =>
v.FieldId == f.FieldId &&
v.Value != null &&
v.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase)
)
);
// ✅ Изменяем на `All` — все фильтры должны подходить
bool hasMatch = activeFilters.All(f =>
{
var value = link.Values.FirstOrDefault(v => v.FieldId == f.FieldId);
return value != null && value.Value != null && value.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase);
});
if (hasMatch)
matchingCount++;