Compare commits
12 Commits
4c80e247f5
...
98245e73e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98245e73e6 | ||
|
|
cdcb4fd9bc | ||
|
|
bbb14ee4ea | ||
|
|
5edbcff35b | ||
|
|
6c93e1971f | ||
|
|
68696a2fdc | ||
|
|
751b693e72 | ||
|
|
6ca6bfbc2e | ||
|
|
178a991d8b | ||
|
|
a087958fd5 | ||
|
|
4ee2880a8f | ||
|
|
497d241caf |
@@ -1,236 +1,261 @@
|
|||||||
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;
|
||||||
using PARR.AIHITRelationshipsSyncer.Settings;
|
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.Domain.Entities.Unit;
|
using PARR.Domain.Entities.Unit;
|
||||||
|
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 WorkerSettings workerSettings;
|
_settingsFromDb = settingsFromDb;
|
||||||
|
|
||||||
public RelationshipsSyncService(
|
|
||||||
ILogger<RelationshipsSyncService> logger,
|
|
||||||
IUnitRepository unitService,
|
|
||||||
WorkerSettings workerSettings
|
|
||||||
)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.unitService = unitService;
|
|
||||||
this.workerSettings = workerSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task SyncAsync(List<AihitData> aihitdata)
|
|
||||||
{
|
|
||||||
var validPairs = ValidateInput(aihitdata);
|
|
||||||
if (!validPairs.Any()) return;
|
|
||||||
|
|
||||||
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/
|
||||||
|
|
||||||
@@ -224,6 +226,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 +327,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using PARR.API.Contracts.V1.Responses.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.RobotTask;
|
using PARR.Domain.DTOs.RobotTask;
|
||||||
using PARR.Domain.DTOs.Shortcode;
|
using PARR.Domain.DTOs.Shortcode;
|
||||||
@@ -500,6 +501,14 @@ namespace PARR.API.MappingProfiles
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region StatRobotMetrics
|
||||||
|
|
||||||
|
CreateMap<TaskStatusChartPoint, StatTaskStatusChartPoint>();
|
||||||
|
CreateMap<RobotStatusChartPoint, StatRobotStatusChartPoint>();
|
||||||
|
CreateMap<FilteredChartPoint, StatFilteredChartPoint>();
|
||||||
|
|
||||||
|
#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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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.RobotTask.Implementations;
|
using PARR.Core.Services.RobotTask.Implementations;
|
||||||
using PARR.Core.Services.RobotTask.Interfaces;
|
using PARR.Core.Services.RobotTask.Interfaces;
|
||||||
@@ -28,7 +29,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;
|
||||||
|
|
||||||
@@ -115,6 +115,8 @@ namespace PARR.Core
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ namespace PARR.Core.Services.UnitFilterService.Matchers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class UnitFieldMatcher : IUnitFieldMatcher
|
internal class UnitFieldMatcher : IUnitFieldMatcher
|
||||||
{
|
{
|
||||||
private const int chunkSize = 1000;
|
private const int chunkSize = 200;
|
||||||
private readonly IUnitRepository unitRepository;
|
private readonly IUnitRepository unitRepository;
|
||||||
private readonly ILogger<UnitFieldMatcher> logger;
|
private readonly ILogger<UnitFieldMatcher> logger;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ 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)
|
public IQueryable<Domain.Entities.Unit.Unit> GetUnitByFieldAndValue(IQueryable<Domain.Entities.Unit.Unit> query, Guid fieldId, string valueMask, bool isInverse = false)
|
||||||
{
|
{
|
||||||
//TODO: вынесено из UnitFilterService
|
//TODO: вынесено из UnitFilterService
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
26
PARR.Domain/Enums/ChartPeriod.cs
Normal file
26
PARR.Domain/Enums/ChartPeriod.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace PARR.Domain.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Периоды для графиков
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum ChartPeriod
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Последние 2 часа
|
||||||
|
/// </summary>
|
||||||
|
TwoHours = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Последние 24 часа
|
||||||
|
/// </summary>
|
||||||
|
TwentyFourHours = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Последняя неделя
|
||||||
|
/// </summary>
|
||||||
|
SevenDays = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,26 +16,53 @@ namespace PARR.TemplateMatcher.Services.Implementations
|
|||||||
bool defaultTemplateState = false,
|
bool defaultTemplateState = false,
|
||||||
bool defaultScheduleState = false)
|
bool defaultScheduleState = false)
|
||||||
{
|
{
|
||||||
// Тип группы определяет источник настроек
|
// Защита от оптимизации: если группу забыли подгрузить, метод честно падает,
|
||||||
var isGroupLevel = jobGroup?.GroupType?.IsJobGroupAutoControl == true;
|
// потому что без GroupType бизнес-логика не может определить уровень управления
|
||||||
|
if (jobGroup == null)
|
||||||
if (isGroupLevel && jobGroup!.AutoControl != null)
|
|
||||||
{
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Критическая ошибка бизнес-логики: Для работы '{job.Name}' (ID: {job.Id}) " +
|
||||||
|
$"не передана группа (null). Нужно добавить '.Include(j => j.Group)'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Проверяем, что разработчики подгрузили GroupType из базы данных
|
||||||
|
if (jobGroup.GroupType == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Для работы '{job.Name}' (ID: {job.Id}) передана группа, " +
|
||||||
|
$"но её GroupType = null. Нужно добавить '.ThenInclude(g => g.GroupType)' в запрос.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Теперь компилятор знает, что jobGroup и GroupType гарантированно не null
|
||||||
|
var isGroupLevel = jobGroup.GroupType.IsJobGroupAutoControl;
|
||||||
|
|
||||||
|
// 2. Сценарий: Управление на уровне Группы Работ
|
||||||
|
if (isGroupLevel)
|
||||||
|
{
|
||||||
|
if (jobGroup.AutoControl == null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"В типе группы '{jobGroup.GroupType.Id}' указано управление " +
|
||||||
|
$"на уровне ГРУППЫ, но у группы '{jobGroup.GroupName}' (ID: {jobGroup.Id}) " +
|
||||||
|
$"отсутствуют настройки автоконтроля (JobGroup.AutoControl равен null)!");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jobGroup.AutoControl.InitUsedTemplateState,
|
jobGroup.AutoControl.InitUsedTemplateState,
|
||||||
jobGroup.AutoControl.InitUsedScheduleState
|
jobGroup.AutoControl.InitUsedScheduleState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGroupLevel && job.AutoControl != null)
|
// 3. Сценарий: Управление на уровне конкретной Работы
|
||||||
{
|
if (job.AutoControl == null)
|
||||||
return (
|
throw new InvalidOperationException(
|
||||||
job.AutoControl.InitUsedTemplateState,
|
$"В типе группы '{jobGroup.GroupType.Id}' указано управление " +
|
||||||
job.AutoControl.InitUsedScheduleState
|
$"на уровне РАБОТЫ, но у работы '{job.Name}' (ID: {job.Id}) " +
|
||||||
);
|
$"отсутствуют настройки автоконтроля (Job.AutoControl равен null)!");
|
||||||
}
|
|
||||||
|
|
||||||
return (defaultTemplateState, defaultScheduleState);
|
return (
|
||||||
|
job.AutoControl.InitUsedTemplateState,
|
||||||
|
job.AutoControl.InitUsedScheduleState
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ internal class LoadJobStage : ISimpleSyncStage
|
|||||||
.Include(j => j.AutoControl)
|
.Include(j => j.AutoControl)
|
||||||
.Include(j => j.Tnk)
|
.Include(j => j.Tnk)
|
||||||
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
|
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
|
||||||
.Include(j => j.AutoControl)
|
.Include(j => j.Group).ThenInclude(g => g!.AutoControl)
|
||||||
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
|
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
|
||||||
.FirstOrDefaultAsync(j => j.Id == context.JobId, ct);
|
.FirstOrDefaultAsync(j => j.Id == context.JobId, ct);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user