diff --git a/PARR.BLL/Domain/Mq/TemplateGeneratorWorkerMq.cs b/PARR.BLL/Domain/Mq/TemplateGeneratorMq.cs
similarity index 71%
rename from PARR.BLL/Domain/Mq/TemplateGeneratorWorkerMq.cs
rename to PARR.BLL/Domain/Mq/TemplateGeneratorMq.cs
index 9029aca1..372c2ffc 100644
--- a/PARR.BLL/Domain/Mq/TemplateGeneratorWorkerMq.cs
+++ b/PARR.BLL/Domain/Mq/TemplateGeneratorMq.cs
@@ -5,7 +5,7 @@ namespace PARR.BLL.Domain.Mq
///
/// Модель в MQ, простого создания Template
///
- public class TemplateGeneratorWorkerMq
+ public class TemplateGeneratorMq
{
///
/// Id регламентной работы
@@ -32,5 +32,15 @@ namespace PARR.BLL.Domain.Mq
///
public HistoryInitiator? HistoryInitiator { get; set; }
+ ///
+ /// Связанные ЭК для сгруппированного типа JobGroup
+ ///
+ public required List UnitsInTemplate { get; set; }
+
+ ///
+ /// Индекс, используется в групповых шаблонах
+ ///
+ public int? Index { get; set; }
+
}
}
diff --git a/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs b/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs
index 8c83ea01..d2de59aa 100644
--- a/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs
+++ b/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs
@@ -7,24 +7,47 @@ namespace PARR.BLL.Domain.Mq
{
public Guid TemplateId { get; set; }
+ ///
+ /// Id регламентной работы
+ ///
public Guid JobId { get; set; }
public required string Name { get; set; }
+ ///
+ /// Актировать шаблон при инициализации
+ ///
public bool IsActiveTemplate { get; set; }
+ ///
+ /// Актировать расписание при инициализации
+ ///
public bool IsActiveSchedule { get; set; }
public DateTimeOffset? LastRun { get; set; }
public DateTimeOffset NextRun { get; set; }
+ ///
+ /// Id Юнита(единицы обслуживания)/ЭК
+ ///
public Guid UnitId { get; set; }
+ ///
+ /// Индекс, используется в групповых шаблонах
+ ///
public int? Index { get; set; }
public TemplateStatusTypeEnum StatusTypeId { get; set; }
+ ///
+ /// Инициатор запроса к генератору
+ ///
public required HistoryInitiator Initiator { get; set; }
+
+ ///
+ /// Связанные ЭК для сгруппированного типа JobGroup
+ ///
+ public required List UnitsInTemplate { get; set; }
}
}
diff --git a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs b/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs
index df9a78c1..7b9f482e 100644
--- a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs
+++ b/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs
@@ -48,7 +48,7 @@ namespace PARR.DAL.DomainServices.Implementations
this.unitFilterService = unitFilterService;
}
- public async Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId)
+ public async Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null)
{
logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', unitId={UnitId}, jobId={JobId}", str, unitId, jobId);
@@ -95,7 +95,7 @@ namespace PARR.DAL.DomainServices.Implementations
// Делаем замену
var oldResult = resultName;
- resultName = ReplaceStandardShortcodes(job, unit, resultName);
+ resultName = ReplaceStandardShortcodes(job, unit, resultName, index);
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
.Replace("%ЭК%", unit.Name, StringComparison.OrdinalIgnoreCase)
.Replace("%ГРУППА_РАБОТ%", job.Group?.GroupName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("%РАБОТА%", job.WorkName, 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);
}
diff --git a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs b/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs
index cca7e67f..c57fc0ad 100644
--- a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs
+++ b/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs
@@ -4,7 +4,7 @@ namespace PARR.DAL.DomainServices.Interfaces
{
public interface IShortcodesService
{
- Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId);
+ Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null);
bool IsAnyShortcodes(string str);
diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs
index cd80eabc..a5db2d68 100644
--- a/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs
+++ b/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs
@@ -21,6 +21,11 @@ namespace PARR.DAL.Services.Implementations.Unit
}
+ public IQueryable Get()
+ {
+ return dataContext.UnitInUnits;
+ }
+
public Task> GetByParentIdAsync(Guid parentId)
{
@@ -29,6 +34,7 @@ namespace PARR.DAL.Services.Implementations.Unit
.ToListAsync();
}
+
public Task> GetByChildIdAsync(Guid childId)
{
return dataContext.UnitInUnits
diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs
index a3426b97..eec6f078 100644
--- a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs
+++ b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs
@@ -80,5 +80,10 @@ namespace PARR.DAL.Services.Implementations.Unit
.Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId))
.ToListAsync();
}
+
+ public IQueryable Get()
+ {
+ return dataContext.UnitInValues;
+ }
}
}
diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs
index 969d0e74..fcc6ee68 100644
--- a/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs
+++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs
@@ -16,5 +16,8 @@ namespace PARR.DAL.Services.Interfaces.Unit
/// Получает связи, где ParentUnitId unitIds (для IsParent=False).
///
Task> GetChildLinksByParentIdsAsync(IEnumerable parentUnitIds);
+
+
+ IQueryable Get();
}
}
diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs
index 5b56ed49..e4f011b7 100644
--- a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs
+++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs
@@ -12,6 +12,6 @@ namespace PARR.DAL.Services.Interfaces.Unit
/// Получает UnitInValue (с Value) для заданных UnitId и FieldId.
///
Task> GetByUnitIdsAndFieldIdsAsync(IEnumerable unitIds, IEnumerable fieldIds);
-
+ IQueryable Get();
}
}
diff --git a/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs b/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs
index 34a57370..3d70937b 100644
--- a/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs
+++ b/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs
@@ -2,6 +2,6 @@
{
public interface IValidatorService
{
- Task IsValidAsync(Guid jobId, Guid unitId);
+ Task IsValidAsync(Guid jobId, Guid unitId, int? index, List? unitsInTemplate = null);
}
}
diff --git a/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs b/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs
index a0bb92e4..9309f96f 100644
--- a/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs
+++ b/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs
@@ -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.Unit;
@@ -9,22 +11,25 @@ namespace PARR.TemplateGeneratorWorker.Services
private readonly ILogger logger;
private readonly IJobService jobService;
private readonly IUnitService unitService;
+ private readonly ITemplateService templateService;
public ValidatorService(
ILogger logger,
IJobService jobService,
- IUnitService unitService
+ IUnitService unitService,
+ ITemplateService templateService
)
{
this.logger = logger;
this.jobService = jobService;
this.unitService = unitService;
+ this.templateService = templateService;
}
- public async Task IsValidAsync(Guid jobId, Guid unitId)
+ public async Task IsValidAsync(Guid jobId, Guid unitId, int? index, List? unitsInTemplate = null)
{
-
+ // Проверяем JobId
var job = await jobService
.Get().AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == jobId);
@@ -35,6 +40,7 @@ namespace PARR.TemplateGeneratorWorker.Services
return false;
}
+ // Проверяем UnitId (UnitId - это ID регионального юнита)
var unit = await unitService
.Get().AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == unitId);
@@ -45,7 +51,35 @@ namespace PARR.TemplateGeneratorWorker.Services
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;
}
}
-}
+}
\ No newline at end of file
diff --git a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs
index 3e67cde5..37cfc4fe 100644
--- a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs
+++ b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs
@@ -1,5 +1,4 @@
-
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain;
@@ -43,31 +42,23 @@ namespace PARR.TemplateGeneratorWorker
this.templateService = templateService;
this.esppScheduleTransformService = esppScheduleTransformService;
}
+
public async Task GenerateTemplateAsync(string msg)
{
logger.LogInformation($"Получили запрос: {msg}");
- var query = transformService.GetModelFromJson(msg);
+ var query = transformService.GetModelFromJson(msg);
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;
}
- var existing = await templateService
- .Get()
- .AsNoTracking()
- .FirstOrDefaultAsync(t => t.JobId == query.JobId && t.UnitId == query.UnitId);
-
-
- if (existing != null)
+ // Проверяем всё
+ if (!await validatorService.IsValidAsync(query.JobId, query.UnitId, query.Index, query.UnitsInTemplate))
{
- 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;
}
@@ -101,22 +92,32 @@ namespace PARR.TemplateGeneratorWorker
IsActiveTemplate = query.IsActiveTemplate ?? false,
IsActiveSchedule = query.IsActiveSchedule ?? false,
NextRun = nextRun,
- //IsUnused = false,
StatusTypeId = TemplateStatusTypeEnum.Used,
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.Id, template.Name, query.JobId, query.UnitId);
+ template.UnitsInTemplate = query.UnitsInTemplate.Select(unitId => new UnitsInTemplate { UnitId = unitId }).ToList();
}
else
{
- logger.LogError("Ошибка создания шаблона: Name={Name}", templateName);
+ // Если список пуст, все равно инициализируем коллекцию, чтобы избежать NullReferenceException при сохранении (если это не nullable)
+ template.UnitsInTemplate = new List();
+ }
+
+ 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);
}
}
-
}
-}
+}
\ No newline at end of file
diff --git a/PARR.TemplateMatcher/ITemplateMatcher.cs b/PARR.TemplateMatcher/ITemplateMatcher.cs
index c9f18af0..c923797e 100644
--- a/PARR.TemplateMatcher/ITemplateMatcher.cs
+++ b/PARR.TemplateMatcher/ITemplateMatcher.cs
@@ -2,9 +2,10 @@
namespace PARR.TemplateMatcher
{
- internal interface ITemplateMatcher
+ public interface ITemplateMatcher
{
Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator);
Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator);
+ Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
}
}
diff --git a/PARR.TemplateMatcher/MqTemplateMatcher.cs b/PARR.TemplateMatcher/MqTemplateMatcher.cs
index 5af00a58..8361a7d8 100644
--- a/PARR.TemplateMatcher/MqTemplateMatcher.cs
+++ b/PARR.TemplateMatcher/MqTemplateMatcher.cs
@@ -59,8 +59,8 @@ namespace PARR.TemplateMatcher
switch (query.EntityType)
{
case SyncTaskEntityTypeEnum.Job:
- var validatorService = GetServiceInScope(scope);
- if (!await validatorService.IsValidAsync(query.Id))
+ var jobValidatorService = GetServiceInScope(scope);
+ if (!await jobValidatorService.IsValidJobAsync(query.Id))
{
logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id);
return;
@@ -83,9 +83,29 @@ namespace PARR.TemplateMatcher
break;
case SyncTaskEntityTypeEnum.JobGroup:
- logger.LogWarning("Обработка EntityType JobGroup не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
- break;
+ var jobGroupValidatorService = GetServiceInScope(scope);
+ 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:
logger.LogWarning("Обработка EntityType Template не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
break;
diff --git a/PARR.TemplateMatcher/Services/Implemetaions/JobGroupValidatorService.cs b/PARR.TemplateMatcher/Services/Implemetaions/JobGroupValidatorService.cs
new file mode 100644
index 00000000..4e5057b3
--- /dev/null
+++ b/PARR.TemplateMatcher/Services/Implemetaions/JobGroupValidatorService.cs
@@ -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 logger;
+ private readonly IJobGroupService jobGroupService;
+
+ public JobGroupValidatorService(
+ ILogger logger,
+ IJobGroupService jobGroupService
+ )
+ {
+ this.logger = logger;
+ this.jobGroupService = jobGroupService;
+ }
+ public async Task IsValidJobGroupAsync(Guid jobGroupId)
+ {
+ var isExist = await jobGroupService.GetAsync(jobGroupId);
+
+ if (isExist == null)
+ {
+ logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs b/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs
index 741e761b..99038eac 100644
--- a/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs
+++ b/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs
@@ -17,7 +17,7 @@ namespace PARR.TemplateMatcher.Services.Implemetaions
this.logger = logger;
this.jobService = jobService;
}
- public async Task IsValidAsync(Guid jobId)
+ public async Task IsValidJobAsync(Guid jobId)
{
var isExist = await jobService.GetAsync(jobId);
diff --git a/PARR.TemplateMatcher/Services/Interfaces/IJobGroupValidatorService.cs b/PARR.TemplateMatcher/Services/Interfaces/IJobGroupValidatorService.cs
new file mode 100644
index 00000000..ca32fdf2
--- /dev/null
+++ b/PARR.TemplateMatcher/Services/Interfaces/IJobGroupValidatorService.cs
@@ -0,0 +1,12 @@
+namespace PARR.TemplateMatcher.Services.Interfaces
+{
+ internal interface IJobGroupValidatorService
+ {
+ ///
+ /// Проверяет существование группы регламентной работы
+ ///
+ ///
+ ///
+ Task IsValidJobGroupAsync(Guid jobGroupId);
+ }
+}
diff --git a/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs b/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs
index 8fa90b2e..9586b88d 100644
--- a/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs
+++ b/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs
@@ -7,6 +7,6 @@
///
///
///
- Task IsValidAsync(Guid jobId);
+ Task IsValidJobAsync(Guid jobId);
}
}
diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/TemplateMatcher.cs
index d32a1d21..b299ac0f 100644
--- a/PARR.TemplateMatcher/TemplateMatcher.cs
+++ b/PARR.TemplateMatcher/TemplateMatcher.cs
@@ -4,11 +4,14 @@ using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain;
using PARR.Constants;
+using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
+using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;
+using PARR.DAL.Services.Interfaces.Unit;
using PARR.DAL.TransformServices;
using PARR.TemplateMatcher.Settings;
using System.Text.Json;
@@ -25,29 +28,41 @@ namespace PARR.TemplateMatcher
private readonly ILogger logger;
private readonly IUnitFilterService unitFilterService;
+ private readonly IUnitInUnitService unitInUnitService;
+ private readonly IUnitInValueService unitInValueService; // Добавлено
+ private readonly IUnitService unitService; // Добавлено
private readonly MqSettings mqSettings;
private readonly IMqService mqService;
private readonly ITemplateService templateService;
private readonly IJobService jobService;
+ private readonly IJobGroupService jobGroupService;
private readonly IShortcodesService shortcodesService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
public TemplateMatcher(
ILogger logger,
IUnitFilterService unitFilterService,
+ IUnitInUnitService unitInUnitService,
+ IUnitInValueService unitInValueService,
+ IUnitService unitService,
MqSettings mqSettings,
IMqService mqService,
ITemplateService templateService,
IJobService jobService,
+ IJobGroupService jobGroupService,
IShortcodesService shortcodesService,
IEsppScheduleTransformService esppScheduleTransformService)
{
this.logger = logger;
this.unitFilterService = unitFilterService;
+ this.unitInUnitService = unitInUnitService;
+ this.unitInValueService = unitInValueService;
+ this.unitService = unitService;
this.mqSettings = mqSettings;
this.mqService = mqService;
this.templateService = templateService;
this.jobService = jobService;
+ this.jobGroupService = jobGroupService;
this.shortcodesService = shortcodesService;
this.esppScheduleTransformService = esppScheduleTransformService;
}
@@ -66,12 +81,393 @@ namespace PARR.TemplateMatcher
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();
+
+ 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();
+ foreach (var link in potentialUnitInUnitLinks)
+ {
+ bool linkMatchesAllFilters = true;
+
+ foreach (var rf in relationshipFilters)
+ {
+ var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List());
+
+ 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 expectedUnitIds, HistoryInitiator initiator)
+ {
+ logger.LogDebug("Синхронизация обычных шаблонов для JobId {JobId}", job.Id);
+
var existingTemplates = await templateService.Get()
- .Where(t => t.JobId == jobId)
+ .AsNoTracking() // Добавлено
+ .Where(t => t.JobId == job.Id)
.ToListAsync();
logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.",
- jobId, expectedUnitIds.Count, existingTemplates.Count);
+ job.Id, expectedUnitIds.Count, existingTemplates.Count);
// Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны
if (!expectedUnitIds.Any())
@@ -79,19 +475,19 @@ namespace PARR.TemplateMatcher
if (existingTemplates.Any())
{
logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.",
- jobId, existingTemplates.Count);
+ job.Id, existingTemplates.Count);
foreach (var template in existingTemplates)
{
- await DeactivateTemplateAsync(template, jobId, initiator);
+ await DeactivateTemplateAsync(template, job.Id, initiator);
}
}
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;
}
@@ -102,12 +498,13 @@ namespace PARR.TemplateMatcher
foreach (var template in templatesToDeactivate)
{
- await DeactivateTemplateAsync(template, jobId, initiator);
+ await DeactivateTemplateAsync(template, job.Id, initiator);
}
// Перечитываем шаблоны после деактивации
existingTemplates = await templateService.Get()
- .Where(t => t.JobId == jobId)
+ .AsNoTracking() // Добавлено
+ .Where(t => t.JobId == job.Id)
.ToListAsync();
var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t);
@@ -121,7 +518,7 @@ namespace PARR.TemplateMatcher
foreach (var unitId in unitIdsMissingTemplates)
{
- var reused = await TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
+ var reused = await TryReuseOneUnusedTemplateAsync(job.Id, unitId, initiator);
if (reused != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId);
@@ -132,14 +529,15 @@ namespace PARR.TemplateMatcher
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reused.Id,
- JobId = jobId,
+ JobId = job.Id,
UnitId = unitId,
Name = expectedName,
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
- NextRun = nextRun
+ NextRun = nextRun,
+ UnitsInTemplate = new List()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -165,17 +563,17 @@ namespace PARR.TemplateMatcher
// Создание новых шаблонов
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)
{
logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
var existingTemplates = await templateService.Get()
+ .AsNoTracking() // Добавлено
.Where(t => t.JobId == jobId)
.ToListAsync();
@@ -251,7 +649,8 @@ namespace PARR.TemplateMatcher
NextRun = nextRun,
Index = template.Index,
StatusTypeId = targetStatus,
- Initiator = initiator
+ Initiator = initiator,
+ UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -261,6 +660,66 @@ namespace PARR.TemplateMatcher
}
+ private async Task UpdateTemplateUnitsAsync(Template template, List 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 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 DeactivateTemplateAsync(
Template template,
Guid jobId,
@@ -293,7 +752,8 @@ namespace PARR.TemplateMatcher
NextRun = template.NextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused,
- Initiator = initiator
+ Initiator = initiator,
+ UnitsInTemplate = new List()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -339,7 +799,8 @@ namespace PARR.TemplateMatcher
NextRun = nextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
- Initiator = initiator
+ Initiator = initiator,
+ UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -352,11 +813,12 @@ namespace PARR.TemplateMatcher
HistoryInitiator initiator)
{
logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId);
- var mqRequest = new TemplateGeneratorWorkerMq
+ var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = unitId,
- HistoryInitiator = initiator
+ HistoryInitiator = initiator,
+ UnitsInTemplate = new List()
};
var msg = JsonSerializer.Serialize(mqRequest);
@@ -387,9 +849,10 @@ namespace PARR.TemplateMatcher
}
}
+ // --- ИЗМЕНЕННЫЙ МЕТОД: Теперь используется как для простых, так и для групповых шаблонов ---
private async Task TryReuseOneUnusedTemplateAsync(
- Guid jobId,
- Guid unitId,
+ Guid jobId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
+ Guid unitId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
HistoryInitiator initiator,
int maxAttempts = 3)
{
@@ -398,6 +861,7 @@ namespace PARR.TemplateMatcher
try
{
var unusedCandidates = await templateService.Get()
+ .AsNoTracking() // Добавлено
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.OrderBy(t => t.DateModified ?? t.DateCreated)
.Take(UnusedCandidateBatchSize)
@@ -421,9 +885,9 @@ namespace PARR.TemplateMatcher
if (await templateService.CommitAsync(initiator))
{
- logger.LogInformation("Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}).",
- candidate.Id, unitId, attempt);
- return candidate;
+ logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).",
+ candidate.Id, attempt);
+ return candidate; // Возвращаем захваченный шаблон
}
// Откат при неудаче
@@ -462,7 +926,9 @@ namespace PARR.TemplateMatcher
private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId)
{
return await jobService.Get()
+ .AsNoTracking() // Добавлено
.Include(j => j.Group)
+ .ThenInclude(j => j.GroupType)
.Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId);
}
@@ -473,9 +939,9 @@ namespace PARR.TemplateMatcher
return units?.ToHashSet() ?? new HashSet();
}
- private async Task GetNormalizedTemplateNameAsync(Job job, Guid unitId)
+ private async Task 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();
}
diff --git a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs
index 941bbee5..e22ae913 100644
--- a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs
+++ b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs
@@ -21,6 +21,7 @@ namespace PARR.TemplateMatcher
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
}
diff --git a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs
index b3e0922f..7d3453d0 100644
--- a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs
+++ b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs
@@ -41,7 +41,7 @@ namespace PARR.TemplateTaskGenerator
foreach (var unitId in unitIdsToCreateTemplate)
{
- var mqRequest = new TemplateGeneratorWorkerMq
+ var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = unitId,