feat(templateMatcher,dal): Первая реализация SyncTemplatesForJobGroup для сгруппированных типов групп регламентных работ.

This commit is contained in:
Mikhail Kuznetsov
2025-12-19 19:15:58 +10:00
parent cc104c30b1
commit 7ca536ce52
20 changed files with 687 additions and 71 deletions

View File

@@ -5,7 +5,7 @@ namespace PARR.BLL.Domain.Mq
/// <summary> /// <summary>
/// Модель в MQ, простого создания Template /// Модель в MQ, простого создания Template
/// </summary> /// </summary>
public class TemplateGeneratorWorkerMq public class TemplateGeneratorMq
{ {
/// <summary> /// <summary>
/// Id регламентной работы /// Id регламентной работы
@@ -32,5 +32,15 @@ namespace PARR.BLL.Domain.Mq
/// </summary> /// </summary>
public HistoryInitiator? HistoryInitiator { get; set; } public HistoryInitiator? HistoryInitiator { get; set; }
/// <summary>
/// Связанные ЭК для сгруппированного типа JobGroup
/// </summary>
public required List<Guid> UnitsInTemplate { get; set; }
/// <summary>
/// Индекс, используется в групповых шаблонах
/// </summary>
public int? Index { get; set; }
} }
} }

View File

@@ -7,24 +7,47 @@ namespace PARR.BLL.Domain.Mq
{ {
public Guid TemplateId { get; set; } public Guid TemplateId { get; set; }
/// <summary>
/// Id регламентной работы
/// </summary>
public Guid JobId { get; set; } public Guid JobId { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
/// <summary>
/// Актировать шаблон при инициализации
/// </summary>
public bool IsActiveTemplate { get; set; } public bool IsActiveTemplate { get; set; }
/// <summary>
/// Актировать расписание при инициализации
/// </summary>
public bool IsActiveSchedule { get; set; } public bool IsActiveSchedule { get; set; }
public DateTimeOffset? LastRun { get; set; } public DateTimeOffset? LastRun { get; set; }
public DateTimeOffset NextRun { get; set; } public DateTimeOffset NextRun { get; set; }
/// <summary>
/// Id Юнита(единицы обслуживания)/ЭК
/// </summary>
public Guid UnitId { get; set; } public Guid UnitId { get; set; }
/// <summary>
/// Индекс, используется в групповых шаблонах
/// </summary>
public int? Index { get; set; } public int? Index { get; set; }
public TemplateStatusTypeEnum StatusTypeId { get; set; } public TemplateStatusTypeEnum StatusTypeId { get; set; }
/// <summary>
/// Инициатор запроса к генератору
/// </summary>
public required HistoryInitiator Initiator { get; set; } public required HistoryInitiator Initiator { get; set; }
/// <summary>
/// Связанные ЭК для сгруппированного типа JobGroup
/// </summary>
public required List<Guid> UnitsInTemplate { get; set; }
} }
} }

View File

@@ -48,7 +48,7 @@ namespace PARR.DAL.DomainServices.Implementations
this.unitFilterService = unitFilterService; this.unitFilterService = unitFilterService;
} }
public async Task<string> ApplyShortcodesAsync(string str, Guid unitId, Guid jobId) public async Task<string> ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null)
{ {
logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', unitId={UnitId}, jobId={JobId}", str, unitId, jobId); logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', unitId={UnitId}, jobId={JobId}", str, unitId, jobId);
@@ -95,7 +95,7 @@ namespace PARR.DAL.DomainServices.Implementations
// Делаем замену // Делаем замену
var oldResult = resultName; var oldResult = resultName;
resultName = ReplaceStandardShortcodes(job, unit, resultName); resultName = ReplaceStandardShortcodes(job, unit, resultName, index);
iteration++; iteration++;
// Защита от "бесполезных" итераций (строка не изменилась) // Защита от "бесполезных" итераций (строка не изменилась)
@@ -262,14 +262,15 @@ namespace PARR.DAL.DomainServices.Implementations
} }
private static string ReplaceStandardShortcodes(Job job, Unit unit, string input) private static string ReplaceStandardShortcodes(Job job, Unit unit, string input, int? index = null)
{ {
return input return input
.Replace("%ЭК%", unit.Name, StringComparison.OrdinalIgnoreCase) .Replace("%ЭК%", unit.Name, StringComparison.OrdinalIgnoreCase)
.Replace("%ГРУППА_РАБОТ%", job.Group?.GroupName ?? "", StringComparison.OrdinalIgnoreCase) .Replace("%ГРУППА_РАБОТ%", job.Group?.GroupName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("%РАБОТА%", job.WorkName, StringComparison.OrdinalIgnoreCase) .Replace("%РАБОТА%", job.WorkName, StringComparison.OrdinalIgnoreCase)
.Replace("%ТНК%", job.Tnk?.Name ?? "", StringComparison.OrdinalIgnoreCase) .Replace("%ТНК%", job.Tnk?.Name ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("%ТНК-КРАТКО%", job.Tnk?.ShortName ?? "", StringComparison.OrdinalIgnoreCase); .Replace("%ТНК-КРАТКО%", job.Tnk?.ShortName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("%ИНДЕКС%", index?.ToString() ?? "", StringComparison.OrdinalIgnoreCase);
} }

View File

@@ -4,7 +4,7 @@ namespace PARR.DAL.DomainServices.Interfaces
{ {
public interface IShortcodesService public interface IShortcodesService
{ {
Task<string> ApplyShortcodesAsync(string str, Guid unitId, Guid jobId); Task<string> ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null);
bool IsAnyShortcodes(string str); bool IsAnyShortcodes(string str);

View File

@@ -21,6 +21,11 @@ namespace PARR.DAL.Services.Implementations.Unit
} }
public IQueryable<UnitInUnit> Get()
{
return dataContext.UnitInUnits;
}
public Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId) public Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId)
{ {
@@ -29,6 +34,7 @@ namespace PARR.DAL.Services.Implementations.Unit
.ToListAsync(); .ToListAsync();
} }
public Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId) public Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId)
{ {
return dataContext.UnitInUnits return dataContext.UnitInUnits

View File

@@ -80,5 +80,10 @@ namespace PARR.DAL.Services.Implementations.Unit
.Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId)) .Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId))
.ToListAsync(); .ToListAsync();
} }
public IQueryable<UnitInValue> Get()
{
return dataContext.UnitInValues;
}
} }
} }

View File

@@ -16,5 +16,8 @@ namespace PARR.DAL.Services.Interfaces.Unit
/// Получает связи, где ParentUnitId unitIds (для IsParent=False). /// Получает связи, где ParentUnitId unitIds (для IsParent=False).
/// </summary> /// </summary>
Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds); Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds);
IQueryable<UnitInUnit> Get();
} }
} }

View File

@@ -12,6 +12,6 @@ namespace PARR.DAL.Services.Interfaces.Unit
/// Получает UnitInValue (с Value) для заданных UnitId и FieldId. /// Получает UnitInValue (с Value) для заданных UnitId и FieldId.
/// </summary> /// </summary>
Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds); Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds);
IQueryable<UnitInValue> Get();
} }
} }

View File

@@ -2,6 +2,6 @@
{ {
public interface IValidatorService public interface IValidatorService
{ {
Task<bool> IsValidAsync(Guid jobId, Guid unitId); Task<bool> IsValidAsync(Guid jobId, Guid unitId, int? index, List<Guid>? unitsInTemplate = null);
} }
} }

View File

@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore; // ValidatorService.cs
using Microsoft.EntityFrameworkCore;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.Services.Interfaces.Unit;
@@ -9,22 +11,25 @@ namespace PARR.TemplateGeneratorWorker.Services
private readonly ILogger<ValidatorService> logger; private readonly ILogger<ValidatorService> logger;
private readonly IJobService jobService; private readonly IJobService jobService;
private readonly IUnitService unitService; private readonly IUnitService unitService;
private readonly ITemplateService templateService;
public ValidatorService( public ValidatorService(
ILogger<ValidatorService> logger, ILogger<ValidatorService> logger,
IJobService jobService, IJobService jobService,
IUnitService unitService IUnitService unitService,
ITemplateService templateService
) )
{ {
this.logger = logger; this.logger = logger;
this.jobService = jobService; this.jobService = jobService;
this.unitService = unitService; this.unitService = unitService;
this.templateService = templateService;
} }
public async Task<bool> IsValidAsync(Guid jobId, Guid unitId) public async Task<bool> IsValidAsync(Guid jobId, Guid unitId, int? index, List<Guid>? unitsInTemplate = null)
{ {
// Проверяем JobId
var job = await jobService var job = await jobService
.Get().AsNoTracking() .Get().AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == jobId); .FirstOrDefaultAsync(t => t.Id == jobId);
@@ -35,6 +40,7 @@ namespace PARR.TemplateGeneratorWorker.Services
return false; return false;
} }
// Проверяем UnitId (UnitId - это ID регионального юнита)
var unit = await unitService var unit = await unitService
.Get().AsNoTracking() .Get().AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == unitId); .FirstOrDefaultAsync(t => t.Id == unitId);
@@ -45,6 +51,34 @@ namespace PARR.TemplateGeneratorWorker.Services
return false; return false;
} }
// Проверяем уникальность (JobId, UnitId, Index)
var existingTemplate = await templateService
.Get().AsNoTracking()
.FirstOrDefaultAsync(t => t.JobId == jobId && t.UnitId == unitId && t.Index == index);
if (existingTemplate != null)
{
logger.LogError("Шаблон с JobId={JobId}, UnitId={UnitId}, Index={Index} уже существует.", jobId, unitId, index);
return false;
}
// Проверяем, что все UnitId в UnitsInTemplate существуют (если список не null и не пуст)
if (unitsInTemplate != null && unitsInTemplate.Any())
{
var unitIdsToCheck = unitsInTemplate.ToHashSet();
var existingUnitIdsCount = await unitService.Get()
.AsNoTracking()
.Where(u => unitIdsToCheck.Contains(u.Id))
.Select(u => u.Id)
.CountAsync();
if (existingUnitIdsCount != unitIdsToCheck.Count)
{
logger.LogError("Не все UnitId из UnitsInTemplate существуют в базе данных. Ожидается: {ExpectedCount}, Найдено: {FoundCount}", unitIdsToCheck.Count, existingUnitIdsCount);
return false;
}
}
return true; return true;
} }
} }

View File

@@ -1,5 +1,4 @@
 using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using PARR.BLL.Domain.Mq; using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces; using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain; using PARR.Common.Domain;
@@ -43,31 +42,23 @@ namespace PARR.TemplateGeneratorWorker
this.templateService = templateService; this.templateService = templateService;
this.esppScheduleTransformService = esppScheduleTransformService; this.esppScheduleTransformService = esppScheduleTransformService;
} }
public async Task GenerateTemplateAsync(string msg) public async Task GenerateTemplateAsync(string msg)
{ {
logger.LogInformation($"Получили запрос: {msg}"); logger.LogInformation($"Получили запрос: {msg}");
var query = transformService.GetModelFromJson<TemplateGeneratorWorkerMq>(msg); var query = transformService.GetModelFromJson<TemplateGeneratorMq>(msg);
if (query == null) if (query == null)
return;
if (!await validatorService.IsValidAsync(query.JobId, query.UnitId))
{ {
logger.LogError($"Некорректные параметры регламентной работы или Unit {nameof(Job)}: {query.JobId}, {nameof(Unit)}: {query.UnitId}"); logger.LogError("Не удалось десериализовать запрос: {Message}", msg);
return; return;
} }
var existing = await templateService // Проверяем всё
.Get() if (!await validatorService.IsValidAsync(query.JobId, query.UnitId, query.Index, query.UnitsInTemplate))
.AsNoTracking()
.FirstOrDefaultAsync(t => t.JobId == query.JobId && t.UnitId == query.UnitId);
if (existing != null)
{ {
logger.LogWarning("Шаблон уже существует (Job={JobId}, Unit={UnitId}) → пропускаем Create", query.JobId, query.UnitId); logger.LogError($"Параметры запроса не прошли валидацию: JobId={query.JobId}, UnitId={query.UnitId}, Index={query.Index}, UnitsInTemplateCount={query.UnitsInTemplate?.Count ?? 0}");
return; return;
} }
@@ -101,22 +92,32 @@ namespace PARR.TemplateGeneratorWorker
IsActiveTemplate = query.IsActiveTemplate ?? false, IsActiveTemplate = query.IsActiveTemplate ?? false,
IsActiveSchedule = query.IsActiveSchedule ?? false, IsActiveSchedule = query.IsActiveSchedule ?? false,
NextRun = nextRun, NextRun = nextRun,
//IsUnused = false,
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
InitiatorComment = query.HistoryInitiator?.InitiatorComment, InitiatorComment = query.HistoryInitiator?.InitiatorComment,
InitiatorParrComponentId = query.HistoryInitiator?.InitiatorParrComponentId InitiatorParrComponentId = query.HistoryInitiator?.InitiatorParrComponentId,
Index = query.Index
}; };
if (await templateService.CreateAsync(template) && await templateService.CommitAsync(new HistoryInitiator { InitiatorComment = "Запрос на генерацию с тестового шаблона", InitiatorParrComponentId = ParrComponentsEnum.TemplateTaskGenerator })) // Устанавливаем UnitsInTemplate
if (query.UnitsInTemplate != null && query.UnitsInTemplate.Any())
{ {
logger.LogInformation("Создан шаблон: Id={TemplateId}, Name={Name}, Job={JobId}, Unit={UnitId}", template.UnitsInTemplate = query.UnitsInTemplate.Select(unitId => new UnitsInTemplate { UnitId = unitId }).ToList();
template.Id, template.Name, query.JobId, query.UnitId);
} }
else else
{ {
logger.LogError("Ошибка создания шаблона: Name={Name}", templateName); // Если список пуст, все равно инициализируем коллекцию, чтобы избежать NullReferenceException при сохранении (если это не nullable)
} template.UnitsInTemplate = new List<UnitsInTemplate>();
} }
if (await templateService.CreateAsync(template) && await templateService.CommitAsync(new HistoryInitiator { InitiatorComment = "Запрос на генерацию с тестового шаблона", InitiatorParrComponentId = ParrComponentsEnum.TemplateTaskGenerator }))
{
logger.LogInformation("Создан шаблон: Id={TemplateId}, Name={Name}, Job={JobId}, Unit={UnitId}, Index={Index}, UnitsInTemplateCount={UnitsCount}",
template.Id, template.Name, query.JobId, query.UnitId, query.Index, template.UnitsInTemplate.Count);
}
else
{
logger.LogError("Ошибка создания шаблона: Name={Name}, JobId={JobId}, UnitId={UnitId}, Index={Index}", templateName, query.JobId, query.UnitId, query.Index);
}
}
} }
} }

View File

@@ -2,9 +2,10 @@
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher
{ {
internal interface ITemplateMatcher public interface ITemplateMatcher
{ {
Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator); Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator);
Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator); Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator);
Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
} }
} }

View File

@@ -59,8 +59,8 @@ namespace PARR.TemplateMatcher
switch (query.EntityType) switch (query.EntityType)
{ {
case SyncTaskEntityTypeEnum.Job: case SyncTaskEntityTypeEnum.Job:
var validatorService = GetServiceInScope<IJobValidatorService>(scope); var jobValidatorService = GetServiceInScope<IJobValidatorService>(scope);
if (!await validatorService.IsValidAsync(query.Id)) if (!await jobValidatorService.IsValidJobAsync(query.Id))
{ {
logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id); logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id);
return; return;
@@ -83,9 +83,29 @@ namespace PARR.TemplateMatcher
break; break;
case SyncTaskEntityTypeEnum.JobGroup: case SyncTaskEntityTypeEnum.JobGroup:
logger.LogWarning("Обработка EntityType JobGroup не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action); var jobGroupValidatorService = GetServiceInScope<IJobGroupValidatorService>(scope);
break; if (!await jobGroupValidatorService.IsValidJobGroupAsync(query.Id))
{
logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id);
return;
}
switch (query.Action)
{
case TemplateMatcherActionEnum.Sync:
// Создать недостающие шаблоны, привязать к нужному Job, включить/выключить по фильтрам
await templateMatcherService.SyncTemplatesForJobGroup(query.Id, query.Initiator);
break;
case TemplateMatcherActionEnum.Update:
// Обновить существующие шаблоны: имя, привязка к Job, вкл/выкл по фильтрам
//await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator);
logger.LogWarning("Обработка EntityType JobGroup и TemplateMatcherActionEnum.Update не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
break;
default:
logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action);
break;
}
break;
case SyncTaskEntityTypeEnum.Template: case SyncTaskEntityTypeEnum.Template:
logger.LogWarning("Обработка EntityType Template не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action); logger.LogWarning("Обработка EntityType Template не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
break; break;

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.DAL.Services.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions
{
internal class JobGroupValidatorService : IJobGroupValidatorService
{
private readonly ILogger<IJobValidatorService> logger;
private readonly IJobGroupService jobGroupService;
public JobGroupValidatorService(
ILogger<IJobValidatorService> logger,
IJobGroupService jobGroupService
)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
}
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
{
var isExist = await jobGroupService.GetAsync(jobGroupId);
if (isExist == null)
{
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
return false;
}
return true;
}
}
}

View File

@@ -17,7 +17,7 @@ namespace PARR.TemplateMatcher.Services.Implemetaions
this.logger = logger; this.logger = logger;
this.jobService = jobService; this.jobService = jobService;
} }
public async Task<bool> IsValidAsync(Guid jobId) public async Task<bool> IsValidJobAsync(Guid jobId)
{ {
var isExist = await jobService.GetAsync(jobId); var isExist = await jobService.GetAsync(jobId);

View File

@@ -0,0 +1,12 @@
namespace PARR.TemplateMatcher.Services.Interfaces
{
internal interface IJobGroupValidatorService
{
/// <summary>
/// Проверяет существование группы регламентной работы
/// </summary>
/// <param name="jobId"></param>
/// <returns></returns>
Task<bool> IsValidJobGroupAsync(Guid jobGroupId);
}
}

View File

@@ -7,6 +7,6 @@
/// </summary> /// </summary>
/// <param name="jobId"></param> /// <param name="jobId"></param>
/// <returns></returns> /// <returns></returns>
Task<bool> IsValidAsync(Guid jobId); Task<bool> IsValidJobAsync(Guid jobId);
} }
} }

View File

@@ -4,11 +4,14 @@ using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces; using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain; using PARR.Common.Domain;
using PARR.Constants; using PARR.Constants;
using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.Models; using PARR.DAL.Models;
using PARR.DAL.Models.Job; using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
using PARR.DAL.TransformServices; using PARR.DAL.TransformServices;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
using System.Text.Json; using System.Text.Json;
@@ -25,29 +28,41 @@ namespace PARR.TemplateMatcher
private readonly ILogger<TemplateMatcher> logger; private readonly ILogger<TemplateMatcher> logger;
private readonly IUnitFilterService unitFilterService; private readonly IUnitFilterService unitFilterService;
private readonly IUnitInUnitService unitInUnitService;
private readonly IUnitInValueService unitInValueService; // Добавлено
private readonly IUnitService unitService; // Добавлено
private readonly MqSettings mqSettings; private readonly MqSettings mqSettings;
private readonly IMqService mqService; private readonly IMqService mqService;
private readonly ITemplateService templateService; private readonly ITemplateService templateService;
private readonly IJobService jobService; private readonly IJobService jobService;
private readonly IJobGroupService jobGroupService;
private readonly IShortcodesService shortcodesService; private readonly IShortcodesService shortcodesService;
private readonly IEsppScheduleTransformService esppScheduleTransformService; private readonly IEsppScheduleTransformService esppScheduleTransformService;
public TemplateMatcher( public TemplateMatcher(
ILogger<TemplateMatcher> logger, ILogger<TemplateMatcher> logger,
IUnitFilterService unitFilterService, IUnitFilterService unitFilterService,
IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService,
IUnitService unitService,
MqSettings mqSettings, MqSettings mqSettings,
IMqService mqService, IMqService mqService,
ITemplateService templateService, ITemplateService templateService,
IJobService jobService, IJobService jobService,
IJobGroupService jobGroupService,
IShortcodesService shortcodesService, IShortcodesService shortcodesService,
IEsppScheduleTransformService esppScheduleTransformService) IEsppScheduleTransformService esppScheduleTransformService)
{ {
this.logger = logger; this.logger = logger;
this.unitFilterService = unitFilterService; this.unitFilterService = unitFilterService;
this.unitInUnitService = unitInUnitService;
this.unitInValueService = unitInValueService;
this.unitService = unitService;
this.mqSettings = mqSettings; this.mqSettings = mqSettings;
this.mqService = mqService; this.mqService = mqService;
this.templateService = templateService; this.templateService = templateService;
this.jobService = jobService; this.jobService = jobService;
this.jobGroupService = jobGroupService;
this.shortcodesService = shortcodesService; this.shortcodesService = shortcodesService;
this.esppScheduleTransformService = esppScheduleTransformService; this.esppScheduleTransformService = esppScheduleTransformService;
} }
@@ -66,12 +81,393 @@ namespace PARR.TemplateMatcher
return; return;
} }
// Проверяем, является ли Job "групповым"
bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group;
if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue)
{
logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId);
return; // Ничего не делаем для группового Job
}
else
{
await SyncSimpleTemplatesAsync(job, expectedUnitIds, initiator);
}
logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId);
}
public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// 1. Получить JobGroup и связанные Job'ы
var jobGroup = await jobGroupService.Get()
.AsNoTracking() // Добавлено
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
return;
}
var jobsInGroup = jobGroup.Jobs.ToList();
// 2. Найти Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
// Возможно, нужно обработать случай, когда MaxValueRelationships не установлено ни у одного Job.
// Пока просто выйдем.
return;
}
// Проверяем, что UnitFilters и RelationshipFilters загружены
if (maxJob.UnitFilters == null)
{
logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id);
// Продолжить с пустыми фильтрами или выйти?
// Пока продолжим с пустым списком.
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Использовать фильтры maxJob для получения expectedUnitIds
var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id);
if (expectedUnitIds == null || !expectedUnitIds.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId);
// Деактивировать все шаблоны для всех Job в группе?
// Пока просто выйдем.
return;
}
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
return;
}
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
// --- ИСПРАВЛЕНИЕ: Разбиваем запрос на части ---
// Загрузить UnitValues для отфильтрованных юнитов, чтобы проверить GroupingUnitFieldId
var filteredUnits = await unitService.Get()
.AsNoTracking() // Добавлено
.AsSplitQuery() // Добавлено
.Include(t => t.UnitValues) // Добавлено
.ThenInclude(t => t.Value) // Добавлено
.Where(u => expectedUnitIds.Contains(u.Id))
.ToListAsync(); // Сначала загружаем Unit'ы
// Затем фильтруем их UnitValues и собираем UnitId
var unitIdsWithValidGroupingFieldSet = filteredUnits
.Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)))
.Select(u => u.Id)
.ToHashSet(); // Создаем HashSet
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
if (!unitIdsWithValidGroupingFieldSet.Any())
{
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
return;
}
// --- НОВАЯ ЛОГИКА: Применение RelationshipFilters ---
// Найти связи UnitInUnit для unitIdsWithValidGroupingFieldSet
var potentialUnitInUnitLinks = await unitInUnitService.Get()
.AsNoTracking() // Добавлено
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
.ToListAsync();
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit до применения RelationshipFilters.", potentialUnitInUnitLinks.Count);
// Получить RelationshipFilters из maxJob
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
if (relationshipFilters.Any())
{
// Загрузить UnitInValue для ParentUnitId и ChildUnitId из potentialUnitInUnitLinks
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
var parentUnitValues = await unitInValueService.Get()
.AsNoTracking() // Добавлено
.Include(uv => uv.Field) // Добавлено
.Include(uv => uv.Value) // Добавлено
.Where(uv => allParentIds.Contains(uv.UnitId))
.ToListAsync();
var childUnitValues = await unitInValueService.Get()
.AsNoTracking() // Добавлено
.Include(uv => uv.Field) // Добавлено
.Include(uv => uv.Value) // Добавлено
.Where(uv => allChildIds.Contains(uv.UnitId))
.ToListAsync();
// Сгруппировать значения по UnitId для быстрого доступа
var parentValuesMap = parentUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
var childValuesMap = childUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
// Применить фильтры к связям
var filteredUnitInUnitLinks = new List<UnitInUnit>();
foreach (var link in potentialUnitInUnitLinks)
{
bool linkMatchesAllFilters = true;
foreach (var rf in relationshipFilters)
{
var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List<UnitInValue>()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List<UnitInValue>());
bool filterMatch = valuesToCheck.Any(uv =>
uv.FieldId == rf.FieldId &&
uv.Value != null &&
uv.Value.Value != null &&
uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase)
);
if (rf.IsInverse)
filterMatch = !filterMatch;
if (!filterMatch)
{
linkMatchesAllFilters = false;
break; // Не подходит под один из фильтров
}
}
if (linkMatchesAllFilters)
{
filteredUnitInUnitLinks.Add(link);
}
}
potentialUnitInUnitLinks = filteredUnitInUnitLinks; // Заменяем на отфильтрованные
}
logger.LogDebug("Осталось {Count} связей UnitInUnit после применения RelationshipFilters.", potentialUnitInUnitLinks.Count);
// 6. Сгруппировать ChildUnitId по ParentUnitId (региональный ЭК) из ОТФИЛЬТРОВАННЫХ связей
var groupedByRegional = potentialUnitInUnitLinks
.GroupBy(link => link.ParentUnitId)
.ToDictionary(g => g.Key, g => g.Select(l => l.ChildUnitId).ToList());
logger.LogDebug("Сформировано {Count} групп по региональным юнитам.", groupedByRegional.Count);
// 7. Разбить каждую группу и сопоставить с Job
// Для каждого регионального юнита и его дочерних юнитов:
foreach (var kvp in groupedByRegional)
{
var regionalUnitId = kvp.Key;
var childUnitIds = kvp.Value;
logger.LogDebug("Обработка регионального юнита {RegionalUnitId} с {Count} дочерними юнитами.", regionalUnitId, childUnitIds.Count);
// Применяем ограничение MaxValueRelationships maxJob
int maxValueForSplitting = maxJob.MaxValueRelationships.Value; // Уже проверили, что не null
var childUnitGroups = childUnitIds
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
logger.LogDebug("Региональный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", regionalUnitId, childUnitGroups.Count);
// Для каждой подгруппы:
for (int i = 0; i < childUnitGroups.Count; i++)
{
var subGroup = childUnitGroups[i];
var subGroupSize = subGroup.Count;
logger.LogDebug("Обработка подгруппы {Index} регионального юнита {RegionalUnitId}, размер {Size}.", i, regionalUnitId, subGroupSize);
// 8. Найти подходящий Job для подгруппы
// Попробовать найти Job с MaxValueRelationships, равным размеру подгруппы
var targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize)
.FirstOrDefault();
if (targetJob == null)
{
// Найти Job с MaxValueRelationships >= размеру подгруппы, но минимально подходящее
targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize)
.OrderBy(j => j.MaxValueRelationships.Value)
.FirstOrDefault();
}
if (targetJob == null)
{
// Если подходящий Job не найден, используем maxJob
targetJob = maxJob;
logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, regionalUnitId, maxJob.Id);
}
else
{
logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, regionalUnitId, targetJob.Id, targetJob.MaxValueRelationships);
}
// 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId
var existingTemplatesForRegional = await templateService.Get()
.AsNoTracking() // Добавлено
.Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == targetJob.Id && t.UnitId == regionalUnitId && t.Index == i)
.ToListAsync();
Template existingTemplateForSubGroup = existingTemplatesForRegional.FirstOrDefault();
if (existingTemplateForSubGroup != null)
{
// Проверить, изменились ли юниты
var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet();
var newUnitIds = subGroup.ToHashSet();
if (existingUnitIds.SetEquals(newUnitIds))
{
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) актуален.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i);
// Возможно, нужно обновить имя или статус, если изменились фильтры или AutoControl
// Пока оставим как есть, если структура не изменилась.
}
else
{
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i);
// Обновляем существующий шаблон
await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, initiator);
}
}
else
{
// --- ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ МЕТОД TryReuseOneUnusedTemplateAsync ---
var reusableTemplate = await TryReuseOneUnusedTemplateAsync(targetJob.Id, regionalUnitId, initiator); // передаём regionalUnitId как unitId для старого метода
if (reusableTemplate != null) // если захват успешен
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, регионального юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, regionalUnitId, i);
// Подготовить сообщение для TemplateUpdater с новыми параметрами
var expectedName = await GetNormalizedTemplateNameAsync(targetJob, regionalUnitId, i);
var nextRun = await GetNextRunAsync(targetJob); // всегда пересчитываем для нового назначения
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reusableTemplate.Id, // ID захваченного шаблона
JobId = targetJob.Id, // Новый JobId
UnitId = regionalUnitId, // Новый UnitId (региональный)
Name = expectedName,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
NextRun = nextRun,
Index = i, // Новый Index
UnitsInTemplate = subGroup // Новые UnitsInTemplate
};
await SendTemplateUpdateMessage(updateRequest);
}
else
{
// Создать новый шаблон
logger.LogDebug("Создание нового шаблона для Job {JobId}, Regional {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, regionalUnitId, i, subGroup.Count);
await CreateGroupedTemplateAsync(targetJob.Id, regionalUnitId, subGroup, i, initiator);
}
}
}
}
// 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе
// Это требует сбора всех ожидаемых (JobId, UnitId, Index) и сравнения с существующими.
// Соберем ожидаемые комбинации
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvp in groupedByRegional)
{
var regionalUnitId = kvp.Key;
var childUnitIds = kvp.Value;
int maxValueForSplitting = maxJob.MaxValueRelationships.Value;
var childUnitGroups = childUnitIds
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
for (int i = 0; i < childUnitGroups.Count; i++)
{
var subGroup = childUnitGroups[i];
var subGroupSize = subGroup.Count;
var targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize)
.FirstOrDefault();
if (targetJob == null)
{
targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize)
.OrderBy(j => j.MaxValueRelationships.Value)
.FirstOrDefault();
}
if (targetJob == null) targetJob = maxJob;
// Исправлено: используем конкретные типы для кортежа
expectedTemplateKeys.Add((targetJob.Id, regionalUnitId, i));
}
}
// Загрузить *все* шаблоны для всех Job в группе, связанные с региональными юнитами из групп
var allRegionalUnitIds = groupedByRegional.Keys.ToHashSet();
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking() // Добавлено
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) && allRegionalUnitIds.Contains(t.UnitId))
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
// Исправлено: используем конкретные типы для ключа
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedTemplateKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Regional {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await DeactivateTemplateAsync(existingTemplate, existingTemplate.JobId, initiator);
}
}
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
}
private async Task SyncSimpleTemplatesAsync(Job job, HashSet<Guid> expectedUnitIds, HistoryInitiator initiator)
{
logger.LogDebug("Синхронизация обычных шаблонов для JobId {JobId}", job.Id);
var existingTemplates = await templateService.Get() var existingTemplates = await templateService.Get()
.Where(t => t.JobId == jobId) .AsNoTracking() // Добавлено
.Where(t => t.JobId == job.Id)
.ToListAsync(); .ToListAsync();
logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.",
jobId, expectedUnitIds.Count, existingTemplates.Count); job.Id, expectedUnitIds.Count, existingTemplates.Count);
// Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны // Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны
if (!expectedUnitIds.Any()) if (!expectedUnitIds.Any())
@@ -79,19 +475,19 @@ namespace PARR.TemplateMatcher
if (existingTemplates.Any()) if (existingTemplates.Any())
{ {
logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.", logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.",
jobId, existingTemplates.Count); job.Id, existingTemplates.Count);
foreach (var template in existingTemplates) foreach (var template in existingTemplates)
{ {
await DeactivateTemplateAsync(template, jobId, initiator); await DeactivateTemplateAsync(template, job.Id, initiator);
} }
} }
else else
{ {
logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", jobId); logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", job.Id);
} }
logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", jobId); logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", job.Id);
return; return;
} }
@@ -102,12 +498,13 @@ namespace PARR.TemplateMatcher
foreach (var template in templatesToDeactivate) foreach (var template in templatesToDeactivate)
{ {
await DeactivateTemplateAsync(template, jobId, initiator); await DeactivateTemplateAsync(template, job.Id, initiator);
} }
// Перечитываем шаблоны после деактивации // Перечитываем шаблоны после деактивации
existingTemplates = await templateService.Get() existingTemplates = await templateService.Get()
.Where(t => t.JobId == jobId) .AsNoTracking() // Добавлено
.Where(t => t.JobId == job.Id)
.ToListAsync(); .ToListAsync();
var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t);
@@ -121,7 +518,7 @@ namespace PARR.TemplateMatcher
foreach (var unitId in unitIdsMissingTemplates) foreach (var unitId in unitIdsMissingTemplates)
{ {
var reused = await TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); var reused = await TryReuseOneUnusedTemplateAsync(job.Id, unitId, initiator);
if (reused != null) if (reused != null)
{ {
logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId);
@@ -132,14 +529,15 @@ namespace PARR.TemplateMatcher
var updateRequest = new TemplateUpdaterMq var updateRequest = new TemplateUpdaterMq
{ {
TemplateId = reused.Id, TemplateId = reused.Id,
JobId = jobId, JobId = job.Id,
UnitId = unitId, UnitId = unitId,
Name = expectedName, Name = expectedName,
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator, Initiator = initiator,
NextRun = nextRun NextRun = nextRun,
UnitsInTemplate = new List<Guid>()
}; };
await SendTemplateUpdateMessage(updateRequest); await SendTemplateUpdateMessage(updateRequest);
@@ -165,17 +563,17 @@ namespace PARR.TemplateMatcher
// Создание новых шаблонов // Создание новых шаблонов
foreach (var unitId in unitIdsToCreateFresh) foreach (var unitId in unitIdsToCreateFresh)
{ {
await SendTemplateGeneratorMessageAsync(jobId, unitId, initiator); await SendTemplateGeneratorMessageAsync(job.Id, unitId, initiator);
}
} }
logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId);
}
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
var existingTemplates = await templateService.Get() var existingTemplates = await templateService.Get()
.AsNoTracking() // Добавлено
.Where(t => t.JobId == jobId) .Where(t => t.JobId == jobId)
.ToListAsync(); .ToListAsync();
@@ -251,7 +649,8 @@ namespace PARR.TemplateMatcher
NextRun = nextRun, NextRun = nextRun,
Index = template.Index, Index = template.Index,
StatusTypeId = targetStatus, StatusTypeId = targetStatus,
Initiator = initiator Initiator = initiator,
UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
}; };
await SendTemplateUpdateMessage(updateRequest); await SendTemplateUpdateMessage(updateRequest);
@@ -261,6 +660,66 @@ namespace PARR.TemplateMatcher
} }
private async Task UpdateTemplateUnitsAsync(Template template, List<Guid> newUnitIds, Job job, HistoryInitiator initiator)
{
// Обновляем шаблон как "Updating"
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id);
return;
}
// Здесь нужно обновить UnitsInTemplate.
// Это может быть сделано через TemplateUpdaterMq, если он поддерживает передачу нового списка юнитов.
// Или напрямую в сервисе шаблонов, если логика обновления простая.
// Пока отправим сообщение в TemplateUpdater.
var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId);
var nextRun = await GetNextRunAsync(job, template.NextRun);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = job.Id,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
LastRun = template.LastRun,
NextRun = nextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used, // Предполагаем, что используется
Initiator = initiator,
UnitsInTemplate = newUnitIds // Передаем обновленный список юнитов
};
await SendTemplateUpdateMessage(updateRequest);
}
private async Task CreateGroupedTemplateAsync(Guid jobId, Guid regionalUnitId, List<Guid> unitIds, int index, HistoryInitiator initiator)
{
logger.LogInformation("Создание нового группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}, с {Count} юнитами.", jobId, regionalUnitId, index, unitIds.Count);
// Предполагаем, что TemplateGeneratorMq может обрабатывать UnitsInTemplate и Index
var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = regionalUnitId, // UnitId шаблона
UnitsInTemplate = unitIds, // Юниты для UnitsInTemplate
Index = index, // Индекс шаблона
HistoryInitiator = initiator
};
var msg = JsonSerializer.Serialize(mqRequest);
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}.", jobId, regionalUnitId, index);
}
private async Task<bool> DeactivateTemplateAsync( private async Task<bool> DeactivateTemplateAsync(
Template template, Template template,
Guid jobId, Guid jobId,
@@ -293,7 +752,8 @@ namespace PARR.TemplateMatcher
NextRun = template.NextRun, NextRun = template.NextRun,
Index = template.Index, Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused, StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator Initiator = initiator,
UnitsInTemplate = new List<Guid>()
}; };
await SendTemplateUpdateMessage(updateRequest); await SendTemplateUpdateMessage(updateRequest);
@@ -339,7 +799,8 @@ namespace PARR.TemplateMatcher
NextRun = nextRun, NextRun = nextRun,
Index = template.Index, Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator Initiator = initiator,
UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
}; };
await SendTemplateUpdateMessage(updateRequest); await SendTemplateUpdateMessage(updateRequest);
@@ -352,11 +813,12 @@ namespace PARR.TemplateMatcher
HistoryInitiator initiator) HistoryInitiator initiator)
{ {
logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId);
var mqRequest = new TemplateGeneratorWorkerMq var mqRequest = new TemplateGeneratorMq
{ {
JobId = jobId, JobId = jobId,
UnitId = unitId, UnitId = unitId,
HistoryInitiator = initiator HistoryInitiator = initiator,
UnitsInTemplate = new List<Guid>()
}; };
var msg = JsonSerializer.Serialize(mqRequest); var msg = JsonSerializer.Serialize(mqRequest);
@@ -387,9 +849,10 @@ namespace PARR.TemplateMatcher
} }
} }
// --- ИЗМЕНЕННЫЙ МЕТОД: Теперь используется как для простых, так и для групповых шаблонов ---
private async Task<Template?> TryReuseOneUnusedTemplateAsync( private async Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId, Guid jobId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
Guid unitId, Guid unitId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
HistoryInitiator initiator, HistoryInitiator initiator,
int maxAttempts = 3) int maxAttempts = 3)
{ {
@@ -398,6 +861,7 @@ namespace PARR.TemplateMatcher
try try
{ {
var unusedCandidates = await templateService.Get() var unusedCandidates = await templateService.Get()
.AsNoTracking() // Добавлено
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.OrderBy(t => t.DateModified ?? t.DateCreated) .OrderBy(t => t.DateModified ?? t.DateCreated)
.Take(UnusedCandidateBatchSize) .Take(UnusedCandidateBatchSize)
@@ -421,9 +885,9 @@ namespace PARR.TemplateMatcher
if (await templateService.CommitAsync(initiator)) if (await templateService.CommitAsync(initiator))
{ {
logger.LogInformation("Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}).", logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).",
candidate.Id, unitId, attempt); candidate.Id, attempt);
return candidate; return candidate; // Возвращаем захваченный шаблон
} }
// Откат при неудаче // Откат при неудаче
@@ -462,7 +926,9 @@ namespace PARR.TemplateMatcher
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId) private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{ {
return await jobService.Get() return await jobService.Get()
.AsNoTracking() // Добавлено
.Include(j => j.Group) .Include(j => j.Group)
.ThenInclude(j => j.GroupType)
.Include(j => j.AutoControl) .Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId); .FirstOrDefaultAsync(j => j.Id == jobId);
} }
@@ -473,9 +939,9 @@ namespace PARR.TemplateMatcher
return units?.ToHashSet() ?? new HashSet<Guid>(); return units?.ToHashSet() ?? new HashSet<Guid>();
} }
private async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId) private async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null)
{ {
var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id); var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id, index);
return rawName.ToUpper(); return rawName.ToUpper();
} }

View File

@@ -21,6 +21,7 @@ namespace PARR.TemplateMatcher
services.AddTransient<IMqTemplateMatcher, MqTemplateMatcher>(); services.AddTransient<IMqTemplateMatcher, MqTemplateMatcher>();
services.AddTransient<IJobValidatorService, JobValidatorService>(); services.AddTransient<IJobValidatorService, JobValidatorService>();
services.AddTransient<IJobGroupValidatorService, JobGroupValidatorService>();
services.AddTransient<ITemplateMatcher, TemplateMatcher>(); services.AddTransient<ITemplateMatcher, TemplateMatcher>();
} }

View File

@@ -41,7 +41,7 @@ namespace PARR.TemplateTaskGenerator
foreach (var unitId in unitIdsToCreateTemplate) foreach (var unitId in unitIdsToCreateTemplate)
{ {
var mqRequest = new TemplateGeneratorWorkerMq var mqRequest = new TemplateGeneratorMq
{ {
JobId = jobId, JobId = jobId,
UnitId = unitId, UnitId = unitId,