2026-05-29 17:09:36 +10:00
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2026-06-03 12:09:47 +10:00
|
|
|
|
using Newtonsoft.Json.Linq;
|
2026-05-29 17:09:36 +10:00
|
|
|
|
using PARR.Core.Common.Interfaces;
|
|
|
|
|
|
using PARR.Core.Repositories.Interfaces.Unit;
|
|
|
|
|
|
using PARR.Domain.Cache;
|
|
|
|
|
|
using PARR.Domain.DTOs.UnitDto;
|
|
|
|
|
|
|
|
|
|
|
|
namespace PARR.Core.Services.UnitService.Implementations
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Сервис по работе с юнитами из кэш и бд
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
internal class UnitCacheService
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// TTL для кэш
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private readonly TimeSpan CacheTtl = TimeSpan.FromHours(24);
|
|
|
|
|
|
|
2026-06-03 12:09:47 +10:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Размер порции данных, для запроса в БД
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private readonly int DatabaseBatchSize = 500;
|
|
|
|
|
|
|
2026-05-29 17:09:36 +10:00
|
|
|
|
private readonly ILogger<UnitCacheService> logger;
|
|
|
|
|
|
private readonly IUnitRepository unitRepository;
|
|
|
|
|
|
private readonly IRedisCacheService redisCacheService;
|
|
|
|
|
|
|
|
|
|
|
|
public UnitCacheService(
|
|
|
|
|
|
ILogger<UnitCacheService> logger,
|
|
|
|
|
|
IUnitRepository unitRepository,
|
|
|
|
|
|
IRedisCacheService redisCacheService
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
this.logger = logger;
|
|
|
|
|
|
this.unitRepository = unitRepository;
|
|
|
|
|
|
this.redisCacheService = redisCacheService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Получить UnitInfo по id
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="id"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public async Task<UnitInfo?> GetAsync(Guid id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(id));
|
|
|
|
|
|
var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(id));
|
|
|
|
|
|
|
|
|
|
|
|
// Смотрим, есть ли в кэш
|
|
|
|
|
|
var cache = await redisCacheService.GetHashFieldAsync<UnitInfo?>(hashKey, unitKey, true);
|
|
|
|
|
|
if (cache != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogDebug("Данные о юните {UnitId} получены из кэша.", id);
|
|
|
|
|
|
return cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Нет в кэш, ищем в бд
|
|
|
|
|
|
var dbData = await GetUnitAsync(id);
|
|
|
|
|
|
if (dbData == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogWarning("В БД нет данных о юните {UnitId}. Вернулся null", id);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Сохраняем в кэш
|
|
|
|
|
|
await redisCacheService.SetHashFieldAsync(hashKey, unitKey, dbData, CacheTtl, true);
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Вернули юнит полученный из БД {UnitId}.", id);
|
|
|
|
|
|
|
|
|
|
|
|
return dbData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Получить словарь UnitInfo
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ids"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public async Task<Dictionary<Guid, UnitInfo>> GetAsync(IReadOnlySet<Guid> ids)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ids == null || ids.Count == 0)
|
|
|
|
|
|
return new Dictionary<Guid, UnitInfo>();
|
|
|
|
|
|
|
|
|
|
|
|
// Формируем ключи
|
|
|
|
|
|
var keys = ids.Select(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var (hashKey, fieldKey) = (redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(t)), redisCacheService.GetKey(CacheKeys.Unit.UnitItem(t)));
|
|
|
|
|
|
return (HashKey: hashKey, FieldKey: fieldKey);
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
2026-06-01 11:58:08 +10:00
|
|
|
|
logger.LogDebug("Необходимо найти инорфмацию для {Count} объектов.", ids.Count);
|
|
|
|
|
|
|
2026-05-29 17:09:36 +10:00
|
|
|
|
// Получаем из кэш
|
|
|
|
|
|
var cacheData = await redisCacheService.GetHashFieldsAsync<UnitInfo>(keys, true);
|
|
|
|
|
|
|
2026-06-01 11:58:08 +10:00
|
|
|
|
logger.LogDebug("Из кэша получена информация о {Count} объектах.", cacheData.Count);
|
|
|
|
|
|
|
|
|
|
|
|
var resultDictionary = cacheData.ToDictionary(unit => unit.Id, unit => unit);
|
|
|
|
|
|
|
|
|
|
|
|
// Проверям каких данных нет в кэш, догрузим из из БД
|
|
|
|
|
|
if (resultDictionary.Count < ids.Count)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Находим id которых нет в кэше
|
|
|
|
|
|
var missingIds = ids.Where(id => !resultDictionary.ContainsKey(id)).ToHashSet();
|
2026-05-29 17:09:36 +10:00
|
|
|
|
|
2026-06-01 11:58:08 +10:00
|
|
|
|
logger.LogDebug("Запрашиваем из БД дополнительно {Count} юнитов", missingIds.Count);
|
2026-05-29 17:09:36 +10:00
|
|
|
|
|
2026-06-01 11:58:08 +10:00
|
|
|
|
var dbData = await GetUnitsAsync(missingIds);
|
2026-05-29 17:09:36 +10:00
|
|
|
|
|
2026-06-03 12:09:47 +10:00
|
|
|
|
// список объектов для сохранения в кэш
|
|
|
|
|
|
var dataToCache = new List<(string HashKey, string FieldKey, UnitInfo Value)>();
|
|
|
|
|
|
|
2026-06-01 11:58:08 +10:00
|
|
|
|
foreach (var unit in dbData)
|
|
|
|
|
|
{
|
|
|
|
|
|
resultDictionary[unit.Id] = unit;
|
2026-05-29 17:09:36 +10:00
|
|
|
|
|
2026-06-01 11:58:08 +10:00
|
|
|
|
// Добавляем в кэш
|
|
|
|
|
|
var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(unit.Id));
|
|
|
|
|
|
var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(unit.Id));
|
|
|
|
|
|
|
2026-06-03 12:09:47 +10:00
|
|
|
|
//await redisCacheService.SetHashFieldAsync(hashKey, unitKey, unit, CacheTtl, true);
|
|
|
|
|
|
dataToCache.Add((hashKey, unitKey, unit));
|
2026-06-01 11:58:08 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 12:09:47 +10:00
|
|
|
|
if (dataToCache.Count > 0)
|
|
|
|
|
|
await redisCacheService.SetHashFieldsAsync<UnitInfo>(dataToCache, CacheTtl, true);
|
|
|
|
|
|
}
|
2026-05-29 17:09:36 +10:00
|
|
|
|
|
2026-06-01 11:58:08 +10:00
|
|
|
|
return resultDictionary;
|
2026-05-29 17:09:36 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Удалить запись UnitInfo из кэш
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="id"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
/// <exception cref="NotImplementedException"></exception>
|
2026-06-01 16:10:00 +10:00
|
|
|
|
public async Task RemoveFromCacheAsync(Guid id)
|
2026-05-29 17:09:36 +10:00
|
|
|
|
{
|
2026-06-01 16:10:00 +10:00
|
|
|
|
var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(id));
|
|
|
|
|
|
var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(id));
|
|
|
|
|
|
|
|
|
|
|
|
await redisCacheService.DeleteHashFieldAsync(hashKey, unitKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Удалить записи UnitInfo из кэш
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ids"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public async Task RemoveFromCacheAsync(IReadOnlySet<Guid> ids)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ids == null || ids.Count == 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// Формируем ключи
|
|
|
|
|
|
var keys = ids.Select(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var (hashKey, fieldKey) = (redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(t)), redisCacheService.GetKey(CacheKeys.Unit.UnitItem(t)));
|
|
|
|
|
|
return (HashKey: hashKey, FieldKey: fieldKey);
|
|
|
|
|
|
}).ToList();
|
2026-05-29 17:09:36 +10:00
|
|
|
|
|
2026-06-01 16:10:00 +10:00
|
|
|
|
await redisCacheService.DeleteHashFieldsAsync(keys);
|
2026-05-29 17:09:36 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Получить значение по одному юниту из БД
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unitId"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
private async Task<UnitInfo?> GetUnitAsync(Guid unitId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var unit = await unitRepository.Get()
|
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Select(t => new
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = t.Id,
|
|
|
|
|
|
Name = t.Name,
|
|
|
|
|
|
Values = t.UnitValues.Select(x => new { FieldId = x.FieldId, Value = x.Value!.Value }),
|
|
|
|
|
|
ParentUnits = t.ParentUnits.Select(t => t.ParentUnitId),
|
|
|
|
|
|
ChildUnits = t.ChildUnits.Select(t => t.ChildUnitId)
|
|
|
|
|
|
})
|
|
|
|
|
|
.FirstOrDefaultAsync(t => t.Id == unitId);
|
|
|
|
|
|
|
|
|
|
|
|
if (unit == null)
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
|
|
// Склеим вместе родителей и детей, чтоб не делать лишних запросов к бд
|
|
|
|
|
|
var relatives = unit.ParentUnits.Union(unit.ChildUnits).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
// Получаем базовую инфу о родственниках
|
|
|
|
|
|
var relativesDictionary = await unitRepository.Get()
|
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Where(t => relatives.Contains(t.Id))
|
|
|
|
|
|
.Select(t => new UnitInfoBase
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = t.Id,
|
|
|
|
|
|
Name = t.Name,
|
|
|
|
|
|
Values = t.UnitValues.Select(x => new UnitInfoAttribute
|
|
|
|
|
|
{
|
|
|
|
|
|
FieldId = x.FieldId,
|
|
|
|
|
|
Value = x.Value!.Value
|
|
|
|
|
|
}).ToList()
|
|
|
|
|
|
}).ToDictionaryAsync(t => t.Id);
|
|
|
|
|
|
|
|
|
|
|
|
// Собираем итоговый UnitInfo
|
|
|
|
|
|
return new UnitInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = unit.Id,
|
|
|
|
|
|
Name = unit.Name,
|
|
|
|
|
|
Values = unit.Values.Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value }).ToList(),
|
|
|
|
|
|
ParentUnits = unit.ParentUnits
|
|
|
|
|
|
.Where(id => relativesDictionary.ContainsKey(id))
|
|
|
|
|
|
.Select(id => relativesDictionary[id])
|
|
|
|
|
|
.ToList(),
|
|
|
|
|
|
ChildUnits = unit.ChildUnits
|
|
|
|
|
|
.Where(id => relativesDictionary.ContainsKey(id))
|
|
|
|
|
|
.Select(id => relativesDictionary[id])
|
|
|
|
|
|
.ToList()
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Получить значения по списку юнитов
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ids"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
private async Task<List<UnitInfo>> GetUnitsAsync(IReadOnlySet<Guid> ids)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ids == null || ids.Count == 0)
|
|
|
|
|
|
return new List<UnitInfo>();
|
|
|
|
|
|
|
|
|
|
|
|
// Пакетно получаем основные данные для юнитов
|
|
|
|
|
|
var dbUnits = new List<dynamic>();
|
|
|
|
|
|
|
|
|
|
|
|
// Выполняем порционно
|
2026-06-03 12:09:47 +10:00
|
|
|
|
foreach (var chank in ids.Chunk(DatabaseBatchSize))
|
2026-05-29 17:09:36 +10:00
|
|
|
|
{
|
|
|
|
|
|
var chunkUnits = await unitRepository.Get()
|
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Where(t => chank.Contains(t.Id))
|
|
|
|
|
|
.Select(t => new
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = t.Id,
|
|
|
|
|
|
Name = t.Name,
|
|
|
|
|
|
Values = t.UnitValues.Select(x => new { FieldId = x.FieldId, Value = x.Value!.Value }).ToList(),
|
|
|
|
|
|
ParentUnits = t.ParentUnits.Select(p => p.ParentUnitId).ToList(),
|
|
|
|
|
|
ChildUnits = t.ChildUnits.Select(c => c.ChildUnitId).ToList()
|
|
|
|
|
|
})
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
dbUnits.AddRange(chunkUnits);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (dbUnits.Count == 0)
|
|
|
|
|
|
return new List<UnitInfo>();
|
|
|
|
|
|
|
|
|
|
|
|
// Собираем ВСЕ ID родственников(и родителей, и детей) для ВСЕХ найденных юнитов в один HashSet
|
|
|
|
|
|
var allRelativeIds = new HashSet<Guid>();
|
|
|
|
|
|
foreach (var unit in dbUnits)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var parentId in unit.ParentUnits)
|
|
|
|
|
|
allRelativeIds.Add(parentId);
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var childId in unit.ChildUnits)
|
|
|
|
|
|
allRelativeIds.Add(childId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Пакетно загружаем базовую информацию обо всех родственниках за один раз
|
|
|
|
|
|
var relativesDictionary = new Dictionary<Guid, UnitInfoBase>();
|
|
|
|
|
|
|
2026-06-03 12:09:47 +10:00
|
|
|
|
foreach (var chunk in allRelativeIds.Chunk(DatabaseBatchSize))
|
2026-05-29 17:09:36 +10:00
|
|
|
|
{
|
|
|
|
|
|
var chunkRelatives = await unitRepository.Get()
|
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Where(t => chunk.Contains(t.Id))
|
|
|
|
|
|
.Select(t => new UnitInfoBase
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = t.Id,
|
|
|
|
|
|
Name = t.Name,
|
|
|
|
|
|
Values = t.UnitValues.Select(x => new UnitInfoAttribute
|
|
|
|
|
|
{
|
|
|
|
|
|
FieldId = x.FieldId,
|
|
|
|
|
|
Value = x.Value!.Value
|
|
|
|
|
|
}).ToList()
|
|
|
|
|
|
})
|
|
|
|
|
|
.ToDictionaryAsync(t => t.Id);
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var kvp in chunkRelatives)
|
|
|
|
|
|
{
|
|
|
|
|
|
relativesDictionary[kvp.Key] = kvp.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Собираем итоговый список UnitInfo, маппим родственников из словаря
|
|
|
|
|
|
var result = new List<UnitInfo>(dbUnits.Count);
|
|
|
|
|
|
foreach (var unit in dbUnits)
|
|
|
|
|
|
{
|
|
|
|
|
|
var unitInfo = new UnitInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = unit.Id,
|
|
|
|
|
|
Name = unit.Name,
|
|
|
|
|
|
Values = ((IEnumerable<dynamic>)unit.Values)
|
|
|
|
|
|
.Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value })
|
|
|
|
|
|
.ToList(),
|
|
|
|
|
|
|
|
|
|
|
|
ParentUnits = ((IEnumerable<Guid>)unit.ParentUnits)
|
|
|
|
|
|
.Where(id => relativesDictionary.ContainsKey(id))
|
|
|
|
|
|
.Select(id => relativesDictionary[id])
|
|
|
|
|
|
.ToList(),
|
|
|
|
|
|
|
|
|
|
|
|
ChildUnits = ((IEnumerable<Guid>)unit.ChildUnits)
|
|
|
|
|
|
.Where(id => relativesDictionary.ContainsKey(id))
|
|
|
|
|
|
.Select(id => relativesDictionary[id])
|
|
|
|
|
|
.ToList()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
result.Add(unitInfo);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Получено из БД данных для {Count} юнитов", result.Count);
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|