Compare commits
34 Commits
36ccf5b051
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb20395e53 | ||
|
|
94bea5c46d | ||
|
|
349cf55862 | ||
|
|
4cd6134ad4 | ||
|
|
f10a50edae | ||
|
|
0ab5b49371 | ||
|
|
acdb6ec893 | ||
|
|
7d5fb23daf | ||
|
|
e36e08aa73 | ||
|
|
343468cf71 | ||
|
|
56cdca0502 | ||
|
|
8798529b4d | ||
|
|
556d895c7c | ||
|
|
58275d73f4 | ||
|
|
eacda75649 | ||
|
|
90e9f80505 | ||
|
|
b3879062a6 | ||
|
|
58b4d98b16 | ||
|
|
d1889460de | ||
|
|
5f384947a6 | ||
|
|
1cc2beb9ca | ||
|
|
0b0c0b04ad | ||
|
|
5094945c8e | ||
|
|
98245e73e6 | ||
|
|
cdcb4fd9bc | ||
|
|
bbb14ee4ea | ||
|
|
5edbcff35b | ||
|
|
6c93e1971f | ||
|
|
68696a2fdc | ||
|
|
751b693e72 | ||
|
|
6ca6bfbc2e | ||
|
|
178a991d8b | ||
|
|
a087958fd5 | ||
|
|
4ee2880a8f |
@@ -196,7 +196,6 @@ namespace PARR.AIHITMainSyncer.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Поиск конфигурации поля для тега
|
// Поиск конфигурации поля для тега
|
||||||
// Рекомендация: если метод вызывается в цикле, передавайте найденное поле внешним слоем
|
|
||||||
var tagUnitField = fieldsFromDB.FirstOrDefault(f => f.Code == "tag");
|
var tagUnitField = fieldsFromDB.FirstOrDefault(f => f.Code == "tag");
|
||||||
bool isTagProperty = tagUnitField != null && multiValueProperty.Key == tagUnitField.AihitName;
|
bool isTagProperty = tagUnitField != null && multiValueProperty.Key == tagUnitField.AihitName;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.AIHITRelationshipsSyncer.Models;
|
using PARR.AIHITRelationshipsSyncer.Models;
|
||||||
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
|
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
|
||||||
@@ -6,263 +6,256 @@ using PARR.Core.Repositories.Interfaces.Unit;
|
|||||||
using PARR.Domain.Entities.Unit;
|
using PARR.Domain.Entities.Unit;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
|
|
||||||
namespace PARR.AIHITRelationshipsSyncer.Services.Implementations
|
namespace PARR.AIHITRelationshipsSyncer.Services.Implementations;
|
||||||
|
|
||||||
|
internal class RelationshipsSyncService : IRelationshipsSyncService
|
||||||
{
|
{
|
||||||
internal class RelationshipsSyncService : IRelationshipsSyncService
|
private readonly ILogger<RelationshipsSyncService> _logger;
|
||||||
|
private readonly IUnitRepository _unitRepository;
|
||||||
|
private readonly SettingsFromDb _settingsFromDb;
|
||||||
|
|
||||||
|
public RelationshipsSyncService(
|
||||||
|
ILogger<RelationshipsSyncService> logger,
|
||||||
|
IUnitRepository unitRepository,
|
||||||
|
SettingsFromDb settingsFromDb)
|
||||||
{
|
{
|
||||||
private readonly ILogger<RelationshipsSyncService> logger;
|
_logger = logger;
|
||||||
private readonly IUnitRepository unitService;
|
_unitRepository = unitRepository;
|
||||||
private readonly SettingsFromDb _settingsFromDb;
|
_settingsFromDb = settingsFromDb;
|
||||||
|
|
||||||
public RelationshipsSyncService(
|
|
||||||
ILogger<RelationshipsSyncService> logger,
|
|
||||||
IUnitRepository unitService,
|
|
||||||
SettingsFromDb settingsFromDb
|
|
||||||
)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.unitService = unitService;
|
|
||||||
_settingsFromDb = settingsFromDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task SyncAsync(List<AihitData> aihitdata)
|
|
||||||
{
|
|
||||||
// 1. Сначала фильтруем и нормализуем входящий мусор
|
|
||||||
var validPairs = ValidateInput(aihitdata);
|
|
||||||
|
|
||||||
// Если пришел пустой список, то и проверять порог нет смысла (лог уже записан внутри)
|
|
||||||
if (!validPairs.Any()) return;
|
|
||||||
|
|
||||||
// 2. FAIL-FAST: Быстро узнаем общее число связей в БД без выкачивания самих данных
|
|
||||||
int previousCount = await unitService.Get()
|
|
||||||
.SelectMany(u => u.ChildUnits)
|
|
||||||
.CountAsync();
|
|
||||||
|
|
||||||
// 3. SAFEGUARD COMPLIANCE: Проверяем защитный порог падения данных
|
|
||||||
if (previousCount > 0)
|
|
||||||
{
|
|
||||||
// 1. Считаем в decimal с абсолютной точностью
|
|
||||||
decimal exactPercentage = ((decimal)validPairs.Count / previousCount) * 100;
|
|
||||||
|
|
||||||
// 2. Округляем до 1 знака после запятой (например, 96.98% -> 97.0%)
|
|
||||||
// Это защитит от ложных срабатываний из-за пары недостающих связей на больших объемах
|
|
||||||
decimal currentPercentage = Math.Round(exactPercentage, 1, MidpointRounding.AwayFromZero);
|
|
||||||
|
|
||||||
// 3. Строгое сравнение (<) гарантирует пропуск при ровно 97% и работу "0" как выключателя
|
|
||||||
if (currentPercentage < _settingsFromDb.MinRelationshipsThresholdPct)
|
|
||||||
{
|
|
||||||
logger.LogWarning(
|
|
||||||
"Синхронизация отменена: количество полученных связей ниже порогового значения! " +
|
|
||||||
"Получено: {CurrentCount} ({CurrentPercentage:F1}%), ожидалось >= {Threshold}% от прошлого объема ({PreviousCount}).",
|
|
||||||
validPairs.Count, currentPercentage, _settingsFromDb.MinRelationshipsThresholdPct, previousCount);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. HAPPY PATH: Если проверка пройдена, выполняем тяжелую работу
|
|
||||||
var sourceUnitNames = ExtractUnitNames(validPairs);
|
|
||||||
var existingUnits = await GetOrCreateUnitsAsync(sourceUnitNames);
|
|
||||||
|
|
||||||
var (toRemove, toAdd) = await GetDeltaAsync(validPairs);
|
|
||||||
|
|
||||||
await RemoveRelationshipsAsync(toRemove);
|
|
||||||
await AddRelationshipsAsync(toAdd, existingUnits);
|
|
||||||
|
|
||||||
await FinalCommitAsync(toRemove.Count, toAdd.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<(string Parent, string Child)> ValidateInput(List<AihitData> data)
|
|
||||||
{
|
|
||||||
if (data == null || data.Count == 0)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Получены пустые или null данные из АИХ ИТ. Пропускаю синхронизацию");
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
var validPairs = data
|
|
||||||
.Where(t => !string.IsNullOrWhiteSpace(t.ParentName) && !string.IsNullOrWhiteSpace(t.ChildName))
|
|
||||||
.Where(t => t.ParentName != t.ChildName)
|
|
||||||
.Select(t => (
|
|
||||||
Parent: t.ParentName!.Trim().ToUpperInvariant(),
|
|
||||||
Child: t.ChildName!.Trim().ToUpperInvariant()
|
|
||||||
))
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
if (validPairs.Count == 0)
|
|
||||||
{
|
|
||||||
logger.LogWarning("После нормализации не осталось валидных связей. Пропускаю синхронизацию");
|
|
||||||
return new();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Получено {Count} валидных связей", validPairs.Count);
|
|
||||||
return validPairs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static List<string> ExtractUnitNames(List<(string Parent, string Child)> pairs)
|
|
||||||
{
|
|
||||||
return pairs
|
|
||||||
.SelectMany(x => new[] { x.Parent, x.Child })
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<Dictionary<string, Unit>> GetOrCreateUnitsAsync(List<string> names)
|
|
||||||
{
|
|
||||||
var existing = await unitService.Get()
|
|
||||||
.AsNoTracking()
|
|
||||||
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
|
||||||
|
|
||||||
var missing = names.Except(existing.Keys).ToList();
|
|
||||||
if (!missing.Any()) return existing;
|
|
||||||
|
|
||||||
logger.LogInformation("Создание отсутствующих Unit:{Count}", missing.Count);
|
|
||||||
foreach (var name in missing)
|
|
||||||
{
|
|
||||||
var unit = new Unit
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Name = name,
|
|
||||||
DateCreated = DateTimeOffset.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
if (await unitService.CreateAsync(unit))
|
|
||||||
{
|
|
||||||
logger.LogInformation("Создан Unit: {Name}", name);
|
|
||||||
existing[name] = unit;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
logger.LogError("Не удалось создать Unit: {Name}", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
await unitService.CommitAsync();
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<(List<ExistingRelationship> ToRemove, List<(string, string)> ToAdd)> GetDeltaAsync(List<(string Parent, string Child)> sourcePairs)
|
|
||||||
{
|
|
||||||
var allExistings = await unitService.Get()
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(u => u.ChildUnits).ThenInclude(r => r.ChildUnit)
|
|
||||||
.Where(u => u.ChildUnits.Any())
|
|
||||||
.SelectMany(u => u.ChildUnits, (parent, relationship) => new ExistingRelationship
|
|
||||||
{
|
|
||||||
ParentName = parent.Name,
|
|
||||||
ChildName = relationship.ChildUnit!.Name,
|
|
||||||
Relationship = relationship
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var sourceSet = new HashSet<(string, string)>(sourcePairs);
|
|
||||||
var existingSet = new HashSet<(string, string)>(allExistings.Select(x => (x.ParentName, x.ChildName)));
|
|
||||||
|
|
||||||
var toRemove = allExistings.Where(r => !sourceSet.Contains((r.ParentName, r.ChildName))).ToList();
|
|
||||||
var toAdd = sourcePairs.Where(p => !existingSet.Contains(p)).ToList();
|
|
||||||
|
|
||||||
logger.LogInformation("Связей для удаления: {ToRemove}, для добавления: {ToAdd}", toRemove.Count, toAdd.Count);
|
|
||||||
return (toRemove, toAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task RemoveRelationshipsAsync(List<ExistingRelationship> toRemove)
|
|
||||||
{
|
|
||||||
if (!toRemove.Any()) return;
|
|
||||||
|
|
||||||
var parentNames = toRemove.Select(r => r.ParentName).Distinct().ToList();
|
|
||||||
|
|
||||||
var parents = await unitService.Get()
|
|
||||||
.Include(u => u.ChildUnits)
|
|
||||||
.Where(u => parentNames.Contains(u.Name))
|
|
||||||
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
|
||||||
|
|
||||||
foreach (var rel in toRemove)
|
|
||||||
{
|
|
||||||
if (!parents.TryGetValue(rel.ParentName, out var parent))
|
|
||||||
{
|
|
||||||
logger.LogWarning("Родитель '{Parent}' не найден при попытке удаления связи - '{Child}'", rel.ParentName, rel.ChildName);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var relationshipToRemove = parent.ChildUnits
|
|
||||||
.FirstOrDefault(r => r.ChildUnitId == rel.Relationship.ChildUnitId);
|
|
||||||
|
|
||||||
if (relationshipToRemove != null)
|
|
||||||
{
|
|
||||||
logger.LogInformation("Удаление связи: '{Parent}' - '{Child}'", rel.ParentName, rel.ChildName);
|
|
||||||
parent.ChildUnits.Remove(relationshipToRemove);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.LogWarning("Связь '{Parent}'-'{Child}' не найдена в коллекции ChildUnits для удаления", rel.ParentName, rel.ChildName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddRelationshipsAsync(List<(string Parent, string Child)> toAdd, Dictionary<string, Unit> allUnits)
|
|
||||||
{
|
|
||||||
if (!toAdd.Any()) return;
|
|
||||||
|
|
||||||
var parentNames = toAdd.Select(x => x.Parent).Distinct().ToList();
|
|
||||||
var parents = await unitService.Get()
|
|
||||||
.Include(u => u.ChildUnits)
|
|
||||||
.Where(u => parentNames.Contains(u.Name))
|
|
||||||
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
|
||||||
|
|
||||||
foreach (var (Parent, Child) in toAdd)
|
|
||||||
{
|
|
||||||
if (!parents.TryGetValue(Parent, out var parent) ||
|
|
||||||
!allUnits.TryGetValue(Child, out var child))
|
|
||||||
{
|
|
||||||
logger.LogWarning("Пропущена связь '{Parent}' - '{Child}': Unit не найден", Parent, Child);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parent!.ChildUnits.Any(r => r.ChildUnitId == child.Id))
|
|
||||||
{
|
|
||||||
logger.LogWarning("Связь уже существует: '{Parent}' - '{Child}'", Parent, Child);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.ChildUnits.Add(new UnitInUnit
|
|
||||||
{
|
|
||||||
ParentUnitId = parent.Id,
|
|
||||||
ChildUnitId = child.Id,
|
|
||||||
DateCreated = DateTimeOffset.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.LogInformation("Добавлена связь: '{Parent}' - '{Child}'", Parent, Child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task FinalCommitAsync(int removedCount, int addedCount)
|
|
||||||
{
|
|
||||||
if (removedCount > 0 || addedCount > 0)
|
|
||||||
{
|
|
||||||
if (await unitService.CommitAsync())
|
|
||||||
{
|
|
||||||
logger.LogInformation("Синхронизация завершена. Удалено: {Removed}, добавлено: {Added}", removedCount, addedCount);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
logger.LogError("Не удалось сохранить изменения в БД");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
logger.LogInformation("Изменений не обнаружено");
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ExistingRelationship
|
|
||||||
{
|
|
||||||
public string ParentName { get; init; }
|
|
||||||
public string ChildName { get; init; }
|
|
||||||
public UnitInUnit Relationship { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
public async Task SyncAsync(List<AihitData> aihitData)
|
||||||
|
{
|
||||||
|
var validPairs = ValidateInput(aihitData);
|
||||||
|
|
||||||
|
if (validPairs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!await PassSafeguardCheckAsync(validPairs.Count))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var unitNames = ExtractUnitNames(validPairs);
|
||||||
|
var unitsByName = await GetOrCreateUnitsAsync(unitNames);
|
||||||
|
|
||||||
|
var (toRemove, toAdd) = await GetDeltaAsync(validPairs);
|
||||||
|
|
||||||
|
// Удаление и добавление накапливаются в одном ChangeTracker.
|
||||||
|
// CommitAsync сохраняет всё атомарно через SaveChangesAsync.
|
||||||
|
await RemoveRelationshipsAsync(toRemove);
|
||||||
|
await AddRelationshipsAsync(toAdd, unitsByName);
|
||||||
|
await CommitChangesAsync(toRemove.Count, toAdd.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<(string ParentName, string ChildName)> ValidateInput(List<AihitData> data)
|
||||||
|
{
|
||||||
|
if (data == null || data.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Получены пустые данные из АИХ ИТ. Синхронизация пропущена");
|
||||||
|
return new List<(string, string)>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var validPairs = data
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item.ParentName) && !string.IsNullOrWhiteSpace(item.ChildName))
|
||||||
|
.Where(item => item.ParentName != item.ChildName)
|
||||||
|
.Select(item => (
|
||||||
|
ParentName: item.ParentName!.Trim().ToUpperInvariant(),
|
||||||
|
ChildName: item.ChildName!.Trim().ToUpperInvariant()
|
||||||
|
))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (validPairs.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("После нормализации не осталось валидных связей. Синхронизация пропущена");
|
||||||
|
return new List<(string, string)>();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Получено {Count} валидных связей", validPairs.Count);
|
||||||
|
return validPairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> PassSafeguardCheckAsync(int incomingCount)
|
||||||
|
{
|
||||||
|
var previousCount = await _unitRepository.Get()
|
||||||
|
.SelectMany(u => u.ChildUnits)
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
if (previousCount == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var currentPercentage = Math.Round((decimal)incomingCount / previousCount * 100, 1, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
if (currentPercentage < _settingsFromDb.MinRelationshipsThresholdPct)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Синхронизация отменена: количество полученных связей ниже порогового значения. " +
|
||||||
|
"Получено: {CurrentCount} ({CurrentPercentage:F1}%), порог: {Threshold}% от прошлого объема ({PreviousCount})",
|
||||||
|
incomingCount, currentPercentage, _settingsFromDb.MinRelationshipsThresholdPct, previousCount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ExtractUnitNames(List<(string ParentName, string ChildName)> pairs)
|
||||||
|
{
|
||||||
|
return pairs
|
||||||
|
.SelectMany(pair => new[] { pair.ParentName, pair.ChildName })
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, Unit>> GetOrCreateUnitsAsync(List<string> names)
|
||||||
|
{
|
||||||
|
var existingUnits = await _unitRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var missingNames = names.Except(existingUnits.Keys).ToList();
|
||||||
|
|
||||||
|
if (missingNames.Count == 0)
|
||||||
|
return existingUnits;
|
||||||
|
|
||||||
|
_logger.LogInformation("Создание отсутствующих юнитов: {Count}", missingNames.Count);
|
||||||
|
|
||||||
|
foreach (var name in missingNames)
|
||||||
|
{
|
||||||
|
var unit = new Unit
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = name,
|
||||||
|
DateCreated = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!await _unitRepository.CreateAsync(unit))
|
||||||
|
{
|
||||||
|
_logger.LogError("Не удалось создать юнит: {Name}", name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUnits[name] = unit;
|
||||||
|
_logger.LogDebug("Создан юнит: {Name}", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _unitRepository.CommitAsync();
|
||||||
|
return existingUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(List<ExistingRelationship> ToRemove, List<(string ParentName, string ChildName)> ToAdd)> GetDeltaAsync(
|
||||||
|
List<(string ParentName, string ChildName)> sourcePairs)
|
||||||
|
{
|
||||||
|
var relevantParentNames = sourcePairs.Select(p => p.ParentName).Distinct().ToList();
|
||||||
|
|
||||||
|
var existingForParents = await _unitRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(u => u.ChildUnits).ThenInclude(r => r.ChildUnit)
|
||||||
|
.Where(u => relevantParentNames.Contains(u.Name))
|
||||||
|
.SelectMany(u => u.ChildUnits, (parent, relationship) => new ExistingRelationship
|
||||||
|
{
|
||||||
|
ParentName = parent.Name,
|
||||||
|
ChildName = relationship.ChildUnit!.Name,
|
||||||
|
Relationship = relationship
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var sourceSet = new HashSet<(string, string)>(sourcePairs);
|
||||||
|
var existingSet = new HashSet<(string, string)>(
|
||||||
|
existingForParents.Select(x => (x.ParentName, x.ChildName)));
|
||||||
|
|
||||||
|
var toRemove = existingForParents
|
||||||
|
.Where(r => !sourceSet.Contains((r.ParentName, r.ChildName)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var toAdd = sourcePairs
|
||||||
|
.Where(p => !existingSet.Contains(p))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_logger.LogInformation("Дельта: удаление {ToRemove}, добавление {ToAdd}", toRemove.Count, toAdd.Count);
|
||||||
|
return (toRemove, toAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveRelationshipsAsync(List<ExistingRelationship> toRemove)
|
||||||
|
{
|
||||||
|
if (toRemove.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parentNames = toRemove.Select(r => r.ParentName).Distinct().ToList();
|
||||||
|
|
||||||
|
var parents = await _unitRepository.Get()
|
||||||
|
.Include(u => u.ChildUnits)
|
||||||
|
.Where(u => parentNames.Contains(u.Name))
|
||||||
|
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var relationship in toRemove)
|
||||||
|
{
|
||||||
|
if (!parents.TryGetValue(relationship.ParentName, out var parent))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Родитель '{Parent}' не найден при удалении связи с '{Child}'",
|
||||||
|
relationship.ParentName, relationship.ChildName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityToRemove = parent.ChildUnits
|
||||||
|
.FirstOrDefault(r => r.ChildUnitId == relationship.Relationship.ChildUnitId);
|
||||||
|
|
||||||
|
if (entityToRemove != null)
|
||||||
|
parent.ChildUnits.Remove(entityToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddRelationshipsAsync(
|
||||||
|
List<(string ParentName, string ChildName)> toAdd,
|
||||||
|
Dictionary<string, Unit> unitsByName)
|
||||||
|
{
|
||||||
|
if (toAdd.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parentNames = toAdd.Select(x => x.ParentName).Distinct().ToList();
|
||||||
|
|
||||||
|
var parents = await _unitRepository.Get()
|
||||||
|
.Include(u => u.ChildUnits)
|
||||||
|
.Where(u => parentNames.Contains(u.Name))
|
||||||
|
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var (parentName, childName) in toAdd)
|
||||||
|
{
|
||||||
|
if (!parents.TryGetValue(parentName, out var parent) ||
|
||||||
|
!unitsByName.TryGetValue(childName, out var child))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Пропущена связь '{Parent}' - '{Child}': юнит не найден", parentName, childName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.ChildUnits.Any(r => r.ChildUnitId == child.Id))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
parent.ChildUnits.Add(new UnitInUnit
|
||||||
|
{
|
||||||
|
ParentUnitId = parent.Id,
|
||||||
|
ChildUnitId = child.Id,
|
||||||
|
DateCreated = DateTimeOffset.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CommitChangesAsync(int removedCount, int addedCount)
|
||||||
|
{
|
||||||
|
if (removedCount == 0 && addedCount == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Изменений не обнаружено");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _unitRepository.CommitAsync())
|
||||||
|
throw new InvalidOperationException("Не удалось сохранить изменения связей в БД");
|
||||||
|
|
||||||
|
_logger.LogInformation("Синхронизация завершена. Удалено: {Removed}, добавлено: {Added}", removedCount, addedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ExistingRelationship
|
||||||
|
{
|
||||||
|
public string ParentName { get; init; } = null!;
|
||||||
|
public string ChildName { get; init; } = null!;
|
||||||
|
public UnitInUnit Relationship { get; init; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace PARR.API.Contracts.V1
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
|
||||||
|
|
||||||
|
namespace PARR.API.Contracts.V1
|
||||||
{
|
{
|
||||||
// https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/
|
// https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/
|
||||||
|
|
||||||
@@ -210,11 +212,21 @@
|
|||||||
public const string GetPeriodStatistics = BaseStat + "/robot-tasks/{robot}/period/";
|
public const string GetPeriodStatistics = BaseStat + "/robot-tasks/{robot}/period/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class StatRobotTaskDetails
|
||||||
|
{
|
||||||
|
public const string Details = BaseStat + "/robot-tasks/details/{robot}/{task}";
|
||||||
|
}
|
||||||
|
|
||||||
public static class StatRobotStatus
|
public static class StatRobotStatus
|
||||||
{
|
{
|
||||||
public const string Get = BaseStat + "/robot-statuses/";
|
public const string Get = BaseStat + "/robot-statuses/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class StatRobotStatusDetails
|
||||||
|
{
|
||||||
|
public const string Details = BaseStat + "/robot-statuses/details/{robot}/{status}";
|
||||||
|
}
|
||||||
|
|
||||||
public static class StatRobotHistory
|
public static class StatRobotHistory
|
||||||
{
|
{
|
||||||
public const string Get = BaseStat + "/robot-histories/";
|
public const string Get = BaseStat + "/robot-histories/";
|
||||||
@@ -224,6 +236,7 @@
|
|||||||
{
|
{
|
||||||
public const string Get = BaseStat + "/templates/";
|
public const string Get = BaseStat + "/templates/";
|
||||||
public const string GetForPeriod = BaseStat + "/templates/period";
|
public const string GetForPeriod = BaseStat + "/templates/period";
|
||||||
|
public const string GetTemplatesWithoutScheduleAndTaskCount = BaseStat + "/templates/without-schedule";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class StatStatusTypeTemplates
|
public static class StatStatusTypeTemplates
|
||||||
@@ -324,6 +337,16 @@
|
|||||||
public const string GetWorkloadTemplateReport = BaseStat + "/workload/templates/{reportType}/{filter}/{state}/{date}";
|
public const string GetWorkloadTemplateReport = BaseStat + "/workload/templates/{reportType}/{filter}/{state}/{date}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class StatRobotMetrics
|
||||||
|
{
|
||||||
|
public const string GetRobotStatusMetrics = BaseStat + "/robot-metrics/robot-status/{robotCode}/{period}";
|
||||||
|
|
||||||
|
public const string GetTaskStatusMetrics = BaseStat + "/robot-metrics/task-status/{robotCode}/{period}";
|
||||||
|
|
||||||
|
public const string GetFilteredMetrics = BaseStat + "/robot-metrics/filtered";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Наряды
|
#region Наряды
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.API.Contracts.V1.Requests.Queries
|
||||||
|
{
|
||||||
|
public record RobotFilteredMetricsQuery
|
||||||
|
{
|
||||||
|
public RobotsEnum? RobotCode { get; init; }
|
||||||
|
|
||||||
|
public RobotStatusEnum? RobotStatusCode { get; init; }
|
||||||
|
|
||||||
|
public TaskStatusEnum? TaskStatusCode { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset? DateFrom { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset? DateTo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Шаг группировки в минутах (например, 2, 30, 60, 1440)
|
||||||
|
/// </summary>
|
||||||
|
public int IntervalMinutes { get; set; } = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
namespace PARR.API.Contracts.V1.Responses
|
namespace PARR.API.Contracts.V1.Responses
|
||||||
{
|
{
|
||||||
public class JobGroupBaseResponse
|
public class JobGroupShortResponse
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
//public bool? IsUmbrella { get; set; }
|
public class JobGroupBaseResponse : JobGroupShortResponse
|
||||||
|
{
|
||||||
|
//public Guid Id { get; set; }
|
||||||
|
|
||||||
|
//public required string Name { get; set; }
|
||||||
|
|
||||||
public required string ShortDescription { get; set; }
|
public required string ShortDescription { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatFilteredChartPoint
|
||||||
|
{
|
||||||
|
public DateTimeOffset Timestamp { get; init; }
|
||||||
|
public int Count { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatRobotStatusChartPoint
|
||||||
|
{
|
||||||
|
public DateTimeOffset Timestamp { get; init; }
|
||||||
|
public int Wait { get; init; }
|
||||||
|
public int InProgress { get; init; }
|
||||||
|
public int Error { get; init; }
|
||||||
|
public int Complete { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatRobotTaskDetailsResponse
|
||||||
|
{
|
||||||
|
public RobotResponse Robot { get; init; } = null!;
|
||||||
|
public TaskStatusResponse Task { get; init; } = null!;
|
||||||
|
|
||||||
|
public List<StatRobotTaskGroupDetailsResponse> Details { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StatRobotTaskGroupDetailsResponse
|
||||||
|
{
|
||||||
|
public JobGroupShortResponse JobGroup { get; init; } = null!;
|
||||||
|
public int TemplatesCount { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatTaskStatusChartPoint
|
||||||
|
{
|
||||||
|
public DateTimeOffset Timestamp { get; init; }
|
||||||
|
public int Creating { get; init; }
|
||||||
|
public int Updating { get; init; }
|
||||||
|
public int Ok { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatTemplatesWithoutScheduleResponse(int Count);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ using PARR.API.Services.Interfaces;
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Domain.Common.Pagination;
|
using PARR.Domain.Common.Pagination;
|
||||||
using PARR.Domain.Common.Roles;
|
using PARR.Domain.Common.Roles;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.API.Controllers.V1
|
namespace PARR.API.Controllers.V1
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,39 +1,34 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using PARR.API.Contracts.V1;
|
using PARR.API.Contracts.V1;
|
||||||
using PARR.API.Contracts.V1.Requests;
|
using PARR.API.Contracts.V1.Requests;
|
||||||
using PARR.API.Contracts.V1.Responses;
|
using PARR.API.Contracts.V1.Responses;
|
||||||
using PARR.API.Contracts.V1.Responses.Base;
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
using PARR.API.Controllers.V1.Base;
|
using PARR.API.Controllers.V1.Base;
|
||||||
using PARR.API.Services.Interfaces;
|
using PARR.API.Services.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Services.RobotTaskRobotStatus.Interfaces;
|
||||||
using PARR.Domain.Common.Roles;
|
using PARR.Domain.Common.Roles;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.DTOs.RobotTaskRobotStatus;
|
||||||
using PARR.Domain.Enums;
|
|
||||||
|
|
||||||
namespace PARR.API.Controllers.V1
|
namespace PARR.API.Controllers.V1
|
||||||
{
|
{
|
||||||
[Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)]
|
[Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)]
|
||||||
public class RobotTaskRobotStatusController : BaseApiController
|
public class RobotTaskRobotStatusController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IRobotConfigurationRepository robotConfigurationService;
|
private readonly IMapper _mapper;
|
||||||
private readonly IRobotHistoryRepository robotHistoryService;
|
private readonly IClientService _clientService;
|
||||||
private readonly IMapper mapper;
|
private readonly IRobotTaskRobotStatusService _robotTaskRobotStatusService;
|
||||||
private readonly IClientService clientService;
|
|
||||||
|
|
||||||
public RobotTaskRobotStatusController(
|
public RobotTaskRobotStatusController(
|
||||||
IRobotConfigurationRepository robotConfigurationService,
|
|
||||||
IRobotHistoryRepository robotHistoryService,
|
|
||||||
IMapper mapper,
|
IMapper mapper,
|
||||||
IClientService clientService
|
IClientService clientService,
|
||||||
|
IRobotTaskRobotStatusService robotTaskRobotStatusService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.robotConfigurationService = robotConfigurationService;
|
_mapper = mapper;
|
||||||
this.robotHistoryService = robotHistoryService;
|
_clientService = clientService;
|
||||||
this.mapper = mapper;
|
_robotTaskRobotStatusService = robotTaskRobotStatusService;
|
||||||
this.clientService = clientService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -45,47 +40,58 @@ namespace PARR.API.Controllers.V1
|
|||||||
[HttpPut(ApiRoutes.RobotTaskRobotStatus.ChangeRobotStatus)]
|
[HttpPut(ApiRoutes.RobotTaskRobotStatus.ChangeRobotStatus)]
|
||||||
public async Task<IActionResult> ChangeStatus([FromRoute] Guid taskId, [FromBody] RobotTaskChangeRobotStatusRequest request)
|
public async Task<IActionResult> ChangeStatus([FromRoute] Guid taskId, [FromBody] RobotTaskChangeRobotStatusRequest request)
|
||||||
{
|
{
|
||||||
var config = await robotConfigurationService.Get()
|
#region Old
|
||||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
|
||||||
|
|
||||||
if (config == null)
|
//var config = await _robotConfigurationRepository.Get()
|
||||||
return BadRequest(new Response(false, new List<ErrorModel> { new ErrorModel { Message = $"Не найдено задание с id: {taskId}" } }));
|
// .FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
|
||||||
//изменение статуса робота
|
//if (config == null)
|
||||||
robotConfigurationService.ChangeRobotStatus(request.RobotStatusCode, config);
|
// return BadRequest(new Response(false, new List<ErrorModel> { new ErrorModel { Message = $"Не найдено задание с id: {taskId}" } }));
|
||||||
|
|
||||||
//если успех, изменяем статус задания на успех
|
////изменение статуса робота
|
||||||
if (request.RobotStatusCode == RobotStatusEnum.Complete)
|
//_robotConfigurationRepository.ChangeRobotStatus(request.RobotStatusCode, config);
|
||||||
robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Ok, config);
|
|
||||||
|
|
||||||
if (!await robotConfigurationService.CommitAsync())
|
////если успех, изменяем статус задания на успех
|
||||||
return BadRequest("Ошибка при изменении статуса работы робота.");
|
//if (request.RobotStatusCode == RobotStatusEnum.Complete)
|
||||||
|
// _robotConfigurationRepository.ChangeTaskStatus(TaskStatusEnum.Ok, config);
|
||||||
|
|
||||||
//записываем в лог робота
|
//if (!await _robotConfigurationRepository.CommitAsync())
|
||||||
if (request.RobotStatusCode == RobotStatusEnum.InProgress || request.RobotStatusCode == RobotStatusEnum.Complete)
|
// return BadRequest("Ошибка при изменении статуса работы робота.");
|
||||||
{
|
|
||||||
var historyLevel = request.RobotStatusCode == RobotStatusEnum.InProgress ? RobotHistoryLevelEnum.Start : RobotHistoryLevelEnum.Complete;
|
|
||||||
|
|
||||||
var history = new RobotHistory
|
////записываем в лог робота
|
||||||
{
|
//if (request.RobotStatusCode == RobotStatusEnum.InProgress || request.RobotStatusCode == RobotStatusEnum.Complete)
|
||||||
Id = Guid.NewGuid(),
|
//{
|
||||||
HistoryLevel = (int)historyLevel,
|
// var historyLevel = request.RobotStatusCode == RobotStatusEnum.InProgress ? RobotHistoryLevelEnum.Start : RobotHistoryLevelEnum.Complete;
|
||||||
TaskStatusCode = config.TaskStatusCode,
|
|
||||||
RobotConfigurationId = config.Id,
|
|
||||||
RobotIp = clientService.GetClientIp()?.ToString(),
|
|
||||||
RobotId = request.RobotId
|
|
||||||
};
|
|
||||||
await robotHistoryService.CreateAsync(history);
|
|
||||||
await robotHistoryService.CommitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
var configToResponse = await robotConfigurationService.Get()
|
// var history = new RobotHistory
|
||||||
.Include(t => t.Robot)
|
// {
|
||||||
.Include(t => t.TaskStatus)
|
// Id = Guid.NewGuid(),
|
||||||
.Include(t => t.RobotStatus)
|
// HistoryLevel = (int)historyLevel,
|
||||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
// TaskStatusCode = config.TaskStatusCode,
|
||||||
|
// RobotConfigurationId = config.Id,
|
||||||
|
// RobotIp = _clientService.GetClientIp()?.ToString(),
|
||||||
|
// RobotId = request.RobotId
|
||||||
|
// };
|
||||||
|
// await _robotHistoryRepository.CreateAsync(history);
|
||||||
|
// await _robotHistoryRepository.CommitAsync();
|
||||||
|
//}
|
||||||
|
|
||||||
var response = mapper.Map<RobotConfigurationResponse>(configToResponse);
|
//var configToResponse = await _robotConfigurationRepository.Get()
|
||||||
|
// .Include(t => t.Robot)
|
||||||
|
// .Include(t => t.TaskStatus)
|
||||||
|
// .Include(t => t.RobotStatus)
|
||||||
|
// .FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
|
||||||
|
//var response = _mapper.Map<RobotConfigurationResponse>(configToResponse);
|
||||||
|
|
||||||
|
//return Ok(new Response<RobotConfigurationResponse>(response, true));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var changeRequest = new ChangeRobotStatus(taskId, request.RobotStatusCode, request.RobotId, _clientService.GetClientIp()?.ToString());
|
||||||
|
var result = await _robotTaskRobotStatusService.ChangeStatusAsync(changeRequest);
|
||||||
|
|
||||||
|
var response = _mapper.Map<RobotConfigurationResponse>(result);
|
||||||
|
|
||||||
return Ok(new Response<RobotConfigurationResponse>(response, true));
|
return Ok(new Response<RobotConfigurationResponse>(response, true));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using PARR.API.Controllers.V1.Base;
|
|||||||
using PARR.API.Helpers;
|
using PARR.API.Helpers;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Domain.Common.Roles;
|
using PARR.Domain.Common.Roles;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.API.Controllers.V1.Statistics
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PARR.API.Contracts.V1;
|
||||||
|
using PARR.API.Contracts.V1.Requests.Queries;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Statistics;
|
||||||
|
using PARR.API.Controllers.V1.Base;
|
||||||
|
using PARR.Core.Services.RobotMetrics;
|
||||||
|
using PARR.Domain.Common.Roles;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Метрики заданий роботам
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = ParrRoles.Administrator.Role)]
|
||||||
|
public class StatRobotMetricsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IRobotMetricsService _robotMetricsService;
|
||||||
|
|
||||||
|
public StatRobotMetricsController(
|
||||||
|
IMapper mapper,
|
||||||
|
IRobotMetricsService robotMetricsService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_robotMetricsService = robotMetricsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Заданиям Роботу, график
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotMetrics.GetRobotStatusMetrics)]
|
||||||
|
public async Task<IActionResult> GetRobotStatusMetrics([FromRoute] RobotsEnum robotCode, [FromRoute] ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await _robotMetricsService.GetRobotStatusMetricsAsync(robotCode, period, cancellationToken);
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<StatRobotStatusChartPoint>>(data);
|
||||||
|
|
||||||
|
return Ok(new Response<List<StatRobotStatusChartPoint>>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Статусам Заданий, график
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotMetrics.GetTaskStatusMetrics)]
|
||||||
|
public async Task<IActionResult> GetTaskStatusMetrics([FromRoute] RobotsEnum robotCode, [FromRoute] ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await _robotMetricsService.GetTaskStatusMetricsAsync(robotCode, period, cancellationToken);
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<StatTaskStatusChartPoint>>(data);
|
||||||
|
|
||||||
|
return Ok(new Response<List<StatTaskStatusChartPoint>>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по статусам заданий, гибкий фильтр
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotMetrics.GetFilteredMetrics)]
|
||||||
|
public async Task<IActionResult> GetFilteredMetrics([FromQuery] RobotFilteredMetricsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// ------- Правильность расчетов этого метода доконца не проверена -------
|
||||||
|
|
||||||
|
var filter = _mapper.Map<MetricFilter>(request);
|
||||||
|
|
||||||
|
var data = await _robotMetricsService.GetFilteredMetricsAsync(filter, cancellationToken);
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<StatFilteredChartPoint>>(data);
|
||||||
|
|
||||||
|
return Ok(new Response<List<StatFilteredChartPoint>>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PARR.API.Contracts.V1;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
|
using PARR.API.Controllers.V1.Base;
|
||||||
|
using PARR.Core.Services.RobotStatusDetails.Interfaces;
|
||||||
|
using PARR.Domain.Common.Roles;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Детальная статистика по статусам заданий роботам
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)]
|
||||||
|
public class StatRobotStatusDetailsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IRobotStatusDetailsService _robotStatusDetailsService;
|
||||||
|
|
||||||
|
public StatRobotStatusDetailsController(
|
||||||
|
IMapper mapper,
|
||||||
|
IRobotStatusDetailsService robotStatusDetailsService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_robotStatusDetailsService = robotStatusDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Список групп работ по статусам заданий роботам
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robot"></param>
|
||||||
|
/// <param name="status"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotStatusDetails.Details)]
|
||||||
|
public async Task<IActionResult> Details([FromRoute] RobotsEnum robot, [FromRoute] RobotStatusEnum status)
|
||||||
|
{
|
||||||
|
var details = await _robotStatusDetailsService.GetDetailsAsync(robot, status);
|
||||||
|
var response = _mapper.Map<StatRobotStatusDetailsResponse>(details);
|
||||||
|
|
||||||
|
return Ok(new Response<StatRobotStatusDetailsResponse>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using PARR.API.Contracts.V1.Responses;
|
||||||
|
|
||||||
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
|
{
|
||||||
|
public record StatRobotStatusDetailsResponse
|
||||||
|
{
|
||||||
|
public RobotResponse Robot { get; init; } = null!;
|
||||||
|
public RobotStatusResponse Status { get; init; } = null!;
|
||||||
|
|
||||||
|
public List<StatRobotStatusGroupDetailsResponse> Details { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StatRobotStatusGroupDetailsResponse
|
||||||
|
{
|
||||||
|
public JobGroupShortResponse JobGroup { get; init; } = null!;
|
||||||
|
public int TemplatesCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PARR.API.Contracts.V1;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Statistics;
|
||||||
|
using PARR.API.Controllers.V1.Base;
|
||||||
|
using PARR.Core.Services.RobotTaskDetailsServices.Interfaces;
|
||||||
|
using PARR.Domain.Common.Roles;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Детальная статистика по заданиям роботам
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)]
|
||||||
|
public class StatRobotTaskDetailsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IRobotTaskDetailsService _robotTaskDetailsService;
|
||||||
|
|
||||||
|
public StatRobotTaskDetailsController(
|
||||||
|
IMapper mapper,
|
||||||
|
IRobotTaskDetailsService robotTaskDetailsService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_robotTaskDetailsService = robotTaskDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Список групп работ по заданиям роботам
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robot"></param>
|
||||||
|
/// <param name="task"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotTaskDetails.Details)]
|
||||||
|
public async Task<IActionResult> Details([FromRoute] RobotsEnum robot, [FromRoute] TaskStatusEnum task)
|
||||||
|
{
|
||||||
|
var details = await _robotTaskDetailsService.GetDetailsAsync(robot, task);
|
||||||
|
var response = _mapper.Map<StatRobotTaskDetailsResponse>(details);
|
||||||
|
|
||||||
|
return Ok(new Response<StatRobotTaskDetailsResponse>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PARR.API.Contracts.V1;
|
using PARR.API.Contracts.V1;
|
||||||
using PARR.API.Contracts.V1.Requests.BaseRequests;
|
using PARR.API.Contracts.V1.Requests.BaseRequests;
|
||||||
@@ -8,10 +9,12 @@ using PARR.API.Controllers.V1.Base;
|
|||||||
using PARR.API.Helpers;
|
using PARR.API.Helpers;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
|
using PARR.Domain.Common.Roles;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
namespace PARR.API.Controllers.V1.Statistics
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
{
|
{
|
||||||
|
[Authorize(Roles = ParrRoles.EsppRobot.RoleOrAdmin)]
|
||||||
public class StatTemplateAutoControlController : BaseApiController
|
public class StatTemplateAutoControlController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly ITemplateRepository templateService;
|
private readonly ITemplateRepository templateService;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using PARR.API.Helpers;
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
using PARR.Domain.Common.Roles;
|
using PARR.Domain.Common.Roles;
|
||||||
|
using PARR.Domain.Entities;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
namespace PARR.API.Controllers.V1.Statistics
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
@@ -20,16 +21,16 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
[Authorize(Roles = ParrRoles.Administrator.Role)]
|
[Authorize(Roles = ParrRoles.Administrator.Role)]
|
||||||
public class StatTemplateController : BaseApiController
|
public class StatTemplateController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly ITemplateRepository templateService;
|
private readonly ITemplateRepository _templateRepository;
|
||||||
private readonly INextRunService nextRunService;
|
private readonly INextRunService _nextRunService;
|
||||||
|
|
||||||
public StatTemplateController(
|
public StatTemplateController(
|
||||||
ITemplateRepository templateService,
|
ITemplateRepository templateRepository,
|
||||||
INextRunService nextRunService
|
INextRunService nextRunService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.templateService = templateService;
|
_templateRepository = templateRepository;
|
||||||
this.nextRunService = nextRunService;
|
_nextRunService = nextRunService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,12 +42,12 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
{
|
{
|
||||||
var response = new StatTemplateResponse
|
var response = new StatTemplateResponse
|
||||||
{
|
{
|
||||||
ActivateScheduleCount = await templateService.Get().AsNoTracking().CountAsync(t => t.IsActiveSchedule),
|
ActivateScheduleCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.IsActiveSchedule),
|
||||||
ActivateTemplateCount = await templateService.Get().AsNoTracking().CountAsync(t => t.IsActiveTemplate),
|
ActivateTemplateCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.IsActiveTemplate),
|
||||||
TemplateAgentCount = await templateService.Get().AsNoTracking().CountAsync(t => t.Job!.Group!.IsAgent),
|
TemplateAgentCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.Job!.Group!.IsAgent),
|
||||||
TemplateCount = await templateService.Get().AsNoTracking().CountAsync(),
|
TemplateCount = await _templateRepository.Get().AsNoTracking().CountAsync(),
|
||||||
SyncEsppScheduleCount = await templateService.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.ScheduleOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok)),
|
SyncEsppScheduleCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.ScheduleOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok)),
|
||||||
SyncEsppTemplatesCount = await templateService.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.TemplateOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok))
|
SyncEsppTemplatesCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.TemplateOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok))
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(new Response<StatTemplateResponse>(response, true));
|
return Ok(new Response<StatTemplateResponse>(response, true));
|
||||||
@@ -71,7 +72,7 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
userEnd.AddDays(1),
|
userEnd.AddDays(1),
|
||||||
timeZoneQuery.TimeZoneOffset);
|
timeZoneQuery.TimeZoneOffset);
|
||||||
|
|
||||||
var allRecords = await templateService.Get()
|
var allRecords = await _templateRepository.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
|
.FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
|
||||||
.Select(t => new { t.Id, t.DateCreated })
|
.Select(t => new { t.Id, t.DateCreated })
|
||||||
@@ -80,7 +81,7 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
var resultDict = allRecords.GroupByUserDate(t => t.DateCreated, timeZoneQuery.TimeZoneOffset);
|
var resultDict = allRecords.GroupByUserDate(t => t.DateCreated, timeZoneQuery.TimeZoneOffset);
|
||||||
|
|
||||||
|
|
||||||
var daysList = await nextRunService.GetWorkDaysAsync(userStart, userEnd, false);
|
var daysList = await _nextRunService.GetWorkDaysAsync(userStart, userEnd, false);
|
||||||
|
|
||||||
var response = daysList.Select(date => new StatTemplatePeriodResponse
|
var response = daysList.Select(date => new StatTemplatePeriodResponse
|
||||||
{
|
{
|
||||||
@@ -92,5 +93,27 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить кол-во шаблонов у которых ИД расписания null и нет задания на создание расписания
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatTemplate.GetTemplatesWithoutScheduleAndTaskCount)]
|
||||||
|
public async Task<IActionResult> GetTemplatesWithoutScheduleAndTaskCount()
|
||||||
|
{
|
||||||
|
var count = await _templateRepository.Get()
|
||||||
|
.CountAsync(t =>
|
||||||
|
t.ScheduleEsppId == null
|
||||||
|
&& !t.RobotConfigurations.Any(x =>
|
||||||
|
x.RobotCode == (int)RobotsEnum.ScheduleOrder
|
||||||
|
&& x.TaskStatusCode == (int)TaskStatusEnum.Creating
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var resposne = new StatTemplatesWithoutScheduleResponse(count);
|
||||||
|
|
||||||
|
return Ok(new Response<StatTemplatesWithoutScheduleResponse>(resposne, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace PARR.API.Infrastructure.Middleware
|
|||||||
|
|
||||||
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
logger.LogError(exception, "Ошибка во время запроса {TraceId}: {Message}", httpContext.TraceIdentifier, exception.Message);
|
logger.LogDebug(exception, "Ошибка во время запроса {TraceId}: {Message}", httpContext.TraceIdentifier, exception.Message);
|
||||||
|
|
||||||
// определяем статус код, в зависимости от типа исключения
|
// определяем статус код, в зависимости от типа исключения
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,17 @@
|
|||||||
using PARR.API.Authentication.Models;
|
using PARR.API.Authentication.Models;
|
||||||
using PARR.API.Contracts.V1.Responses;
|
using PARR.API.Contracts.V1.Responses;
|
||||||
using PARR.API.Contracts.V1.Responses.Statistics;
|
using PARR.API.Contracts.V1.Responses.Statistics;
|
||||||
|
using PARR.API.Controllers.V1.Statistics;
|
||||||
using PARR.API.MappingProfiles.Resolvers;
|
using PARR.API.MappingProfiles.Resolvers;
|
||||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||||
using PARR.Domain.DTOs.Matching;
|
using PARR.Domain.DTOs.Matching;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
||||||
|
using PARR.Domain.DTOs.RobotStatusDetails;
|
||||||
using PARR.Domain.DTOs.RobotTask;
|
using PARR.Domain.DTOs.RobotTask;
|
||||||
|
using PARR.Domain.DTOs.RobotTaskDetails;
|
||||||
|
using PARR.Domain.DTOs.RobotTaskRobotStatus;
|
||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
using PARR.Domain.DTOs.Shortcode;
|
using PARR.Domain.DTOs.Shortcode;
|
||||||
using PARR.Domain.DTOs.TaskDTO;
|
using PARR.Domain.DTOs.TaskDTO;
|
||||||
using PARR.Domain.DTOs.User;
|
using PARR.Domain.DTOs.User;
|
||||||
@@ -15,6 +21,7 @@ using PARR.Domain.Entities;
|
|||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
using PARR.Domain.Entities.JobEntities;
|
using PARR.Domain.Entities.JobEntities;
|
||||||
using PARR.Domain.Entities.JobGroupEntities;
|
using PARR.Domain.Entities.JobGroupEntities;
|
||||||
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
using PARR.Domain.Entities.Schedule;
|
using PARR.Domain.Entities.Schedule;
|
||||||
using PARR.Domain.Entities.Unit;
|
using PARR.Domain.Entities.Unit;
|
||||||
|
|
||||||
@@ -78,7 +85,7 @@ namespace PARR.API.MappingProfiles
|
|||||||
.ForMember(d => d.Script, o => o.MapFrom(s => s.AgentScript))
|
.ForMember(d => d.Script, o => o.MapFrom(s => s.AgentScript))
|
||||||
.ForMember(d => d.TimeOutSec, o => o.MapFrom(s => s.AgentTimeOutSec));
|
.ForMember(d => d.TimeOutSec, o => o.MapFrom(s => s.AgentTimeOutSec));
|
||||||
|
|
||||||
CreateMap<PARR.Domain.Entities.TaskStatus, TaskStatusResponse>();
|
CreateMap<PARR.Domain.Entities.RobotEntities.TaskStatus, TaskStatusResponse>();
|
||||||
|
|
||||||
#region ScheduleResponseAreaTimeOffsetResponse
|
#region ScheduleResponseAreaTimeOffsetResponse
|
||||||
|
|
||||||
@@ -254,12 +261,21 @@ namespace PARR.API.MappingProfiles
|
|||||||
|
|
||||||
CreateMap<Robot, RobotResponse>();
|
CreateMap<Robot, RobotResponse>();
|
||||||
|
|
||||||
CreateMap<PARR.Domain.Entities.TaskStatus, TaskStatusResponse>();
|
CreateMap<PARR.Domain.Entities.RobotEntities.TaskStatus, TaskStatusResponse>();
|
||||||
|
|
||||||
|
//TODO: удалить этот маппинг, пока он нужен для шаблонов. TemplateController
|
||||||
CreateMap<RobotConfiguration, RobotConfigurationResponse>()
|
CreateMap<RobotConfiguration, RobotConfigurationResponse>()
|
||||||
.ForMember(d => d.Robot, o => o.MapFrom(s => s.Robot))
|
.ForMember(d => d.Robot, o => o.MapFrom(s => s.Robot))
|
||||||
.ForMember(d => d.TaskStatus, o => o.MapFrom(s => s.TaskStatus))
|
.ForMember(d => d.TaskStatus, o => o.MapFrom(s => s.TaskStatus))
|
||||||
.ForMember(d => d.RobotStatus, o => o.MapFrom(s => s.RobotStatus));
|
.ForMember(d => d.RobotStatus, o => o.MapFrom(s => s.RobotStatus));
|
||||||
|
//---
|
||||||
|
|
||||||
|
CreateMap<RobotResult, RobotResponse>();
|
||||||
|
CreateMap<RobotTaskStatusResult, TaskStatusResponse>();
|
||||||
|
CreateMap<RobotStatusResult, RobotStatusResponse>();
|
||||||
|
|
||||||
|
CreateMap<RobotConfigurationResult, RobotConfigurationResponse>();
|
||||||
|
|
||||||
// === RobotConfiguration ===
|
// === RobotConfiguration ===
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -363,6 +379,9 @@ namespace PARR.API.MappingProfiles
|
|||||||
|
|
||||||
#region JobGroup
|
#region JobGroup
|
||||||
|
|
||||||
|
CreateMap<JobGroupShortResult, JobGroupShortResponse>()
|
||||||
|
.ForMember(d => d.Name, o => o.MapFrom(s => s.GroupName));
|
||||||
|
|
||||||
CreateMap<JobGroup, JobGroupBaseResponse>()
|
CreateMap<JobGroup, JobGroupBaseResponse>()
|
||||||
.Include<JobGroup, JobGroupResponse>()
|
.Include<JobGroup, JobGroupResponse>()
|
||||||
.Include<JobGroup, JobGroupWithDistributionConfigResponse>()
|
.Include<JobGroup, JobGroupWithDistributionConfigResponse>()
|
||||||
@@ -500,6 +519,35 @@ namespace PARR.API.MappingProfiles
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region StatRobotMetrics
|
||||||
|
|
||||||
|
CreateMap<TaskStatusChartPoint, StatTaskStatusChartPoint>();
|
||||||
|
CreateMap<RobotStatusChartPoint, StatRobotStatusChartPoint>();
|
||||||
|
CreateMap<FilteredChartPoint, StatFilteredChartPoint>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region StatRobotTaskDetailsResponse
|
||||||
|
|
||||||
|
CreateMap<RobotTaskGroupDetailsResult, StatRobotTaskGroupDetailsResponse>();
|
||||||
|
//todo: ForMember не нужен?
|
||||||
|
//.ForMember(d => d.JobGroup, o => o.MapFrom(s => s.JobGroup));
|
||||||
|
|
||||||
|
CreateMap<RobotTaskDetailsResult, StatRobotTaskDetailsResponse>();
|
||||||
|
//todo: ForMember не нужен?
|
||||||
|
//.ForMember(d => d.Details, o => o.MapFrom(s => s.Details));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region StatRobotStatusDetailsResponse
|
||||||
|
|
||||||
|
CreateMap<RobotStatusDetailsResult, StatRobotStatusDetailsResponse>();
|
||||||
|
|
||||||
|
CreateMap<RobotStatusGroupDetailsResult, StatRobotStatusGroupDetailsResponse>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using PARR.API.Contracts.V1.Requests;
|
using PARR.API.Contracts.V1.Requests;
|
||||||
using PARR.API.Contracts.V1.Requests.Queries;
|
using PARR.API.Contracts.V1.Requests.Queries;
|
||||||
using PARR.Domain.Common.Pagination;
|
using PARR.Domain.Common.Pagination;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
||||||
using PARR.Domain.Entities.JobEntities;
|
using PARR.Domain.Entities.JobEntities;
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ namespace PARR.API.MappingProfiles
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
CreateMap<StatRobotSnapshotQuery, RobotSnapshotQuery>();
|
CreateMap<StatRobotSnapshotQuery, RobotSnapshotQuery>();
|
||||||
|
|
||||||
|
|
||||||
|
CreateMap<RobotFilteredMetricsQuery, MetricFilter>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
@@ -14,7 +15,8 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Override": {
|
"Override": {
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Information",
|
||||||
|
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "Fatal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"RabbitMq": {
|
"RabbitMq": {
|
||||||
"ThresholdConnections": 33
|
"ThresholdConnections": 32
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CommonSettings": {
|
"CommonSettings": {
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ using PARR.Core.Common.Interfaces;
|
|||||||
using PARR.Core.Services.MatchingStatusService;
|
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.RobotMetrics;
|
||||||
using PARR.Core.Services.RobotSnapshotServices;
|
using PARR.Core.Services.RobotSnapshotServices;
|
||||||
|
using PARR.Core.Services.RobotStatusDetails.Implementations;
|
||||||
|
using PARR.Core.Services.RobotStatusDetails.Interfaces;
|
||||||
using PARR.Core.Services.RobotTask.Implementations;
|
using PARR.Core.Services.RobotTask.Implementations;
|
||||||
using PARR.Core.Services.RobotTask.Interfaces;
|
using PARR.Core.Services.RobotTask.Interfaces;
|
||||||
|
using PARR.Core.Services.RobotTaskDetailsServices.Implementations;
|
||||||
|
using PARR.Core.Services.RobotTaskDetailsServices.Interfaces;
|
||||||
|
using PARR.Core.Services.RobotTaskRobotStatus.Implemetations;
|
||||||
|
using PARR.Core.Services.RobotTaskRobotStatus.Interfaces;
|
||||||
using PARR.Core.Services.Shortcodes;
|
using PARR.Core.Services.Shortcodes;
|
||||||
using PARR.Core.Services.Shortcodes.Handlers;
|
using PARR.Core.Services.Shortcodes.Handlers;
|
||||||
using PARR.Core.Services.Snapshots.Implementations;
|
using PARR.Core.Services.Snapshots.Implementations;
|
||||||
@@ -28,7 +35,6 @@ using PARR.Core.Services.UnitService.Implementations;
|
|||||||
using PARR.Core.Services.UnitService.Interfaces;
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
using PARR.Core.Services.Workload.Implementations;
|
using PARR.Core.Services.Workload.Implementations;
|
||||||
using PARR.Core.Services.Workload.Interfaces;
|
using PARR.Core.Services.Workload.Interfaces;
|
||||||
using PARR.Domain.Entities.RobotEntities;
|
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
|
|
||||||
@@ -111,10 +117,15 @@ namespace PARR.Core
|
|||||||
|
|
||||||
services.AddScoped<IRobotTaskService, RobotTaskService>();
|
services.AddScoped<IRobotTaskService, RobotTaskService>();
|
||||||
services.AddScoped<IRobotSnapshotService, RobotSnapshotService>();
|
services.AddScoped<IRobotSnapshotService, RobotSnapshotService>();
|
||||||
|
services.AddScoped<IRobotTaskRobotStatusService, RobotTaskRobotStatusService>();
|
||||||
|
services.AddScoped<IRobotTaskDetailsService, RobotTaskDetailsService>();
|
||||||
|
services.AddScoped<IRobotStatusDetailsService, RobotStatusDetailsService>();
|
||||||
|
|
||||||
services.AddScoped<IUnitService, UnitService>();
|
services.AddScoped<IUnitService, UnitService>();
|
||||||
services.AddScoped<UnitCacheService>();
|
services.AddScoped<UnitCacheService>();
|
||||||
|
|
||||||
|
services.AddScoped<IRobotMetricsService, RobotMetricsService>();
|
||||||
|
|
||||||
//services.AddScoped<IUserService, UserService>();
|
//services.AddScoped<IUserService, UserService>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
10
PARR.Core/Extensions/EnumerableExtensions.cs
Normal file
10
PARR.Core/Extensions/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace PARR.Core.Extensions
|
||||||
|
{
|
||||||
|
public static class EnumerableExtensions
|
||||||
|
{
|
||||||
|
public static int MaxOrDefault(this IEnumerable<int> source)
|
||||||
|
{
|
||||||
|
return source.Any() ? source.Max() : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using PARR.Domain.DTOs.RobotTaskRobotStatus;
|
||||||
|
using PARR.Domain.Entities;
|
||||||
|
|
||||||
|
namespace PARR.Core.Infrastructure.Mapping.RobotTaskRobotStatus
|
||||||
|
{
|
||||||
|
internal class RobotConfigurationResultMappingProfile : Profile
|
||||||
|
{
|
||||||
|
public RobotConfigurationResultMappingProfile()
|
||||||
|
{
|
||||||
|
CreateMap<RobotConfiguration, RobotConfigurationResult>()
|
||||||
|
.ForMember(d => d.Robot, o => o.MapFrom(s => s.Robot))
|
||||||
|
.ForMember(d => d.TaskStatus, o => o.MapFrom(s => s.TaskStatus))
|
||||||
|
.ForMember(d => d.RobotStatus, o => o.MapFrom(s => s.RobotStatus));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
using PARR.Domain.Entities.JobGroupEntities;
|
||||||
|
|
||||||
|
namespace PARR.Core.Infrastructure.Mapping.Shared
|
||||||
|
{
|
||||||
|
public class JobGroupResultMappingProfile: Profile
|
||||||
|
{
|
||||||
|
public JobGroupResultMappingProfile()
|
||||||
|
{
|
||||||
|
CreateMap<JobGroup, JobGroupShortResult>()
|
||||||
|
.Include<JobGroup, JobGroupResult>();
|
||||||
|
|
||||||
|
CreateMap<JobGroup, JobGroupResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
|
namespace PARR.Core.Infrastructure.Mapping.Shared
|
||||||
|
{
|
||||||
|
public class RobotResultMappingProfile : Profile
|
||||||
|
{
|
||||||
|
public RobotResultMappingProfile()
|
||||||
|
{
|
||||||
|
CreateMap<Robot, RobotResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
|
namespace PARR.Core.Infrastructure.Mapping.Shared
|
||||||
|
{
|
||||||
|
public class RobotStatusResultMappingProfile : Profile
|
||||||
|
{
|
||||||
|
public RobotStatusResultMappingProfile()
|
||||||
|
{
|
||||||
|
CreateMap<RobotStatus, RobotStatusResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
|
||||||
|
namespace PARR.Core.Infrastructure.Mapping.Shared
|
||||||
|
{
|
||||||
|
public class RobotTaskStatusResultMappingProfile : Profile
|
||||||
|
{
|
||||||
|
public RobotTaskStatusResultMappingProfile()
|
||||||
|
{
|
||||||
|
CreateMap<PARR.Domain.Entities.RobotEntities.TaskStatus, RobotTaskStatusResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,5 +49,11 @@ namespace PARR.Core.Repositories.Interfaces
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<bool> SetInProgressStatusAsync(Guid id);
|
Task<bool> SetInProgressStatusAsync(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Установить статус робота - Ошибка, и поставить максимальное значение попыток
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration"></param>
|
||||||
|
void SetErrorRobotStatusAndMaxAttempts(RobotConfiguration configuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.Core.Repositories.Interfaces
|
namespace PARR.Core.Repositories.Interfaces
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using PARR.Core.Repositories.Base;
|
using PARR.Core.Repositories.Base;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.Core.Repositories.Interfaces
|
namespace PARR.Core.Repositories.Interfaces
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.Core.Repositories.Interfaces
|
namespace PARR.Core.Repositories.Interfaces
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.Core.Repositories.Interfaces
|
namespace PARR.Core.Repositories.Interfaces
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
{
|
{
|
||||||
public interface IStatusTemplateRepository
|
public interface IStatusTemplateRepository
|
||||||
{
|
{
|
||||||
IQueryable<Domain.Entities.TaskStatus> Get();
|
IQueryable<Domain.Entities.RobotEntities.TaskStatus> Get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
{
|
{
|
||||||
public interface ITaskStatusRepository
|
public interface ITaskStatusRepository
|
||||||
{
|
{
|
||||||
IQueryable<Domain.Entities.TaskStatus> Get();
|
IQueryable<Domain.Entities.RobotEntities.TaskStatus> Get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using PARR.Domain.Entities.TemplateEntities;
|
||||||
|
|
||||||
|
namespace PARR.Core.Repositories.Interfaces.TemplateRepositories
|
||||||
|
{
|
||||||
|
public interface ITemplateRenamePendingRepository
|
||||||
|
{
|
||||||
|
Task<bool> CreateAsync(TemplateRenamePending obj);
|
||||||
|
IQueryable<TemplateRenamePending> Get();
|
||||||
|
void Remove(TemplateRenamePending obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ using PARR.Domain.Entities.Unit;
|
|||||||
|
|
||||||
namespace PARR.Core.Repositories.Interfaces.Unit
|
namespace PARR.Core.Repositories.Interfaces.Unit
|
||||||
{
|
{
|
||||||
public interface IUnitFieldValueRepository: IBaseRepository<UnitFieldValue>
|
public interface IUnitFieldValueRepository : IBaseRepository<UnitFieldValue>
|
||||||
{
|
{
|
||||||
Task<UnitFieldValue?> GetByValueNameAsync(string? value);
|
Task<List<Guid>> FindValueIdsByMaskAsync(string mask, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,11 @@ namespace PARR.Core.Repositories.Interfaces.Unit
|
|||||||
{
|
{
|
||||||
public interface IUnitInUnitRepository
|
public interface IUnitInUnitRepository
|
||||||
{
|
{
|
||||||
Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId);
|
|
||||||
Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает связи, где ChildUnitId unitIds (для IsParent=True).
|
/// Возвращает все связанные UnitId для заданного юнита в обоих направлениях.
|
||||||
|
/// Единая точка загрузки связей.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds);
|
Task<List<Guid>> GetRelatedUnitIdsAsync(Guid unitId, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает связи, где ParentUnitId unitIds (для IsParent=False).
|
|
||||||
/// </summary>
|
|
||||||
Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds);
|
|
||||||
|
|
||||||
|
|
||||||
IQueryable<UnitInUnit> Get();
|
IQueryable<UnitInUnit> Get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ namespace PARR.Core.Repositories.Interfaces.Unit
|
|||||||
{
|
{
|
||||||
public interface IUnitRepository : IBaseRepository<Domain.Entities.Unit.Unit>
|
public interface IUnitRepository : IBaseRepository<Domain.Entities.Unit.Unit>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Поиск юнитов по списку ID значений.
|
||||||
|
/// Используется для эффективной фильтрации после предварительного поиска ValueId.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<Guid>> FindUnitIdsByValueIdsAsync(
|
||||||
|
IReadOnlyList<Guid> unitIds,
|
||||||
|
Guid fieldId,
|
||||||
|
IReadOnlyList<Guid> valueIds,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
IQueryable<Guid> GetInitialUnitIds(string dbValueMask);
|
IQueryable<Guid> GetInitialUnitIds(string dbValueMask);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получить юниты по Id атрибута и маски значения
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query"></param>
|
|
||||||
/// <param name="fieldId"></param>
|
|
||||||
/// <param name="valueMask"></param>
|
|
||||||
/// <param name="isInverse">true - не содержит, false - содержит</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IQueryable<Domain.Entities.Unit.Unit> GetUnitByFieldAndValue(IQueryable<Domain.Entities.Unit.Unit> query, Guid fieldId, string valueMask, bool isInverse = false);
|
|
||||||
|
|
||||||
IQueryable<Domain.Entities.Unit.Unit> GetWithIncludes();
|
IQueryable<Domain.Entities.Unit.Unit> GetWithIncludes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
PARR.Core/Services/RobotMetrics/IRobotMetricsService.cs
Normal file
37
PARR.Core/Services/RobotMetrics/IRobotMetricsService.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Отчетность по метрикам заданий и работы роботов.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRobotMetricsService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Заданиям Роботу
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robotCode"></param>
|
||||||
|
/// <param name="period"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<RobotStatusChartPoint>> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Статусам Заданий
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robotCode"></param>
|
||||||
|
/// <param name="period"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<TaskStatusChartPoint>> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика с применением гибких фильтров
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<FilteredChartPoint>> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
260
PARR.Core/Services/RobotMetrics/RobotMetricsService.cs
Normal file
260
PARR.Core/Services/RobotMetrics/RobotMetricsService.cs
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Extensions;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.RobotRepositories;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
using PARR.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotMetrics
|
||||||
|
{
|
||||||
|
internal class RobotMetricsService : IRobotMetricsService
|
||||||
|
{
|
||||||
|
private readonly IRobotConfigurationSnapshotRepository _snapshotRepository;
|
||||||
|
private readonly IRobotConfigurationRepository _configurationRepository;
|
||||||
|
private readonly ILogger<RobotMetricsService> _logger;
|
||||||
|
|
||||||
|
public RobotMetricsService(
|
||||||
|
IRobotConfigurationSnapshotRepository snapshotRepository,
|
||||||
|
IRobotConfigurationRepository configurationRepository,
|
||||||
|
ILogger<RobotMetricsService> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_snapshotRepository = snapshotRepository;
|
||||||
|
_configurationRepository = configurationRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<FilteredChartPoint>> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// ------- Правильность расчетов этого метода доконца не проверена -------
|
||||||
|
|
||||||
|
if (filter == null)
|
||||||
|
throw new AppValidationException("Фильтр не может быть пустым.");
|
||||||
|
|
||||||
|
if (filter.IntervalMinutes < 1)
|
||||||
|
throw new AppValidationException("Интервал группировки не может быть меньше 1 минуты.");
|
||||||
|
|
||||||
|
var query = _snapshotRepository.Get().AsNoTracking();
|
||||||
|
|
||||||
|
if (filter.RobotCode.HasValue)
|
||||||
|
query = query.Where(t => t.RobotCode == (int)filter.RobotCode.Value);
|
||||||
|
|
||||||
|
if (filter.RobotStatusCode.HasValue)
|
||||||
|
query = query.Where(t => t.RobotStatusCode == (int)filter.RobotStatusCode.Value);
|
||||||
|
|
||||||
|
if (filter.TaskStatusCode.HasValue)
|
||||||
|
query = query.Where(t => t.TaskStatusCode == (int)filter.TaskStatusCode);
|
||||||
|
|
||||||
|
// Если даты не переданы, берем последние 24 часа по умолчанию
|
||||||
|
var dateFrom = filter.DateFrom ?? DateTimeOffset.UtcNow.AddDays(-1);
|
||||||
|
query = query.Where(s => s.DateCreated >= dateFrom);
|
||||||
|
|
||||||
|
if (filter.DateTo.HasValue)
|
||||||
|
query = query.Where(s => s.DateCreated <= filter.DateTo.Value);
|
||||||
|
|
||||||
|
var dbData = await query.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var dbGrouped = dbData
|
||||||
|
.GroupBy(s => RoundToInterval(s.DateCreated, filter.IntervalMinutes))
|
||||||
|
.ToDictionary(t => t.Key, t => t.ToList());
|
||||||
|
|
||||||
|
var dateTo = filter.DateTo ?? DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var result = GenerateTimeGrid(dateFrom, dateTo, filter.IntervalMinutes)
|
||||||
|
.Select(time => new FilteredChartPoint(
|
||||||
|
Timestamp: time,
|
||||||
|
//Count: dbGrouped.TryGetValue(time, out var points) ? points.Max(x => x.Count) : 0
|
||||||
|
Count: dbGrouped.TryGetValue(time, out var points)
|
||||||
|
? points.GroupBy(x => x.DateCreated) // Группируем по точной минуте снапшота
|
||||||
|
.Select(g => g.Sum(x => x.Count)) // Складываем всё, что подошли под фильтр в эту минуту
|
||||||
|
.MaxOrDefault() // Берем максимальный пик за весь интервал (например, за час)
|
||||||
|
: 0 // Если снапшотов не было — честный ноль
|
||||||
|
)).ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<RobotStatusChartPoint>> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateRobot(robotCode);
|
||||||
|
CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
|
||||||
|
|
||||||
|
// История из снапшотов
|
||||||
|
var snapshots = await _snapshotRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var dbGrouped = snapshots
|
||||||
|
.GroupBy(t => RoundToInterval(t.DateCreated, intervalMinutes))
|
||||||
|
.ToDictionary(t => t.Key, t => t.ToList());
|
||||||
|
|
||||||
|
// Генерим сетку значений, если значений нет, вставляем нули
|
||||||
|
var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
|
||||||
|
.Select(time => dbGrouped.TryGetValue(time, out var points)
|
||||||
|
? new RobotStatusChartPoint(
|
||||||
|
Timestamp: time,
|
||||||
|
// группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
|
||||||
|
Wait: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(t => t.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
InProgress: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Error: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).MaxOrDefault(x => x.Count)
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Complete:
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
|
||||||
|
.MaxOrDefault()
|
||||||
|
)
|
||||||
|
: new RobotStatusChartPoint(time, Wait: 0, InProgress: 0, Error: 0, Complete: 0)
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
// Последнее значение в конце графика из реальной таблицы
|
||||||
|
var liveRaw = await _configurationRepository
|
||||||
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.RobotCode == (int)robotCode)
|
||||||
|
.GroupBy(t => t.RobotStatusCode)
|
||||||
|
.Select(g => new { RobotStatusCode = g.Key, Count = g.Count() })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
history.Add(new RobotStatusChartPoint(
|
||||||
|
Timestamp: DateTimeOffset.UtcNow,
|
||||||
|
Wait: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).Sum(x => x.Count),
|
||||||
|
InProgress: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).Sum(x => x.Count),
|
||||||
|
Error: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).Sum(x => x.Count),
|
||||||
|
Complete: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete).Sum(x => x.Count)
|
||||||
|
));
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<TaskStatusChartPoint>> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateRobot(robotCode);
|
||||||
|
CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
|
||||||
|
|
||||||
|
// История из снапшотов
|
||||||
|
var snapshots = await _snapshotRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var dbGrouped = snapshots
|
||||||
|
.GroupBy(s => RoundToInterval(s.DateCreated, intervalMinutes))
|
||||||
|
.ToDictionary(t => t.Key, t => t.ToList());
|
||||||
|
|
||||||
|
var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
|
||||||
|
.Select(time => dbGrouped.TryGetValue(time, out var points)
|
||||||
|
? new TaskStatusChartPoint(
|
||||||
|
Timestamp: time,
|
||||||
|
// группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
|
||||||
|
Creating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Updating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Ok: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).MaxOrDefault(x => x.Count)
|
||||||
|
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault()
|
||||||
|
)
|
||||||
|
: new TaskStatusChartPoint(time, Creating: 0, Updating: 0, Ok: 0)
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
// Живой текущий кадр в конец графика
|
||||||
|
var liveRaw = await _configurationRepository
|
||||||
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.RobotCode == (int)robotCode)
|
||||||
|
.GroupBy(t => t.TaskStatusCode)
|
||||||
|
.Select(g => new { TaskStatusCode = g.Key, Count = g.Count() })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
history.Add(new TaskStatusChartPoint(
|
||||||
|
Timestamp: DateTimeOffset.UtcNow,
|
||||||
|
Creating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).Sum(x => x.Count),
|
||||||
|
Updating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).Sum(x => x.Count),
|
||||||
|
Ok: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).Sum(x => x.Count)
|
||||||
|
));
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ValidateRobot(RobotsEnum robotCode)
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(typeof(RobotsEnum), robotCode))
|
||||||
|
throw new NotFoundException($"Робот с кодом {robotCode} не найден в системе.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void CalculatePeriodDates(ChartPeriod period, out DateTimeOffset fromDate, out int intervalMinutes)
|
||||||
|
{
|
||||||
|
switch (period)
|
||||||
|
{
|
||||||
|
case ChartPeriod.TwoHours:
|
||||||
|
fromDate = DateTimeOffset.UtcNow.AddHours(-2);
|
||||||
|
intervalMinutes = 2;
|
||||||
|
break;
|
||||||
|
case ChartPeriod.TwentyFourHours:
|
||||||
|
fromDate = DateTimeOffset.UtcNow.AddDays(-1);
|
||||||
|
intervalMinutes = 30;
|
||||||
|
break;
|
||||||
|
case ChartPeriod.SevenDays:
|
||||||
|
fromDate = DateTimeOffset.UtcNow.AddDays(-7);
|
||||||
|
intervalMinutes = 60;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AppValidationException("Указан неподдерживаемый период времени.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private DateTimeOffset RoundToInterval(DateTimeOffset dt, int intervalMinutes)
|
||||||
|
{
|
||||||
|
var minutes = (dt.Minute / intervalMinutes) * intervalMinutes;
|
||||||
|
return new DateTimeOffset(dt.Year, dt.Month, dt.Day, dt.Hour, minutes, 0, dt.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Генератор сетки времени
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fromDate"></param>
|
||||||
|
/// <param name="toDate"></param>
|
||||||
|
/// <param name="intervalMinutes"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private IEnumerable<DateTimeOffset> GenerateTimeGrid(DateTimeOffset fromDate, DateTimeOffset toDate, int intervalMinutes)
|
||||||
|
{
|
||||||
|
var startTime = RoundToInterval(fromDate, intervalMinutes);
|
||||||
|
var endTime = RoundToInterval(toDate, intervalMinutes);
|
||||||
|
|
||||||
|
for (var time = startTime; time <= endTime; time = time.AddMinutes(intervalMinutes))
|
||||||
|
{
|
||||||
|
yield return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Services.RobotStatusDetails.Interfaces;
|
||||||
|
using PARR.Domain.DTOs.RobotStatusDetails;
|
||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
using PARR.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotStatusDetails.Implementations
|
||||||
|
{
|
||||||
|
internal class RobotStatusDetailsService : IRobotStatusDetailsService
|
||||||
|
{
|
||||||
|
private readonly ILogger<RobotStatusDetailsService> _logger;
|
||||||
|
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IRobotRepository _robotRepository;
|
||||||
|
private readonly IRobotStatusRepository _robotStatusRepository;
|
||||||
|
|
||||||
|
public RobotStatusDetailsService(
|
||||||
|
ILogger<RobotStatusDetailsService> logger,
|
||||||
|
IRobotConfigurationRepository robotConfigurationRepository,
|
||||||
|
IMapper mapper,
|
||||||
|
IRobotRepository robotRepository,
|
||||||
|
IRobotStatusRepository robotStatusRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_robotConfigurationRepository = robotConfigurationRepository;
|
||||||
|
_mapper = mapper;
|
||||||
|
_robotRepository = robotRepository;
|
||||||
|
_robotStatusRepository = robotStatusRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<RobotStatusDetailsResult> GetDetailsAsync(RobotsEnum robot, RobotStatusEnum status, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var groupedDetails = await _robotConfigurationRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(config => config.RobotCode == (int)robot && config.RobotStatusCode == (int)status)
|
||||||
|
.GroupBy(config => config.Template!.Job!.Group)
|
||||||
|
.Select(t => new
|
||||||
|
{
|
||||||
|
JobGroup = t.Key,
|
||||||
|
TemplatesCount = t.Count()
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var robotEntity = await _robotRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(r => r.Code == (int)robot, cancellationToken);
|
||||||
|
|
||||||
|
var robotStatusEntity = await _robotStatusRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(t => t.Code == (int)status, cancellationToken);
|
||||||
|
|
||||||
|
if (robotEntity == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Робот с кодом {RobotCode} не найден в БД", robot);
|
||||||
|
throw new AppValidationException($"Робот с кодом {(int)robot} не найден");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (robotStatusEntity == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Статус робота с кодом {StatusCode} не найден в БД", status);
|
||||||
|
throw new AppValidationException($"Статус робота с кодом {(int)status} не найден");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new RobotStatusDetailsResult
|
||||||
|
{
|
||||||
|
Robot = _mapper.Map<RobotResult>(robotEntity),
|
||||||
|
Status = _mapper.Map<RobotStatusResult>(robotStatusEntity),
|
||||||
|
Details = groupedDetails
|
||||||
|
.Select(t => new RobotStatusGroupDetailsResult
|
||||||
|
{
|
||||||
|
JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup),
|
||||||
|
TemplatesCount = t.TemplatesCount
|
||||||
|
}).OrderBy(t => t.JobGroup.GroupName)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using PARR.Domain.DTOs.RobotStatusDetails;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotStatusDetails.Interfaces
|
||||||
|
{
|
||||||
|
public interface IRobotStatusDetailsService
|
||||||
|
{
|
||||||
|
Task<RobotStatusDetailsResult> GetDetailsAsync(RobotsEnum robot, RobotStatusEnum status, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,15 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.BLL.Helpers;
|
using PARR.BLL.Helpers;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.TemplateRepositories;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
using PARR.Core.Services.RobotTask.Interfaces;
|
using PARR.Core.Services.RobotTask.Interfaces;
|
||||||
|
using PARR.Core.Services.RobotTask.Models;
|
||||||
using PARR.Core.Services.Shortcodes;
|
using PARR.Core.Services.Shortcodes;
|
||||||
using PARR.Domain.DTOs.RobotTask;
|
using PARR.Domain.DTOs.RobotTask;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities;
|
||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
using PARR.Domain.Exceptions;
|
using PARR.Domain.Exceptions;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
@@ -19,16 +22,18 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Количество заданий которые рассматриваем для взятия в работу.
|
/// Количество заданий которые рассматриваем для взятия в работу.
|
||||||
|
/// Рекомендованное значение, кол-во роботов * 3
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly int TakeTasks = 10;
|
private readonly int TakeTasks = 15 * 3;
|
||||||
|
|
||||||
private readonly ILogger<RobotTaskService> logger;
|
private readonly ILogger<RobotTaskService> _logger;
|
||||||
private readonly IRobotConfigurationRepository robotConfigurationRepository;
|
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
|
||||||
private readonly SettingsFromDb settingsFromDb;
|
private readonly SettingsFromDb _settingsFromDb;
|
||||||
private readonly IRobotHistoryRepository robotHistoryRepository;
|
private readonly IRobotHistoryRepository _robotHistoryRepository;
|
||||||
private readonly IMapper mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly IShortcodesService shortcodesService;
|
private readonly IShortcodesService _shortcodesService;
|
||||||
private readonly INextRunService nextRunService;
|
private readonly INextRunService _nextRunService;
|
||||||
|
private readonly ITemplateRenamePendingRepository _templateRenamePendingRepository;
|
||||||
|
|
||||||
public RobotTaskService(
|
public RobotTaskService(
|
||||||
ILogger<RobotTaskService> logger,
|
ILogger<RobotTaskService> logger,
|
||||||
@@ -37,16 +42,18 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
IRobotHistoryRepository robotHistoryRepository,
|
IRobotHistoryRepository robotHistoryRepository,
|
||||||
IMapper mapper,
|
IMapper mapper,
|
||||||
IShortcodesService shortcodesService,
|
IShortcodesService shortcodesService,
|
||||||
INextRunService nextRunService
|
INextRunService nextRunService,
|
||||||
|
ITemplateRenamePendingRepository templateRenamePendingRepository
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
_logger = logger;
|
||||||
this.robotConfigurationRepository = robotConfigurationRepository;
|
_robotConfigurationRepository = robotConfigurationRepository;
|
||||||
this.settingsFromDb = settingsFromDb;
|
_settingsFromDb = settingsFromDb;
|
||||||
this.robotHistoryRepository = robotHistoryRepository;
|
_robotHistoryRepository = robotHistoryRepository;
|
||||||
this.mapper = mapper;
|
_mapper = mapper;
|
||||||
this.shortcodesService = shortcodesService;
|
_shortcodesService = shortcodesService;
|
||||||
this.nextRunService = nextRunService;
|
_nextRunService = nextRunService;
|
||||||
|
_templateRenamePendingRepository = templateRenamePendingRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -54,19 +61,19 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
{
|
{
|
||||||
var templateTask = await GetTaskAsync(RobotsEnum.TemplateOrder, taskStatusCode, acquireTask, robotIp, robotId, TimeSpan.Zero);
|
var templateTask = await GetTaskAsync(RobotsEnum.TemplateOrder, taskStatusCode, acquireTask, robotIp, robotId, TimeSpan.Zero);
|
||||||
|
|
||||||
var task = mapper.Map<RobotTaskTemplate>(templateTask);
|
var task = _mapper.Map<RobotTaskTemplate>(templateTask);
|
||||||
|
|
||||||
task = task with { FullDescription = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(task.FullDescription, templateTask.Template!)) };
|
task = task with { FullDescription = NormalizeLineEndingsToCrlf(await _shortcodesService.ApplyShortcodesAsync(task.FullDescription, templateTask.Template!)) };
|
||||||
task = task with { ShortDescription = await shortcodesService.ApplyShortcodesAsync(task.ShortDescription, templateTask.Template!) };
|
task = task with { ShortDescription = await _shortcodesService.ApplyShortcodesAsync(task.ShortDescription, templateTask.Template!) };
|
||||||
task = task with { Solution = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(task.Solution, templateTask.Template!)) };
|
task = task with { Solution = NormalizeLineEndingsToCrlf(await _shortcodesService.ApplyShortcodesAsync(task.Solution, templateTask.Template!)) };
|
||||||
task = task with { TnkName = await shortcodesService.ApplyShortcodesAsync(task.TnkName, templateTask.Template!) };
|
task = task with { TnkName = await _shortcodesService.ApplyShortcodesAsync(task.TnkName, templateTask.Template!) };
|
||||||
task = task with { WorkName = await shortcodesService.ApplyShortcodesAsync(task.WorkName, templateTask.Template!) };
|
task = task with { WorkName = await _shortcodesService.ApplyShortcodesAsync(task.WorkName, templateTask.Template!) };
|
||||||
task = task with { WorkGroup = await shortcodesService.ApplyShortcodesAsync(task.WorkGroup, templateTask.Template!) };
|
task = task with { WorkGroup = await _shortcodesService.ApplyShortcodesAsync(task.WorkGroup, templateTask.Template!) };
|
||||||
task = task with { ResponseArea = await shortcodesService.ApplyShortcodesAsync(task.ResponseArea, templateTask.Template!) };
|
task = task with { ResponseArea = await _shortcodesService.ApplyShortcodesAsync(task.ResponseArea, templateTask.Template!) };
|
||||||
|
|
||||||
task = task with { ClosingCode = settingsFromDb.ClosingCode };
|
task = task with { ClosingCode = _settingsFromDb.ClosingCode };
|
||||||
task = task with { Initiator = settingsFromDb.Initiator };
|
task = task with { Initiator = _settingsFromDb.Initiator };
|
||||||
task = task with { Category = settingsFromDb.Category };
|
task = task with { Category = _settingsFromDb.Category };
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
@@ -81,22 +88,22 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
var resultUpdateNextRun = await UpdateNextRunAsync(scheduleTask, historyInitiator);
|
var resultUpdateNextRun = await UpdateNextRunAsync(scheduleTask, historyInitiator);
|
||||||
if (!resultUpdateNextRun)
|
if (!resultUpdateNextRun)
|
||||||
{
|
{
|
||||||
logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}", scheduleTask.TemplateId);
|
_logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}", scheduleTask.TemplateId);
|
||||||
throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
|
throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var task = mapper.Map<RobotTaskSchedule>(scheduleTask);
|
var task = _mapper.Map<RobotTaskSchedule>(scheduleTask);
|
||||||
|
|
||||||
task = task with { Timezone = settingsFromDb.EsppScheduleTimezone };
|
task = task with { Timezone = _settingsFromDb.EsppScheduleTimezone };
|
||||||
task = task with { WorkGroup = await shortcodesService.ApplyShortcodesAsync(task.WorkGroup, scheduleTask.Template!) };
|
task = task with { WorkGroup = await _shortcodesService.ApplyShortcodesAsync(task.WorkGroup, scheduleTask.Template!) };
|
||||||
task = task with { ResponseArea = await shortcodesService.ApplyShortcodesAsync(task.ResponseArea, scheduleTask.Template!) };
|
task = task with { ResponseArea = await _shortcodesService.ApplyShortcodesAsync(task.ResponseArea, scheduleTask.Template!) };
|
||||||
|
|
||||||
//nextRun в часовой зоне УЗ Робота ЕСПП
|
//nextRun в часовой зоне УЗ Робота ЕСПП
|
||||||
var nextRunWithRobotTz = scheduleTask.Template!.NextRun.Add(nextRunService.GetEsppAccountOffset());
|
var nextRunWithRobotTz = scheduleTask.Template!.NextRun.Add(_nextRunService.GetEsppAccountOffset());
|
||||||
//на всякий случай еще раз проверяем, что дата не устарела и отправляем задание
|
//на всякий случай еще раз проверяем, что дата не устарела и отправляем задание
|
||||||
if (nextRunWithRobotTz < DateTimeOffset.UtcNow)
|
if (nextRunWithRobotTz < DateTimeOffset.UtcNow)
|
||||||
{
|
{
|
||||||
logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}",
|
_logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}",
|
||||||
task.TemplateId, nextRunWithRobotTz, DateTimeOffset.UtcNow);
|
task.TemplateId, nextRunWithRobotTz, DateTimeOffset.UtcNow);
|
||||||
throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
|
throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
|
||||||
}
|
}
|
||||||
@@ -104,7 +111,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
task = task with { NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithRobotTz) };
|
task = task with { NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithRobotTz) };
|
||||||
task = task with { GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithRobotTz) };
|
task = task with { GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithRobotTz) };
|
||||||
|
|
||||||
task = task with { RepeatRange = settingsFromDb.ScheduleRepeatRange };
|
task = task with { RepeatRange = _settingsFromDb.ScheduleRepeatRange };
|
||||||
task = task with { };
|
task = task with { };
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
@@ -124,7 +131,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
private async Task<RobotConfiguration> GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId, TimeSpan scheduleCooldownDuration)
|
private async Task<RobotConfiguration> GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId, TimeSpan scheduleCooldownDuration)
|
||||||
{
|
{
|
||||||
// 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
|
// 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
|
||||||
await robotConfigurationRepository.MarkExpiredTasksAsFailedAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime);
|
await _robotConfigurationRepository.MarkExpiredTasksAsFailedAsync(_settingsFromDb.RobotAttemptsNumber, _settingsFromDb.RobotWaitTime);
|
||||||
|
|
||||||
|
|
||||||
// 2. Ищем доступные задания
|
// 2. Ищем доступные задания
|
||||||
@@ -147,7 +154,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
{
|
{
|
||||||
// Берем первую задачу из списка доступных
|
// Берем первую задачу из списка доступных
|
||||||
acquiredTaskId = availableTasks.First();
|
acquiredTaskId = availableTasks.First();
|
||||||
logger.LogDebug("Задача не требует захвата, взята первая из доступных: {TaskId}", acquiredTaskId);
|
_logger.LogDebug("Задача не требует захвата, взята первая из доступных: {TaskId}", acquiredTaskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -167,20 +174,14 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task<List<Guid>> GetAvailableTasksAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, TimeSpan scheduleCooldownDuration)
|
private async Task<List<Guid>> GetAvailableTasksAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, TimeSpan scheduleCooldownDuration)
|
||||||
{
|
{
|
||||||
var query = robotConfigurationRepository.Get()
|
var query = _robotConfigurationRepository.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(t => t.RobotCode == (int)robotCode/* && t.TaskStatusCode == (int)taskStatusCode*/);
|
.Where(t => t.RobotCode == (int)robotCode);
|
||||||
|
|
||||||
// Если это задание для робота расписаний
|
// Если это задание для робота расписаний
|
||||||
if (robotCode == RobotsEnum.ScheduleOrder)
|
if (robotCode == RobotsEnum.ScheduleOrder)
|
||||||
{
|
{
|
||||||
// Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания
|
// Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания
|
||||||
#region Старый не оптимизированный запрос
|
|
||||||
//var createdTemplates = robotConfigurationRepository.Get()
|
|
||||||
// .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && t.TaskStatusCode == (int)TaskStatusEnum.Ok)
|
|
||||||
// .Select(t => t.TemplateId);
|
|
||||||
//query = query.Where(t => createdTemplates.Contains(t.TemplateId));
|
|
||||||
#endregion
|
|
||||||
query = query.Where(t => t.Template!.RobotConfigurations.Any(x => x.RobotCode == (int)RobotsEnum.TemplateOrder && x.TaskStatusCode == (int)TaskStatusEnum.Ok));
|
query = query.Where(t => t.Template!.RobotConfigurations.Any(x => x.RobotCode == (int)RobotsEnum.TemplateOrder && x.TaskStatusCode == (int)TaskStatusEnum.Ok));
|
||||||
|
|
||||||
|
|
||||||
@@ -199,20 +200,22 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
// Сортируем по nextRun, чтобы те, у кого nextRun ближе к текущей, выполнились скорее
|
// Сортируем по nextRun, чтобы те, у кого nextRun ближе к текущей, выполнились скорее
|
||||||
query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template!.IsActiveTemplate);
|
query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template!.IsActiveTemplate);
|
||||||
|
|
||||||
// Кандидаты заданий
|
// Кандидаты заданий, Id задания и имя шаблона
|
||||||
var tasks = new List<Guid>();
|
//var tasks = new List<Guid>();
|
||||||
|
var tasks = new List<RobotTaskDetails>();
|
||||||
|
|
||||||
// Ещем первые 10 заданий в статусе ОЖИДАНИЕ
|
// Ищем первые TakeTasks заданий в статусе ОЖИДАНИЕ
|
||||||
tasks = await query
|
tasks = await query
|
||||||
.Where(t =>
|
.Where(t =>
|
||||||
t.RobotStatusCode == (int)RobotStatusEnum.Wait
|
t.RobotStatusCode == (int)RobotStatusEnum.Wait
|
||||||
&& t.TaskStatusCode == (int)taskStatusCode
|
&& t.TaskStatusCode == (int)taskStatusCode
|
||||||
).Take(TakeTasks)
|
).Take(TakeTasks)
|
||||||
.Select(t => t.Id)
|
//.Select(t => t.Id)
|
||||||
|
.Select(t => new RobotTaskDetails(t.Id, t.Template!.Name, t.Template.NextRun))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|
||||||
logger.LogDebug("Найдено заданий в статусе 'Ожидание' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
|
_logger.LogDebug("Найдено заданий в статусе 'Ожидание' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
|
||||||
|
|
||||||
if (tasks.Count == 0)
|
if (tasks.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -221,20 +224,274 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
// Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`)
|
// Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`)
|
||||||
// и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись.
|
// и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись.
|
||||||
|
|
||||||
var endDate = DateTimeOffset.UtcNow.Add(-settingsFromDb.RobotWaitTime);
|
var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime);
|
||||||
|
|
||||||
tasks = await query.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
tasks = await query.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
||||||
&& t.TaskStatusCode==(int)taskStatusCode
|
&& t.TaskStatusCode == (int)taskStatusCode
|
||||||
&& t.AttemptsNumber < settingsFromDb.RobotAttemptsNumber
|
&& t.AttemptsNumber < _settingsFromDb.RobotAttemptsNumber
|
||||||
&& t.LastRobotStatusUpdated < endDate)
|
&& t.LastRobotStatusUpdated < endDate)
|
||||||
.Take(TakeTasks)
|
.Take(TakeTasks)
|
||||||
.Select(t => t.Id)
|
//.Select(t => t.Id)
|
||||||
|
.Select(t => new RobotTaskDetails(t.Id, t.Template!.Name, t.Template.NextRun))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
logger.LogDebug("Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
|
_logger.LogDebug("Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks;
|
if (robotCode == RobotsEnum.TemplateOrder)
|
||||||
|
{
|
||||||
|
// Если запрашиваем шаблоны, смотрим корректируем список заданий в зависимости от статуса переименования.
|
||||||
|
// Это не относится к расписаниям, потому что у переименованных расписаний статус Updating, а оно не возьмется в работу, пока не обновится шаблон
|
||||||
|
tasks = await ReplaceTemplateTasksForRenameAsync(tasks, robotCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks.Select(t => t.TaskId).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет наличие шаблонов в процессе переименования и заменяет обычные задания на задания по переименованию.
|
||||||
|
/// Если связанный шаблон не переименован, и у него статус ошибки, целевому шаблону устанавливается статус ошибки.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tasks"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<List<RobotTaskDetails>> ReplaceTemplateTasksForRenameAsync(List<RobotTaskDetails> tasks, RobotsEnum robotCode)
|
||||||
|
{
|
||||||
|
if (tasks.Count == 0 || robotCode != RobotsEnum.TemplateOrder)
|
||||||
|
return tasks;
|
||||||
|
|
||||||
|
_logger.LogDebug("Исходный пул задач для проверки переименования: {Tasks}",
|
||||||
|
string.Join(" | ", tasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
|
||||||
|
|
||||||
|
// Ищем есть ли связанные шаблоны с таким имененм на переименование
|
||||||
|
var taskTemplateNames = tasks.Select(t => t.TemplateName).Distinct().ToList();
|
||||||
|
// Ищем записи в таблице переименований, где OldName совпадает с именами наших новых задач
|
||||||
|
var templatesToRename = await _templateRenamePendingRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => taskTemplateNames.Contains(t.OldName))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
_logger.LogDebug("Найдено записей в TemplateRenamePending для текущих задач: {Count} шт.", templatesToRename.Count);
|
||||||
|
|
||||||
|
if (templatesToRename.Count == 0)
|
||||||
|
return tasks;
|
||||||
|
|
||||||
|
// Создаем словарь маппинга TemplateId -> OldName.
|
||||||
|
var templateIdToOldName = templatesToRename.ToDictionary(t => t.TemplateId, t => t.OldName);
|
||||||
|
|
||||||
|
// Ищем конфигурации роботов для СТАРЫХ шаблонов (которые переименовываются) по ИД, смотрим, можем ли взять их в работу
|
||||||
|
var renameTemplateIds = templatesToRename.Select(t => t.TemplateId).ToList();
|
||||||
|
var renameTasks = await _robotConfigurationRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.Template)
|
||||||
|
.Where(t =>
|
||||||
|
t.RobotCode == (int)robotCode
|
||||||
|
&& renameTemplateIds.Contains(t.TemplateId)
|
||||||
|
// Это может быть только обновление. Так как переименования для создаваемого шаблона быть не может
|
||||||
|
&& t.TaskStatusCode == (int)TaskStatusEnum.Updating
|
||||||
|
).ToListAsync();
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// БЛОК 1: ОБРАБОТКА ОШИБОК (Правило: если ХОТЯ БЫ ОДНА упала в ошибку -> оригинал в ошибку)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Если старый шаблон в ошибке и лимит попыток исчерпан, ставим ошибку и новому шаблону
|
||||||
|
var errorTasks = renameTasks
|
||||||
|
.Where(t =>
|
||||||
|
t.RobotStatusCode == (int)RobotStatusEnum.Error
|
||||||
|
&& t.AttemptsNumber >= _settingsFromDb.RobotAttemptsNumber
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
var tasksToSetErrorStatus = new List<Guid>();
|
||||||
|
if (errorTasks.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Найдено связанных заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям.", errorTasks.Count);
|
||||||
|
|
||||||
|
// Собираем ВСЕ OldName, для которых есть хотя бы одна упавшая в ошибку задача.
|
||||||
|
// Использование ToHashSet() гарантирует, что если 1 или 10 задач в ошибке, OldName попадет в набор один раз.
|
||||||
|
var errorOldNames = errorTasks
|
||||||
|
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||||||
|
.Select(t => templateIdToOldName[t.TemplateId])
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
// Находим оригинальные задачи, чье имя совпадает с любым из "ошибочных" OldName
|
||||||
|
tasksToSetErrorStatus = tasks
|
||||||
|
.Where(t => errorOldNames.Contains(t.TemplateName))
|
||||||
|
.Select(t => t.TaskId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (tasksToSetErrorStatus.Count > 0)
|
||||||
|
{
|
||||||
|
// Устанавливаем ошибку целевым + пишем комментарий от робота + нажимаем комит
|
||||||
|
var logMessage = "[RobotTaskService] Установлен статус ошибки, так как хотя бы одна из связанных задач переименования не была успешно выполнена.";
|
||||||
|
await SetErrorStatusAsync(tasksToSetErrorStatus, logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// БЛОК 2: ПОДМЕНА ЗАДАЧ (Правило: берем ПЕРВУЮ валидную задачу для подмены)
|
||||||
|
// =========================================================================
|
||||||
|
var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime);
|
||||||
|
|
||||||
|
// Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InProgress но которые еще не просрочены)
|
||||||
|
var allowedTasks = renameTasks.Where(t =>
|
||||||
|
t.RobotStatusCode == (int)RobotStatusEnum.Wait
|
||||||
|
|| (t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
||||||
|
&& t.AttemptsNumber < _settingsFromDb.RobotAttemptsNumber
|
||||||
|
&& t.LastRobotStatusUpdated < endDate)
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
// Проверим StatusTypeId у старых шаблонов в процессе переименования
|
||||||
|
// 1. Находим задачи переименования, у которых StatusTypeId шаблона НЕ является допустимым (!= Used и != Unused)
|
||||||
|
var invalidRenameTasks = allowedTasks
|
||||||
|
.Where(t => t.Template != null && t.Template.StatusTypeId != TemplateStatusTypeEnum.Used && t.Template.StatusTypeId != TemplateStatusTypeEnum.Unused)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 2. Создаем словарь для быстрого поиска и логирования: OldName -> StatusTypeId
|
||||||
|
// Так как OldName не уникален, используем GroupBy, чтобы избежать ArgumentException, при наличии нескольких невалидных задач с одинаковым OldName.
|
||||||
|
//var invalidOldNamesWithStatus = invalidRenameTasks
|
||||||
|
// .Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||||||
|
// .Select(t => new { OldName = templateIdToOldName[t.TemplateId], StatusTypeId = t.Template!.StatusTypeId })
|
||||||
|
// .ToDictionary(x => x.OldName, x => x.StatusTypeId);
|
||||||
|
var invalidOldNamesWithStatus = invalidRenameTasks
|
||||||
|
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||||||
|
.GroupBy(t => templateIdToOldName[t.TemplateId]) // Группируем по OldName
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key, // Ключ = OldName
|
||||||
|
g => g.First().Template!.StatusTypeId // Значение = StatusTypeId первой задачи в группе (для лога)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Оставляем для подмены только те задачи, у которых StatusTypeId является допустимым (== Used или == Unused)
|
||||||
|
var validAllowedTasks = allowedTasks
|
||||||
|
.Where(t => t.Template != null && (t.Template.StatusTypeId == TemplateStatusTypeEnum.Used || t.Template.StatusTypeId == TemplateStatusTypeEnum.Unused))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Формируем список заданий
|
||||||
|
var originalCount = tasks.Count;
|
||||||
|
var errorTaskIdsSet = tasksToSetErrorStatus.ToHashSet();
|
||||||
|
var errorCount = errorTaskIdsSet.Count;
|
||||||
|
|
||||||
|
// Создаем словарь подмены ТОЛЬКО из валидных задач (где StatusTypeId == Used или Unused)
|
||||||
|
// ГРУППИРУЕМ по OldName и берем .First()!
|
||||||
|
// Это реализует правило: "если записей несколько, берем из них первую и подменяем ей оригинальное задание".
|
||||||
|
var renameTasksToDictionary = validAllowedTasks
|
||||||
|
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||||||
|
.GroupBy(t => templateIdToOldName[t.TemplateId])
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key, // Ключ = OldName
|
||||||
|
g => new RobotTaskDetails(g.First().Id, g.First().Template!.Name, g.First().Template!.NextRun)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проходим по ИСХОДНОМУ списку, чтобы сохранить порядок сортировки
|
||||||
|
var finalTasks = new List<RobotTaskDetails>(tasks.Count);
|
||||||
|
int replacedCount = 0;
|
||||||
|
int excludedByStatusCount = 0; // Счетчик для логов
|
||||||
|
|
||||||
|
foreach (var task in tasks)
|
||||||
|
{
|
||||||
|
// 1. Если задаче нужно поставить ошибку, пропускаем ее
|
||||||
|
if (errorTaskIdsSet.Contains(task.TaskId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Если этот шаблон связан с переименованием, но у старого шаблона StatusTypeId != Used
|
||||||
|
if (invalidOldNamesWithStatus.TryGetValue(task.TemplateName, out var badStatusId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Задача для шаблона '{TemplateName}' (TaskId: {TaskId}) ИСКЛЮЧЕНА из выдачи. " +
|
||||||
|
"Связанный шаблон в процессе переименования имеет недопустимый StatusTypeId = {StatusTypeId} (ожидалось Used или Unused). " +
|
||||||
|
"Исходная задача также не выполняется.",
|
||||||
|
task.TemplateName, task.TaskId, badStatusId);
|
||||||
|
|
||||||
|
excludedByStatusCount++;
|
||||||
|
continue; // Не добавляем ни старую, ни новую задачу в итоговый список
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Если для этого имени шаблона есть разрешенная задача на переименование (и она валидна) - вставляем ее
|
||||||
|
// Подменяем оригинальную задачу на ПЕРВУЮ валидную задачу переименования
|
||||||
|
if (renameTasksToDictionary.TryGetValue(task.TemplateName, out var renameTask))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("ПОДМЕНА ЗАДАЧИ: Исходная [Id: {OriginalId}, Name: '{OriginalName}'] " +
|
||||||
|
"-> Заменена на [Id: {NewId}, Name: '{NewName}']",
|
||||||
|
task.TaskId, task.TemplateName, renameTask.TaskId, renameTask.TemplateName);
|
||||||
|
|
||||||
|
finalTasks.Add(renameTask);
|
||||||
|
replacedCount++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Иначе оставляем исходную задачу на месте
|
||||||
|
finalTasks.Add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Итоговый пул задач после трансформации: {Tasks}",
|
||||||
|
string.Join(" | ", finalTasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " +
|
||||||
|
"Отклонено (ошибка): {ErrorCount} шт. Исключено (невалидный StatusTypeId): {ExcludedCount} шт. " +
|
||||||
|
"Заменено на старые (взята первая из группы): {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт.",
|
||||||
|
originalCount, errorCount, excludedByStatusCount, replacedCount, finalTasks.Count);
|
||||||
|
|
||||||
|
// Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка
|
||||||
|
return finalTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Установить статус задания - ошибка
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskIds"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task SetErrorStatusAsync(List<Guid> taskIds, string logMessage)
|
||||||
|
{
|
||||||
|
if (taskIds == null || taskIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tasks = await _robotConfigurationRepository.Get()
|
||||||
|
.Include(t => t.Template)
|
||||||
|
.Where(t => taskIds.Contains(t.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (tasks.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var task in tasks)
|
||||||
|
{
|
||||||
|
// Так как это целевой шаблон, то ставим ему сразу максимальное кол-во попыток и ошибку, чтоб больше он не выдавался в заданиях, пока не исправим связанный
|
||||||
|
// Устанавливаем статус ошибки
|
||||||
|
_robotConfigurationRepository.SetErrorRobotStatusAndMaxAttempts(task);
|
||||||
|
|
||||||
|
// Пишем в лог роботу
|
||||||
|
var history = new RobotHistory
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
HistoryLevel = (int)RobotStatusEnum.Error,
|
||||||
|
TaskStatusCode = task.TaskStatusCode,
|
||||||
|
RobotConfigurationId = task.Id,
|
||||||
|
RobotIp = null,
|
||||||
|
RobotId = ParrComponentsEnum.Api.ToString(),
|
||||||
|
RobotMessage = logMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
await _robotHistoryRepository.CreateAsync(history);
|
||||||
|
|
||||||
|
_logger.LogInformation("Для целевого задания {TaskId} (шаблон '{TemplateName}') установлен статус ошибки, " +
|
||||||
|
"так как связанное задание со старым шаблоном не было успешно выполнено.",
|
||||||
|
task.Id, task.Template!.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await _robotHistoryRepository.CommitAsync())
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Установлен статус 'Ошибка', для заданий {TaskCount} шт.", tasks.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("Ошибка при установке статуса задания 'Ошибка', для заданий {TaskCount} шт. Транзакция отменена", tasks.Count);
|
||||||
|
throw new DbErrorException("Не удалось сохранить изменения статусов заданий при обработке переименования шаблона.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -247,12 +504,12 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
{
|
{
|
||||||
foreach (var taskId in tasks)
|
foreach (var taskId in tasks)
|
||||||
{
|
{
|
||||||
var isChangedStatus = await robotConfigurationRepository.SetInProgressStatusAsync(taskId);
|
var isChangedStatus = await _robotConfigurationRepository.SetInProgressStatusAsync(taskId);
|
||||||
if (isChangedStatus)
|
if (isChangedStatus)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Захвачена задача {TaskId}", taskId);
|
_logger.LogDebug("Захвачена задача {TaskId}", taskId);
|
||||||
|
|
||||||
var task = await robotConfigurationRepository.Get()
|
var task = await _robotConfigurationRepository.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstAsync(t => t.Id == taskId);
|
.FirstAsync(t => t.Id == taskId);
|
||||||
|
|
||||||
@@ -267,18 +524,18 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
RobotId = robotId
|
RobotId = robotId
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!await robotHistoryRepository.CreateAsync(history) || !await robotHistoryRepository.CommitAsync())
|
if (!await _robotHistoryRepository.CreateAsync(history) || !await _robotHistoryRepository.CommitAsync())
|
||||||
throw new DbErrorException("Ошибка при добавлении истории робота, при взятии задания в работу.");
|
throw new DbErrorException("Ошибка при добавлении истории робота, при взятии задания в работу.");
|
||||||
|
|
||||||
return taskId;
|
return taskId;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Не удалось захватить задачу {TaskId}", taskId);
|
_logger.LogDebug("Не удалось захватить задачу {TaskId}", taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("Не удалось захватить ни одну из доступных задач для робота");
|
_logger.LogDebug("Не удалось захватить ни одну из доступных задач для робота");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -292,7 +549,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task<RobotConfiguration> GetTaskWithAllDataAsync(Guid taskId, RobotsEnum robotCode)
|
private async Task<RobotConfiguration> GetTaskWithAllDataAsync(Guid taskId, RobotsEnum robotCode)
|
||||||
{
|
{
|
||||||
IQueryable<RobotConfiguration> query = robotConfigurationRepository.Get()
|
IQueryable<RobotConfiguration> query = _robotConfigurationRepository.Get()
|
||||||
//.AsNoTracking() // нужно обязательно трекать, так как может измениться nextRun и его нужно будет сохранить
|
//.AsNoTracking() // нужно обязательно трекать, так как может измениться nextRun и его нужно будет сохранить
|
||||||
.AsSingleQuery()
|
.AsSingleQuery()
|
||||||
// Общие инклуды для шаблонов и расписаний
|
// Общие инклуды для шаблонов и расписаний
|
||||||
@@ -371,23 +628,23 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
var template = task.Template!;
|
var template = task.Template!;
|
||||||
|
|
||||||
//var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job!.GroupId, template!.Job!.Group!.ReferenceDate);
|
//var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job!.GroupId, template!.Job!.Group!.ReferenceDate);
|
||||||
var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
var nextRun = await _nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
||||||
|
|
||||||
if (!nextRun.HasValue)
|
if (!nextRun.HasValue)
|
||||||
{
|
{
|
||||||
logger.LogError("При обновлении nextRun для шаблона {templateId}, расчитанный nextRun=null, ошибка в расчетах.", template.Id);
|
_logger.LogError("При обновлении nextRun для шаблона {TemplateId}, расчитанный nextRun=null, ошибка в расчетах.", template.Id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextRun.Value < DateTimeOffset.UtcNow)
|
if (nextRun.Value < DateTimeOffset.UtcNow)
|
||||||
{
|
{
|
||||||
logger.LogError("При обновлении nextRun для шаблона {templateId}, расчитанный nextRun<Now [{nextRun}<{now}], ошибка в расчетах.", template.Id, nextRun.Value, DateTimeOffset.UtcNow);
|
_logger.LogError("При обновлении nextRun для шаблона {TemplateId}, расчитанный nextRun<Now [{NextRun}<{Now}], ошибка в расчетах.", template.Id, nextRun.Value, DateTimeOffset.UtcNow);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextRun != template.NextRun)
|
if (nextRun != template.NextRun)
|
||||||
{
|
{
|
||||||
logger.LogDebug($"Для шаблона id {template.Id} обновляю nextRun, новое значение {nextRun}, старое значение {template.NextRun}");
|
_logger.LogDebug("Для шаблона {TemplateId} обновляю nextRun. Новое: {NewNextRun}, старое: {OldNextRun}", template.Id, nextRun, template.NextRun);
|
||||||
|
|
||||||
template.LastRun = template.NextRun;
|
template.LastRun = template.NextRun;
|
||||||
template.NextRun = nextRun.Value;
|
template.NextRun = nextRun.Value;
|
||||||
@@ -398,7 +655,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
|
|||||||
? suffix
|
? suffix
|
||||||
: $"{historyInitiator.InitiatorComment}. {suffix}";
|
: $"{historyInitiator.InitiatorComment}. {suffix}";
|
||||||
|
|
||||||
if (!await robotConfigurationRepository.CommitAsync(historyInitiator))
|
if (!await _robotConfigurationRepository.CommitAsync(historyInitiator))
|
||||||
throw new DbErrorException("Ошибка при сохранении изменения NextRun");
|
throw new DbErrorException("Ошибка при сохранении изменения NextRun");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
PARR.Core/Services/RobotTask/Models/RobotTaskDetails.cs
Normal file
9
PARR.Core/Services/RobotTask/Models/RobotTaskDetails.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Core.Services.RobotTask.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Модель задания для робота
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="TaskId"></param>
|
||||||
|
/// <param name="TemplateName"></param>
|
||||||
|
internal record RobotTaskDetails(Guid TaskId, string TemplateName, DateTimeOffset NextRun);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Services.RobotTaskDetailsServices.Interfaces;
|
||||||
|
using PARR.Domain.DTOs.RobotTaskDetails;
|
||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
using PARR.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotTaskDetailsServices.Implementations
|
||||||
|
{
|
||||||
|
internal class RobotTaskDetailsService : IRobotTaskDetailsService
|
||||||
|
{
|
||||||
|
private readonly ILogger<RobotTaskDetailsService> _logger;
|
||||||
|
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IRobotRepository _robotRepository;
|
||||||
|
private readonly ITaskStatusRepository _taskStatusRepository;
|
||||||
|
|
||||||
|
public RobotTaskDetailsService(
|
||||||
|
ILogger<RobotTaskDetailsService> logger,
|
||||||
|
IRobotConfigurationRepository robotConfigurationRepository,
|
||||||
|
IMapper mapper,
|
||||||
|
IRobotRepository robotRepository,
|
||||||
|
ITaskStatusRepository taskStatusRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_robotConfigurationRepository = robotConfigurationRepository;
|
||||||
|
_mapper = mapper;
|
||||||
|
_robotRepository = robotRepository;
|
||||||
|
_taskStatusRepository = taskStatusRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. Получаем группировку конфигураций
|
||||||
|
var groupedDetails = await _robotConfigurationRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(config => config.RobotCode == (int)robot && config.TaskStatusCode == (int)task)
|
||||||
|
.GroupBy(config => config.Template!.Job!.Group)
|
||||||
|
.Select(t => new
|
||||||
|
{
|
||||||
|
JobGroup = t.Key,
|
||||||
|
TemplatesCount = t.Count()
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 2. Получаем сущности робота и статуса задачи)
|
||||||
|
var robotEntity = await _robotRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(r => r.Code == (int)robot, cancellationToken);
|
||||||
|
|
||||||
|
var taskStatusEntity = await _taskStatusRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(t => t.Code == (int)task, cancellationToken);
|
||||||
|
|
||||||
|
|
||||||
|
if (robotEntity == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Робот с кодом {RobotCode} не найден в БД", robot);
|
||||||
|
throw new AppValidationException($"Робот с кодом {(int)robot} не найден");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskStatusEntity == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Статус задачи с кодом {TaskCode} не найден в БД", task);
|
||||||
|
throw new AppValidationException($"Статус задачи с кодом {(int)task} не найден");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Маппинг и сборка результирующего DTO
|
||||||
|
var result = new RobotTaskDetailsResult
|
||||||
|
{
|
||||||
|
Robot = _mapper.Map<RobotResult>(robotEntity),
|
||||||
|
Task = _mapper.Map<RobotTaskStatusResult>(taskStatusEntity),
|
||||||
|
Details = groupedDetails
|
||||||
|
.Select(t => new RobotTaskGroupDetailsResult
|
||||||
|
{
|
||||||
|
JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup),
|
||||||
|
TemplatesCount = t.TemplatesCount
|
||||||
|
})
|
||||||
|
.OrderBy(d => d.JobGroup.GroupName)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using PARR.Domain.DTOs.RobotTaskDetails;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotTaskDetailsServices.Interfaces
|
||||||
|
{
|
||||||
|
public interface IRobotTaskDetailsService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Список групп работ по заданиям робота
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robot"></param>
|
||||||
|
/// <param name="task"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.TemplateRepositories;
|
||||||
|
using PARR.Core.Services.RobotTaskRobotStatus.Interfaces;
|
||||||
|
using PARR.Domain.DTOs.RobotTaskRobotStatus;
|
||||||
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
using PARR.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotTaskRobotStatus.Implemetations
|
||||||
|
{
|
||||||
|
internal class RobotTaskRobotStatusService : IRobotTaskRobotStatusService
|
||||||
|
{
|
||||||
|
private readonly ILogger<RobotTaskRobotStatusService> _logger;
|
||||||
|
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
|
||||||
|
private readonly IRobotHistoryRepository _robotHistoryRepository;
|
||||||
|
private readonly ITemplateRenamePendingRepository _templateRenamePendingRepository;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public RobotTaskRobotStatusService(
|
||||||
|
ILogger<RobotTaskRobotStatusService> logger,
|
||||||
|
IRobotConfigurationRepository robotConfigurationRepository,
|
||||||
|
IRobotHistoryRepository robotHistoryRepository,
|
||||||
|
ITemplateRenamePendingRepository templateRenamePendingRepository,
|
||||||
|
IMapper mapper
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_robotConfigurationRepository = robotConfigurationRepository;
|
||||||
|
_robotHistoryRepository = robotHistoryRepository;
|
||||||
|
_templateRenamePendingRepository = templateRenamePendingRepository;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<RobotConfigurationResult> ChangeStatusAsync(ChangeRobotStatus request)
|
||||||
|
{
|
||||||
|
var config = await _robotConfigurationRepository.Get()
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == request.TaskId);
|
||||||
|
|
||||||
|
if (config == null)
|
||||||
|
throw new NotFoundException($"Не найдено задание с id: {request.TaskId}");
|
||||||
|
|
||||||
|
// изменение статуса робота
|
||||||
|
_robotConfigurationRepository.ChangeRobotStatus(request.RobotStatusCode, config);
|
||||||
|
|
||||||
|
// если успех, изменяем статус задания на успех
|
||||||
|
if (request.RobotStatusCode == RobotStatusEnum.Complete)
|
||||||
|
{
|
||||||
|
_robotConfigurationRepository.ChangeTaskStatus(TaskStatusEnum.Ok, config);
|
||||||
|
// Тут нужно посмотреть, если этот шаблон был на переименование, удалить у него старое название, так как он успешно переименовался
|
||||||
|
if (config.RobotCode == (int)RobotsEnum.TemplateOrder)
|
||||||
|
{
|
||||||
|
var renaming = await _templateRenamePendingRepository.Get().FirstOrDefaultAsync(t => t.TemplateId == config.TemplateId);
|
||||||
|
if (renaming != null)
|
||||||
|
{
|
||||||
|
// Удаляем
|
||||||
|
_templateRenamePendingRepository.Remove(renaming);
|
||||||
|
_logger.LogInformation("Шаблон {TemplateId} успешно переименован. Запись TemplateRenamePending удалена.", config.TemplateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _robotConfigurationRepository.CommitAsync())
|
||||||
|
throw new DbErrorException("Ошибка при сохранении в БД");
|
||||||
|
|
||||||
|
|
||||||
|
//записываем в лог робота
|
||||||
|
if (request.RobotStatusCode == RobotStatusEnum.InProgress || request.RobotStatusCode == RobotStatusEnum.Complete)
|
||||||
|
{
|
||||||
|
var historyLevel = request.RobotStatusCode == RobotStatusEnum.InProgress ? RobotHistoryLevelEnum.Start : RobotHistoryLevelEnum.Complete;
|
||||||
|
|
||||||
|
var history = new RobotHistory
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
HistoryLevel = (int)historyLevel,
|
||||||
|
TaskStatusCode = config.TaskStatusCode,
|
||||||
|
RobotConfigurationId = config.Id,
|
||||||
|
RobotIp = request.RobotIp,
|
||||||
|
RobotId = request.RobotId
|
||||||
|
};
|
||||||
|
await _robotHistoryRepository.CreateAsync(history);
|
||||||
|
await _robotHistoryRepository.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var configToResponse = await _robotConfigurationRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.Robot)
|
||||||
|
.Include(t => t.TaskStatus)
|
||||||
|
.Include(t => t.RobotStatus)
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == request.TaskId);
|
||||||
|
|
||||||
|
return _mapper.Map<RobotConfigurationResult>(configToResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using PARR.Domain.DTOs.RobotTaskRobotStatus;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotTaskRobotStatus.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис по изменению статуса выполнения задания роботами
|
||||||
|
/// </summary>
|
||||||
|
public interface IRobotTaskRobotStatusService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Изменить статус выполнения задания роботом по ИД задания
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<RobotConfigurationResult> ChangeStatusAsync(ChangeRobotStatus request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,15 +26,20 @@ internal class RelationshipsShortcodeHandler : IShortcodeHandler
|
|||||||
var hasNumbered = input.Contains("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase);
|
var hasNumbered = input.Contains("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase);
|
||||||
if (!hasPlain && !hasNumbered) return input;
|
if (!hasPlain && !hasNumbered) return input;
|
||||||
|
|
||||||
var relatedNames = await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId, ct);
|
|
||||||
var result = input;
|
var result = input;
|
||||||
|
|
||||||
|
var relatedNames = await unitFilterService.GetRelatedUnitNamesAsync(template.JobId, template.UnitId, ct);
|
||||||
|
|
||||||
|
var orderedNames = relatedNames
|
||||||
|
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (hasPlain)
|
if (hasPlain)
|
||||||
result = result.Replace("%СВЯЗИ%", string.Join("\n", relatedNames), StringComparison.OrdinalIgnoreCase);
|
result = result.Replace("%СВЯЗИ%", string.Join("\n", orderedNames), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (hasNumbered)
|
if (hasNumbered)
|
||||||
{
|
{
|
||||||
var numbered = relatedNames.Select((n, i) => $"{i + 1}. {n}");
|
var numbered = orderedNames.Select((n, i) => $"{i + 1}. {n}");
|
||||||
result = result.Replace("%СВЯЗИ-ПН%", string.Join("\n", numbered), StringComparison.OrdinalIgnoreCase);
|
result = result.Replace("%СВЯЗИ-ПН%", string.Join("\n", numbered), StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
||||||
using PARR.Domain.Entities.JobEntities;
|
using PARR.Domain.Entities.JobEntities;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace PARR.Core.Services.UnitFilterService.Matchers;
|
namespace PARR.Core.Services.UnitFilterService.Matchers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сопоставляет юниты с фильтрами по атрибутам.
|
/// Сопоставляет юниты с фильтрами по атрибутам.
|
||||||
|
/// Использует двухэтапный поиск: сначала ValueId, затем UnitId.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class UnitFieldMatcher : IUnitFieldMatcher
|
internal class UnitFieldMatcher : IUnitFieldMatcher
|
||||||
{
|
{
|
||||||
private const int chunkSize = 1000;
|
private readonly IUnitRepository _unitRepository;
|
||||||
private readonly IUnitRepository unitRepository;
|
private readonly IUnitFieldValueRepository _unitFieldValueRepository;
|
||||||
private readonly ILogger<UnitFieldMatcher> logger;
|
private readonly ILogger<UnitFieldMatcher> _logger;
|
||||||
|
|
||||||
public UnitFieldMatcher(
|
public UnitFieldMatcher(
|
||||||
IUnitRepository unitRepository,
|
IUnitRepository unitRepository,
|
||||||
|
IUnitFieldValueRepository unitFieldValueRepository,
|
||||||
ILogger<UnitFieldMatcher> logger)
|
ILogger<UnitFieldMatcher> logger)
|
||||||
{
|
{
|
||||||
this.unitRepository = unitRepository;
|
_unitRepository = unitRepository;
|
||||||
this.logger = logger;
|
_unitFieldValueRepository = unitFieldValueRepository;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Guid>> MatchAsync(
|
public async Task<List<Guid>> MatchAsync(
|
||||||
@@ -32,32 +35,63 @@ internal class UnitFieldMatcher : IUnitFieldMatcher
|
|||||||
if (filters == null || filters.Count == 0)
|
if (filters == null || filters.Count == 0)
|
||||||
return new List<Guid>(unitIds);
|
return new List<Guid>(unitIds);
|
||||||
|
|
||||||
logger.LogDebug("UnitFieldMatcher: вход {UnitCount} юнитов, фильтров: {FilterCount}",
|
_logger.LogDebug("UnitFieldMatcher: вход {UnitCount} юнитов, фильтров: {FilterCount}",
|
||||||
unitIds.Count, filters.Count);
|
unitIds.Count, filters.Count);
|
||||||
|
|
||||||
var result = new List<Guid>(unitIds.Count);
|
var currentIds = new HashSet<Guid>(unitIds);
|
||||||
|
|
||||||
foreach (var chunk in unitIds.Chunk(chunkSize))
|
for (int i = 0; i < filters.Count; i++)
|
||||||
{
|
{
|
||||||
var query = unitRepository.Get().AsNoTracking()
|
var filter = filters[i];
|
||||||
.Where(u => chunk.Contains(u.Id));
|
var mask = filter.ValueMask?.Trim();
|
||||||
|
|
||||||
foreach (var fieldFilter in filters)
|
if (string.IsNullOrEmpty(mask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var inputCount = currentIds.Count;
|
||||||
|
|
||||||
|
var matchingValueIds = await _unitFieldValueRepository
|
||||||
|
.FindValueIdsByMaskAsync(mask, ct);
|
||||||
|
|
||||||
|
if (matchingValueIds.Count == 0 && !filter.IsInverse)
|
||||||
{
|
{
|
||||||
var valueMask = fieldFilter.ValueMask?.Trim();
|
sw.Stop();
|
||||||
if (string.IsNullOrEmpty(valueMask))
|
_logger.LogDebug(
|
||||||
continue;
|
"UnitFieldMatcher: фильтр #{Index} (FieldId={FieldId}, Mask='{Mask}') | Вход: {InCount}, Выход: 0 (нет значений), Время: {Ms}ms",
|
||||||
|
i + 1, filter.FieldId, mask, inputCount, sw.ElapsedMilliseconds);
|
||||||
query = unitRepository.GetUnitByFieldAndValue(
|
currentIds.Clear();
|
||||||
query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse);
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunkResult = await query.Select(u => u.Id).ToListAsync(ct);
|
// Шаг 2: Найти/исключить юниты по ValueId
|
||||||
result.AddRange(chunkResult);
|
if (filter.IsInverse)
|
||||||
|
{
|
||||||
|
var unitsToExclude = await _unitRepository
|
||||||
|
.FindUnitIdsByValueIdsAsync(currentIds.ToList(), filter.FieldId, matchingValueIds, ct);
|
||||||
|
currentIds.ExceptWith(unitsToExclude);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var unitsToKeep = await _unitRepository
|
||||||
|
.FindUnitIdsByValueIdsAsync(currentIds.ToList(), filter.FieldId, matchingValueIds, ct);
|
||||||
|
currentIds.IntersectWith(unitsToKeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
_logger.LogDebug(
|
||||||
|
"UnitFieldMatcher: фильтр #{Index} (FieldId={FieldId}, Mask='{Mask}', Inverse={IsInverse}, Values={ValCount}) | Вход: {InCount}, Выход: {OutCount}, Время: {Ms}ms",
|
||||||
|
i + 1, filter.FieldId, mask, filter.IsInverse, matchingValueIds.Count,
|
||||||
|
inputCount, currentIds.Count, sw.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
if (currentIds.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("UnitFieldMatcher: прерывание на фильтре #{Index} (0 юнитов)", i + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("UnitFieldMatcher: выход {UnitCount} юнитов", result.Count);
|
_logger.LogDebug("UnitFieldMatcher: итоговый выход {UnitCount} юнитов", currentIds.Count);
|
||||||
|
return currentIds.ToList();
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using PARR.Core.Repositories.Interfaces.Unit;
|
|||||||
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
||||||
using PARR.Core.Services.UnitFilterService.Models;
|
using PARR.Core.Services.UnitFilterService.Models;
|
||||||
using PARR.Domain.Entities.JobEntities;
|
using PARR.Domain.Entities.JobEntities;
|
||||||
|
using PARR.Domain.Entities.Unit;
|
||||||
|
|
||||||
namespace PARR.Core.Services.UnitFilterService.Matchers;
|
namespace PARR.Core.Services.UnitFilterService.Matchers;
|
||||||
|
|
||||||
@@ -238,9 +239,9 @@ internal class UnitRelationshipMatcher : IUnitRelationshipMatcher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обрабатывает маску LIKE для корректной работы с SQL
|
/// Нормализует пользовательскую маску в формат, совместимый с PostgreSQL ILIKE.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string NormalizeLikeMask(string valueMask)
|
internal static string NormalizeLikeMask(string valueMask)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(valueMask))
|
if (string.IsNullOrWhiteSpace(valueMask))
|
||||||
return valueMask;
|
return valueMask;
|
||||||
@@ -258,4 +259,62 @@ internal class UnitRelationshipMatcher : IUnitRelationshipMatcher
|
|||||||
else
|
else
|
||||||
return valueMask;
|
return valueMask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет соответствие значения маске в формате ILIKE.
|
||||||
|
/// Эмулирует поведение PostgreSQL ILIKE для использования в C#-коде.
|
||||||
|
/// Регистронезависима.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool MatchesLikeMask(string value, string mask)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(mask))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
bool startsWithWildcard = mask.StartsWith('%');
|
||||||
|
bool endsWithWildcard = mask.EndsWith('%');
|
||||||
|
var core = mask.Trim('%');
|
||||||
|
|
||||||
|
if (startsWithWildcard && endsWithWildcard)
|
||||||
|
return value.Contains(core, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (endsWithWildcard)
|
||||||
|
return value.StartsWith(core, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (startsWithWildcard)
|
||||||
|
return value.EndsWith(core, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return value.Equals(core, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, проходит ли один target-юнит один RelationshipFilter.
|
||||||
|
/// Единая точка истины для UnitRelationshipMatcher и GetRelatedUnitNamesAsync.
|
||||||
|
/// Учитывает IsInverse. Не учитывает IsFullMatch (это ответственность вызывающего кода).
|
||||||
|
/// </summary>
|
||||||
|
internal static bool TargetPassesFilter(
|
||||||
|
IReadOnlyList<UnitInValue> unitValues,
|
||||||
|
JobRelationshipFilter rf)
|
||||||
|
{
|
||||||
|
var normalizedMask = NormalizeLikeMask(rf.ValueMask?.Trim() ?? string.Empty);
|
||||||
|
if (string.IsNullOrEmpty(normalizedMask))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var matchingValues = unitValues
|
||||||
|
.Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
bool hasMatch;
|
||||||
|
if (!matchingValues.Any())
|
||||||
|
{
|
||||||
|
hasMatch = rf.IsInverse;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hasMatch = matchingValues.Any(uv => MatchesLikeMask(uv.Value!.Value!, normalizedMask));
|
||||||
|
if (rf.IsInverse)
|
||||||
|
hasMatch = !hasMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasMatch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using PARR.Core.Common.Interfaces;
|
using PARR.Core.Common.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces.JobRepositories;
|
using PARR.Core.Repositories.Interfaces.JobRepositories;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.UnitFilterService.Matchers;
|
||||||
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
||||||
using PARR.Core.Services.UnitFilterService.Models;
|
using PARR.Core.Services.UnitFilterService.Models;
|
||||||
using PARR.Core.Services.UnitService.Interfaces;
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
@@ -84,8 +85,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
public async Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(
|
public async Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(
|
||||||
Job job,
|
Job job,
|
||||||
int? takeCount = null,
|
int? takeCount = null,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
if (job.Group == null)
|
if (job.Group == null)
|
||||||
throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group");
|
throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group");
|
||||||
@@ -97,13 +97,9 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
var totalStopwatch = Stopwatch.StartNew();
|
var totalStopwatch = Stopwatch.StartNew();
|
||||||
logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count);
|
logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count);
|
||||||
|
|
||||||
// Собираем все контексты юнитов, прошедших фильтрацию
|
|
||||||
var allFilteredContexts = new List<UnitFilterMatchResult>();
|
var allFilteredContexts = new List<UnitFilterMatchResult>();
|
||||||
|
|
||||||
// Преобразуем в список для индексации
|
|
||||||
var unitFiltersList = job.UnitFilters.ToList();
|
var unitFiltersList = job.UnitFilters.ToList();
|
||||||
|
|
||||||
// Этап 1: Применение основных фильтров (Field, Relationship) на уровне SQL
|
|
||||||
for (int i = 0; i < unitFiltersList.Count; i++)
|
for (int i = 0; i < unitFiltersList.Count; i++)
|
||||||
{
|
{
|
||||||
var filter = unitFiltersList[i];
|
var filter = unitFiltersList[i];
|
||||||
@@ -113,14 +109,21 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
{
|
{
|
||||||
logger.LogDebug("Применение фильтра #{Index} (Id={FilterId})", i + 1, filter.Id);
|
logger.LogDebug("Применение фильтра #{Index} (Id={FilterId})", i + 1, filter.Id);
|
||||||
|
|
||||||
// 1. Найти ID юнитов по UnitFilter
|
// 1. Resolve
|
||||||
|
var resolveSw = Stopwatch.StartNew();
|
||||||
var initialUnitIds = await nameResolver.ResolveAsync(filter.UnitFilter, cancellationToken);
|
var initialUnitIds = await nameResolver.ResolveAsync(filter.UnitFilter, cancellationToken);
|
||||||
|
resolveSw.Stop();
|
||||||
|
|
||||||
if (!initialUnitIds.Any())
|
if (!initialUnitIds.Any())
|
||||||
{
|
{
|
||||||
logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов)", i + 1);
|
logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов после Resolve, {ResolveMs}ms)",
|
||||||
|
i + 1, resolveSw.ElapsedMilliseconds);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Фильтр #{Index}: Resolve вернул {Count} юнитов за {Ms}ms",
|
||||||
|
i + 1, initialUnitIds.Count, resolveSw.ElapsedMilliseconds);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (initialUnitIds.Contains(debugTargetUnitId))
|
if (initialUnitIds.Contains(debugTargetUnitId))
|
||||||
{
|
{
|
||||||
@@ -128,14 +131,19 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 2. Применить FieldFilters
|
// 2. FieldFilters
|
||||||
var fieldStopwatch = Stopwatch.StartNew();
|
var fieldSw = Stopwatch.StartNew();
|
||||||
//var fieldFilteredIds = await ApplyFieldFiltersOnDbAsync(initialUnitIds, filter.FieldFilters, cancellationToken);
|
|
||||||
var fieldFilteredIds = await unitFieldMatcher.MatchAsync(initialUnitIds, filter.FieldFilters, cancellationToken);
|
var fieldFilteredIds = await unitFieldMatcher.MatchAsync(initialUnitIds, filter.FieldFilters, cancellationToken);
|
||||||
|
fieldSw.Stop();
|
||||||
|
|
||||||
|
logger.LogDebug("Фильтр #{Index}: FieldMatcher вернул {Count} юнитов за {Ms}ms",
|
||||||
|
i + 1, fieldFilteredIds.Count, fieldSw.ElapsedMilliseconds);
|
||||||
|
|
||||||
if (!fieldFilteredIds.Any())
|
if (!fieldFilteredIds.Any())
|
||||||
{
|
{
|
||||||
logger.LogDebug("Фильтр #{Index}: 0 юнитов после FieldFilters", i + 1);
|
filterStopwatch.Stop();
|
||||||
|
logger.LogDebug("Фильтр #{Index}: завершён (0 юнитов). [Resolve: {R}ms, Field: {F}ms, Total: {T}ms]",
|
||||||
|
i + 1, resolveSw.ElapsedMilliseconds, fieldSw.ElapsedMilliseconds, filterStopwatch.ElapsedMilliseconds);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,16 +154,24 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 3. Применить RelationshipFilters
|
// 3. RelationshipFilters
|
||||||
var relStopwatch = Stopwatch.StartNew();
|
var relSw = Stopwatch.StartNew();
|
||||||
var relationshipFilteredContexts = await unitRelationshipMatcher.MatchAsync(
|
var relationshipFilteredContexts = await unitRelationshipMatcher.MatchAsync(
|
||||||
fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
|
fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
|
||||||
|
relSw.Stop();
|
||||||
|
|
||||||
if (!relationshipFilteredContexts.Any())
|
filterStopwatch.Stop();
|
||||||
{
|
allFilteredContexts.AddRange(relationshipFilteredContexts);
|
||||||
logger.LogDebug("Фильтр #{Index}: 0 юнитов после RelationshipFilters", i + 1);
|
|
||||||
continue;
|
logger.LogDebug(
|
||||||
}
|
"Фильтр #{Index}: добавлено {Count} юнитов. Всего: {Total}. [Resolve: {R}ms, Field: {F}ms, Rel: {Rel}ms, Total: {T}ms]",
|
||||||
|
i + 1,
|
||||||
|
relationshipFilteredContexts.Count,
|
||||||
|
allFilteredContexts.Count,
|
||||||
|
resolveSw.ElapsedMilliseconds,
|
||||||
|
fieldSw.ElapsedMilliseconds,
|
||||||
|
relSw.ElapsedMilliseconds,
|
||||||
|
filterStopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||||
@@ -165,25 +181,13 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
|
debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Добавляем отфильтрованные контексты в общий набор
|
|
||||||
allFilteredContexts.AddRange(relationshipFilteredContexts);
|
|
||||||
|
|
||||||
filterStopwatch.Stop();
|
|
||||||
logger.LogDebug(
|
|
||||||
"Фильтр #{Index}: добавлено {Count} юнитов. Всего: {Total}. [Field: {F}ms, Rel: {R}ms, Total: {T}ms]",
|
|
||||||
i + 1,
|
|
||||||
relationshipFilteredContexts.Count,
|
|
||||||
allFilteredContexts.Count,
|
|
||||||
fieldStopwatch.ElapsedMilliseconds,
|
|
||||||
relStopwatch.ElapsedMilliseconds,
|
|
||||||
filterStopwatch.ElapsedMilliseconds
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при обработке фильтра {FilterId} для Job {JobId}",
|
filterStopwatch.Stop();
|
||||||
filter.Id, job.Id);
|
logger.LogError(ex, "Ошибка при обработке фильтра #{Index} (Id={FilterId}) для Job {JobId}. Время до ошибки: {Ms}ms",
|
||||||
|
i + 1, filter.Id, job.Id, filterStopwatch.ElapsedMilliseconds);
|
||||||
|
throw; // КРИТИЧНО: прерываем выполнение, чтобы не маскировать проблему
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +201,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
ValidChildIds = new HashSet<Guid>(g.SelectMany(c => c.ValidChildIds))
|
ValidChildIds = new HashSet<Guid>(g.SelectMany(c => c.ValidChildIds))
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
logger.LogInformation("Этап базовой фильтрации завершён: собрано {UnitCount} уникальных юнитов", mergedContexts.Count);
|
logger.LogInformation("Этап базовой фильтрации завершён: собрано {UnitCount} уникальных юнитов", mergedContexts.Count);
|
||||||
|
|
||||||
// Этап 2: Применение Umbrella-фильтра
|
// Этап 2: Применение Umbrella-фильтра
|
||||||
@@ -219,6 +224,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
|
private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await jobRepository
|
return await jobRepository
|
||||||
@@ -231,7 +237,8 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<string>> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId, CancellationToken cancellationToken = default)
|
public async Task<List<string>> GetRelatedUnitNamesAsync(
|
||||||
|
Guid jobId, Guid unitId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId);
|
logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId);
|
||||||
|
|
||||||
@@ -246,71 +253,47 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
throw new ArgumentException($"Job {jobId} не найден.", nameof(jobId));
|
throw new ArgumentException($"Job {jobId} не найден.", nameof(jobId));
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("Найден Job: {JobName}. Количество UnitFilters: {FilterCount}", job.Name, job.UnitFilters.Count());
|
// Единая точка загрузки связей
|
||||||
|
var allRelatedUnitIds = await unitInUnitRepository
|
||||||
|
.GetRelatedUnitIdsAsync(unitId, cancellationToken);
|
||||||
|
|
||||||
|
if (!allRelatedUnitIds.Any())
|
||||||
|
return new List<string>();
|
||||||
|
|
||||||
|
// Загружаем значения всех связанных юнитов одним запросом
|
||||||
|
var allUnitValues = await unitInValueRepository.GetByUnitIdsAsync(allRelatedUnitIds);
|
||||||
|
var valuesByUnit = allUnitValues
|
||||||
|
.GroupBy(uv => uv.UnitId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var filter in job.UnitFilters)
|
foreach (var filter in job.UnitFilters)
|
||||||
{
|
{
|
||||||
if (!filter.RelationshipFilters.Any()) continue;
|
var relFilters = filter.RelationshipFilters
|
||||||
|
.Where(rf => !string.IsNullOrWhiteSpace(rf.ValueMask?.Trim()))
|
||||||
logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count());
|
|
||||||
|
|
||||||
// Получить все связи для юнита
|
|
||||||
var parentLinks = await unitInUnitRepository.GetByChildIdAsync(unitId);
|
|
||||||
var childLinks = await unitInUnitRepository.GetByParentIdAsync(unitId);
|
|
||||||
|
|
||||||
// Собрать все UnitId, участвующие в связях
|
|
||||||
var allRelatedUnitIds = parentLinks
|
|
||||||
.Select(l => l.ParentUnitId)
|
|
||||||
.Concat(childLinks.Select(l => l.ChildUnitId))
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (!allRelatedUnitIds.Any()) continue;
|
if (!relFilters.Any()) continue;
|
||||||
|
|
||||||
// Получить значения для всех связанных юнитов
|
// Проверяем каждый связанный юнит через единую точку проверки
|
||||||
var allUnitValues = await unitInValueRepository.GetByUnitIdsAsync(allRelatedUnitIds);
|
var passedUnitIds = new HashSet<Guid>();
|
||||||
|
|
||||||
// Сгруппировать значения по 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)
|
foreach (var relatedUnitId in allRelatedUnitIds)
|
||||||
{
|
{
|
||||||
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
|
var unitValues = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
|
||||||
{
|
|
||||||
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
|
|
||||||
|
|
||||||
var matchingValues = values
|
// Все фильтры должны пройти (AND между фильтрами в рамках одного UnitFilter)
|
||||||
.Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null)
|
bool passesAll = relFilters.All(rf =>
|
||||||
.ToList();
|
UnitRelationshipMatcher.TargetPassesFilter(unitValues, rf));
|
||||||
|
|
||||||
if (!matchingValues.Any())
|
if (passesAll)
|
||||||
{
|
passedUnitIds.Add(relatedUnitId);
|
||||||
return rf.IsInverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (rf.IsInverse)
|
|
||||||
hasMatch = !hasMatch;
|
|
||||||
|
|
||||||
return hasMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (passesAllFilters)
|
|
||||||
matchingUnitIds.Add(relatedUnitId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchingUnitIds.Any())
|
if (passedUnitIds.Any())
|
||||||
{
|
{
|
||||||
// Используем кэширующий сервис вместо прямого запроса к БД
|
var cachedUnits = await unitService.GetWithCachingAsync(passedUnitIds);
|
||||||
var cachedUnits = await unitService.GetWithCachingAsync(matchingUnitIds);
|
|
||||||
var names = cachedUnits.Values
|
var names = cachedUnits.Values
|
||||||
.Select(u => u.Name)
|
.Select(u => u.Name)
|
||||||
.Where(n => !string.IsNullOrEmpty(n));
|
.Where(n => !string.IsNullOrEmpty(n));
|
||||||
@@ -318,6 +301,8 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.ToList();
|
return result
|
||||||
|
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.UnitFilterService.Matchers;
|
||||||
|
using PARR.Domain.Entities.JobEntities;
|
||||||
|
|
||||||
|
namespace PARR.Core.Tests.Services.UnitFilterService.Matchers
|
||||||
|
{
|
||||||
|
public class UnitFieldMatcherTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IUnitRepository> _unitRepoMock;
|
||||||
|
private readonly Mock<IUnitFieldValueRepository> _fieldValueRepoMock;
|
||||||
|
private readonly UnitFieldMatcher _sut;
|
||||||
|
|
||||||
|
public UnitFieldMatcherTests()
|
||||||
|
{
|
||||||
|
_unitRepoMock = new Mock<IUnitRepository>();
|
||||||
|
_fieldValueRepoMock = new Mock<IUnitFieldValueRepository>();
|
||||||
|
var logger = NullLoggerFactory.Instance.CreateLogger<UnitFieldMatcher>();
|
||||||
|
_sut = new UnitFieldMatcher(
|
||||||
|
_unitRepoMock.Object,
|
||||||
|
_fieldValueRepoMock.Object,
|
||||||
|
logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Если фильтры не заданы, возвращается исходный набор юнитов без изменений.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_NoFilters_ReturnsAllUnitIds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unitIds = new List<Guid> { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, Enumerable.Empty<JobFieldFilter>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(unitIds.Count, result.Count);
|
||||||
|
Assert.Equal(unitIds, result);
|
||||||
|
_fieldValueRepoMock.Verify(
|
||||||
|
r => r.FindValueIdsByMaskAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Прямой фильтр оставляет только юниты, имеющие совпадающие значения.
|
||||||
|
/// Проверяется логика IntersectWith.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_SingleDirectFilter_IntersectsWithMatchingUnits()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unit1 = Guid.NewGuid();
|
||||||
|
var unit2 = Guid.NewGuid();
|
||||||
|
var unit3 = Guid.NewGuid();
|
||||||
|
var unitIds = new List<Guid> { unit1, unit2, unit3 };
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var valueId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var filters = new List<JobFieldFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = fieldId, ValueMask = "коммутатор", IsInverse = false }
|
||||||
|
};
|
||||||
|
|
||||||
|
_fieldValueRepoMock
|
||||||
|
.Setup(r => r.FindValueIdsByMaskAsync("коммутатор", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { valueId });
|
||||||
|
|
||||||
|
_unitRepoMock
|
||||||
|
.Setup(r => r.FindUnitIdsByValueIdsAsync(
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
fieldId,
|
||||||
|
It.Is<IReadOnlyList<Guid>>(v => v.Contains(valueId)),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { unit1, unit3 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(unit1, result);
|
||||||
|
Assert.Contains(unit3, result);
|
||||||
|
Assert.DoesNotContain(unit2, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инверсный фильтр исключает юниты, имеющие совпадающие значения.
|
||||||
|
/// Проверяется логика ExceptWith.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_SingleInverseFilter_ExcludesMatchingUnits()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unit1 = Guid.NewGuid();
|
||||||
|
var unit2 = Guid.NewGuid();
|
||||||
|
var unit3 = Guid.NewGuid();
|
||||||
|
var unitIds = new List<Guid> { unit1, unit2, unit3 };
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var valueId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var filters = new List<JobFieldFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = fieldId, ValueMask = "1", IsInverse = true }
|
||||||
|
};
|
||||||
|
|
||||||
|
_fieldValueRepoMock
|
||||||
|
.Setup(r => r.FindValueIdsByMaskAsync("1", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { valueId });
|
||||||
|
|
||||||
|
_unitRepoMock
|
||||||
|
.Setup(r => r.FindUnitIdsByValueIdsAsync(
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
fieldId,
|
||||||
|
It.Is<IReadOnlyList<Guid>>(v => v.Contains(valueId)),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { unit2 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(unit1, result);
|
||||||
|
Assert.Contains(unit3, result);
|
||||||
|
Assert.DoesNotContain(unit2, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Несколько фильтров применяются последовательно (AND).
|
||||||
|
/// Каждый следующий фильтр сужает набор, полученный от предыдущего.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_MultipleFilters_AppliesSequentially()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unit1 = Guid.NewGuid();
|
||||||
|
var unit2 = Guid.NewGuid();
|
||||||
|
var unit3 = Guid.NewGuid();
|
||||||
|
var unit4 = Guid.NewGuid();
|
||||||
|
var unitIds = new List<Guid> { unit1, unit2, unit3, unit4 };
|
||||||
|
|
||||||
|
var fieldId1 = Guid.NewGuid();
|
||||||
|
var fieldId2 = Guid.NewGuid();
|
||||||
|
var valueId1 = Guid.NewGuid();
|
||||||
|
var valueId2 = Guid.NewGuid();
|
||||||
|
|
||||||
|
var filters = new List<JobFieldFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = fieldId1, ValueMask = "СХД", IsInverse = false },
|
||||||
|
new() { FieldId = fieldId2, ValueMask = "коммутатор", IsInverse = false }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Первый фильтр: значения найдены для unit1, unit2, unit3
|
||||||
|
_fieldValueRepoMock
|
||||||
|
.Setup(r => r.FindValueIdsByMaskAsync("СХД", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { valueId1 });
|
||||||
|
_unitRepoMock
|
||||||
|
.Setup(r => r.FindUnitIdsByValueIdsAsync(
|
||||||
|
It.Is<IReadOnlyList<Guid>>(ids => ids.Count == 4),
|
||||||
|
fieldId1,
|
||||||
|
It.Is<IReadOnlyList<Guid>>(v => v.Contains(valueId1)),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { unit1, unit2, unit3 });
|
||||||
|
|
||||||
|
// Второй фильтр: из оставшихся 3 юнитов значение найдено для unit1 и unit3
|
||||||
|
_fieldValueRepoMock
|
||||||
|
.Setup(r => r.FindValueIdsByMaskAsync("коммутатор", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { valueId2 });
|
||||||
|
_unitRepoMock
|
||||||
|
.Setup(r => r.FindUnitIdsByValueIdsAsync(
|
||||||
|
It.Is<IReadOnlyList<Guid>>(ids => ids.Count == 3),
|
||||||
|
fieldId2,
|
||||||
|
It.Is<IReadOnlyList<Guid>>(v => v.Contains(valueId2)),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { unit1, unit3 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(unit1, result);
|
||||||
|
Assert.Contains(unit3, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Если прямой фильтр не нашёл ни одного значения, результат пуст.
|
||||||
|
/// Проверяется раннее прерывание.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_NoMatchingValues_DirectFilter_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unitIds = new List<Guid> { Guid.NewGuid(), Guid.NewGuid() };
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var filters = new List<JobFieldFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = fieldId, ValueMask = "несуществующее", IsInverse = false }
|
||||||
|
};
|
||||||
|
|
||||||
|
_fieldValueRepoMock
|
||||||
|
.Setup(r => r.FindValueIdsByMaskAsync("несуществующее", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result);
|
||||||
|
_unitRepoMock.Verify(
|
||||||
|
r => r.FindUnitIdsByValueIdsAsync(
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
It.IsAny<Guid>(),
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Если инверсный фильтр не нашёл ни одного значения, все юниты проходят.
|
||||||
|
/// Исключать нечего, поэтому FindUnitIdsByValueIdsAsync не вызывается.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_NoMatchingValues_InverseFilter_ReturnsAll()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unit1 = Guid.NewGuid();
|
||||||
|
var unit2 = Guid.NewGuid();
|
||||||
|
var unitIds = new List<Guid> { unit1, unit2 };
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var filters = new List<JobFieldFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = fieldId, ValueMask = "несуществующее", IsInverse = true }
|
||||||
|
};
|
||||||
|
|
||||||
|
_fieldValueRepoMock
|
||||||
|
.Setup(r => r.FindValueIdsByMaskAsync("несуществующее", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(unit1, result);
|
||||||
|
Assert.Contains(unit2, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Фильтр с пустой маской пропускается без вызова репозиториев.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_EmptyMask_SkipsFilter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unitIds = new List<Guid> { Guid.NewGuid(), Guid.NewGuid() };
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var filters = new List<JobFieldFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = fieldId, ValueMask = " ", IsInverse = false }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(unitIds.Count, result.Count);
|
||||||
|
_fieldValueRepoMock.Verify(
|
||||||
|
r => r.FindValueIdsByMaskAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Если после одного из фильтров набор стал пустым,
|
||||||
|
/// последующие фильтры не выполняются.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_EarlyBreak_WhenNoUnitsLeft()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unit1 = Guid.NewGuid();
|
||||||
|
var unitIds = new List<Guid> { unit1 };
|
||||||
|
|
||||||
|
var fieldId1 = Guid.NewGuid();
|
||||||
|
var fieldId2 = Guid.NewGuid();
|
||||||
|
var valueId1 = Guid.NewGuid();
|
||||||
|
|
||||||
|
var filters = new List<JobFieldFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = fieldId1, ValueMask = "СХД", IsInverse = false },
|
||||||
|
new() { FieldId = fieldId2, ValueMask = "коммутатор", IsInverse = false }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Первый фильтр возвращает пустой результат
|
||||||
|
_fieldValueRepoMock
|
||||||
|
.Setup(r => r.FindValueIdsByMaskAsync("СХД", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid> { valueId1 });
|
||||||
|
_unitRepoMock
|
||||||
|
.Setup(r => r.FindUnitIdsByValueIdsAsync(
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
fieldId1,
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Guid>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result);
|
||||||
|
|
||||||
|
// Второй фильтр не должен вызываться
|
||||||
|
_fieldValueRepoMock.Verify(
|
||||||
|
r => r.FindValueIdsByMaskAsync("коммутатор", It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.UnitFilterService.Matchers;
|
||||||
|
using PARR.DAL.Context;
|
||||||
|
using PARR.Domain.Entities.JobEntities;
|
||||||
|
using PARR.Domain.Entities.Unit;
|
||||||
|
|
||||||
|
namespace PARR.Core.Tests.Services.UnitFilterService.Matchers
|
||||||
|
{
|
||||||
|
public class UnitRelationshipMatcherTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IUnitInUnitRepository> _unitInUnitRepoMock;
|
||||||
|
private readonly Mock<IUnitInValueRepository> _unitInValueRepoMock;
|
||||||
|
private readonly UnitRelationshipMatcher _sut;
|
||||||
|
|
||||||
|
public UnitRelationshipMatcherTests()
|
||||||
|
{
|
||||||
|
_unitInUnitRepoMock = new Mock<IUnitInUnitRepository>();
|
||||||
|
_unitInValueRepoMock = new Mock<IUnitInValueRepository>();
|
||||||
|
var logger = NullLoggerFactory.Instance.CreateLogger<UnitRelationshipMatcher>();
|
||||||
|
_sut = new UnitRelationshipMatcher(
|
||||||
|
_unitInUnitRepoMock.Object,
|
||||||
|
_unitInValueRepoMock.Object,
|
||||||
|
logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт юнит со значением поля и добавляет его в контекст.
|
||||||
|
/// </summary>
|
||||||
|
private static Unit BuildUnit(Guid id, string name, Guid fieldId, string fieldValue)
|
||||||
|
{
|
||||||
|
var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue };
|
||||||
|
return new Unit
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Name = name,
|
||||||
|
UnitValues = new List<UnitInValue>
|
||||||
|
{
|
||||||
|
new UnitInValue
|
||||||
|
{
|
||||||
|
UnitId = id,
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueId = fieldVal.Id,
|
||||||
|
Value = fieldVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает моки репозиториев для работы с InMemory-контекстом.
|
||||||
|
/// GetMatchingTargetIds эмулирует ILike через Contains с обрезкой символов '%'.
|
||||||
|
/// </summary>
|
||||||
|
private void SetupMocks(DataContext context)
|
||||||
|
{
|
||||||
|
_unitInUnitRepoMock
|
||||||
|
.Setup(r => r.Get())
|
||||||
|
.Returns(context.UnitInUnits.AsQueryable());
|
||||||
|
|
||||||
|
_unitInValueRepoMock
|
||||||
|
.Setup(r => r.GetMatchingTargetIds(It.IsAny<Guid>(), It.IsAny<string>()))
|
||||||
|
.Returns((Guid fieldId, string mask) =>
|
||||||
|
{
|
||||||
|
var trimmed = mask.Trim('%');
|
||||||
|
return context.UnitInValues
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(uv => uv.FieldId == fieldId
|
||||||
|
&& uv.Value != null
|
||||||
|
&& uv.Value.Value != null
|
||||||
|
&& uv.Value.Value.Contains(trimmed, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(uv => uv.UnitId)
|
||||||
|
.Distinct()
|
||||||
|
.AsQueryable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Если фильтры не заданы, все юниты возвращаются как контексты без изменений.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_NoFilters_ReturnsAllUnitsAsContexts()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unit1 = Guid.NewGuid();
|
||||||
|
var unit2 = Guid.NewGuid();
|
||||||
|
var unitIds = new List<Guid> { unit1, unit2 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(unitIds, Enumerable.Empty<JobRelationshipFilter>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(result, c => c.UnitId == unit1);
|
||||||
|
Assert.Contains(result, c => c.UnitId == unit2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Если входной набор пуст, возвращается пустой список.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_EmptyUnitIds_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new() { FieldId = Guid.NewGuid(), ValueMask = "%test%", IsParent = true }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(new List<Guid>(), filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Прямой родительский фильтр (IsFullMatch=false):
|
||||||
|
/// юнит проходит, если ХОТЯ БЫ ОДИН из его родителей соответствует маске.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_ParentFilter_DirectAnyMatch_OnlyUnitsWithMatchingParentPass()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var parentGood = Guid.NewGuid();
|
||||||
|
var parentBad = Guid.NewGuid();
|
||||||
|
var unitWithGoodParent = Guid.NewGuid();
|
||||||
|
var unitWithBadParent = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(parentGood, "Parent_Good", fieldId, "VALID-TAG"),
|
||||||
|
BuildUnit(parentBad, "Parent_Bad", fieldId, "OTHER-TAG"),
|
||||||
|
BuildUnit(unitWithGoodParent, "Unit_Good", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(unitWithBadParent, "Unit_Bad", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
new UnitInUnit { ParentUnitId = parentGood, ChildUnitId = unitWithGoodParent, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentBad, ChildUnitId = unitWithBadParent, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueMask = "%VALID%",
|
||||||
|
IsParent = true,
|
||||||
|
IsInverse = false,
|
||||||
|
IsFullMatch = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(
|
||||||
|
new List<Guid> { unitWithGoodParent, unitWithBadParent }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultIds = result.Select(c => c.UnitId).ToList();
|
||||||
|
Assert.Contains(unitWithGoodParent, resultIds);
|
||||||
|
Assert.DoesNotContain(unitWithBadParent, resultIds);
|
||||||
|
|
||||||
|
var ctx = result.First(c => c.UnitId == unitWithGoodParent);
|
||||||
|
Assert.Contains(parentGood, ctx.ValidParentIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инверсный родительский фильтр (IsInverse=true, IsFullMatch=false):
|
||||||
|
/// юнит проходит, если ХОТЯ БЫ ОДИН родитель НЕ соответствует маске.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_ParentFilter_Inverse_UnitsWithOnlyForbiddenParentsExcluded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var parentForbidden = Guid.NewGuid();
|
||||||
|
var parentClean = Guid.NewGuid();
|
||||||
|
var unitWithForbiddenOnly = Guid.NewGuid();
|
||||||
|
var unitWithCleanParent = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(parentForbidden, "Parent_Forbidden", fieldId, "FORBIDDEN"),
|
||||||
|
BuildUnit(parentClean, "Parent_Clean", fieldId, "CLEAN"),
|
||||||
|
BuildUnit(unitWithForbiddenOnly, "Unit_Forbidden", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(unitWithCleanParent, "Unit_Clean", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
new UnitInUnit { ParentUnitId = parentForbidden, ChildUnitId = unitWithForbiddenOnly, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentClean, ChildUnitId = unitWithCleanParent, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueMask = "%FORBIDDEN%",
|
||||||
|
IsParent = true,
|
||||||
|
IsInverse = true,
|
||||||
|
IsFullMatch = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(
|
||||||
|
new List<Guid> { unitWithForbiddenOnly, unitWithCleanParent }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultIds = result.Select(c => c.UnitId).ToList();
|
||||||
|
Assert.Contains(unitWithCleanParent, resultIds);
|
||||||
|
Assert.DoesNotContain(unitWithForbiddenOnly, resultIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IsFullMatch=true: юнит проходит, только если ВСЕ его родители соответствуют маске.
|
||||||
|
/// Юнит с одним "плохим" родителем исключается.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_ParentFilter_IsFullMatch_AllParentsMustPass()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var parentGood1 = Guid.NewGuid();
|
||||||
|
var parentGood2 = Guid.NewGuid();
|
||||||
|
var parentBad = Guid.NewGuid();
|
||||||
|
var unitAllGood = Guid.NewGuid();
|
||||||
|
var unitMixed = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(parentGood1, "Parent_Good1", fieldId, "Good"),
|
||||||
|
BuildUnit(parentGood2, "Parent_Good2", fieldId, "Good"),
|
||||||
|
BuildUnit(parentBad, "Parent_Bad", fieldId, "Bad"),
|
||||||
|
BuildUnit(unitAllGood, "Unit_AllGood", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(unitMixed, "Unit_Mixed", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
new UnitInUnit { ParentUnitId = parentGood1, ChildUnitId = unitAllGood, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentGood2, ChildUnitId = unitAllGood, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentGood1, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentBad, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueMask = "%Good%",
|
||||||
|
IsParent = true,
|
||||||
|
IsInverse = false,
|
||||||
|
IsFullMatch = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(
|
||||||
|
new List<Guid> { unitAllGood, unitMixed }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultIds = result.Select(c => c.UnitId).ToList();
|
||||||
|
Assert.Contains(unitAllGood, resultIds);
|
||||||
|
Assert.DoesNotContain(unitMixed, resultIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Комбинация IsInverse=true и IsFullMatch=true:
|
||||||
|
/// юнит проходит, только если НИ ОДИН родитель не соответствует маске.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_ParentFilter_InverseAndFullMatch_UnitWithAnyForbiddenParentExcluded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var parentForbidden = Guid.NewGuid();
|
||||||
|
var parentClean = Guid.NewGuid();
|
||||||
|
var unitMixed = Guid.NewGuid();
|
||||||
|
var unitAllClean = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(parentForbidden, "Parent_Forbidden", fieldId, "FORBIDDEN"),
|
||||||
|
BuildUnit(parentClean, "Parent_Clean", fieldId, "CLEAN"),
|
||||||
|
BuildUnit(unitMixed, "Unit_Mixed", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(unitAllClean, "Unit_AllClean", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
new UnitInUnit { ParentUnitId = parentForbidden, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentClean, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentClean, ChildUnitId = unitAllClean, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueMask = "%FORBIDDEN%",
|
||||||
|
IsParent = true,
|
||||||
|
IsInverse = true,
|
||||||
|
IsFullMatch = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(
|
||||||
|
new List<Guid> { unitMixed, unitAllClean }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultIds = result.Select(c => c.UnitId).ToList();
|
||||||
|
Assert.Contains(unitAllClean, resultIds);
|
||||||
|
Assert.DoesNotContain(unitMixed, resultIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дочерний фильтр (IsParent=false):
|
||||||
|
/// юнит проходит, если хотя бы один из его детей соответствует маске.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_ChildFilter_DirectMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var childValid = Guid.NewGuid();
|
||||||
|
var childInvalid = Guid.NewGuid();
|
||||||
|
var parentWithValidChild = Guid.NewGuid();
|
||||||
|
var parentWithInvalidChild = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(childValid, "Child_Valid", fieldId, "VALID"),
|
||||||
|
BuildUnit(childInvalid, "Child_Invalid", fieldId, "OTHER"),
|
||||||
|
BuildUnit(parentWithValidChild, "Parent_Valid", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(parentWithInvalidChild, "Parent_Invalid", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
new UnitInUnit { ParentUnitId = parentWithValidChild, ChildUnitId = childValid, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = parentWithInvalidChild, ChildUnitId = childInvalid, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueMask = "%VALID%",
|
||||||
|
IsParent = false,
|
||||||
|
IsInverse = false,
|
||||||
|
IsFullMatch = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(
|
||||||
|
new List<Guid> { parentWithValidChild, parentWithInvalidChild }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultIds = result.Select(c => c.UnitId).ToList();
|
||||||
|
Assert.Contains(parentWithValidChild, resultIds);
|
||||||
|
Assert.DoesNotContain(parentWithInvalidChild, resultIds);
|
||||||
|
|
||||||
|
var ctx = result.First(c => c.UnitId == parentWithValidChild);
|
||||||
|
Assert.Contains(childValid, ctx.ValidChildIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Юнит без связей исключается, если есть фильтры с непустой маской.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_UnitWithoutRelatedUnits_IsExcluded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var unitWithParent = Guid.NewGuid();
|
||||||
|
var unitOrphan = Guid.NewGuid();
|
||||||
|
var parent = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(parent, "Parent", fieldId, "VALID"),
|
||||||
|
BuildUnit(unitWithParent, "Unit_WithParent", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(unitOrphan, "Unit_Orphan", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
new UnitInUnit { ParentUnitId = parent, ChildUnitId = unitWithParent, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueMask = "%VALID%",
|
||||||
|
IsParent = true,
|
||||||
|
IsInverse = false,
|
||||||
|
IsFullMatch = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(
|
||||||
|
new List<Guid> { unitWithParent, unitOrphan }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultIds = result.Select(c => c.UnitId).ToList();
|
||||||
|
Assert.Contains(unitWithParent, resultIds);
|
||||||
|
Assert.DoesNotContain(unitOrphan, resultIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Фильтр с пустой маской пропускается: юниты не исключаются.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_EmptyMask_FilterSkipped()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var fieldId = Guid.NewGuid();
|
||||||
|
var unit1 = Guid.NewGuid();
|
||||||
|
var parent = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(parent, "Parent", fieldId, "Any"),
|
||||||
|
BuildUnit(unit1, "Unit", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
new UnitInUnit { ParentUnitId = parent, ChildUnitId = unit1, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = fieldId,
|
||||||
|
ValueMask = " ",
|
||||||
|
IsParent = true,
|
||||||
|
IsInverse = false,
|
||||||
|
IsFullMatch = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(new List<Guid> { unit1 }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(unit1, result.First().UnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Комбинация родительского и дочернего фильтров:
|
||||||
|
/// юнит должен пройти оба направления.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task MatchAsync_CombinedParentAndChildFilters_BothDirectionsApplied()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
using var context = new DataContext(dbOptions);
|
||||||
|
|
||||||
|
var parentFieldId = Guid.NewGuid();
|
||||||
|
var childFieldId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var goodParent = Guid.NewGuid();
|
||||||
|
var badParent = Guid.NewGuid();
|
||||||
|
var goodChild = Guid.NewGuid();
|
||||||
|
var badChild = Guid.NewGuid();
|
||||||
|
|
||||||
|
var unitAllGood = Guid.NewGuid();
|
||||||
|
var unitBadParent = Guid.NewGuid();
|
||||||
|
var unitBadChild = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.Units.AddRange(
|
||||||
|
BuildUnit(goodParent, "GoodParent", parentFieldId, "GOOD-PARENT"),
|
||||||
|
BuildUnit(badParent, "BadParent", parentFieldId, "BAD-PARENT"),
|
||||||
|
BuildUnit(goodChild, "GoodChild", childFieldId, "GOOD-CHILD"),
|
||||||
|
BuildUnit(badChild, "BadChild", childFieldId, "BAD-CHILD"),
|
||||||
|
BuildUnit(unitAllGood, "Unit_AllGood", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(unitBadParent, "Unit_BadParent", Guid.NewGuid(), "x"),
|
||||||
|
BuildUnit(unitBadChild, "Unit_BadChild", Guid.NewGuid(), "x")
|
||||||
|
);
|
||||||
|
|
||||||
|
context.UnitInUnits.AddRange(
|
||||||
|
// unitAllGood: хороший родитель + хороший ребёнок
|
||||||
|
new UnitInUnit { ParentUnitId = goodParent, ChildUnitId = unitAllGood, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = unitAllGood, ChildUnitId = goodChild, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
// unitBadParent: плохой родитель + хороший ребёнок
|
||||||
|
new UnitInUnit { ParentUnitId = badParent, ChildUnitId = unitBadParent, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = unitBadParent, ChildUnitId = goodChild, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
// unitBadChild: хороший родитель + плохой ребёнок
|
||||||
|
new UnitInUnit { ParentUnitId = goodParent, ChildUnitId = unitBadChild, DateCreated = DateTimeOffset.UtcNow },
|
||||||
|
new UnitInUnit { ParentUnitId = unitBadChild, ChildUnitId = badChild, DateCreated = DateTimeOffset.UtcNow }
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
SetupMocks(context);
|
||||||
|
|
||||||
|
var filters = new List<JobRelationshipFilter>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = parentFieldId,
|
||||||
|
ValueMask = "%GOOD-PARENT%",
|
||||||
|
IsParent = true,
|
||||||
|
IsInverse = false,
|
||||||
|
IsFullMatch = false
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldId = childFieldId,
|
||||||
|
ValueMask = "%GOOD-CHILD%",
|
||||||
|
IsParent = false,
|
||||||
|
IsInverse = false,
|
||||||
|
IsFullMatch = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.MatchAsync(
|
||||||
|
new List<Guid> { unitAllGood, unitBadParent, unitBadChild }, filters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultIds = result.Select(c => c.UnitId).ToList();
|
||||||
|
Assert.Contains(unitAllGood, resultIds);
|
||||||
|
Assert.DoesNotContain(unitBadParent, resultIds);
|
||||||
|
Assert.DoesNotContain(unitBadChild, resultIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,14 +21,6 @@ public class UnitFilterServiceTests
|
|||||||
{
|
{
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создаёт тестовую сущность Unit с заданным значением поля.
|
|
||||||
/// Используется для подготовки данных в тестах фильтрации.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Уникальный идентификатор юнита</param>
|
|
||||||
/// <param name="name">Имя юнита для отладки</param>
|
|
||||||
/// <param name="fieldId">Идентификатор поля, к которому привязывается значение</param>
|
|
||||||
/// <param name="fieldValue">Значение поля</param>
|
|
||||||
private static Unit BuildTestUnit(Guid id, string name, Guid fieldId, string fieldValue)
|
private static Unit BuildTestUnit(Guid id, string name, Guid fieldId, string fieldValue)
|
||||||
{
|
{
|
||||||
var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue };
|
var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue };
|
||||||
@@ -49,10 +41,6 @@ public class UnitFilterServiceTests
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создаёт тестовый объект JobGroupType с кодом Umbrella.
|
|
||||||
/// Используется для тестирования логики групповых работ типа "зонтик".
|
|
||||||
/// </summary>
|
|
||||||
private static JobGroupType BuildTestJobGroupType() => new()
|
private static JobGroupType BuildTestJobGroupType() => new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
@@ -61,10 +49,6 @@ public class UnitFilterServiceTests
|
|||||||
Description = "Test"
|
Description = "Test"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создаёт тестовый объект JobGroup с заполненными обязательными полями.
|
|
||||||
/// Используется для подготовки навигационных свойств в тестах.
|
|
||||||
/// </summary>
|
|
||||||
private static JobGroup BuildTestJobGroup() => new()
|
private static JobGroup BuildTestJobGroup() => new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
@@ -79,20 +63,12 @@ public class UnitFilterServiceTests
|
|||||||
GroupType = BuildTestJobGroupType()
|
GroupType = BuildTestJobGroupType()
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создаёт тестовый объект JobAutoControl с отключённым автоконтролем.
|
|
||||||
/// Используется для заполнения обязательного свойства в сущности Job.
|
|
||||||
/// </summary>
|
|
||||||
private static JobAutoControl BuildTestAutoControl(Guid jobId) => new()
|
private static JobAutoControl BuildTestAutoControl(Guid jobId) => new()
|
||||||
{
|
{
|
||||||
JobId = jobId,
|
JobId = jobId,
|
||||||
IsEnable = false
|
IsEnable = false
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Контейнер для моков репозиториев.
|
|
||||||
/// Упрощает передачу зависимостей в метод создания сервиса.
|
|
||||||
/// </summary>
|
|
||||||
private class RepositoryMocks
|
private class RepositoryMocks
|
||||||
{
|
{
|
||||||
public Mock<IUnitRepository> Unit { get; set; }
|
public Mock<IUnitRepository> Unit { get; set; }
|
||||||
@@ -101,7 +77,6 @@ public class UnitFilterServiceTests
|
|||||||
public Mock<IUnitFieldRepository> UnitField { get; set; }
|
public Mock<IUnitFieldRepository> UnitField { get; set; }
|
||||||
public Mock<IJobRepository> Job { get; set; }
|
public Mock<IJobRepository> Job { get; set; }
|
||||||
public Mock<IRedisCacheService> Cache { get; set; }
|
public Mock<IRedisCacheService> Cache { get; set; }
|
||||||
// Новые зависимости после рефакторинга
|
|
||||||
public Mock<IUnitService> UnitService { get; set; }
|
public Mock<IUnitService> UnitService { get; set; }
|
||||||
public Mock<IUnitFieldMatcher> FieldMatcher { get; set; }
|
public Mock<IUnitFieldMatcher> FieldMatcher { get; set; }
|
||||||
public Mock<IUnitRelationshipMatcher> RelationshipMatcher { get; set; }
|
public Mock<IUnitRelationshipMatcher> RelationshipMatcher { get; set; }
|
||||||
@@ -110,11 +85,6 @@ public class UnitFilterServiceTests
|
|||||||
public Mock<IUnitNameResolver> NameResolver { get; set; }
|
public Mock<IUnitNameResolver> NameResolver { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает базовые моки репозиториев с использованием InMemory-контекста.
|
|
||||||
/// Возвращает объект с подготовленными моками для повторного использования в тестах.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">Экземпляр DataContext с тестовыми данными</param>
|
|
||||||
private static RepositoryMocks ArrangeRepositoryMocks(DataContext context)
|
private static RepositoryMocks ArrangeRepositoryMocks(DataContext context)
|
||||||
{
|
{
|
||||||
var unitRepoMock = new Mock<IUnitRepository>();
|
var unitRepoMock = new Mock<IUnitRepository>();
|
||||||
@@ -126,16 +96,6 @@ public class UnitFilterServiceTests
|
|||||||
unitInUnitRepoMock.Setup(r => r.Get()).Returns(context.UnitInUnits.AsQueryable());
|
unitInUnitRepoMock.Setup(r => r.Get()).Returns(context.UnitInUnits.AsQueryable());
|
||||||
|
|
||||||
var unitInValueRepoMock = new Mock<IUnitInValueRepository>();
|
var unitInValueRepoMock = new Mock<IUnitInValueRepository>();
|
||||||
// Единый подход: используем контекст для поддержки IAsyncQueryProvider
|
|
||||||
// Это гарантирует корректную работу .ToListAsync() внутри сервиса
|
|
||||||
unitInValueRepoMock.Setup(r => r.GetMatchingTargetIds(It.IsAny<Guid>(), It.IsAny<string>()))
|
|
||||||
.Returns((Guid fId, string mask) =>
|
|
||||||
context.UnitInValues
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(uv => uv.FieldId == fId && uv.Value != null)
|
|
||||||
.Select(uv => uv.UnitId)
|
|
||||||
.Distinct()
|
|
||||||
.AsQueryable());
|
|
||||||
|
|
||||||
var unitFieldRepoMock = new Mock<IUnitFieldRepository>();
|
var unitFieldRepoMock = new Mock<IUnitFieldRepository>();
|
||||||
unitFieldRepoMock.Setup(r => r.Get()).Returns(new List<UnitField>().AsQueryable());
|
unitFieldRepoMock.Setup(r => r.Get()).Returns(new List<UnitField>().AsQueryable());
|
||||||
@@ -154,46 +114,60 @@ public class UnitFilterServiceTests
|
|||||||
var resultLoaderMock = new Mock<IUnitFilterResultLoader>();
|
var resultLoaderMock = new Mock<IUnitFilterResultLoader>();
|
||||||
var nameResolverMock = new Mock<IUnitNameResolver>();
|
var nameResolverMock = new Mock<IUnitNameResolver>();
|
||||||
|
|
||||||
// Настройка NameResolver: возвращаем все ID юнитов из контекста (эмуляция кэш-промаха + БД)
|
|
||||||
nameResolverMock.Setup(r => r.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
nameResolverMock.Setup(r => r.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync((string mask, CancellationToken ct) =>
|
.ReturnsAsync((string mask, CancellationToken ct) =>
|
||||||
context.Units.Select(u => u.Id).ToList());
|
context.Units.Select(u => u.Id).ToList());
|
||||||
|
|
||||||
// Настройка FieldMatcher: эмуляция SQL-фильтрации через InMemory-контекст
|
// По умолчанию FieldMatcher пропускает все юниты без изменений.
|
||||||
fieldMatcherMock.Setup(r => r.MatchAsync(It.IsAny<IReadOnlyList<Guid>>(), It.IsAny<IEnumerable<JobFieldFilter>>(), It.IsAny<CancellationToken>()))
|
// В конкретных тестах переопределяется для эмуляции фильтрации.
|
||||||
|
fieldMatcherMock.Setup(r => r.MatchAsync(
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
It.IsAny<IEnumerable<JobFieldFilter>>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobFieldFilter> filters, CancellationToken ct) =>
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobFieldFilter> filters, CancellationToken ct) =>
|
||||||
ids.ToList());
|
ids.ToList());
|
||||||
|
|
||||||
// Настройка RelationshipMatcher: возвращаем контексты без изменений
|
// По умолчанию RelationshipMatcher возвращает контексты без изменений.
|
||||||
relationshipMatcherMock.Setup(r => r.MatchAsync(It.IsAny<IReadOnlyList<Guid>>(), It.IsAny<IEnumerable<JobRelationshipFilter>>(), It.IsAny<CancellationToken>()))
|
// В конкретных тестах переопределяется для эмуляции фильтрации связей.
|
||||||
|
relationshipMatcherMock.Setup(r => r.MatchAsync(
|
||||||
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
|
It.IsAny<IEnumerable<JobRelationshipFilter>>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobRelationshipFilter> filters, CancellationToken ct) =>
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobRelationshipFilter> filters, CancellationToken ct) =>
|
||||||
ids.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList());
|
ids.Select(id => new UnitFilterMatchResult
|
||||||
|
{
|
||||||
|
UnitId = id,
|
||||||
|
ValidParentIds = new HashSet<Guid>(),
|
||||||
|
ValidChildIds = new HashSet<Guid>()
|
||||||
|
}).ToList());
|
||||||
|
|
||||||
// Настройка UmbrellaFilter: пропускаем без изменений
|
|
||||||
umbrellaFilterMock.Setup(r => r.Apply(It.IsAny<List<UnitFilterMatchResult>>(), It.IsAny<Job>()))
|
umbrellaFilterMock.Setup(r => r.Apply(It.IsAny<List<UnitFilterMatchResult>>(), It.IsAny<Job>()))
|
||||||
.Returns((List<UnitFilterMatchResult> ctx, Job j) => ctx);
|
.Returns((List<UnitFilterMatchResult> ctx, Job j) => ctx);
|
||||||
|
|
||||||
// Настройка ResultLoader: формируем DTO из контекста
|
|
||||||
resultLoaderMock.Setup(r => r.LoadAsync(It.IsAny<List<UnitFilterMatchResult>>(), It.IsAny<CancellationToken>()))
|
resultLoaderMock.Setup(r => r.LoadAsync(It.IsAny<List<UnitFilterMatchResult>>(), It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync((List<UnitFilterMatchResult> contexts, CancellationToken ct) =>
|
.ReturnsAsync((List<UnitFilterMatchResult> contexts, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var unitIds = contexts.Select(c => c.UnitId).ToHashSet();
|
var unitIds = contexts.Select(c => c.UnitId).ToHashSet();
|
||||||
var units = context.Units.Where(u => unitIds.Contains(u.Id)).ToList();
|
var units = context.Units.Where(u => unitIds.Contains(u.Id)).ToList();
|
||||||
return units.Select(u => new UnitFilterResultDto
|
return units.Select(u =>
|
||||||
{
|
{
|
||||||
Id = u.Id,
|
var ctx = contexts.First(c => c.UnitId == u.Id);
|
||||||
Name = u.Name,
|
return new UnitFilterResultDto
|
||||||
Values = u.UnitValues?.Select(v => new UnitValueDto
|
|
||||||
{
|
{
|
||||||
FieldId = v.FieldId,
|
Id = u.Id,
|
||||||
Value = v.Value?.Value
|
Name = u.Name,
|
||||||
}).ToList() ?? new List<UnitValueDto>(),
|
Values = u.UnitValues?.Select(v => new UnitValueDto
|
||||||
Parents = contexts.First(c => c.UnitId == u.Id).ValidParentIds
|
{
|
||||||
.Select(pid => new RelatedUnitDto { UnitId = pid })
|
FieldId = v.FieldId,
|
||||||
.ToList(),
|
Value = v.Value?.Value
|
||||||
Children = contexts.First(c => c.UnitId == u.Id).ValidChildIds
|
}).ToList() ?? new List<UnitValueDto>(),
|
||||||
.Select(cid => new RelatedUnitDto { UnitId = cid })
|
Parents = (ctx.ValidParentIds ?? new HashSet<Guid>())
|
||||||
.ToList()
|
.Select(pid => new RelatedUnitDto { UnitId = pid })
|
||||||
|
.ToList(),
|
||||||
|
Children = (ctx.ValidChildIds ?? new HashSet<Guid>())
|
||||||
|
.Select(cid => new RelatedUnitDto { UnitId = cid })
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,12 +188,6 @@ public class UnitFilterServiceTests
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создаёт экземпляр тестируемого сервиса (SUT — System Under Test).
|
|
||||||
/// Инкапсулирует логику конструктора для упрощения тестов.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mocks">Подготовленные моки репозиториев</param>
|
|
||||||
/// <param name="logger">Экземпляр логгера для сервиса</param>
|
|
||||||
private static Core.Services.UnitFilterService.UnitFilterService CreateSut(
|
private static Core.Services.UnitFilterService.UnitFilterService CreateSut(
|
||||||
RepositoryMocks mocks,
|
RepositoryMocks mocks,
|
||||||
ILogger<Core.Services.UnitFilterService.UnitFilterService> logger)
|
ILogger<Core.Services.UnitFilterService.UnitFilterService> logger)
|
||||||
@@ -245,8 +213,9 @@ public class UnitFilterServiceTests
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проверяет, что юниты, не прошедшие фильтрацию по полю (FieldFilter)
|
/// Проверяет, что юниты, не прошедшие фильтрацию по полю или по связям,
|
||||||
/// или по связям (RelationshipFilter), корректно исключаются из результата.
|
/// корректно исключаются из результата.
|
||||||
|
/// Логика фильтрации эмулируется через моки FieldMatcher и RelationshipMatcher.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetUnitsByJobFilterAsync_UnitsWithInvalidFieldOrRelationshipValue_AreExcluded()
|
public async Task GetUnitsByJobFilterAsync_UnitsWithInvalidFieldOrRelationshipValue_AreExcluded()
|
||||||
@@ -260,30 +229,24 @@ public class UnitFilterServiceTests
|
|||||||
var jobId = Guid.NewGuid();
|
var jobId = Guid.NewGuid();
|
||||||
var fieldId = Guid.NewGuid();
|
var fieldId = Guid.NewGuid();
|
||||||
var validParentId = Guid.NewGuid();
|
var validParentId = Guid.NewGuid();
|
||||||
var wrongParentId = Guid.NewGuid();
|
|
||||||
|
|
||||||
var unitPassesAll = Guid.NewGuid();
|
var unitPassesAll = Guid.NewGuid();
|
||||||
var unitFailsFieldFilter = Guid.NewGuid();
|
var unitFailsFieldFilter = Guid.NewGuid();
|
||||||
var unitFailsRelationshipFilter = Guid.NewGuid();
|
var unitFailsRelationshipFilter = Guid.NewGuid();
|
||||||
|
|
||||||
// Подготовка тестовых данных: юниты
|
|
||||||
context.Units.AddRange(
|
context.Units.AddRange(
|
||||||
BuildTestUnit(unitPassesAll, "Unit_PassesAll", fieldId, "Accepted Value"),
|
BuildTestUnit(unitPassesAll, "Unit_PassesAll", fieldId, "Accepted Value"),
|
||||||
BuildTestUnit(unitFailsFieldFilter, "Unit_FailsField", fieldId, "Rejected Value"),
|
BuildTestUnit(unitFailsFieldFilter, "Unit_FailsField", fieldId, "Rejected Value"),
|
||||||
BuildTestUnit(unitFailsRelationshipFilter, "Unit_FailsRel", fieldId, "Accepted Value"),
|
BuildTestUnit(unitFailsRelationshipFilter, "Unit_FailsRel", fieldId, "Accepted Value"),
|
||||||
BuildTestUnit(validParentId, "Parent_Valid", fieldId, "Valid Parent"),
|
BuildTestUnit(validParentId, "Parent_Valid", fieldId, "Valid Parent")
|
||||||
BuildTestUnit(wrongParentId, "Parent_Wrong", fieldId, "Wrong Parent")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Подготовка тестовых данных: связи между юнитами
|
|
||||||
context.UnitInUnits.AddRange(
|
context.UnitInUnits.AddRange(
|
||||||
new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitPassesAll, DateCreated = DateTimeOffset.UtcNow },
|
new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitPassesAll, DateCreated = DateTimeOffset.UtcNow }
|
||||||
new UnitInUnit { ParentUnitId = wrongParentId, ChildUnitId = unitFailsRelationshipFilter, DateCreated = DateTimeOffset.UtcNow }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
// Формируем тестовый объект Job с фильтрами
|
|
||||||
var job = new Job
|
var job = new Job
|
||||||
{
|
{
|
||||||
Id = jobId,
|
Id = jobId,
|
||||||
@@ -318,20 +281,29 @@ public class UnitFilterServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Настройка моков репозиториев
|
|
||||||
var mocks = ArrangeRepositoryMocks(context);
|
var mocks = ArrangeRepositoryMocks(context);
|
||||||
|
|
||||||
// Переопределяем мок фильтрации по полю: в реальном коде используется EF.Functions.ILike,
|
// Эмуляция FieldMatcher: пропускает только юниты со значением "Accepted Value"
|
||||||
// в тесте заменяем на прямое сравнение строк для предсказуемости
|
mocks.FieldMatcher.Setup(r => r.MatchAsync(
|
||||||
mocks.Unit.Setup(r => r.GetUnitByFieldAndValue(It.IsAny<IQueryable<Unit>>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<bool>()))
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
.Returns((IQueryable<Unit> q, Guid fId, string mask, bool inv) =>
|
It.IsAny<IEnumerable<JobFieldFilter>>(),
|
||||||
q.Where(u => u.UnitValues != null && u.UnitValues.Any(v => v.FieldId == fId && v.Value != null && v.Value.Value == "Accepted Value")));
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobFieldFilter> filters, CancellationToken ct) =>
|
||||||
|
ids.Where(id => id == unitPassesAll || id == unitFailsRelationshipFilter).ToList());
|
||||||
|
|
||||||
// ИСПРАВЛЕНО: используем запрос к контексту вместо массива.
|
// Эмуляция RelationshipMatcher: пропускает только unitPassesAll
|
||||||
// Массивный AsQueryable() не поддерживает IAsyncQueryProvider, что вызывает крах при вызове .ToListAsync() внутри сервиса.
|
mocks.RelationshipMatcher.Setup(r => r.MatchAsync(
|
||||||
// Запрос к InMemory DbSet гарантирует корректную асинхронную материализацию.
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(fieldId, It.IsAny<string>()))
|
It.IsAny<IEnumerable<JobRelationshipFilter>>(),
|
||||||
.Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable());
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobRelationshipFilter> filters, CancellationToken ct) =>
|
||||||
|
ids.Where(id => id == unitPassesAll)
|
||||||
|
.Select(id => new UnitFilterMatchResult
|
||||||
|
{
|
||||||
|
UnitId = id,
|
||||||
|
ValidParentIds = new HashSet<Guid> { validParentId },
|
||||||
|
ValidChildIds = new HashSet<Guid>()
|
||||||
|
}).ToList());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
@@ -354,8 +326,8 @@ public class UnitFilterServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проверяет, что юниты исключаются, если их единственный родительский юнит
|
/// Проверяет, что юниты исключаются, если их родитель содержит запрещённое значение (IsInverse = true).
|
||||||
/// содержит запрещённое значение тега (сценарий с IsInverse = true).
|
/// Логика фильтрации связей эмулируется через мок RelationshipMatcher.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetUnitsByJobFilterAsync_ParentWithForbiddenTag_UnitIsExcluded()
|
public async Task GetUnitsByJobFilterAsync_ParentWithForbiddenTag_UnitIsExcluded()
|
||||||
@@ -373,7 +345,6 @@ public class UnitFilterServiceTests
|
|||||||
var unitWithValidParent = Guid.NewGuid();
|
var unitWithValidParent = Guid.NewGuid();
|
||||||
var unitWithInvalidParent = Guid.NewGuid();
|
var unitWithInvalidParent = Guid.NewGuid();
|
||||||
|
|
||||||
// Подготовка тестовых данных: юниты
|
|
||||||
context.Units.AddRange(
|
context.Units.AddRange(
|
||||||
BuildTestUnit(validParentId, "Parent_Valid", tagFieldId, "ОТВ.ЭК"),
|
BuildTestUnit(validParentId, "Parent_Valid", tagFieldId, "ОТВ.ЭК"),
|
||||||
BuildTestUnit(invalidParentId, "Parent_Invalid", tagFieldId, "ПАРР-РРПТК-ОТВ.ЭК"),
|
BuildTestUnit(invalidParentId, "Parent_Invalid", tagFieldId, "ПАРР-РРПТК-ОТВ.ЭК"),
|
||||||
@@ -381,7 +352,6 @@ public class UnitFilterServiceTests
|
|||||||
BuildTestUnit(unitWithInvalidParent, "Unit_Invalid", Guid.NewGuid(), "Val")
|
BuildTestUnit(unitWithInvalidParent, "Unit_Invalid", Guid.NewGuid(), "Val")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Подготовка тестовых данных: связи между юнитами
|
|
||||||
context.UnitInUnits.AddRange(
|
context.UnitInUnits.AddRange(
|
||||||
new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitWithValidParent, DateCreated = DateTimeOffset.UtcNow },
|
new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitWithValidParent, DateCreated = DateTimeOffset.UtcNow },
|
||||||
new UnitInUnit { ParentUnitId = invalidParentId, ChildUnitId = unitWithInvalidParent, DateCreated = DateTimeOffset.UtcNow }
|
new UnitInUnit { ParentUnitId = invalidParentId, ChildUnitId = unitWithInvalidParent, DateCreated = DateTimeOffset.UtcNow }
|
||||||
@@ -389,8 +359,6 @@ public class UnitFilterServiceTests
|
|||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
// Формируем тестовый объект Job с настройкой IsInverse = true
|
|
||||||
// IsInverse = true означает: исключить родителей, которые СОВПАДАЮТ с маской
|
|
||||||
var job = new Job
|
var job = new Job
|
||||||
{
|
{
|
||||||
Id = jobId,
|
Id = jobId,
|
||||||
@@ -430,20 +398,21 @@ public class UnitFilterServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Настройка моков репозиториев
|
|
||||||
var mocks = ArrangeRepositoryMocks(context);
|
var mocks = ArrangeRepositoryMocks(context);
|
||||||
|
|
||||||
// Переопределяем мок: возвращаем только родителей с запрещённым тегом.
|
// Эмуляция RelationshipMatcher: исключает unitWithInvalidParent
|
||||||
// В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
|
mocks.RelationshipMatcher.Setup(r => r.MatchAsync(
|
||||||
// IsInverse = true в сервисе инвертирует результат, поэтому эти родители будут исключены.
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny<string>()))
|
It.IsAny<IEnumerable<JobRelationshipFilter>>(),
|
||||||
.Returns((Guid fId, string mask) =>
|
It.IsAny<CancellationToken>()))
|
||||||
context.UnitInValues
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobRelationshipFilter> filters, CancellationToken ct) =>
|
||||||
.AsNoTracking()
|
ids.Where(id => id == unitWithValidParent)
|
||||||
.Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("ПАРР-РРПТК-ОТВ.ЭК", StringComparison.OrdinalIgnoreCase))
|
.Select(id => new UnitFilterMatchResult
|
||||||
.Select(uv => uv.UnitId)
|
{
|
||||||
.Distinct()
|
UnitId = id,
|
||||||
.AsQueryable());
|
ValidParentIds = new HashSet<Guid> { validParentId },
|
||||||
|
ValidChildIds = new HashSet<Guid>()
|
||||||
|
}).ToList());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
@@ -459,8 +428,8 @@ public class UnitFilterServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проверяет логику IsFullMatch = true: юнит исключается,
|
/// Проверяет логику IsFullMatch = true: юнит исключается,
|
||||||
/// если хотя бы один из его родительских юнитов не соответствует фильтру.
|
/// если хотя бы один из его родителей не соответствует фильтру.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetUnitsByJobFilterAsync_IsFullMatch_UnitWithAnyNonMatchingParent_IsExcluded()
|
public async Task GetUnitsByJobFilterAsync_IsFullMatch_UnitWithAnyNonMatchingParent_IsExcluded()
|
||||||
@@ -478,7 +447,6 @@ public class UnitFilterServiceTests
|
|||||||
var unitMixedId = Guid.NewGuid();
|
var unitMixedId = Guid.NewGuid();
|
||||||
var unitCleanId = Guid.NewGuid();
|
var unitCleanId = Guid.NewGuid();
|
||||||
|
|
||||||
// Подготовка тестовых данных: юниты
|
|
||||||
context.Units.AddRange(
|
context.Units.AddRange(
|
||||||
BuildTestUnit(parentGoodId, "Parent_Good", checkFieldId, "Good Parent"),
|
BuildTestUnit(parentGoodId, "Parent_Good", checkFieldId, "Good Parent"),
|
||||||
BuildTestUnit(parentBadId, "Parent_Bad", checkFieldId, "Bad Parent"),
|
BuildTestUnit(parentBadId, "Parent_Bad", checkFieldId, "Bad Parent"),
|
||||||
@@ -486,7 +454,6 @@ public class UnitFilterServiceTests
|
|||||||
BuildTestUnit(unitCleanId, "Unit_Clean", Guid.NewGuid(), "Clean")
|
BuildTestUnit(unitCleanId, "Unit_Clean", Guid.NewGuid(), "Clean")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Подготовка тестовых данных: связи между юнитами
|
|
||||||
context.UnitInUnits.AddRange(
|
context.UnitInUnits.AddRange(
|
||||||
new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||||
new UnitInUnit { ParentUnitId = parentBadId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
new UnitInUnit { ParentUnitId = parentBadId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||||
@@ -495,8 +462,6 @@ public class UnitFilterServiceTests
|
|||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
// Формируем тестовый объект Job с настройкой IsFullMatch = true
|
|
||||||
// IsFullMatch = true означает: ВСЕ родители должны соответствовать фильтру
|
|
||||||
var job = new Job
|
var job = new Job
|
||||||
{
|
{
|
||||||
Id = jobId,
|
Id = jobId,
|
||||||
@@ -536,19 +501,21 @@ public class UnitFilterServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Настройка моков репозиториев
|
|
||||||
var mocks = ArrangeRepositoryMocks(context);
|
var mocks = ArrangeRepositoryMocks(context);
|
||||||
|
|
||||||
// Переопределяем мок: возвращаем только родителей, соответствующих маске "Good".
|
// Эмуляция RelationshipMatcher: исключает unitMixedId (один из родителей не проходит)
|
||||||
// В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
|
mocks.RelationshipMatcher.Setup(r => r.MatchAsync(
|
||||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(checkFieldId, It.IsAny<string>()))
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
.Returns((Guid fId, string mask) =>
|
It.IsAny<IEnumerable<JobRelationshipFilter>>(),
|
||||||
context.UnitInValues
|
It.IsAny<CancellationToken>()))
|
||||||
.AsNoTracking()
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobRelationshipFilter> filters, CancellationToken ct) =>
|
||||||
.Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("Good", StringComparison.OrdinalIgnoreCase))
|
ids.Where(id => id == unitCleanId)
|
||||||
.Select(uv => uv.UnitId)
|
.Select(id => new UnitFilterMatchResult
|
||||||
.Distinct()
|
{
|
||||||
.AsQueryable());
|
UnitId = id,
|
||||||
|
ValidParentIds = new HashSet<Guid> { parentGoodId },
|
||||||
|
ValidChildIds = new HashSet<Guid>()
|
||||||
|
}).ToList());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
@@ -563,10 +530,9 @@ public class UnitFilterServiceTests
|
|||||||
Assert.DoesNotContain(unitMixedId, resultIds);
|
Assert.DoesNotContain(unitMixedId, resultIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проверяет комбинацию флагов: IsParent=true, IsInverse=true, IsFullMatch=true.
|
/// Проверяет комбинацию флагов: IsParent=true, IsInverse=true, IsFullMatch=true.
|
||||||
/// Логика: юнит проходит, только если НИ ОДИН из его родителей не содержит запрещённое значение.
|
/// Юнит проходит, только если ни один из его родителей не содержит запрещённое значение.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetUnitsByJobFilterAsync_IsParentAndInverseAndFullMatch_UnitWithAnyForbiddenParent_IsExcluded()
|
public async Task GetUnitsByJobFilterAsync_IsParentAndInverseAndFullMatch_UnitWithAnyForbiddenParent_IsExcluded()
|
||||||
@@ -579,12 +545,11 @@ public class UnitFilterServiceTests
|
|||||||
|
|
||||||
var jobId = Guid.NewGuid();
|
var jobId = Guid.NewGuid();
|
||||||
var tagFieldId = Guid.NewGuid();
|
var tagFieldId = Guid.NewGuid();
|
||||||
var parentForbiddenId = Guid.NewGuid(); // Родитель с запрещённым тегом
|
var parentForbiddenId = Guid.NewGuid();
|
||||||
var parentCleanId = Guid.NewGuid(); // Родитель без запрещённого тега
|
var parentCleanId = Guid.NewGuid();
|
||||||
var unitMixedId = Guid.NewGuid(); // Юнит с одним "плохим" и одним "хорошим" родителем
|
var unitMixedId = Guid.NewGuid();
|
||||||
var unitAllCleanId = Guid.NewGuid(); // Юнит только с "хорошими" родителями
|
var unitAllCleanId = Guid.NewGuid();
|
||||||
|
|
||||||
// Подготовка тестовых данных: юниты
|
|
||||||
context.Units.AddRange(
|
context.Units.AddRange(
|
||||||
BuildTestUnit(parentForbiddenId, "Parent_Forbidden", tagFieldId, "FORBIDDEN-TAG"),
|
BuildTestUnit(parentForbiddenId, "Parent_Forbidden", tagFieldId, "FORBIDDEN-TAG"),
|
||||||
BuildTestUnit(parentCleanId, "Parent_Clean", tagFieldId, "CLEAN-TAG"),
|
BuildTestUnit(parentCleanId, "Parent_Clean", tagFieldId, "CLEAN-TAG"),
|
||||||
@@ -592,9 +557,6 @@ public class UnitFilterServiceTests
|
|||||||
BuildTestUnit(unitAllCleanId, "Unit_AllClean", Guid.NewGuid(), "AllClean")
|
BuildTestUnit(unitAllCleanId, "Unit_AllClean", Guid.NewGuid(), "AllClean")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Подготовка тестовых данных: связи
|
|
||||||
// unitMixed имеет обоих родителей — один с запрещённым тегом, один без
|
|
||||||
// unitAllClean имеет только "чистого" родителя
|
|
||||||
context.UnitInUnits.AddRange(
|
context.UnitInUnits.AddRange(
|
||||||
new UnitInUnit { ParentUnitId = parentForbiddenId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
new UnitInUnit { ParentUnitId = parentForbiddenId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||||
new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||||
@@ -603,10 +565,6 @@ public class UnitFilterServiceTests
|
|||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
// Формируем тестовый объект Job с комбинацией флагов:
|
|
||||||
// IsParent = true: проверяем родительские связи
|
|
||||||
// IsInverse = true: исключаем родителей, которые СОВПАДАЮТ с маской
|
|
||||||
// IsFullMatch = true: ВСЕ родители должны пройти фильтр (ни один не должен совпасть с маской)
|
|
||||||
var job = new Job
|
var job = new Job
|
||||||
{
|
{
|
||||||
Id = jobId,
|
Id = jobId,
|
||||||
@@ -646,21 +604,21 @@ public class UnitFilterServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Настройка моков репозиториев
|
|
||||||
var mocks = ArrangeRepositoryMocks(context);
|
var mocks = ArrangeRepositoryMocks(context);
|
||||||
|
|
||||||
// Переопределяем мок: возвращаем только родителей, содержащих "FORBIDDEN".
|
// Эмуляция RelationshipMatcher: исключает unitMixedId (есть родитель с запрещённым тегом)
|
||||||
// В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
|
mocks.RelationshipMatcher.Setup(r => r.MatchAsync(
|
||||||
// Из-за IsInverse=true эти родители будут исключены из "валидных".
|
It.IsAny<IReadOnlyList<Guid>>(),
|
||||||
// Из-за IsFullMatch=true юнит пройдёт, только если ВСЕ его родители валидны.
|
It.IsAny<IEnumerable<JobRelationshipFilter>>(),
|
||||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny<string>()))
|
It.IsAny<CancellationToken>()))
|
||||||
.Returns((Guid fId, string mask) =>
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobRelationshipFilter> filters, CancellationToken ct) =>
|
||||||
context.UnitInValues
|
ids.Where(id => id == unitAllCleanId)
|
||||||
.AsNoTracking()
|
.Select(id => new UnitFilterMatchResult
|
||||||
.Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("FORBIDDEN", StringComparison.OrdinalIgnoreCase))
|
{
|
||||||
.Select(uv => uv.UnitId)
|
UnitId = id,
|
||||||
.Distinct()
|
ValidParentIds = new HashSet<Guid> { parentCleanId },
|
||||||
.AsQueryable());
|
ValidChildIds = new HashSet<Guid>()
|
||||||
|
}).ToList());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
@@ -671,15 +629,7 @@ public class UnitFilterServiceTests
|
|||||||
// === Assert ===
|
// === Assert ===
|
||||||
var resultIds = result.Select(u => u.Id).ToList();
|
var resultIds = result.Select(u => u.Id).ToList();
|
||||||
|
|
||||||
// unitAllCleanId должен остаться: у него один родитель, и он не содержит запрещённый тег
|
|
||||||
Assert.Contains(unitAllCleanId, resultIds);
|
Assert.Contains(unitAllCleanId, resultIds);
|
||||||
|
|
||||||
// unitMixedId должен быть исключён: у него есть родитель с запрещённым тегом.
|
|
||||||
// Логика:
|
|
||||||
// 1. GetMatchingTargetIds возвращает {parentForbiddenId}
|
|
||||||
// 2. IsInverse=true → валидные родители = все.Кроме({parentForbiddenId}) = {parentCleanId}
|
|
||||||
// 3. IsFullMatch=true → проверяем: все родители {parentForbiddenId, parentCleanId} входят в {parentCleanId}? Нет.
|
|
||||||
// 4. Юнит исключается.
|
|
||||||
Assert.DoesNotContain(unitMixedId, resultIds);
|
Assert.DoesNotContain(unitMixedId, resultIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ using PARR.Domain.Entities.JobGroupEntities;
|
|||||||
using PARR.Domain.Entities.RobotEntities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
using PARR.Domain.Entities.Schedule;
|
using PARR.Domain.Entities.Schedule;
|
||||||
using PARR.Domain.Entities.TaskEntities;
|
using PARR.Domain.Entities.TaskEntities;
|
||||||
|
using PARR.Domain.Entities.TemplateEntities;
|
||||||
using PARR.Domain.Entities.Unit;
|
using PARR.Domain.Entities.Unit;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
@@ -18,34 +19,13 @@ namespace PARR.DAL.Context
|
|||||||
{
|
{
|
||||||
public DataContext(DbContextOptions<DataContext> options) : base(options) { }
|
public DataContext(DbContextOptions<DataContext> options) : base(options) { }
|
||||||
|
|
||||||
//public DbSet<Host> Hosts { get; set; }
|
|
||||||
//public DbSet<WorkGroup> WorkGroups { get; set; }
|
|
||||||
//public DbSet<AppInWorkInWorkGroup> AppInWorkInWorkGroups { get; set; }
|
|
||||||
//public DbSet<EkStatus> EkStatuses { get; set; }
|
|
||||||
//public DbSet<ResponseArea> ResponseAreas { get; set; }
|
|
||||||
//public DbSet<Application> Applications { get; set; }
|
|
||||||
//public DbSet<ApplicationType> ApplicationTypes { get; set; }
|
|
||||||
//public DbSet<ApplicationInHost> ApplicationsInHosts { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Template> Templates { get; set; }
|
|
||||||
public DbSet<TemplateHistory> TemplateHistories { get; set; }
|
|
||||||
public DbSet<TemplateStatusType> TemplateStatusTypes { get; set; }
|
|
||||||
public DbSet<Domain.Entities.TaskStatus> TaskStatuses { get; set; }
|
|
||||||
public DbSet<RobotStatus> RobotStatuses { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Process> Processes { get; set; }
|
public DbSet<Process> Processes { get; set; }
|
||||||
public DbSet<Subprocess> Subprocesses { get; set; }
|
public DbSet<Subprocess> Subprocesses { get; set; }
|
||||||
public DbSet<Tnk> Tnks { get; set; }
|
public DbSet<Tnk> Tnks { get; set; }
|
||||||
|
|
||||||
//public DbSet<ApplicationsInWork> ApplicationsInWorks { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Setting> Setting { get; set; }
|
public DbSet<Setting> Setting { get; set; }
|
||||||
|
|
||||||
public DbSet<RobotHistoryLevel> RobotHistoryLevels { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Robot> Robots { get; set; }
|
|
||||||
public DbSet<RobotConfiguration> RobotConfigurations { get; set; }
|
public DbSet<RobotConfiguration> RobotConfigurations { get; set; }
|
||||||
public DbSet<RobotHistory> RobotHistories { get; set; }
|
|
||||||
|
|
||||||
#region Schedule
|
#region Schedule
|
||||||
|
|
||||||
@@ -59,6 +39,8 @@ namespace PARR.DAL.Context
|
|||||||
public DbSet<ScheduleExcludeType> ScheduleExcludeTypes { get; set; }
|
public DbSet<ScheduleExcludeType> ScheduleExcludeTypes { get; set; }
|
||||||
public DbSet<ScheduleResponseAreaTimeOffset> ScheduleResponseAreaTimeOffsets { get; set; }
|
public DbSet<ScheduleResponseAreaTimeOffset> ScheduleResponseAreaTimeOffsets { get; set; }
|
||||||
|
|
||||||
|
public DbSet<DistributionPeriod> DistributionPeriods { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public DbSet<AgentHistory> AgentHistories { get; set; }
|
public DbSet<AgentHistory> AgentHistories { get; set; }
|
||||||
@@ -73,8 +55,6 @@ namespace PARR.DAL.Context
|
|||||||
|
|
||||||
public DbSet<WeekendDay> WeekendDays { get; set; }
|
public DbSet<WeekendDay> WeekendDays { get; set; }
|
||||||
|
|
||||||
public DbSet<DistributionPeriod> DistributionPeriods { get; set; }
|
|
||||||
|
|
||||||
public DbSet<ParrComponent> ParrComponents { get; set; }
|
public DbSet<ParrComponent> ParrComponents { get; set; }
|
||||||
|
|
||||||
#region Units
|
#region Units
|
||||||
@@ -142,11 +122,25 @@ namespace PARR.DAL.Context
|
|||||||
|
|
||||||
#region Robot
|
#region Robot
|
||||||
|
|
||||||
|
public DbSet<RobotHistory> RobotHistories { get; set; }
|
||||||
|
public DbSet<RobotHistoryLevel> RobotHistoryLevels { get; set; }
|
||||||
|
public DbSet<Robot> Robots { get; set; }
|
||||||
|
public DbSet<Domain.Entities.RobotEntities.TaskStatus> TaskStatuses { get; set; }
|
||||||
|
public DbSet<RobotStatus> RobotStatuses { get; set; }
|
||||||
public DbSet<RobotSnapshot> RobotSnapshots { get; set; }
|
public DbSet<RobotSnapshot> RobotSnapshots { get; set; }
|
||||||
public DbSet<RobotConfigurationSnapshot> RobotConfigurationSnapshots { get; set; }
|
public DbSet<RobotConfigurationSnapshot> RobotConfigurationSnapshots { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Template
|
||||||
|
|
||||||
|
public DbSet<Template> Templates { get; set; }
|
||||||
|
public DbSet<TemplateHistory> TemplateHistories { get; set; }
|
||||||
|
public DbSet<TemplateStatusType> TemplateStatusTypes { get; set; }
|
||||||
|
public DbSet<TemplateRenamePending> TemplateRenamePendings { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -203,7 +197,7 @@ namespace PARR.DAL.Context
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region TaskStatus
|
#region TaskStatus
|
||||||
modelBuilder.Entity<Domain.Entities.TaskStatus>(f =>
|
modelBuilder.Entity<Domain.Entities.RobotEntities.TaskStatus>(f =>
|
||||||
{
|
{
|
||||||
f.HasData(
|
f.HasData(
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using PARR.Core.Repositories.Interfaces.JobRepositories;
|
|||||||
using PARR.Core.Repositories.Interfaces.RobotRepositories;
|
using PARR.Core.Repositories.Interfaces.RobotRepositories;
|
||||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||||
using PARR.Core.Repositories.Interfaces.TaskRepositories;
|
using PARR.Core.Repositories.Interfaces.TaskRepositories;
|
||||||
|
using PARR.Core.Repositories.Interfaces.TemplateRepositories;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.DAL.Configurations.DbSettings;
|
using PARR.DAL.Configurations.DbSettings;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
@@ -18,6 +19,7 @@ using PARR.DAL.Repositories.JobRepositories;
|
|||||||
using PARR.DAL.Repositories.RobotRepositories;
|
using PARR.DAL.Repositories.RobotRepositories;
|
||||||
using PARR.DAL.Repositories.Schedule;
|
using PARR.DAL.Repositories.Schedule;
|
||||||
using PARR.DAL.Repositories.TaskRepositories;
|
using PARR.DAL.Repositories.TaskRepositories;
|
||||||
|
using PARR.DAL.Repositories.TemplateRepositories;
|
||||||
using PARR.DAL.Repositories.Unit;
|
using PARR.DAL.Repositories.Unit;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
|
|
||||||
@@ -39,7 +41,10 @@ namespace PARR.DAL
|
|||||||
services.AddDbContext<DataContext>(opt =>
|
services.AddDbContext<DataContext>(opt =>
|
||||||
opt
|
opt
|
||||||
.EnableSensitiveDataLogging()
|
.EnableSensitiveDataLogging()
|
||||||
.UseNpgsql(configuration.GetConnectionString("DefaultConnection"))
|
.UseNpgsql(
|
||||||
|
configuration.GetConnectionString("DefaultConnection")
|
||||||
|
//, npgsqlOptions => npgsqlOptions.CommandTimeout(300) // Время в секундах (5 минут)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -139,6 +144,12 @@ namespace PARR.DAL
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Templates
|
||||||
|
|
||||||
|
services.AddScoped<ITemplateRenamePendingRepository, TemplateRenamePendingRepository>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
//services.AddTransient<INextRunModifierService, NextRunModifierService>();
|
//services.AddTransient<INextRunModifierService, NextRunModifierService>();
|
||||||
|
|
||||||
#region NextRun Services
|
#region NextRun Services
|
||||||
|
|||||||
4096
PARR.DAL/Migrations/20260720233522_tblRobotsUpdateScheme.Designer.cs
generated
Normal file
4096
PARR.DAL/Migrations/20260720233522_tblRobotsUpdateScheme.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
PARR.DAL/Migrations/20260720233522_tblRobotsUpdateScheme.cs
Normal file
68
PARR.DAL/Migrations/20260720233522_tblRobotsUpdateScheme.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PARR.DAL.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class tblRobotsUpdateScheme : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "TaskStatuses",
|
||||||
|
newName: "TaskStatuses",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotStatuses",
|
||||||
|
newName: "RobotStatuses",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Robots",
|
||||||
|
newName: "Robots",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotHistoryLevels",
|
||||||
|
newName: "RobotHistoryLevels",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotHistories",
|
||||||
|
newName: "RobotHistories",
|
||||||
|
newSchema: "robot");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "TaskStatuses",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "TaskStatuses");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotStatuses",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "RobotStatuses");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Robots",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "Robots");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotHistoryLevels",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "RobotHistoryLevels");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotHistories",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "RobotHistories");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4096
PARR.DAL/Migrations/20260720233632_tblRobotsRename.Designer.cs
generated
Normal file
4096
PARR.DAL/Migrations/20260720233632_tblRobotsRename.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
222
PARR.DAL/Migrations/20260720233632_tblRobotsRename.cs
Normal file
222
PARR.DAL/Migrations/20260720233632_tblRobotsRename.cs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PARR.DAL.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class tblRobotsRename : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_RobotHistories_RobotConfigurations_RobotConfigurationId",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_RobotHistories_RobotHistoryLevels_HistoryLevel",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_RobotHistories_TaskStatuses_TaskStatusCode",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_RobotHistoryLevels",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistoryLevels");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_RobotHistories",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotHistoryLevels",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "HistoryLevels",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "RobotHistories",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "Histories",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_RobotHistories_TaskStatusCode",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
newName: "IX_Histories_TaskStatusCode");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_RobotHistories_RobotConfigurationId_DateCreated",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
newName: "IX_Histories_RobotConfigurationId_DateCreated");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_RobotHistories_HistoryLevel_DateCreated",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
newName: "IX_Histories_HistoryLevel_DateCreated");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_RobotHistories_DateCreated",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
newName: "IX_Histories_DateCreated");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_HistoryLevels",
|
||||||
|
schema: "robot",
|
||||||
|
table: "HistoryLevels",
|
||||||
|
column: "Level");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Histories",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Histories_HistoryLevels_HistoryLevel",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
column: "HistoryLevel",
|
||||||
|
principalSchema: "robot",
|
||||||
|
principalTable: "HistoryLevels",
|
||||||
|
principalColumn: "Level",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Histories_RobotConfigurations_RobotConfigurationId",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
column: "RobotConfigurationId",
|
||||||
|
principalTable: "RobotConfigurations",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Histories_TaskStatuses_TaskStatusCode",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories",
|
||||||
|
column: "TaskStatusCode",
|
||||||
|
principalSchema: "robot",
|
||||||
|
principalTable: "TaskStatuses",
|
||||||
|
principalColumn: "Code",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Histories_HistoryLevels_HistoryLevel",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Histories_RobotConfigurations_RobotConfigurationId",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Histories_TaskStatuses_TaskStatusCode",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_HistoryLevels",
|
||||||
|
schema: "robot",
|
||||||
|
table: "HistoryLevels");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Histories",
|
||||||
|
schema: "robot",
|
||||||
|
table: "Histories");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "HistoryLevels",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "RobotHistoryLevels",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Histories",
|
||||||
|
schema: "robot",
|
||||||
|
newName: "RobotHistories",
|
||||||
|
newSchema: "robot");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Histories_TaskStatusCode",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
newName: "IX_RobotHistories_TaskStatusCode");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Histories_RobotConfigurationId_DateCreated",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
newName: "IX_RobotHistories_RobotConfigurationId_DateCreated");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Histories_HistoryLevel_DateCreated",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
newName: "IX_RobotHistories_HistoryLevel_DateCreated");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Histories_DateCreated",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
newName: "IX_RobotHistories_DateCreated");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_RobotHistoryLevels",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistoryLevels",
|
||||||
|
column: "Level");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_RobotHistories",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_RobotHistories_RobotConfigurations_RobotConfigurationId",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
column: "RobotConfigurationId",
|
||||||
|
principalTable: "RobotConfigurations",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_RobotHistories_RobotHistoryLevels_HistoryLevel",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
column: "HistoryLevel",
|
||||||
|
principalSchema: "robot",
|
||||||
|
principalTable: "RobotHistoryLevels",
|
||||||
|
principalColumn: "Level",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_RobotHistories_TaskStatuses_TaskStatusCode",
|
||||||
|
schema: "robot",
|
||||||
|
table: "RobotHistories",
|
||||||
|
column: "TaskStatusCode",
|
||||||
|
principalSchema: "robot",
|
||||||
|
principalTable: "TaskStatuses",
|
||||||
|
principalColumn: "Code",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4133
PARR.DAL/Migrations/20260721013444_tblTemplateRenamePendings.Designer.cs
generated
Normal file
4133
PARR.DAL/Migrations/20260721013444_tblTemplateRenamePendings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PARR.DAL.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class tblTemplateRenamePendings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.EnsureSchema(
|
||||||
|
name: "template");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TemplateRenamePendings",
|
||||||
|
schema: "template",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
TemplateId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
OldName = table.Column<string>(type: "text", nullable: false, comment: "Старое имя шаблона")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TemplateRenamePendings", x => x.TemplateId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TemplateRenamePendings_Templates_TemplateId",
|
||||||
|
column: x => x.TemplateId,
|
||||||
|
principalTable: "Templates",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
},
|
||||||
|
comment: "Шаблоны находящиеся в процессе переименования");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TemplateRenamePendings_OldName",
|
||||||
|
schema: "template",
|
||||||
|
table: "TemplateRenamePendings",
|
||||||
|
column: "OldName",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TemplateRenamePendings",
|
||||||
|
schema: "template");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4135
PARR.DAL/Migrations/20260728234721_tblRobotConfigurationsAddIndexes.Designer.cs
generated
Normal file
4135
PARR.DAL/Migrations/20260728234721_tblRobotConfigurationsAddIndexes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PARR.DAL.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class tblRobotConfigurationsAddIndexes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_RobotConfigurations_RobotCode",
|
||||||
|
table: "RobotConfigurations");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RobotConfigurations_RobotCode_RobotStatusCode",
|
||||||
|
table: "RobotConfigurations",
|
||||||
|
columns: new[] { "RobotCode", "RobotStatusCode" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RobotConfigurations_RobotCode_TaskStatusCode",
|
||||||
|
table: "RobotConfigurations",
|
||||||
|
columns: new[] { "RobotCode", "TaskStatusCode" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_RobotConfigurations_RobotCode_RobotStatusCode",
|
||||||
|
table: "RobotConfigurations");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_RobotConfigurations_RobotCode_TaskStatusCode",
|
||||||
|
table: "RobotConfigurations");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RobotConfigurations_RobotCode",
|
||||||
|
table: "RobotConfigurations",
|
||||||
|
column: "RobotCode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4137
PARR.DAL/Migrations/20260729041405_tblTemplateRenamePendingAddDateModifiedRemUniqueIndex.Designer.cs
generated
Normal file
4137
PARR.DAL/Migrations/20260729041405_tblTemplateRenamePendingAddDateModifiedRemUniqueIndex.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PARR.DAL.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class tblTemplateRenamePendingAddDateModifiedRemUniqueIndex : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_TemplateRenamePendings_OldName",
|
||||||
|
schema: "template",
|
||||||
|
table: "TemplateRenamePendings");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "DateModified",
|
||||||
|
schema: "template",
|
||||||
|
table: "TemplateRenamePendings",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TemplateRenamePendings_OldName",
|
||||||
|
schema: "template",
|
||||||
|
table: "TemplateRenamePendings",
|
||||||
|
column: "OldName");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_TemplateRenamePendings_OldName",
|
||||||
|
schema: "template",
|
||||||
|
table: "TemplateRenamePendings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DateModified",
|
||||||
|
schema: "template",
|
||||||
|
table: "TemplateRenamePendings");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TemplateRenamePendings_OldName",
|
||||||
|
schema: "template",
|
||||||
|
table: "TemplateRenamePendings",
|
||||||
|
column: "OldName",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -921,44 +921,6 @@ namespace PARR.DAL.Migrations
|
|||||||
b.ToTable("Processes");
|
b.ToTable("Processes");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.Robot", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Code")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Code"));
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Code");
|
|
||||||
|
|
||||||
b.HasIndex("Name")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Robots");
|
|
||||||
|
|
||||||
b.HasData(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Code = 1,
|
|
||||||
Description = "Робот по созданию/изменению шаблона наряда ЕСПП",
|
|
||||||
Name = "TemplateOrder"
|
|
||||||
},
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Code = 2,
|
|
||||||
Description = "Робот по созданию/изменению расписания шаблона наряда в ЕСПП",
|
|
||||||
Name = "ScheduleOrder"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotConfiguration", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotConfiguration", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -988,12 +950,14 @@ namespace PARR.DAL.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("RobotCode");
|
|
||||||
|
|
||||||
b.HasIndex("RobotStatusCode");
|
b.HasIndex("RobotStatusCode");
|
||||||
|
|
||||||
b.HasIndex("TaskStatusCode");
|
b.HasIndex("TaskStatusCode");
|
||||||
|
|
||||||
|
b.HasIndex("RobotCode", "RobotStatusCode");
|
||||||
|
|
||||||
|
b.HasIndex("RobotCode", "TaskStatusCode");
|
||||||
|
|
||||||
b.HasIndex("TemplateId", "RobotCode")
|
b.HasIndex("TemplateId", "RobotCode")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
@@ -1004,6 +968,44 @@ namespace PARR.DAL.Migrations
|
|||||||
b.ToTable("RobotConfigurations");
|
b.ToTable("RobotConfigurations");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.Robot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Code")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Code"));
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Code");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Robots", "robot");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Code = 1,
|
||||||
|
Description = "Робот по созданию/изменению шаблона наряда ЕСПП",
|
||||||
|
Name = "TemplateOrder"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Code = 2,
|
||||||
|
Description = "Робот по созданию/изменению расписания шаблона наряда в ЕСПП",
|
||||||
|
Name = "ScheduleOrder"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotConfigurationSnapshot", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotConfigurationSnapshot", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1040,44 +1042,7 @@ namespace PARR.DAL.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotSnapshot", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotHistory", b =>
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("DateCreated")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Ip")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(45)
|
|
||||||
.HasColumnType("character varying(45)")
|
|
||||||
.HasComment("Текущий IP-адрес сервера");
|
|
||||||
|
|
||||||
b.Property<int>("MaxRobots")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasComment("Максимально разрешенное количество роботов на сервере");
|
|
||||||
|
|
||||||
b.Property<int>("ScheduleRobotsCount")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasComment("Количество запущенных роботов по расписаниям");
|
|
||||||
|
|
||||||
b.Property<int>("TemplateRobotsCount")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasComment("Количество запущенных роботов по шаблонам");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Ip", "DateCreated");
|
|
||||||
|
|
||||||
b.ToTable("Snapshots", "robot", t =>
|
|
||||||
{
|
|
||||||
t.HasComment("Снимки роботов");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotHistory", b =>
|
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -1119,10 +1084,10 @@ namespace PARR.DAL.Migrations
|
|||||||
|
|
||||||
b.HasIndex("RobotConfigurationId", "DateCreated");
|
b.HasIndex("RobotConfigurationId", "DateCreated");
|
||||||
|
|
||||||
b.ToTable("RobotHistories");
|
b.ToTable("Histories", "robot");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotHistoryLevel", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotHistoryLevel", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Level")
|
b.Property<int>("Level")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -1140,7 +1105,7 @@ namespace PARR.DAL.Migrations
|
|||||||
|
|
||||||
b.HasKey("Level");
|
b.HasKey("Level");
|
||||||
|
|
||||||
b.ToTable("RobotHistoryLevels");
|
b.ToTable("HistoryLevels", "robot");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -1169,7 +1134,44 @@ namespace PARR.DAL.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotStatus", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Ip")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(45)
|
||||||
|
.HasColumnType("character varying(45)")
|
||||||
|
.HasComment("Текущий IP-адрес сервера");
|
||||||
|
|
||||||
|
b.Property<int>("MaxRobots")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("Максимально разрешенное количество роботов на сервере");
|
||||||
|
|
||||||
|
b.Property<int>("ScheduleRobotsCount")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("Количество запущенных роботов по расписаниям");
|
||||||
|
|
||||||
|
b.Property<int>("TemplateRobotsCount")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("Количество запущенных роботов по шаблонам");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Ip", "DateCreated");
|
||||||
|
|
||||||
|
b.ToTable("Snapshots", "robot", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("Снимки роботов");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotStatus", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Code")
|
b.Property<int>("Code")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -1187,7 +1189,7 @@ namespace PARR.DAL.Migrations
|
|||||||
|
|
||||||
b.HasKey("Code");
|
b.HasKey("Code");
|
||||||
|
|
||||||
b.ToTable("RobotStatuses");
|
b.ToTable("RobotStatuses", "robot");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -1216,6 +1218,47 @@ namespace PARR.DAL.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.TaskStatus", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Code")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Code"));
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Code");
|
||||||
|
|
||||||
|
b.ToTable("TaskStatuses", "robot");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Code = 10,
|
||||||
|
Description = "Требуется создание объекта в ЕСПП",
|
||||||
|
Name = "Creating"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Code = 20,
|
||||||
|
Description = "Требуется обновление объекта в ЕСПП",
|
||||||
|
Name = "Updating"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Code = 30,
|
||||||
|
Description = "Нормальное состояние объекта в ЕСПП и ПАРР. Объект в ПАРР соответствует объекту в ЕСПП",
|
||||||
|
Name = "Ok"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.Role", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.Role", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2743,47 +2786,6 @@ namespace PARR.DAL.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.TaskStatus", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Code")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Code"));
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Code");
|
|
||||||
|
|
||||||
b.ToTable("TaskStatuses");
|
|
||||||
|
|
||||||
b.HasData(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Code = 10,
|
|
||||||
Description = "Требуется создание объекта в ЕСПП",
|
|
||||||
Name = "Creating"
|
|
||||||
},
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Code = 20,
|
|
||||||
Description = "Требуется обновление объекта в ЕСПП",
|
|
||||||
Name = "Updating"
|
|
||||||
},
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Code = 30,
|
|
||||||
Description = "Нормальное состояние объекта в ЕСПП и ПАРР. Объект в ПАРР соответствует объекту в ЕСПП",
|
|
||||||
Name = "Ok"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.Template", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.Template", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2850,6 +2852,32 @@ namespace PARR.DAL.Migrations
|
|||||||
b.ToTable("Templates");
|
b.ToTable("Templates");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.TemplateEntities.TemplateRenamePending", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("TemplateId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DateModified")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("OldName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("Старое имя шаблона");
|
||||||
|
|
||||||
|
b.HasKey("TemplateId");
|
||||||
|
|
||||||
|
b.HasIndex("OldName");
|
||||||
|
|
||||||
|
b.ToTable("TemplateRenamePendings", "template", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("Шаблоны находящиеся в процессе переименования");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.TemplateHistory", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.TemplateHistory", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -3516,19 +3544,19 @@ namespace PARR.DAL.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotConfiguration", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotConfiguration", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PARR.Domain.Entities.Robot", "Robot")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.Robot", "Robot")
|
||||||
.WithMany("RobotConfigurations")
|
.WithMany("RobotConfigurations")
|
||||||
.HasForeignKey("RobotCode")
|
.HasForeignKey("RobotCode")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PARR.Domain.Entities.RobotStatus", "RobotStatus")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.RobotStatus", "RobotStatus")
|
||||||
.WithMany("RobotConfigurations")
|
.WithMany("RobotConfigurations")
|
||||||
.HasForeignKey("RobotStatusCode")
|
.HasForeignKey("RobotStatusCode")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PARR.Domain.Entities.TaskStatus", "TaskStatus")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.TaskStatus", "TaskStatus")
|
||||||
.WithMany("RobotConfigurations")
|
.WithMany("RobotConfigurations")
|
||||||
.HasForeignKey("TaskStatusCode")
|
.HasForeignKey("TaskStatusCode")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -3551,19 +3579,19 @@ namespace PARR.DAL.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotConfigurationSnapshot", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotConfigurationSnapshot", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PARR.Domain.Entities.Robot", "Robot")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.Robot", "Robot")
|
||||||
.WithMany("ConfigurationSnapshots")
|
.WithMany("ConfigurationSnapshots")
|
||||||
.HasForeignKey("RobotCode")
|
.HasForeignKey("RobotCode")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PARR.Domain.Entities.RobotStatus", "RobotStatus")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.RobotStatus", "RobotStatus")
|
||||||
.WithMany("ConfigurationSnapshots")
|
.WithMany("ConfigurationSnapshots")
|
||||||
.HasForeignKey("RobotStatusCode")
|
.HasForeignKey("RobotStatusCode")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PARR.Domain.Entities.TaskStatus", "TaskStatus")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.TaskStatus", "TaskStatus")
|
||||||
.WithMany("ConfigurationSnapshots")
|
.WithMany("ConfigurationSnapshots")
|
||||||
.HasForeignKey("TaskStatusCode")
|
.HasForeignKey("TaskStatusCode")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -3576,9 +3604,9 @@ namespace PARR.DAL.Migrations
|
|||||||
b.Navigation("TaskStatus");
|
b.Navigation("TaskStatus");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotHistory", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotHistory", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PARR.Domain.Entities.RobotHistoryLevel", "RobotHistoryLevel")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.RobotHistoryLevel", "RobotHistoryLevel")
|
||||||
.WithMany("RobotHistories")
|
.WithMany("RobotHistories")
|
||||||
.HasForeignKey("HistoryLevel")
|
.HasForeignKey("HistoryLevel")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -3590,7 +3618,7 @@ namespace PARR.DAL.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PARR.Domain.Entities.TaskStatus", "StatusTask")
|
b.HasOne("PARR.Domain.Entities.RobotEntities.TaskStatus", "StatusTask")
|
||||||
.WithMany("RobotHistories")
|
.WithMany("RobotHistories")
|
||||||
.HasForeignKey("TaskStatusCode")
|
.HasForeignKey("TaskStatusCode")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -3728,6 +3756,17 @@ namespace PARR.DAL.Migrations
|
|||||||
b.Navigation("Unit");
|
b.Navigation("Unit");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.TemplateEntities.TemplateRenamePending", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PARR.Domain.Entities.Template", "Template")
|
||||||
|
.WithOne("TemplateRenamePending")
|
||||||
|
.HasForeignKey("PARR.Domain.Entities.TemplateEntities.TemplateRenamePending", "TemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Template");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.TemplateHistory", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.TemplateHistory", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PARR.Domain.Entities.Template", "Template")
|
b.HasOne("PARR.Domain.Entities.Template", "Template")
|
||||||
@@ -3927,30 +3966,39 @@ namespace PARR.DAL.Migrations
|
|||||||
b.Navigation("Subprocesses");
|
b.Navigation("Subprocesses");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.Robot", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("ConfigurationSnapshots");
|
|
||||||
|
|
||||||
b.Navigation("RobotConfigurations");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotConfiguration", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotConfiguration", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("RobotHistories");
|
b.Navigation("RobotHistories");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotHistoryLevel", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.Robot", b =>
|
||||||
{
|
|
||||||
b.Navigation("RobotHistories");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.RobotStatus", b =>
|
|
||||||
{
|
{
|
||||||
b.Navigation("ConfigurationSnapshots");
|
b.Navigation("ConfigurationSnapshots");
|
||||||
|
|
||||||
b.Navigation("RobotConfigurations");
|
b.Navigation("RobotConfigurations");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotHistoryLevel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RobotHistories");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotStatus", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ConfigurationSnapshots");
|
||||||
|
|
||||||
|
b.Navigation("RobotConfigurations");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.TaskStatus", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ConfigurationSnapshots");
|
||||||
|
|
||||||
|
b.Navigation("RobotConfigurations");
|
||||||
|
|
||||||
|
b.Navigation("RobotHistories");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.Role", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.Role", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Users");
|
b.Navigation("Users");
|
||||||
@@ -4008,15 +4056,6 @@ namespace PARR.DAL.Migrations
|
|||||||
b.Navigation("Tasks");
|
b.Navigation("Tasks");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.TaskStatus", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("ConfigurationSnapshots");
|
|
||||||
|
|
||||||
b.Navigation("RobotConfigurations");
|
|
||||||
|
|
||||||
b.Navigation("RobotHistories");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PARR.Domain.Entities.Template", b =>
|
modelBuilder.Entity("PARR.Domain.Entities.Template", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("AgentHistories");
|
b.Navigation("AgentHistories");
|
||||||
@@ -4027,6 +4066,8 @@ namespace PARR.DAL.Migrations
|
|||||||
|
|
||||||
b.Navigation("TemplateHistories");
|
b.Navigation("TemplateHistories");
|
||||||
|
|
||||||
|
b.Navigation("TemplateRenamePending");
|
||||||
|
|
||||||
b.Navigation("UnitsInTemplate");
|
b.Navigation("UnitsInTemplate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,23 +15,13 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
{
|
{
|
||||||
internal abstract class BaseRepository<T> : IBaseRepository<T> where T : class, IBaseEntity
|
internal abstract class BaseRepository<T> : IBaseRepository<T> where T : class, IBaseEntity
|
||||||
{
|
{
|
||||||
//private readonly ILogger<BaseRepository<T>> logger;
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
//protected abstract DbSet<T> EntitySet { get; }
|
|
||||||
//protected abstract DataContext EntitiContext { get; }
|
|
||||||
|
|
||||||
//public BaseRepository(ILogger<BaseRepository<T>> logger)
|
|
||||||
//{
|
|
||||||
// this.logger = logger;
|
|
||||||
//}
|
|
||||||
|
|
||||||
protected readonly ILogger logger;
|
|
||||||
protected readonly DbSet<T> EntitySet;
|
protected readonly DbSet<T> EntitySet;
|
||||||
protected readonly DataContext EntityContext;
|
protected readonly DataContext EntityContext;
|
||||||
|
|
||||||
protected BaseRepository(ILogger logger, DataContext dataContext)
|
protected BaseRepository(ILogger logger, DataContext dataContext)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this._logger = logger;
|
||||||
this.EntityContext = dataContext;
|
this.EntityContext = dataContext;
|
||||||
this.EntitySet = dataContext.Set<T>();
|
this.EntitySet = dataContext.Set<T>();
|
||||||
}
|
}
|
||||||
@@ -39,7 +29,7 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
|
|
||||||
public virtual async Task<bool> AddRangeAsync(List<T> objs)
|
public virtual async Task<bool> AddRangeAsync(List<T> objs)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Начинаю добавление диапазона объектов типа {EntityType}, количество: {Count}",
|
_logger.LogDebug("Начинаю добавление диапазона объектов типа {EntityType}, количество: {Count}",
|
||||||
typeof(T).Name, objs.Count);
|
typeof(T).Name, objs.Count);
|
||||||
|
|
||||||
objs.ForEach(item => item.DateCreated = DateTimeOffset.UtcNow);
|
objs.ForEach(item => item.DateCreated = DateTimeOffset.UtcNow);
|
||||||
@@ -47,26 +37,26 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await EntitySet.AddRangeAsync(objs);
|
await EntitySet.AddRangeAsync(objs);
|
||||||
logger.LogDebug("Успешно добавлено {Count} объектов типа {EntityType}",
|
_logger.LogDebug("Успешно добавлено {Count} объектов типа {EntityType}",
|
||||||
objs.Count, typeof(T).Name);
|
objs.Count, typeof(T).Name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при добавлении диапазона объектов типа {EntityType}", typeof(T).Name);
|
_logger.LogError(ex, "Ошибка при добавлении диапазона объектов типа {EntityType}", typeof(T).Name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CommitAsync(IHistoryInitiator? initiator = null)
|
public async Task<bool> CommitAsync(IHistoryInitiator? initiator = null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Начинаю сохранение изменений в БД для объектов типа {EntityType}", typeof(T).Name);
|
_logger.LogDebug("Начинаю сохранение изменений в БД для объектов типа {EntityType}", typeof(T).Name);
|
||||||
|
|
||||||
#region Изменения
|
#region Изменения
|
||||||
var modifiedEntrities = EntityContext.ChangeTracker.Entries()
|
var modifiedEntrities = EntityContext.ChangeTracker.Entries()
|
||||||
.Where(t => t.State == EntityState.Modified/* || t.State == EntityState.Deleted*/);
|
.Where(t => t.State == EntityState.Modified/* || t.State == EntityState.Deleted*/);
|
||||||
|
|
||||||
logger.LogDebug("Найдено {Count} измененных сущностей для обработки истории", modifiedEntrities.Count());
|
_logger.LogDebug("Найдено {Count} измененных сущностей для обработки истории", modifiedEntrities.Count());
|
||||||
|
|
||||||
foreach (var obj in modifiedEntrities)
|
foreach (var obj in modifiedEntrities)
|
||||||
{
|
{
|
||||||
@@ -83,13 +73,13 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var changedCount = await EntityContext.SaveChangesAsync();
|
var changedCount = await EntityContext.SaveChangesAsync();
|
||||||
logger.LogDebug("Успешно сохранено {ChangedCount} изменений в БД для объектов типа {EntityType}",
|
_logger.LogDebug("Успешно сохранено {ChangedCount} изменений в БД для объектов типа {EntityType}",
|
||||||
changedCount, typeof(T).Name);
|
changedCount, typeof(T).Name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при сохранении изменений в БД для объектов типа {EntityType}", typeof(T).Name);
|
_logger.LogError(ex, "Ошибка при сохранении изменений в БД для объектов типа {EntityType}", typeof(T).Name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,14 +94,14 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
if (initiator == null)
|
if (initiator == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
logger.LogDebug("Устанавливаю инициатора для изменений");
|
_logger.LogDebug("Устанавливаю инициатора для изменений");
|
||||||
|
|
||||||
// Задаем инициатора только для новых и измененных записей
|
// Задаем инициатора только для новых и измененных записей
|
||||||
var entrities = EntityContext.ChangeTracker.Entries()
|
var entrities = EntityContext.ChangeTracker.Entries()
|
||||||
.Where(t => t.State == EntityState.Modified || t.State == EntityState.Added);
|
.Where(t => t.State == EntityState.Modified || t.State == EntityState.Added);
|
||||||
|
|
||||||
var entityCount = entrities.Count();
|
var entityCount = entrities.Count();
|
||||||
logger.LogDebug("Найдено {Count} сущностей для установки инициатора", entityCount);
|
_logger.LogDebug("Найдено {Count} сущностей для установки инициатора", entityCount);
|
||||||
|
|
||||||
// смотрим есть ли у объекта интерфейс IHistoryInitiator, если есть, задаём значения
|
// смотрим есть ли у объекта интерфейс IHistoryInitiator, если есть, задаём значения
|
||||||
foreach (var obj in entrities)
|
foreach (var obj in entrities)
|
||||||
@@ -123,7 +113,7 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
(obj.Entity as IHistoryInitiator)!.InitiatorParrComponentId = initiator?.InitiatorParrComponentId ?? null;
|
(obj.Entity as IHistoryInitiator)!.InitiatorParrComponentId = initiator?.InitiatorParrComponentId ?? null;
|
||||||
(obj.Entity as IHistoryInitiator)!.InitiatorComment = initiator?.InitiatorComment ?? null;
|
(obj.Entity as IHistoryInitiator)!.InitiatorComment = initiator?.InitiatorComment ?? null;
|
||||||
|
|
||||||
logger.LogDebug("Установлен инициатор для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
_logger.LogDebug("Установлен инициатор для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,12 +140,12 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
|
|
||||||
if (!isManual)
|
if (!isManual)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Обновляю DateModified для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
_logger.LogDebug("Обновляю DateModified для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
||||||
entity.DateModified = DateTimeOffset.UtcNow;
|
entity.DateModified = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Пропуск обновления DateModified (ManualControl) для {EntityType}", entityType.Name);
|
_logger.LogDebug("Пропуск обновления DateModified (ManualControl) для {EntityType}", entityType.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +156,7 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
/// <param name="obj"></param>
|
/// <param name="obj"></param>
|
||||||
private void TableHistoryResolver(EntityEntry obj)
|
private void TableHistoryResolver(EntityEntry obj)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Проверяю необходимость создания истории для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
_logger.LogDebug("Проверяю необходимость создания истории для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
||||||
|
|
||||||
var myHistoryInterface = obj.Entity.GetType().GetInterfaces()
|
var myHistoryInterface = obj.Entity.GetType().GetInterfaces()
|
||||||
.Where(t => t.IsGenericType)
|
.Where(t => t.IsGenericType)
|
||||||
@@ -176,7 +166,7 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
// у этого объекта нет интерфейса IMyHistory<>. Не ведем историю
|
// у этого объекта нет интерфейса IMyHistory<>. Не ведем историю
|
||||||
if (myHistoryInterface == null)
|
if (myHistoryInterface == null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Сущность типа {EntityType} не требует ведения истории", obj.Entity.GetType().Name);
|
_logger.LogDebug("Сущность типа {EntityType} не требует ведения истории", obj.Entity.GetType().Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,13 +176,13 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
var historyType = myHistoryInterface.GetGenericArguments().First();
|
var historyType = myHistoryInterface.GetGenericArguments().First();
|
||||||
var historyProps = historyType.GetProperties(/*BindingFlags.DeclaredOnly | */ /*BindingFlags.Public*/).ToList();
|
var historyProps = historyType.GetProperties(/*BindingFlags.DeclaredOnly | */ /*BindingFlags.Public*/).ToList();
|
||||||
|
|
||||||
logger.LogDebug("Создаю историю для сущности типа {EntityType}, тип истории: {HistoryType}",
|
_logger.LogDebug("Создаю историю для сущности типа {EntityType}, тип истории: {HistoryType}",
|
||||||
obj.Entity.GetType().Name, historyType.Name);
|
obj.Entity.GetType().Name, historyType.Name);
|
||||||
|
|
||||||
var historyInstance = Activator.CreateInstance(historyType);
|
var historyInstance = Activator.CreateInstance(historyType);
|
||||||
if (historyInstance == null)
|
if (historyInstance == null)
|
||||||
{
|
{
|
||||||
logger.LogError("Не смог создать инстанс для ведения истории {HistoryType}", historyType.Name);
|
_logger.LogError("Не смог создать инстанс для ведения истории {HistoryType}", historyType.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,11 +198,11 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EntityContext.Add(historyInstance);
|
EntityContext.Add(historyInstance);
|
||||||
logger.LogDebug("История добавлена для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
_logger.LogDebug("История добавлена для сущности типа {EntityType}", obj.Entity.GetType().Name);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при добавлении объекта в историю {HistoryType}", historyType.Name);
|
_logger.LogError(ex, "Ошибка при добавлении объекта в историю {HistoryType}", historyType.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +215,7 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
/// <param name="propsList"></param>
|
/// <param name="propsList"></param>
|
||||||
private void FillHistoryProps(EntityEntry originalObj, ref object historyInstance, List<PropertyInfo> propsList)
|
private void FillHistoryProps(EntityEntry originalObj, ref object historyInstance, List<PropertyInfo> propsList)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Заполняю историю для сущности типа {EntityType}", originalObj.Entity.GetType().Name);
|
_logger.LogDebug("Заполняю историю для сущности типа {EntityType}", originalObj.Entity.GetType().Name);
|
||||||
|
|
||||||
foreach (var prop in propsList)
|
foreach (var prop in propsList)
|
||||||
{
|
{
|
||||||
@@ -250,7 +240,7 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
histProp.SetValue(historyInstance, origValues);
|
histProp.SetValue(historyInstance, origValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("Завершено заполнение истории для сущности типа {EntityType}", originalObj.Entity.GetType().Name);
|
_logger.LogDebug("Завершено заполнение истории для сущности типа {EntityType}", originalObj.Entity.GetType().Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -266,14 +256,14 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
var histProp = instanceObj.GetType().GetProperty(propName);
|
var histProp = instanceObj.GetType().GetProperty(propName);
|
||||||
if (histProp == null)
|
if (histProp == null)
|
||||||
{
|
{
|
||||||
logger.LogError("При изменении объекта для БД, не найдено свойство {PropertyName}", propName);
|
_logger.LogError("При изменении объекта для БД, не найдено свойство {PropertyName}", propName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// сравним типы
|
// сравним типы
|
||||||
if (histProp.PropertyType != typeof(TValue))
|
if (histProp.PropertyType != typeof(TValue))
|
||||||
{
|
{
|
||||||
logger.LogError("При изменении объекта для БД, не совпадают типы у свойства {PropertyName}, {PropertyType}!={ValueType}",
|
_logger.LogError("При изменении объекта для БД, не совпадают типы у свойства {PropertyName}, {PropertyType}!={ValueType}",
|
||||||
propName, histProp.PropertyType.Name, typeof(TValue).Name);
|
propName, histProp.PropertyType.Name, typeof(TValue).Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -340,7 +330,7 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
|
|
||||||
public virtual async Task<bool> CreateAsync(T obj)
|
public virtual async Task<bool> CreateAsync(T obj)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Начинаю создание объекта типа {EntityType}", typeof(T).Name);
|
_logger.LogDebug("Начинаю создание объекта типа {EntityType}", typeof(T).Name);
|
||||||
|
|
||||||
if (obj.DateCreated == DateTimeOffset.MinValue)
|
if (obj.DateCreated == DateTimeOffset.MinValue)
|
||||||
obj.DateCreated = DateTimeOffset.UtcNow;
|
obj.DateCreated = DateTimeOffset.UtcNow;
|
||||||
@@ -348,73 +338,73 @@ namespace PARR.DAL.Repositories.Base
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await EntitySet.AddAsync(obj);
|
await EntitySet.AddAsync(obj);
|
||||||
logger.LogDebug("Объект типа {EntityType} добавлен в контекст", typeof(T).Name);
|
_logger.LogDebug("Объект типа {EntityType} добавлен в контекст", typeof(T).Name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при добавлении объекта типа {EntityType} в БД", typeof(T).Name);
|
_logger.LogError(ex, "Ошибка при добавлении объекта типа {EntityType} в БД", typeof(T).Name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool Delete(T obj)
|
public virtual bool Delete(T obj)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Начинаю удаление объекта типа {EntityType}", obj.GetType().Name);
|
_logger.LogDebug("Начинаю удаление объекта типа {EntityType}", obj.GetType().Name);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
EntitySet.Remove(obj);
|
EntitySet.Remove(obj);
|
||||||
logger.LogDebug("Объект типа {EntityType} удален из контекста", obj.GetType().Name);
|
_logger.LogDebug("Объект типа {EntityType} удален из контекста", obj.GetType().Name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД", obj.GetType().Name);
|
_logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД", obj.GetType().Name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<bool> DeleteAsync(Guid id)
|
public virtual async Task<bool> DeleteAsync(Guid id)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Начинаю удаление объекта типа {EntityType} по ID: {Id}", typeof(T).Name, id);
|
_logger.LogDebug("Начинаю удаление объекта типа {EntityType} по ID: {Id}", typeof(T).Name, id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var exist = await GetAsync(id);
|
var exist = await GetAsync(id);
|
||||||
if (exist == null)
|
if (exist == null)
|
||||||
{
|
{
|
||||||
logger.LogError("Ошибка при удалении из БД. Не найдена запись в БД типа {EntityType} с id: {Id}",
|
_logger.LogError("Ошибка при удалении из БД. Не найдена запись в БД типа {EntityType} с id: {Id}",
|
||||||
typeof(T).Name, id);
|
typeof(T).Name, id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
EntitySet.Remove(exist);
|
EntitySet.Remove(exist);
|
||||||
logger.LogDebug("Объект типа {EntityType} с ID {Id} удален из контекста", typeof(T).Name, id);
|
_logger.LogDebug("Объект типа {EntityType} с ID {Id} удален из контекста", typeof(T).Name, id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД по ID: {Id}", typeof(T).Name, id);
|
_logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД по ID: {Id}", typeof(T).Name, id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IQueryable<T> Get()
|
public virtual IQueryable<T> Get()
|
||||||
{
|
{
|
||||||
logger.LogDebug("Получаю набор объектов типа {EntityType}", typeof(T).Name);
|
_logger.LogDebug("Получаю набор объектов типа {EntityType}", typeof(T).Name);
|
||||||
return EntitySet;
|
return EntitySet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<T?> GetAsync(Guid id)
|
public virtual async Task<T?> GetAsync(Guid id)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Получаю объект типа {EntityType} по ID: {Id}", typeof(T).Name, id);
|
_logger.LogDebug("Получаю объект типа {EntityType} по ID: {Id}", typeof(T).Name, id);
|
||||||
return await EntitySet.FirstOrDefaultAsync(t => t.Id == id);
|
return await EntitySet.FirstOrDefaultAsync(t => t.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IQueryable<T> GetPage(IQueryable<T> query, PaginationFilter paginationFilter)
|
public virtual IQueryable<T> GetPage(IQueryable<T> query, PaginationFilter paginationFilter)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Получаю страницу объектов типа {EntityType}, страница: {PageNumber}, размер: {PageSize}",
|
_logger.LogDebug("Получаю страницу объектов типа {EntityType}, страница: {PageNumber}, размер: {PageSize}",
|
||||||
typeof(T).Name, paginationFilter.PageNumber, paginationFilter.PageSize);
|
typeof(T).Name, paginationFilter.PageNumber, paginationFilter.PageSize);
|
||||||
|
|
||||||
int skip = (paginationFilter.PageNumber - 1) * paginationFilter.PageSize;
|
int skip = (paginationFilter.PageNumber - 1) * paginationFilter.PageSize;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using InfluxDB.Client.Api.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
using PARR.DAL.Repositories.Base;
|
using PARR.DAL.Repositories.Base;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities;
|
||||||
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ namespace PARR.DAL.Repositories
|
|||||||
? ((TaskStatusEnum)taskStatusValue).ToString()
|
? ((TaskStatusEnum)taskStatusValue).ToString()
|
||||||
: $"Unknown ({taskStatusValue})";
|
: $"Unknown ({taskStatusValue})";
|
||||||
|
|
||||||
logger.LogInformation("Нельзя установить статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}, так как текущий статус {currentStatus}",
|
_logger.LogInformation("Нельзя установить статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}, так как текущий статус {currentStatus}",
|
||||||
updatingStatus, configuration.Id, configuration.TemplateId, taskStatusName);
|
updatingStatus, configuration.Id, configuration.TemplateId, taskStatusName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -63,13 +65,13 @@ namespace PARR.DAL.Repositories
|
|||||||
// есть ли связь у config с templetes, может инклуда нет, мало ли
|
// есть ли связь у config с templetes, может инклуда нет, мало ли
|
||||||
if (configuration.Template == null)
|
if (configuration.Template == null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("При изменении статуса задания на обновление шаблона, не смог проверить наличае ScheduleEsppId, так как нет Include с Templates. Пропустил эту проверку. configurationId: {configurationId}", configuration.Id);
|
_logger.LogWarning("При изменении статуса задания на обновление шаблона, не смог проверить наличае ScheduleEsppId, так как нет Include с Templates. Пропустил эту проверку. configurationId: {configurationId}", configuration.Id);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (configuration.Template.ScheduleEsppId == null)
|
if (configuration.Template.ScheduleEsppId == null)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Нельзя установить статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}, так как у шаблона отсутсвтует ScheduleEsppId=null",
|
_logger.LogInformation("Нельзя установить статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}, так как у шаблона отсутсвтует ScheduleEsppId=null",
|
||||||
updatingStatus, configuration.Id, configuration.TemplateId);
|
updatingStatus, configuration.Id, configuration.TemplateId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -78,7 +80,7 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
// Статус ОК, можно ставить Updating
|
// Статус ОК, можно ставить Updating
|
||||||
ChangeTaskStatus(updatingStatus, configuration);
|
ChangeTaskStatus(updatingStatus, configuration);
|
||||||
logger.LogInformation("Установлен статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}", updatingStatus, configuration.Id, configuration.TemplateId);
|
_logger.LogInformation("Установлен статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}", updatingStatus, configuration.Id, configuration.TemplateId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -94,8 +96,8 @@ namespace PARR.DAL.Repositories
|
|||||||
configuration.AttemptsNumber++;
|
configuration.AttemptsNumber++;
|
||||||
configuration.LastRobotStatusUpdated = DateTimeOffset.UtcNow;
|
configuration.LastRobotStatusUpdated = DateTimeOffset.UtcNow;
|
||||||
break;
|
break;
|
||||||
//case RobotStatusEnum.Error:
|
case RobotStatusEnum.Error:
|
||||||
// break;
|
break;
|
||||||
case RobotStatusEnum.Complete:
|
case RobotStatusEnum.Complete:
|
||||||
configuration.LastRobotStatusUpdated = DateTimeOffset.UtcNow;
|
configuration.LastRobotStatusUpdated = DateTimeOffset.UtcNow;
|
||||||
break;
|
break;
|
||||||
@@ -108,6 +110,14 @@ namespace PARR.DAL.Repositories
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetErrorRobotStatusAndMaxAttempts(RobotConfiguration configuration)
|
||||||
|
{
|
||||||
|
ChangeRobotStatus(RobotStatusEnum.Error, configuration);
|
||||||
|
|
||||||
|
configuration.AttemptsNumber = settingsFromDb.RobotAttemptsNumber;
|
||||||
|
configuration.LastRobotStatusUpdated = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<bool> SetInProgressStatusAsync(Guid id)
|
public async Task<bool> SetInProgressStatusAsync(Guid id)
|
||||||
{
|
{
|
||||||
@@ -144,7 +154,7 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
if (config == null)
|
if (config == null)
|
||||||
{
|
{
|
||||||
logger.LogError($"У шаблона нет конфигурации роботов. TemplateId: {template.Id}");
|
_logger.LogError($"У шаблона нет конфигурации роботов. TemplateId: {template.Id}");
|
||||||
throw new Exception($"У шаблона нет конфигурации роботов. TemplateId: {template.Id}");
|
throw new Exception($"У шаблона нет конфигурации роботов. TemplateId: {template.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,25 +170,82 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
var endDate = DateTimeOffset.UtcNow.Add(-robotWaitTime);
|
var endDate = DateTimeOffset.UtcNow.Add(-robotWaitTime);
|
||||||
|
|
||||||
var configObjs = await EntitySet.Where(t =>
|
var expiredConfigs = await EntitySet.Where(t =>
|
||||||
t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
||||||
&& t.AttemptsNumber >= robotAttemptsNumber
|
&& t.AttemptsNumber >= robotAttemptsNumber
|
||||||
&& t.LastRobotStatusUpdated <= endDate
|
&& t.LastRobotStatusUpdated <= endDate
|
||||||
).ToListAsync();
|
).ToListAsync();
|
||||||
|
|
||||||
if (!configObjs.Any())
|
if (!expiredConfigs.Any())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
configObjs.ForEach(item =>
|
foreach (var item in expiredConfigs)
|
||||||
{
|
{
|
||||||
ChangeRobotStatus(RobotStatusEnum.Error, item);
|
ChangeRobotStatus(RobotStatusEnum.Error, item);
|
||||||
logger.LogInformation($"Устанавливаю RobotStatus: {RobotStatusEnum.Error} для RobotConfigurationId {item.Id}");
|
_logger.LogInformation("Устанавливаю статус RobotStatus: {RobotStatus} для RobotConfigurationId: {RobotConfigurationId}", RobotStatusEnum.Error, item.Id);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
#region Ищем, есть ли связанные шаблоны, которые должны переименоваться, им тоже нужно установить статус ошибки, но только для Шаблонов
|
||||||
|
|
||||||
|
// Проактивная обработка связанных шаблонов переименования Old->New
|
||||||
|
// Если старый шаблон умен, мы должны сразу убить (!!!замочить!!!) и новый (целевой), чтобы он не висел вечно в ожидании.
|
||||||
|
|
||||||
|
var expiredTemplateIds = expiredConfigs
|
||||||
|
.Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder)
|
||||||
|
.Select(t => t.TemplateId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (expiredTemplateIds.Any())
|
||||||
|
{
|
||||||
|
// Находим OldName для этих шаблонов из таблицы переименований.
|
||||||
|
var oldNamesToFail = await EntityContext.Templates
|
||||||
|
.Where(t => expiredTemplateIds.Contains(t.Id) && t.TemplateRenamePending != null)
|
||||||
|
.Select(t => t.TemplateRenamePending!.OldName)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Находим целевые (новые задачи), имена которых совпадают с найденными OldName
|
||||||
|
if (oldNamesToFail.Any())
|
||||||
|
{
|
||||||
|
var targetConfigs = await EntitySet
|
||||||
|
.Where(t =>
|
||||||
|
t.RobotCode == (int)RobotsEnum.TemplateOrder
|
||||||
|
&& t.RobotStatusCode != (int)RobotStatusEnum.Error // Не трогаем те, что уже в ошибке
|
||||||
|
&& oldNamesToFail.Contains(t.Template!.Name)
|
||||||
|
).ToListAsync();
|
||||||
|
|
||||||
|
foreach (var item in targetConfigs)
|
||||||
|
{
|
||||||
|
SetErrorRobotStatusAndMaxAttempts(item);
|
||||||
|
|
||||||
|
// Пишем в лог роботу
|
||||||
|
var history = new RobotHistory
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
HistoryLevel = (int)RobotStatusEnum.Error,
|
||||||
|
TaskStatusCode = item.TaskStatusCode,
|
||||||
|
RobotConfigurationId = item.Id,
|
||||||
|
RobotIp = null,
|
||||||
|
RobotId = ParrComponentsEnum.Api.ToString(),
|
||||||
|
RobotMessage = "[RobotConfigurationRepository] Установлен статус ошибки, так как не переименован связанный шаблон"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Синхронный Add работает быстрее и безопаснее внутри цикла
|
||||||
|
EntityContext.RobotHistories.Add(history);
|
||||||
|
|
||||||
|
_logger.LogInformation("Проактивно установлен статус {Status} для целевого задания RobotConfigurationID: {Id} из-за ошибки старого шаблона.", RobotStatusEnum.Error, item.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
var result = await CommitAsync();
|
var result = await CommitAsync();
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
logger.LogError($"Ошибка при сохранении изменений RobotStatus для RobotConfigurationId: item.Id, RobotStatus: {RobotStatusEnum.Error}");
|
_logger.LogError("Ошибка при сохранении изменений RobotStatus для просроченных заданий. Откат транзакции.");
|
||||||
|
//else
|
||||||
|
// logger.LogInformation("Успешно обработано и переведено в статус Ошибки просроченных заданий: {Count} шт.", configObjs.Count + linksCount);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.DAL.Repositories
|
namespace PARR.DAL.Repositories
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
using PARR.DAL.Repositories.Base;
|
using PARR.DAL.Repositories.Base;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.DAL.Repositories
|
namespace PARR.DAL.Repositories
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.DAL.Repositories
|
namespace PARR.DAL.Repositories
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
|
|
||||||
namespace PARR.DAL.Repositories
|
namespace PARR.DAL.Repositories
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace PARR.DAL.Repositories
|
|||||||
this.dataContext = dataContext;
|
this.dataContext = dataContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IQueryable<Domain.Entities.TaskStatus> Get()
|
public IQueryable<Domain.Entities.RobotEntities.TaskStatus> Get()
|
||||||
{
|
{
|
||||||
return dataContext.TaskStatuses;
|
return dataContext.TaskStatuses;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace PARR.DAL.Repositories
|
|||||||
this.dataContext = dataContext;
|
this.dataContext = dataContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IQueryable<Domain.Entities.TaskStatus> Get()
|
public IQueryable<Domain.Entities.RobotEntities.TaskStatus> Get()
|
||||||
{
|
{
|
||||||
return dataContext.TaskStatuses;
|
return dataContext.TaskStatuses;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces.TemplateRepositories;
|
||||||
|
using PARR.DAL.Context;
|
||||||
|
using PARR.Domain.Entities.TemplateEntities;
|
||||||
|
|
||||||
|
namespace PARR.DAL.Repositories.TemplateRepositories
|
||||||
|
{
|
||||||
|
internal class TemplateRenamePendingRepository : ITemplateRenamePendingRepository
|
||||||
|
{
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
private readonly ILogger<TemplateRenamePendingRepository> _logger;
|
||||||
|
|
||||||
|
public TemplateRenamePendingRepository(
|
||||||
|
DataContext dataContext,
|
||||||
|
ILogger<TemplateRenamePendingRepository> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_dataContext = dataContext;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IQueryable<TemplateRenamePending> Get()
|
||||||
|
{
|
||||||
|
return _dataContext.TemplateRenamePendings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(TemplateRenamePending obj)
|
||||||
|
{
|
||||||
|
_dataContext.TemplateRenamePendings.Remove(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool> CreateAsync(TemplateRenamePending obj)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dataContext.TemplateRenamePendings.AddAsync(obj);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Ошибка при добавлении объекта типа TemplateRenamePending в БД");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
public async Task<Template?> GetTemplateByNameAsync(string name)
|
public async Task<Template?> GetTemplateByNameAsync(string name)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Поиск шаблона по имени: {TemplateName}", name);
|
_logger.LogDebug("Поиск шаблона по имени: {TemplateName}", name);
|
||||||
|
|
||||||
var template = await GetWithIncludes()
|
var template = await GetWithIncludes()
|
||||||
.Include(t => t.RobotConfigurations)
|
.Include(t => t.RobotConfigurations)
|
||||||
@@ -24,11 +24,11 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
if (template != null)
|
if (template != null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Шаблон найден: {TemplateId}, имя: {TemplateName}", template.Id, template.Name);
|
_logger.LogDebug("Шаблон найден: {TemplateId}, имя: {TemplateName}", template.Id, template.Name);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Шаблон с именем {TemplateName} не найден", name);
|
_logger.LogDebug("Шаблон с именем {TemplateName} не найден", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return template;
|
return template;
|
||||||
@@ -36,7 +36,7 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
public IQueryable<Template> GetWithIncludes()
|
public IQueryable<Template> GetWithIncludes()
|
||||||
{
|
{
|
||||||
logger.LogDebug("Получаю шаблоны с include связями");
|
_logger.LogDebug("Получаю шаблоны с include связями");
|
||||||
|
|
||||||
return Get()
|
return Get()
|
||||||
.Include(h => h.Unit)
|
.Include(h => h.Unit)
|
||||||
@@ -60,7 +60,7 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
public override Task<bool> CreateAsync(Template obj)
|
public override Task<bool> CreateAsync(Template obj)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Создание шаблона: {TemplateName}", obj.Name);
|
_logger.LogDebug("Создание шаблона: {TemplateName}", obj.Name);
|
||||||
|
|
||||||
// добавление роботов для шаблона
|
// добавление роботов для шаблона
|
||||||
obj.RobotConfigurations = new List<RobotConfiguration>
|
obj.RobotConfigurations = new List<RobotConfiguration>
|
||||||
@@ -89,9 +89,10 @@ namespace PARR.DAL.Repositories
|
|||||||
AttemptsNumber = 0,
|
AttemptsNumber = 0,
|
||||||
LastRobotStatusUpdated = null
|
LastRobotStatusUpdated = null
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.LogDebug("Добавлены роботы для шаблона {TemplateName}", obj.Name);
|
_logger.LogDebug("Добавлены роботы для шаблона {TemplateName}", obj.Name);
|
||||||
|
|
||||||
return base.CreateAsync(obj);
|
return base.CreateAsync(obj);
|
||||||
}
|
}
|
||||||
@@ -99,83 +100,85 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
public async Task<Guid?> ReserveUnusedTemplateAsync(Guid newUnitId, HistoryInitiator initiator)
|
public async Task<Guid?> ReserveUnusedTemplateAsync(Guid newUnitId, HistoryInitiator initiator)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Резервирую неиспользуемый шаблон с проверкой конфигураций роботов для UnitId: {UnitId}", newUnitId);
|
_logger.LogDebug("Резервирую неиспользуемый шаблон для UnitId: {UnitId}", newUnitId);
|
||||||
|
|
||||||
var sql = @"
|
// Явная транзакция гарантирует атомарность UPDATE + подзапроса
|
||||||
UPDATE ""Templates""
|
await using var transaction = await EntityContext.Database.BeginTransactionAsync();
|
||||||
SET ""StatusTypeId"" = @NewStatus,
|
|
||||||
""DateModified"" = @DateModified,
|
|
||||||
""InitiatorIp"" = @InitiatorIp,
|
|
||||||
""InitiatorParrComponentId"" = @InitiatorComponent,
|
|
||||||
""InitiatorComment"" = @InitiatorComment
|
|
||||||
WHERE ""Id"" = (
|
|
||||||
SELECT t.""Id""
|
|
||||||
FROM ""Templates"" t
|
|
||||||
WHERE t.""StatusTypeId"" = @OldStatus
|
|
||||||
AND t.""UnitId"" != @NewUnitId
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM ""RobotConfigurations"" rc
|
|
||||||
WHERE rc.""TemplateId"" = t.""Id""
|
|
||||||
AND rc.""RobotCode"" = @RobotCode1
|
|
||||||
AND rc.""TaskStatusCode"" = @TaskStatus
|
|
||||||
AND rc.""RobotStatusCode"" = @RobotStatus
|
|
||||||
)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM ""RobotConfigurations"" rc
|
|
||||||
WHERE rc.""TemplateId"" = t.""Id""
|
|
||||||
AND rc.""RobotCode"" = @RobotCode2
|
|
||||||
AND rc.""TaskStatusCode"" = @TaskStatus
|
|
||||||
AND rc.""RobotStatusCode"" = @RobotStatus
|
|
||||||
)
|
|
||||||
ORDER BY t.""DateCreated"" ASC
|
|
||||||
LIMIT 1
|
|
||||||
)
|
|
||||||
RETURNING ""Id"";";
|
|
||||||
|
|
||||||
var parameters = new[]
|
|
||||||
{
|
|
||||||
new NpgsqlParameter("@NewStatus", (int)TemplateStatusTypeEnum.Updating),
|
|
||||||
new NpgsqlParameter("@DateModified", DateTimeOffset.UtcNow),
|
|
||||||
new NpgsqlParameter("@InitiatorIp", initiator.InitiatorIp ?? (object)DBNull.Value),
|
|
||||||
new NpgsqlParameter("@InitiatorComponent",
|
|
||||||
initiator.InitiatorParrComponentId.HasValue
|
|
||||||
? (object)(int)initiator.InitiatorParrComponentId.Value
|
|
||||||
: DBNull.Value),
|
|
||||||
new NpgsqlParameter("@InitiatorComment", initiator.InitiatorComment ?? (object)DBNull.Value),
|
|
||||||
new NpgsqlParameter("@OldStatus", (int)TemplateStatusTypeEnum.Unused),
|
|
||||||
new NpgsqlParameter("@NewUnitId", newUnitId),
|
|
||||||
// Параметры для проверки конфигураций роботов
|
|
||||||
new NpgsqlParameter("@RobotCode1", (int)RobotsEnum.TemplateOrder),
|
|
||||||
new NpgsqlParameter("@RobotCode2", (int)RobotsEnum.ScheduleOrder),
|
|
||||||
new NpgsqlParameter("@TaskStatus", (int)TaskStatusEnum.Ok),
|
|
||||||
new NpgsqlParameter("@RobotStatus", (int)RobotStatusEnum.Complete)
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var sql = @"
|
||||||
|
UPDATE ""Templates""
|
||||||
|
SET ""StatusTypeId"" = @NewStatus,
|
||||||
|
""DateModified"" = @DateModified,
|
||||||
|
""InitiatorIp"" = @InitiatorIp,
|
||||||
|
""InitiatorParrComponentId"" = @InitiatorComponent,
|
||||||
|
""InitiatorComment"" = @InitiatorComment
|
||||||
|
WHERE ""Id"" = (
|
||||||
|
SELECT t.""Id""
|
||||||
|
FROM ""Templates"" t
|
||||||
|
WHERE t.""StatusTypeId"" = @OldStatus
|
||||||
|
AND t.""UnitId"" != @NewUnitId
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM ""RobotConfigurations"" rc
|
||||||
|
WHERE rc.""TemplateId"" = t.""Id""
|
||||||
|
AND rc.""RobotCode"" = @RobotCode1
|
||||||
|
AND rc.""TaskStatusCode"" = @TaskStatus
|
||||||
|
AND rc.""RobotStatusCode"" = @RobotStatus
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM ""RobotConfigurations"" rc
|
||||||
|
WHERE rc.""TemplateId"" = t.""Id""
|
||||||
|
AND rc.""RobotCode"" = @RobotCode2
|
||||||
|
AND rc.""TaskStatusCode"" = @TaskStatus
|
||||||
|
AND rc.""RobotStatusCode"" = @RobotStatus
|
||||||
|
)
|
||||||
|
ORDER BY t.""DateModified"" ASC NULLS FIRST
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
)
|
||||||
|
RETURNING ""Id"";";
|
||||||
|
|
||||||
|
var parameters = new[]
|
||||||
|
{
|
||||||
|
new NpgsqlParameter("@NewStatus", (int)TemplateStatusTypeEnum.Updating),
|
||||||
|
new NpgsqlParameter("@DateModified", DateTimeOffset.UtcNow),
|
||||||
|
new NpgsqlParameter("@InitiatorIp", initiator.InitiatorIp ?? (object)DBNull.Value),
|
||||||
|
new NpgsqlParameter("@InitiatorComponent",
|
||||||
|
initiator.InitiatorParrComponentId.HasValue
|
||||||
|
? (object)(int)initiator.InitiatorParrComponentId.Value
|
||||||
|
: DBNull.Value),
|
||||||
|
new NpgsqlParameter("@InitiatorComment", initiator.InitiatorComment ?? (object)DBNull.Value),
|
||||||
|
new NpgsqlParameter("@OldStatus", (int)TemplateStatusTypeEnum.Unused),
|
||||||
|
new NpgsqlParameter("@NewUnitId", newUnitId),
|
||||||
|
new NpgsqlParameter("@RobotCode1", (int)RobotsEnum.TemplateOrder),
|
||||||
|
new NpgsqlParameter("@RobotCode2", (int)RobotsEnum.ScheduleOrder),
|
||||||
|
new NpgsqlParameter("@TaskStatus", (int)TaskStatusEnum.Ok),
|
||||||
|
new NpgsqlParameter("@RobotStatus", (int)RobotStatusEnum.Complete)
|
||||||
|
};
|
||||||
|
|
||||||
var result = await EntityContext.Database
|
var result = await EntityContext.Database
|
||||||
.SqlQueryRaw<Guid>(sql, parameters)
|
.SqlQueryRaw<Guid>(sql, parameters)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
var reservedTemplateId = result.FirstOrDefault();
|
var reservedTemplateId = result.FirstOrDefault();
|
||||||
|
|
||||||
if (reservedTemplateId != Guid.Empty)
|
if (reservedTemplateId != Guid.Empty)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Успешно зарезервирован шаблон с ID: {TemplateId} для UnitId: {UnitId}",
|
_logger.LogInformation("Успешно зарезервирован шаблон с ID: {TemplateId} для UnitId: {UnitId}",
|
||||||
reservedTemplateId, newUnitId);
|
reservedTemplateId, newUnitId);
|
||||||
return reservedTemplateId;
|
return reservedTemplateId;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
_logger.LogDebug("Не удалось зарезервировать шаблон для UnitId: {UnitId}", newUnitId);
|
||||||
logger.LogDebug("Не удалось зарезервировать шаблон для UnitId: {UnitId} (не найдено подходящих конфигураций роботов)", newUnitId);
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при резервировании шаблона для UnitId: {UnitId}", newUnitId);
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "Ошибка при резервировании шаблона для UnitId: {UnitId}", newUnitId);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,18 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
{
|
{
|
||||||
public UnitFieldValueRepository(DataContext dataContext, ILogger<UnitFieldValueRepository> logger) : base(logger, dataContext) { }
|
public UnitFieldValueRepository(DataContext dataContext, ILogger<UnitFieldValueRepository> logger) : base(logger, dataContext) { }
|
||||||
|
|
||||||
|
public async Task<List<Guid>> FindValueIdsByMaskAsync(
|
||||||
public async Task<UnitFieldValue?> GetByValueNameAsync(string? value)
|
string mask,
|
||||||
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var query = EntitySet
|
var query = EntitySet.AsNoTracking();
|
||||||
.Include(v => v.FieldValues);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
var valueIds = await query
|
||||||
return await query.FirstOrDefaultAsync(uf => uf.Value == null);
|
.Where(v => EF.Functions.ILike(v.Value!, mask))
|
||||||
|
.Select(v => v.Id)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
return await query.FirstOrDefaultAsync(uf => uf.Value!.ToLower().Trim() == value.ToLower().Trim());
|
return valueIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,38 +27,14 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId)
|
public async Task<List<Guid>> GetRelatedUnitIdsAsync(Guid unitId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return dataContext.UnitInUnits
|
return await Get()
|
||||||
.Where(u => u.ParentUnitId == parentId)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId)
|
|
||||||
{
|
|
||||||
return dataContext.UnitInUnits
|
|
||||||
.Where(u => u.ChildUnitId == childId)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds)
|
|
||||||
{
|
|
||||||
var set = childUnitIds.ToHashSet();
|
|
||||||
return await dataContext.UnitInUnits
|
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(uinu => set.Contains(uinu.ChildUnitId))
|
.Where(link => link.ChildUnitId == unitId || link.ParentUnitId == unitId)
|
||||||
.ToListAsync();
|
.Select(link => link.ChildUnitId == unitId ? link.ParentUnitId : link.ChildUnitId)
|
||||||
}
|
.Distinct()
|
||||||
|
.ToListAsync(ct);
|
||||||
public async Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds)
|
|
||||||
{
|
|
||||||
var set = parentUnitIds.ToHashSet();
|
|
||||||
return await dataContext.UnitInUnits
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(uinu => set.Contains(uinu.ParentUnitId))
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
{
|
{
|
||||||
public UnitRepository(DataContext dataContext, ILogger<UnitRepository> logger) : base(logger, dataContext) { }
|
public UnitRepository(DataContext dataContext, ILogger<UnitRepository> logger) : base(logger, dataContext) { }
|
||||||
|
|
||||||
|
|
||||||
public IQueryable<Domain.Entities.Unit.Unit> GetWithIncludes()
|
public IQueryable<Domain.Entities.Unit.Unit> GetWithIncludes()
|
||||||
{
|
{
|
||||||
return Get()
|
return Get()
|
||||||
@@ -22,40 +21,30 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IQueryable<PARR.Domain.Entities.Unit.Unit> GetUnitByFieldAndValue(IQueryable<PARR.Domain.Entities.Unit.Unit> query, Guid fieldId, string valueMask, bool isInverse = false)
|
|
||||||
{
|
|
||||||
//TODO: вынесено из UnitFilterService
|
|
||||||
|
|
||||||
if (isInverse)
|
|
||||||
{
|
|
||||||
query = query.Where(u => !u.UnitValues.Any(v =>
|
|
||||||
v.FieldId == fieldId &&
|
|
||||||
EF.Functions.ILike(v.Value.Value, valueMask)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
query = query.Where(u => u.UnitValues.Any(v =>
|
|
||||||
v.FieldId == fieldId &&
|
|
||||||
EF.Functions.ILike(v.Value.Value, valueMask)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IQueryable<Guid> GetInitialUnitIds(string dbValueMask)
|
public IQueryable<Guid> GetInitialUnitIds(string dbValueMask)
|
||||||
{
|
{
|
||||||
//TODO: вынесено из UnitFilterService
|
|
||||||
|
|
||||||
//var initialUnitIds = await unitService.Get().AsNoTracking()
|
|
||||||
//.Where(unit => EF.Functions.ILike(unit.Name, dbValueMask))
|
|
||||||
//.Select(u => u.Id)
|
|
||||||
//.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Get().AsNoTracking()
|
return Get().AsNoTracking()
|
||||||
.Where(unit => EF.Functions.ILike(unit.Name, dbValueMask))
|
.Where(unit => EF.Functions.ILike(unit.Name, dbValueMask))
|
||||||
.Select(u => u.Id);
|
.Select(u => u.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<Guid>> FindUnitIdsByValueIdsAsync(
|
||||||
|
IReadOnlyList<Guid> unitIds,
|
||||||
|
Guid fieldId,
|
||||||
|
IReadOnlyList<Guid> valueIds,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (valueIds.Count == 0)
|
||||||
|
return new List<Guid>();
|
||||||
|
|
||||||
|
return await EntitySet.AsNoTracking()
|
||||||
|
.Where(u => unitIds.Contains(u.Id))
|
||||||
|
.Where(u => u.UnitValues.Any(uv =>
|
||||||
|
uv.FieldId == fieldId &&
|
||||||
|
valueIds.Contains(uv.ValueId)))
|
||||||
|
.Select(u => u.Id)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ namespace PARR.DAL.Repositories
|
|||||||
var exist = await GetAsync(id);
|
var exist = await GetAsync(id);
|
||||||
if (exist == null)
|
if (exist == null)
|
||||||
{
|
{
|
||||||
logger.LogError($"Ошибка при удалении из БД. Не найдена запись в БД с id: {id}");
|
_logger.LogError($"Ошибка при удалении из БД. Не найдена запись в БД с id: {id}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,19 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Task = "task";
|
public const string Task = "task";
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Робот
|
/// Робот
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Robot = "robot";
|
public const string Robot = "robot";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Шаблоны
|
||||||
|
/// </summary>
|
||||||
|
public const string Template = "template";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Схема public
|
||||||
|
/// </summary>
|
||||||
|
public const string Public = "public";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
PARR.Domain/DTOs/RobotMetrics/FilteredChartPoint.cs
Normal file
9
PARR.Domain/DTOs/RobotMetrics/FilteredChartPoint.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по роботам, согласно гибким фильтрам.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Timestamp"></param>
|
||||||
|
/// <param name="Count"></param>
|
||||||
|
public record FilteredChartPoint(DateTimeOffset Timestamp, int Count);
|
||||||
|
}
|
||||||
16
PARR.Domain/DTOs/RobotMetrics/MetricFilter.cs
Normal file
16
PARR.Domain/DTOs/RobotMetrics/MetricFilter.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
public record MetricFilter
|
||||||
|
{
|
||||||
|
public RobotsEnum? RobotCode { get; init; }
|
||||||
|
public RobotStatusEnum? RobotStatusCode { get; init; }
|
||||||
|
public TaskStatusEnum? TaskStatusCode { get; init; }
|
||||||
|
public DateTimeOffset? DateFrom { get; init; }
|
||||||
|
public DateTimeOffset? DateTo { get; init; }
|
||||||
|
|
||||||
|
// Шаг группировки в минутах (например, 2, 30, 60, 1440)
|
||||||
|
public int IntervalMinutes { get; set; } = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
PARR.Domain/DTOs/RobotMetrics/RobotStatusChartPoint.cs
Normal file
7
PARR.Domain/DTOs/RobotMetrics/RobotStatusChartPoint.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Робот Сатусам, на графике
|
||||||
|
/// </summary>
|
||||||
|
public record RobotStatusChartPoint(DateTimeOffset Timestamp, int Wait, int InProgress, int Error, int Complete);
|
||||||
|
}
|
||||||
11
PARR.Domain/DTOs/RobotMetrics/TaskStatusChartPoint.cs
Normal file
11
PARR.Domain/DTOs/RobotMetrics/TaskStatusChartPoint.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Статусам заданий роботам, на графике
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Timestamp"></param>
|
||||||
|
/// <param name="Creating"></param>
|
||||||
|
/// <param name="Updating"></param>
|
||||||
|
/// <param name="Ok"></param>
|
||||||
|
public record TaskStatusChartPoint(DateTimeOffset Timestamp, int Creating, int Updating, int Ok);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
|
||||||
|
namespace PARR.Domain.DTOs.RobotStatusDetails
|
||||||
|
{
|
||||||
|
public record RobotStatusDetailsResult
|
||||||
|
{
|
||||||
|
public RobotResult Robot { get; init; } = null!;
|
||||||
|
public RobotStatusResult Status { get; init; } = null!;
|
||||||
|
|
||||||
|
public List<RobotStatusGroupDetailsResult> Details { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RobotStatusGroupDetailsResult
|
||||||
|
{
|
||||||
|
public JobGroupShortResult JobGroup { get; init; } = null!;
|
||||||
|
public int TemplatesCount { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
PARR.Domain/DTOs/RobotTaskDetails/RobotTaskDetailsResult.cs
Normal file
18
PARR.Domain/DTOs/RobotTaskDetails/RobotTaskDetailsResult.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
|
||||||
|
namespace PARR.Domain.DTOs.RobotTaskDetails
|
||||||
|
{
|
||||||
|
public record RobotTaskDetailsResult
|
||||||
|
{
|
||||||
|
public RobotResult Robot { get; init; } = null!;
|
||||||
|
public RobotTaskStatusResult Task { get; init; } = null!;
|
||||||
|
|
||||||
|
public List<RobotTaskGroupDetailsResult> Details { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RobotTaskGroupDetailsResult
|
||||||
|
{
|
||||||
|
public JobGroupShortResult JobGroup { get; init; } = null!;
|
||||||
|
public int TemplatesCount { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
PARR.Domain/DTOs/RobotTaskRobotStatus/ChangeRobotStatus.cs
Normal file
13
PARR.Domain/DTOs/RobotTaskRobotStatus/ChangeRobotStatus.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Domain.DTOs.RobotTaskRobotStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Изменить статус задания робота
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="TaskId"></param>
|
||||||
|
/// <param name="RobotStatusCode"></param>
|
||||||
|
/// <param name="RobotId">Идентификатор робота</param>
|
||||||
|
/// <param name="RobotIp">IP адрес робота</param>
|
||||||
|
public record ChangeRobotStatus(Guid TaskId, RobotStatusEnum RobotStatusCode, string? RobotId, string? RobotIp);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using PARR.Domain.DTOs.Shared;
|
||||||
|
|
||||||
|
namespace PARR.Domain.DTOs.RobotTaskRobotStatus
|
||||||
|
{
|
||||||
|
public record RobotConfigurationResult
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset DateCreated { get; init; }
|
||||||
|
|
||||||
|
public Guid TemplateId { get; init; }
|
||||||
|
|
||||||
|
public RobotResult? Robot { get; init; }
|
||||||
|
|
||||||
|
public RobotTaskStatusResult? TaskStatus { get; init; }
|
||||||
|
|
||||||
|
public RobotStatusResult? RobotStatus { get; init; }
|
||||||
|
|
||||||
|
public int AttemptsNumber { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset? LastRobotStatusUpdated { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
15
PARR.Domain/DTOs/Shared/JobGroupResult.cs
Normal file
15
PARR.Domain/DTOs/Shared/JobGroupResult.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace PARR.Domain.DTOs.Shared
|
||||||
|
{
|
||||||
|
public record JobGroupShortResult
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public required string GroupName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public record JobGroupResult : JobGroupShortResult
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
9
PARR.Domain/DTOs/Shared/RobotResult.cs
Normal file
9
PARR.Domain/DTOs/Shared/RobotResult.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Domain.DTOs.Shared
|
||||||
|
{
|
||||||
|
public record RobotResult
|
||||||
|
{
|
||||||
|
public int Code { get; init; }
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required string Description { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
PARR.Domain/DTOs/Shared/RobotStatusResult.cs
Normal file
9
PARR.Domain/DTOs/Shared/RobotStatusResult.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Domain.DTOs.Shared
|
||||||
|
{
|
||||||
|
public record RobotStatusResult
|
||||||
|
{
|
||||||
|
public int Code { get; init; }
|
||||||
|
public string? Name { get; init; }
|
||||||
|
public string? Description { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
PARR.Domain/DTOs/Shared/RobotTaskStatusResult.cs
Normal file
9
PARR.Domain/DTOs/Shared/RobotTaskStatusResult.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Domain.DTOs.Shared
|
||||||
|
{
|
||||||
|
public record RobotTaskStatusResult
|
||||||
|
{
|
||||||
|
public int Code { get; init; }
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required string Description { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PARR.Domain.Entities.Base;
|
using PARR.Domain.Entities.Base;
|
||||||
|
using PARR.Domain.Entities.RobotEntities;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
@@ -9,6 +10,8 @@ namespace PARR.Domain.Entities
|
|||||||
[Index(nameof(TemplateId), nameof(RobotCode), IsUnique = true)]
|
[Index(nameof(TemplateId), nameof(RobotCode), IsUnique = true)]
|
||||||
[Index(nameof(TemplateId), nameof(TaskStatusCode))]
|
[Index(nameof(TemplateId), nameof(TaskStatusCode))]
|
||||||
[Index(nameof(TemplateId), nameof(RobotStatusCode))]
|
[Index(nameof(TemplateId), nameof(RobotStatusCode))]
|
||||||
|
[Index(nameof(RobotCode), nameof(TaskStatusCode))]
|
||||||
|
[Index(nameof(RobotCode), nameof(RobotStatusCode))]
|
||||||
public class RobotConfiguration : IBaseEntity
|
public class RobotConfiguration : IBaseEntity
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
@@ -53,7 +56,7 @@ namespace PARR.Domain.Entities
|
|||||||
public Robot? Robot { get; set; }
|
public Robot? Robot { get; set; }
|
||||||
|
|
||||||
[ForeignKey(nameof(TaskStatusCode))]
|
[ForeignKey(nameof(TaskStatusCode))]
|
||||||
public TaskStatus? TaskStatus { get; set; }
|
public RobotEntities.TaskStatus? TaskStatus { get; set; }
|
||||||
|
|
||||||
[ForeignKey(nameof(RobotStatusCode))]
|
[ForeignKey(nameof(RobotStatusCode))]
|
||||||
public RobotStatus? RobotStatus { get; set; }
|
public RobotStatus? RobotStatus { get; set; }
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PARR.Domain.Entities.RobotEntities;
|
using PARR.Domain.Constants;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace PARR.Domain.Entities
|
namespace PARR.Domain.Entities.RobotEntities
|
||||||
{
|
{
|
||||||
[Table("Robots")]
|
[Table("Robots", Schema = DatabaseSchemas.Robot)]
|
||||||
[Index(nameof(Name), IsUnique = true)]
|
[Index(nameof(Name), IsUnique = true)]
|
||||||
public class Robot
|
public class Robot
|
||||||
{
|
{
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user