feat(dal): уточнена логика получение имени связанных ЭК, переписано кэширование МАКС:FIELD, в синхронизацию и контролер заданий роботов добавлены include
This commit is contained in:
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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++;
|
||||
|
||||
Reference in New Issue
Block a user