Files
parr_api/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs

582 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.JobRepositories;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.JobEntities;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;
using PARR.Domain.Settings;
using PARR.TemplateMatcher.Constants;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
using PARR.TemplateMatcher.Settings;
using System.Diagnostics;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{
#if DEBUG
private readonly Guid _targetUnitId = Guid.Parse("358437ac-1eeb-4c00-840c-998326f657ac");
#endif
private readonly IEnumerable<ISimpleSyncStage> _readStages;
private readonly IEnumerable<ISimpleSyncWriteStage> _writeStages;
private readonly ILogger<SimpleTemplateSynchronizer> _logger;
private readonly IUnitFilterService _unitFilterService;
private readonly ITemplateRepository _templateService;
private readonly IJobRepository _jobService;
private readonly ITemplateNameNormalizer _templateNameNormalizer;
private readonly ITemplateMqPublisher _templateMqPublisher;
private readonly IMatchingStatusService _matchingStatusService;
private readonly SettingsFromDb _settingsFromDb;
private readonly IOptions<TemplateSettings> _templateSettings;
private readonly IUnitFieldRepository _unitFieldService;
private readonly IUnitInValueRepository _unitInValueService;
private readonly IUnitRepository _unitRepository;
public SimpleTemplateSynchronizer(
IEnumerable<ISimpleSyncStage> readStages,
IEnumerable<ISimpleSyncWriteStage> writeStages,
ILogger<SimpleTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateMqPublisher templateMqPublisher,
IMatchingStatusService matchingStatusService,
SettingsFromDb settingsFromDb,
IOptions<TemplateSettings> templateSettings,
IUnitFieldRepository unitFieldService,
IUnitInValueRepository unitInValueService,
IUnitRepository unitRepository
)
{
this._readStages = readStages;
this._writeStages = writeStages;
this._logger = logger;
this._unitFilterService = unitFilterService;
this._templateService = templateService;
this._jobService = jobService;
this._templateNameNormalizer = templateNameNormalizer;
this._templateMqPublisher = templateMqPublisher;
this._matchingStatusService = matchingStatusService;
this._settingsFromDb = settingsFromDb;
this._templateSettings = templateSettings;
this._unitFieldService = unitFieldService;
this._unitInValueService = unitInValueService;
this._unitRepository = unitRepository;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
if (jobId == _settingsFromDb.JobIdForUnusedTemplates)
{
_logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
await SyncUnusedTemplatesAsync(jobId, initiator);
return;
}
_logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
var existingStatus = await _matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Count > 0)
{
_logger.LogWarning("Синхронизация для Job {JobId} уже запущена. Пропускаем.", jobId);
return;
}
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await _matchingStatusService.SetMatchingStatusAsync(
jobId, SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(35));
// Таймер запускается ПОСЛЕ инфраструктурных операций (статус, проверка блокировки)
var totalSw = Stopwatch.StartNew();
var context = new SimpleSyncContext { JobId = jobId, Initiator = initiator };
try
{
foreach (var stage in _readStages)
{
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
}
foreach (var stage in _writeStages)
{
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
}
totalSw.Stop();
_logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
context.JobName, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
_logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
context.JobName, jobId);
}
catch (Exception ex)
{
totalSw.Stop();
_logger.LogError(ex, "Ошибка при синхронизации Job '{JobName}' ({JobId}) через {ElapsedMs} мс",
string.Empty, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
_logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
_logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
// === Проверка: уже запущена? ===
var existingStatus = await _matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Count > 0)
{
_logger.LogWarning("Обновление для Job {JobId} уже запущено. Пропускаем.", jobId);
return;
}
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Update,
Comment = "Начало обновления имён"
};
await _matchingStatusService.SetMatchingStatusAsync(
jobId,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
try
{
var job = await _jobService.Get()
.AsNoTracking()
.Include(j => j.AutoControl)
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g!.GroupType)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId);
if (job == null)
{
_logger.LogWarning("Job {JobId} не найден.", jobId);
await UpdateMatchingStatusAsync(jobId, "Job не найден");
return;
}
// === Получение отфильтрованных юнитов с полной информацией ===
var filteredUnits = await _unitFilterService.GetUnitsByJobFilterAsync(jobId);
if (filteredUnits == null || !filteredUnits.Any())
{
_logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется");
await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
return;
}
// Извлекаем ID юнитов для последующих операций
var unitIds = filteredUnits.Select(u => u.Id).ToList();
#if DEBUG
// Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(_targetUnitId))
{
_logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", _targetUnitId);
}
else
{
_logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", _targetUnitId);
}
#endif
var existingTemplates = await _templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t!.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync();
foreach (var template in existingTemplates)
{
if (unitIds.Contains(template.UnitId))
{
var expectedName = await _templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await _templateMqPublisher.PublishUpdateAsync(updateRequest);
}
}
}
await UpdateMatchingStatusAsync(jobId, "Обновление завершено");
await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
_logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при обновлении Job {JobId}", jobId);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw;
}
}
private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator, CancellationToken ct = default)
{
// Проверка отмены в самом начале
ct.ThrowIfCancellationRequested();
var existingStatus = await _matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Count > 0)
{
_logger.LogWarning("Синхронизация для Job неиспользуемых шаблонов {JobId} уже запущена. Пропускаем.", unusedJobId);
return;
}
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Синхронизация неиспользуемых шаблонов"
};
await _matchingStatusService.SetMatchingStatusAsync(
unusedJobId,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
var totalSw = Stopwatch.StartNew();
try
{
// 1. Находим ID нужных полей
var responsableAreaField = await _unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName, ct);
var tagField = await _unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName, ct);
if (responsableAreaField == null || tagField == null)
{
_logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue);
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
return;
}
var responsableAreaFieldId = responsableAreaField.Id;
var tagFieldId = tagField.Id;
// 2. Находим ValueId для тега "ПАРР-НЕИСП"
var targetTagValueId = await _unitInValueService.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == UnusedTemplateConstants.NotUsedTagValue)
.Select(uiv => uiv.ValueId)
.FirstOrDefaultAsync(ct);
if (targetTagValueId == Guid.Empty)
{
_logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", UnusedTemplateConstants.NotUsedTagValue, UnusedTemplateConstants.ParrTagFieldName);
}
var unusedJob = await _jobService.Get().AsNoTracking()
.Include(t => t!.Group).ThenInclude(t => t!.GroupType)
.Include(t => t!.Tnk)
.FirstOrDefaultAsync(j => j.Id == unusedJobId, ct);
if (unusedJob == null)
{
_logger.LogError("Job неиспользуемых шаблонов {JobId} не найден.", unusedJobId);
await UpdateMatchingStatusAsync(unusedJobId, "Job не найден");
return;
}
var unusedTemplates = await _templateService.Get()
.Include(t => t.Unit)
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.ToListAsync(ct);
if (!unusedTemplates.Any())
{
_logger.LogInformation("Не найдено шаблонов со статусом Unused.");
await UpdateMatchingStatusAsync(unusedJobId, "Нет шаблонов для обработки");
await _matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
return;
}
await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки");
int processed = 0;
var allTemplateUnitIds = unusedTemplates.Select(t => t.UnitId).Distinct().ToList();
// Получаем значения ЗОНА_ОТВЕТСТВЕННОСТИ для всех юнитов шаблонов
var unitResponsableAreaValues = await _unitInValueService.GetByUnitIdsAndFieldIdsAsync(allTemplateUnitIds, new List<Guid> { responsableAreaFieldId }, ct);
var unitToResponsableAreaValueMap = unitResponsableAreaValues
.Where(uiv => uiv.ValueId != Guid.Empty)
.ToDictionary(uiv => uiv.UnitId, uiv => uiv.ValueId);
// Пакетный поиск целевых юнитов (Один запрос к БД вместо N)
var responsableAreaToTargetUnitMap = new Dictionary<Guid, Guid>();
var distinctResponsableAreaValues = unitToResponsableAreaValueMap.Values.Distinct().ToList();
if (targetTagValueId != Guid.Empty &&
distinctResponsableAreaValues.Any())
{
_logger.LogDebug("Пакетный поиск целевых юнитов с тегом 'ПАРР-НЕИСП' для {Count} уникальных значений ЗОНА_ОТВЕТСТВЕННОСТИ.", distinctResponsableAreaValues.Count);
// Передаем ct в ToListAsync
var matches = await _unitInValueService.Get().AsNoTracking()
.Where(uiv => uiv.FieldId == responsableAreaFieldId && distinctResponsableAreaValues.Contains(uiv.ValueId))
.Join(
_unitInValueService.Get().AsNoTracking().Where(t => t.FieldId == tagFieldId && t.ValueId == targetTagValueId),
responsableArea => responsableArea.UnitId,
tag => tag.UnitId,
(responsableArea, tag) => new { responsableArea.ValueId, responsableArea.UnitId }
)
.ToListAsync(ct);
responsableAreaToTargetUnitMap = matches
.GroupBy(x => x.ValueId)
.ToDictionary(g => g.Key, g => g.First().UnitId);
_logger.LogDebug("Сформирован кэш соответствий: найдено {Count} целевых юнитов.", responsableAreaToTargetUnitMap.Count);
}
foreach (var template in unusedTemplates)
{
try
{
// Проверка отмены внутри цикла (на случай долгих вычислений)
ct.ThrowIfCancellationRequested();
if (template.Unit == null)
{
_logger.LogWarning("У шаблона {TemplateId} отсутствует Unit. Пропускаем.", template.Id);
processed++;
continue;
}
// 1. Определяем текущее значение ЗОНА_ОТВЕТСТВЕННОСТИ
var currentResponsableAreaValueId = Guid.Empty;
var hasResponsableArea = unitToResponsableAreaValueMap.TryGetValue(template.UnitId, out currentResponsableAreaValueId);
// 2. Ищем целевой юнит в кэше
Guid? targetUnitId = null;
if (hasResponsableArea && currentResponsableAreaValueId != Guid.Empty)
{
if (responsableAreaToTargetUnitMap.TryGetValue(currentResponsableAreaValueId, out var foundUnitId) && foundUnitId != Guid.Empty)
{
targetUnitId = foundUnitId;
}
}
// 3. Финализируем UnitId и Unit
Guid finalUnitId = targetUnitId ?? template.UnitId;
Unit finalUnit = template.Unit;
if (targetUnitId.HasValue && targetUnitId.Value != template.UnitId)
{
_logger.LogInformation("Для шаблона {TemplateId} найден новый UnitId {NewUnitId} (был {OldUnitId}).",
template.Id, targetUnitId.Value, template.UnitId);
// Передаем ct в запрос
var newUnit = await _unitRepository.Get().AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == targetUnitId.Value, ct);
if (newUnit != null)
{
finalUnit = newUnit;
}
else
{
_logger.LogWarning("Не удалось загрузить новый юнит {UnitId}. Используем старый.", targetUnitId.Value);
finalUnitId = template.UnitId;
}
}
else if (!hasResponsableArea)
{
_logger.LogDebug("У юнита {UnitId} шаблона {TemplateId} нет значения поля ЗОНА_ОТВЕТСТВЕННОСТИ. Оставляем текущий UnitId.", template.UnitId, template.Id);
}
// 4. Генерация целевого имени
var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob, finalUnit);
// 5. Проверка необходимости обновления
bool unitChanged = template.UnitId != finalUnitId;
bool jobChanged = template.JobId != unusedJobId;
bool nameChanged = !string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase);
if (!unitChanged && !jobChanged && !nameChanged)
{
_logger.LogDebug("Шаблон {TemplateId} уже актуален. Пропуск отправки в MQ.", template.Id);
processed++;
continue;
}
await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId);
_logger.LogDebug("Отправлен запрос на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}",
template.Id, unitChanged, jobChanged, nameChanged);
processed++;
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при обработке шаблона {TemplateId}", template.Id);
}
}
totalSw.Stop();
totalSw.Stop();
_logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
unusedJob.Name, unusedJob.Id, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена");
await _matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
totalSw.Stop();
_logger.LogInformation(
"Синхронизация неиспользуемых шаблонов завершена. Обработано {Count} шаблонов",
unusedTemplates.Count);
}
catch (Exception ex)
{
totalSw.Stop();
_logger.LogError(ex,
"Ошибка при синхронизации неиспользуемых шаблонов для Job {JobId} через {ElapsedMs} мс",
unusedJobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(unusedJobId, $"Ошибка: {ex.Message}");
throw;
}
}
private async Task SendUpdateRequest(Template template, Guid jobId, string name, HistoryInitiator initiator, Guid unitId)
{
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
UnitId = unitId,
Name = name,
IsActiveTemplate = false,
IsActiveSchedule = false,
IsNew = false,
Index = null,
StatusTypeId = template.StatusTypeId,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await _templateMqPublisher.PublishUpdateAsync(updateRequest);
}
private async Task<string> GenerateUnusedTemplateNameAsync(Template template, Job unusedJob, Unit unit)
{
var tempJob = new Job
{
Id = unusedJob.Id,
Name = unusedJob.Name,
WorkName = unusedJob.WorkName,
MinValueRelationships = unusedJob.MinValueRelationships,
MaxValueRelationships = unusedJob.MaxValueRelationships,
IsParentRelationships = unusedJob.IsParentRelationships,
TemplateNameMask = _templateSettings.Value.UnusedTemplateNameMask,
WorkGroupMask = unusedJob.WorkGroupMask,
ResponseAreaMask = unusedJob.ResponseAreaMask,
TnkId = unusedJob.TnkId,
GroupId = unusedJob.GroupId,
Group = unusedJob.Group,
Tnk = unusedJob.Tnk,
UnitFilters = unusedJob.UnitFilters,
Templates = unusedJob.Templates,
AutoControl = unusedJob.AutoControl
};
var tempTemplateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = unusedJob.Id,
UnitId = unit.Id,
Index = null,
Job = tempJob,
Unit = unit,
UnitsInTemplate = new List<UnitsInTemplate>()
};
return await _templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
}
private async Task UpdateMatchingStatusAsync(Guid jobId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await _matchingStatusService.SetMatchingStatusAsync(
jobId,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
}
}