Compare commits

...

10 Commits

Author SHA1 Message Date
Mikhail Kuznetsov
fe4461ee4e feat(esppSync): Меняем любые пробельные символы на нормальный пробел. 2026-06-16 14:28:52 +10:00
Mikhail Trubnikov
108eb05853 feat(dal,domain): Таблицы для JobGroup фильтров. 2026-06-15 16:56:23 +10:00
Mikhail Trubnikov
1fb5cbd2e8 fix(core): Исправлена функция расчета кол-во работающих роботов 2026-06-15 14:39:03 +10:00
Mikhail Kuznetsov
c0720b38de Merge branch 'dev' of http://gitlab.dvgd.oao.rzd/devptk/parr/parr_api into dev 2026-06-15 11:48:05 +10:00
Mikhail Kuznetsov
db270dbb1c fix(shortcodesService): Для реализации шорткода %ГР_ПОЛЕ-ПН% добавлена сортировка для детерминации выходного списка. 2026-06-15 11:47:55 +10:00
Mikhail Trubnikov
dd206b44e9 Merge branch 'robot-report' into dev 2026-06-15 11:02:00 +10:00
Mikhail Trubnikov
21e00ce42e feat(api,core,domain): Статистика по роботам, кол-во работающих роботов в час 2026-06-15 11:00:53 +10:00
Mikhail Kuznetsov
1b21ee4a79 Merge branch 'dev' of http://gitlab.dvgd.oao.rzd/devptk/parr/parr_api into dev 2026-06-11 15:10:08 +10:00
Mikhail Kuznetsov
a438f18480 refactor(templateMatcher): Переход на Pipeline-архитектуру для SimpleSync и GroupedSync.
- SimpleTemplateSynchronizer и GroupedTemplateSynchronizer переведены на паттерн Pipeline с разделением на Read/Write этапы
- Выделены контракты этапов (ISimpleSyncStage, IGroupedSyncStage) и контексты (SimpleSyncContext, GroupedSyncContext)
- Read-этапы безопасны для тестов (не пишут в БД/MQ), Write-этапы изолированы через отдельные интерфейсы
- Добавлено [Perf]-логирование каждого этапа с метриками времени выполнения
- Логи приведены к человекочитаемому формату 'Имя' (ID) для Job, JobGroup и Unit
- Устранено дублирование данных в контекстах (FilteredUnits перезаписывается, TemplateGroups строго типизирован)
- Константы неиспользуемых шаблонов вынесены в UnusedTemplateConstants
- Структура проекта реорганизована: SimpleSync, GroupedSync, Implementations, Interfaces
2026-06-11 15:09:46 +10:00
Mikhail Trubnikov
19cb6b50bf feat(api,core,domain): RobotSnapshot - почасовой отчет о загрузке роботов 2026-06-11 14:44:39 +10:00
100 changed files with 6601 additions and 749 deletions

View File

@@ -50,6 +50,8 @@
public const string GetNextRun = Base + "/tests/next-run"; public const string GetNextRun = Base + "/tests/next-run";
public const string CreateCache = Base + "/tests/cache/"; public const string CreateCache = Base + "/tests/cache/";
public const string TestHandler = Base + "/tests/test/";
} }
public static class Template public static class Template

View File

@@ -23,5 +23,10 @@
/// Ошибок /// Ошибок
/// </summary> /// </summary>
public int Errors { get; set; } public int Errors { get; set; }
/// <summary>
/// Среднее кол-во роботов работающих в течении часа
/// </summary>
public double AvgRobots { get; set; }
} }
} }

View File

@@ -21,7 +21,7 @@ using PARR.Domain.Common.Pagination;
using PARR.Domain.Common.Rabbit.Messages; using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Roles; using PARR.Domain.Common.Roles;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Entities.Schedule; using PARR.Domain.Entities.Schedule;
using PARR.Domain.Enums; using PARR.Domain.Enums;

View File

@@ -8,7 +8,9 @@ using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics; using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base; using PARR.API.Controllers.V1.Base;
using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.RobotSnapshotServices;
using PARR.Domain.Common.Roles; using PARR.Domain.Common.Roles;
using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.Enums; using PARR.Domain.Enums;
namespace PARR.API.Controllers.V1.Statistics namespace PARR.API.Controllers.V1.Statistics
@@ -24,13 +26,15 @@ namespace PARR.API.Controllers.V1.Statistics
private readonly ITaskStatusRepository taskStatusService; private readonly ITaskStatusRepository taskStatusService;
private readonly IRobotHistoryRepository robotHistoryService; private readonly IRobotHistoryRepository robotHistoryService;
private readonly IMapper mapper; private readonly IMapper mapper;
private readonly IRobotSnapshotService _robotSnapshotService;
public StatRobotTaskController( public StatRobotTaskController(
IRobotRepository robotService, IRobotRepository robotService,
IRobotConfigurationRepository robotConfigurationService, IRobotConfigurationRepository robotConfigurationService,
ITaskStatusRepository taskStatusService, ITaskStatusRepository taskStatusService,
IRobotHistoryRepository robotHistoryService, IRobotHistoryRepository robotHistoryService,
IMapper mapper IMapper mapper,
IRobotSnapshotService robotSnapshotService
) )
{ {
this.robotService = robotService; this.robotService = robotService;
@@ -38,6 +42,7 @@ namespace PARR.API.Controllers.V1.Statistics
this.taskStatusService = taskStatusService; this.taskStatusService = taskStatusService;
this.robotHistoryService = robotHistoryService; this.robotHistoryService = robotHistoryService;
this.mapper = mapper; this.mapper = mapper;
_robotSnapshotService = robotSnapshotService;
} }
/// <summary> /// <summary>
@@ -87,11 +92,11 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatRobotTask.GetPeriodStatistics)] [HttpGet(ApiRoutes.StatRobotTask.GetPeriodStatistics)]
public async Task<IActionResult> GetPeriodStatistics([FromRoute] RobotsEnum robot) public async Task<IActionResult> GetPeriodStatistics([FromRoute] RobotsEnum robot)
{ {
var offset = TimeSpan.FromHours(0);
int minusHour = 24; int minusHour = 24;
var queryDate = DateTimeOffset.UtcNow.AddHours(-minusHour); var queryDate = DateTimeOffset.UtcNow.AddHours(-minusHour);
queryDate = new DateTimeOffset(queryDate.Year, queryDate.Month, queryDate.Day, queryDate.Hour, 0, 0, new TimeSpan(0)); queryDate = new DateTimeOffset(queryDate.Year, queryDate.Month, queryDate.Day, queryDate.Hour, 0, 0, offset);
//queryDate = queryDate.Date + new TimeSpan(queryDate.Hour, 0, 0);
//queryDate=queryDate.ToOffset(TimeSpan.Zero);
var query = robotHistoryService.Get() var query = robotHistoryService.Get()
@@ -110,16 +115,24 @@ namespace PARR.API.Controllers.V1.Statistics
var statistics = await query.ToListAsync(); var statistics = await query.ToListAsync();
//if (!statistics.Any())
// return NoContent();
var periodList = GetPeriodList(queryDate, DateTimeOffset.UtcNow); var periodList = GetPeriodList(queryDate, DateTimeOffset.UtcNow);
if (!periodList.Any()) if (!periodList.Any())
return NoContent(); return NoContent();
// Статистика по роботам (кол-во роботов)
var robotStats = await _robotSnapshotService.GetHourlyAnalyticsByRobotTypeAsync(new RobotAnalyticsRobotTypeQuery
{
DateStart = queryDate,
DateEnd = DateTimeOffset.UtcNow,
Offset = offset,
RobotType = robot
});
periodList.ForEach(item => periodList.ForEach(item =>
{ {
var robotCount = robotStats.FirstOrDefault(t => t.Hour.Date == item.Date.Date && t.Hour.Hour == item.Date.Hour);
var statItem = statistics.FirstOrDefault(t => t.Date.Date == item.Date.Date && t.Date.Hour == item.Date.Hour); var statItem = statistics.FirstOrDefault(t => t.Date.Date == item.Date.Date && t.Date.Hour == item.Date.Hour);
if (statItem != null) if (statItem != null)
{ {
@@ -127,6 +140,7 @@ namespace PARR.API.Controllers.V1.Statistics
item.Creating = statItem.Creating; item.Creating = statItem.Creating;
item.Updating = statItem.Updating; item.Updating = statItem.Updating;
item.Ok = statItem.Ok; item.Ok = statItem.Ok;
item.AvgRobots = robotCount?.AvgRobots ?? 0;
} }
}); });
@@ -150,6 +164,7 @@ namespace PARR.API.Controllers.V1.Statistics
Errors = 0, Errors = 0,
Ok = 0, Ok = 0,
Updating = 0, Updating = 0,
AvgRobots = 0,
Date = new DateTimeOffset(periodDate.Date.Year, periodDate.Date.Month, periodDate.Date.Day, periodDate.Hour, 0, 0, new TimeSpan(0)), Date = new DateTimeOffset(periodDate.Date.Year, periodDate.Date.Month, periodDate.Date.Day, periodDate.Hour, 0, 0, new TimeSpan(0)),
}); });

View File

@@ -15,7 +15,7 @@ using PARR.Core.Services.NextRunServices;
using PARR.Core.Services.Shortcodes; using PARR.Core.Services.Shortcodes;
using PARR.Domain.Common.Roles; using PARR.Domain.Common.Roles;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Enums; using PARR.Domain.Enums;
namespace PARR.API.Controllers.V1.Statistics namespace PARR.API.Controllers.V1.Statistics

View File

@@ -10,9 +10,11 @@ using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.NextRunServices; using PARR.Core.Services.NextRunServices;
using PARR.Core.Services.RobotSnapshotServices;
using PARR.Core.Services.UnitService.Interfaces; using PARR.Core.Services.UnitService.Interfaces;
using PARR.Core.Services.Workload.Implementations; using PARR.Core.Services.Workload.Implementations;
using PARR.Domain.Cache; using PARR.Domain.Cache;
using PARR.Domain.DTOs.RobotSnapshotDTO;
namespace PARR.API.Controllers.V1 namespace PARR.API.Controllers.V1
{ {
@@ -27,6 +29,7 @@ namespace PARR.API.Controllers.V1
private readonly ILogger<TestController> logger; private readonly ILogger<TestController> logger;
private readonly IUnitService unitService; private readonly IUnitService unitService;
private readonly IUnitRepository unitRepository; private readonly IUnitRepository unitRepository;
private readonly IRobotSnapshotService _robotSnapshotService;
public TestController( public TestController(
IClientService clientService, IClientService clientService,
@@ -37,7 +40,8 @@ namespace PARR.API.Controllers.V1
WorkloadCacheService workloadCacheService, WorkloadCacheService workloadCacheService,
ILogger<TestController> logger, ILogger<TestController> logger,
IUnitService unitService, IUnitService unitService,
IUnitRepository unitRepository IUnitRepository unitRepository,
IRobotSnapshotService robotSnapshotService
) )
{ {
this.clientService = clientService; this.clientService = clientService;
@@ -48,6 +52,7 @@ namespace PARR.API.Controllers.V1
this.logger = logger; this.logger = logger;
this.unitService = unitService; this.unitService = unitService;
this.unitRepository = unitRepository; this.unitRepository = unitRepository;
_robotSnapshotService = robotSnapshotService;
} }
@@ -161,6 +166,32 @@ namespace PARR.API.Controllers.V1
} }
/// <summary>
/// Тестовый метод
/// </summary>
/// <returns></returns>
[HttpGet(ApiRoutes.Test.TestHandler)]
public async Task<IActionResult> Test()
{
//var data = await _robotSnapshotService.GetHourlyAnalyticsAsync(new RobotAnalyticsQuery
//{
// DateStart = DateTimeOffset.UtcNow.AddHours(-1),
// DateEnd = DateTimeOffset.UtcNow,
// Offset = TimeSpan.FromMinutes(600)
//});
//var data = await _robotSnapshotService.GetHourlyAnalyticsByRobotTypeAsync(new RobotAnalyticsRobotTypeQuery
//{
// DateStart = DateTimeOffset.UtcNow.AddHours(-12),
// DateEnd = DateTimeOffset.UtcNow,
// Offset = TimeSpan.FromMinutes(600),
// RobotType = PARR.Domain.Enums.RobotsEnum.TemplateOrder
//});
//return Ok(data);
return Ok();
}

View File

@@ -14,6 +14,7 @@ using PARR.Domain.DTOs.Workload;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Entities.Schedule; using PARR.Domain.Entities.Schedule;
using PARR.Domain.Entities.Unit; using PARR.Domain.Entities.Unit;

View File

@@ -1,7 +1,7 @@
using AutoMapper; using AutoMapper;
using PARR.API.Contracts.V1.Responses; using PARR.API.Contracts.V1.Responses;
using PARR.Core.Repositories.Interfaces.Schedule; using PARR.Core.Repositories.Interfaces.Schedule;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Settings; using PARR.Domain.Settings;
namespace PARR.API.MappingProfiles.Resolvers namespace PARR.API.MappingProfiles.Resolvers

View File

@@ -3,7 +3,7 @@ using PARR.API.Contracts.V1.Requests;
using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.API.Validators namespace PARR.API.Validators
{ {

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Entities.Schedule; using PARR.Domain.Entities.Schedule;
namespace PARR.Core.Common.Helpers; namespace PARR.Core.Common.Helpers;

View File

@@ -1,5 +1,5 @@
using PARR.Core.Repositories.Base; using PARR.Core.Repositories.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.Core.Repositories.Interfaces.Job namespace PARR.Core.Repositories.Interfaces.Job
{ {

View File

@@ -1,5 +1,5 @@
using PARR.Core.Repositories.Base; using PARR.Core.Repositories.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.Core.Repositories.Interfaces.Job namespace PARR.Core.Repositories.Interfaces.Job
{ {

View File

@@ -0,0 +1,9 @@
using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.Core.Repositories.Interfaces.JobGroupRepositories
{
public interface IJobGroupAutoControlRepository
{
IQueryable<JobGroupAutoControl> Get();
}
}

View File

@@ -0,0 +1,9 @@
using PARR.Core.Repositories.Base;
using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.Core.Repositories.Interfaces.JobGroupRepositories
{
public interface IJobGroupFieldFilterRepository : IBaseRepository<JobGroupFieldFilter>
{
}
}

View File

@@ -0,0 +1,6 @@
namespace PARR.Core.Repositories.Interfaces.JobGroupRepositories
{
public interface IJobGroupRelationshipFilterRepository
{
}
}

View File

@@ -0,0 +1,9 @@
using PARR.Core.Repositories.Base;
using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.Core.Repositories.Interfaces.JobGroupRepositories
{
public interface IJobGroupUnitFilterRepository : IBaseRepository<JobGroupUnitFilter>
{
}
}

View File

@@ -1,5 +1,5 @@
using PARR.Core.Services.NextRunServices.Models; using PARR.Core.Services.NextRunServices.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Enums; using PARR.Domain.Enums;
namespace PARR.Core.Services.NextRunServices namespace PARR.Core.Services.NextRunServices

View File

@@ -7,7 +7,7 @@ using PARR.Core.Services.NextRunServices.Models;
using PARR.Core.Services.NextRunServices.Subservices; using PARR.Core.Services.NextRunServices.Subservices;
using PARR.Core.Services.Shortcodes; using PARR.Core.Services.Shortcodes;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.Domain.Settings; using PARR.Domain.Settings;

View File

@@ -4,8 +4,34 @@ namespace PARR.Core.Services.RobotSnapshotServices
{ {
public interface IRobotSnapshotService public interface IRobotSnapshotService
{ {
/// <summary>
/// Получить статистику
/// </summary>
/// <param name="queryDto"></param>
/// <returns></returns>
Task<List<RobotSnapshotItemDto>> GetAsync(RobotSnapshotQuery queryDto); Task<List<RobotSnapshotItemDto>> GetAsync(RobotSnapshotQuery queryDto);
/// <summary>
/// Записать статистику
/// </summary>
/// <param name="robotSnapshot"></param>
/// <returns></returns>
Task<RobotSnapshotItemDto> CreateAsync(CreateRobotSnapshot robotSnapshot); Task<RobotSnapshotItemDto> CreateAsync(CreateRobotSnapshot robotSnapshot);
/// <summary>
/// Получить аналитику за период с группировкой по ip серверов. Разбивка по часам.
/// Максимальная одновременная работа роботов. Среднее кол-во роботов в час. Максимально возможное кол-во роботов.
/// </summary>
/// <param name="queryDto"></param>
/// <returns></returns>
Task<List<ServerHourlyAnalyticsDto>> GetHourlyAnalyticsAsync(RobotAnalyticsQuery queryDto);
/// <summary>
/// Получить аналитику за период по типу робота. Разбивка по часам.
/// Среднее кол-во роботов в час.
/// </summary>
/// <param name="queryDto"></param>
/// <returns></returns>
Task<List<HourlyRobotTypeAnalyticsDto>> GetHourlyAnalyticsByRobotTypeAsync(RobotAnalyticsRobotTypeQuery queryDto);
} }
} }

View File

@@ -11,9 +11,9 @@ namespace PARR.Core.Services.RobotSnapshotServices
{ {
internal class RobotSnapshotService : IRobotSnapshotService internal class RobotSnapshotService : IRobotSnapshotService
{ {
private readonly IRobotSnapshotRepository robotSnapshotRepository; private readonly IRobotSnapshotRepository _robotSnapshotRepository;
private readonly IUserRepository userRepository; private readonly IUserRepository _userRepository;
private readonly IMapper mapper; private readonly IMapper _mapper;
public RobotSnapshotService( public RobotSnapshotService(
IRobotSnapshotRepository robotSnapshotRepository, IRobotSnapshotRepository robotSnapshotRepository,
@@ -21,9 +21,9 @@ namespace PARR.Core.Services.RobotSnapshotServices
IMapper mapper IMapper mapper
) )
{ {
this.robotSnapshotRepository = robotSnapshotRepository; _robotSnapshotRepository = robotSnapshotRepository;
this.userRepository = userRepository; _userRepository = userRepository;
this.mapper = mapper; _mapper = mapper;
} }
@@ -37,7 +37,7 @@ namespace PARR.Core.Services.RobotSnapshotServices
var dateEnd = DateTimeOffset.UtcNow; var dateEnd = DateTimeOffset.UtcNow;
var dateStart = dateEnd.AddMinutes(-minutes); var dateStart = dateEnd.AddMinutes(-minutes);
var query = robotSnapshotRepository.Get().AsNoTracking() var query = _robotSnapshotRepository.Get().AsNoTracking()
.Where(t => dateStart <= t.DateCreated && t.DateCreated <= dateEnd); .Where(t => dateStart <= t.DateCreated && t.DateCreated <= dateEnd);
if (!string.IsNullOrEmpty(queryDto.Ip)) if (!string.IsNullOrEmpty(queryDto.Ip))
@@ -47,7 +47,7 @@ namespace PARR.Core.Services.RobotSnapshotServices
var robotIps = groupingSnapshots.Select(t => t.Key).ToHashSet(); var robotIps = groupingSnapshots.Select(t => t.Key).ToHashSet();
var robotList = await userRepository.Get() var robotList = await _userRepository.Get()
.AsNoTracking() .AsNoTracking()
.Where(t => robotIps.Contains(t.Ip)) .Where(t => robotIps.Contains(t.Ip))
.ToDictionaryAsync(t => t.Ip); .ToDictionaryAsync(t => t.Ip);
@@ -59,12 +59,262 @@ namespace PARR.Core.Services.RobotSnapshotServices
robotList.TryGetValue(item.Key, out var robot); robotList.TryGetValue(item.Key, out var robot);
var userDto = new UserBaseDto { Ip = item.Key, Description = robot?.Description ?? string.Empty, Name = robot?.Name ?? string.Empty }; var userDto = new UserBaseDto { Ip = item.Key, Description = robot?.Description ?? string.Empty, Name = robot?.Name ?? string.Empty };
var snapshots = mapper.Map<List<RobotSnapshotDto>>(item).OrderBy(t => t.DateCreated).ToList(); var snapshots = _mapper.Map<List<RobotSnapshotDto>>(item).OrderBy(t => t.DateCreated).ToList();
result.Add(new RobotSnapshotItemDto { Robot = userDto, Snapshots = snapshots }); result.Add(new RobotSnapshotItemDto { Robot = userDto, Snapshots = snapshots });
} }
return result.OrderBy(t => t.Robot.Ip).ThenBy(t => t.Robot.Name).ToList(); return result.OrderBy(t => t.Robot.Name).ThenBy(t => t.Robot.Ip).ToList();
}
public async Task<List<ServerHourlyAnalyticsDto>> GetHourlyAnalyticsAsync(RobotAnalyticsQuery queryDto)
{
var snapshotsQuery = _robotSnapshotRepository.Get()
.AsNoTracking()
.Where(t => queryDto.DateStart <= t.DateCreated && t.DateCreated <= queryDto.DateEnd);
// Фильтруем по конкретному IP
if (!string.IsNullOrEmpty(queryDto.Ip))
snapshotsQuery = snapshotsQuery.Where(t => t.Ip == queryDto.Ip);
// Группируем на стороне PostgreSQL с учетом часового пояса пользователя.
// Метод .Add(offset) сдвигает UTC-дату в Postgres на интервал времени пользователя,
// благодаря чему минуты объединяются в правильные локальные "часовые корзины".
var rawGroupedData = await snapshotsQuery
.GroupBy(t => new
{
t.Ip,
// Явно сдвигаем дату и достаем только нужные компоненты
Year = (t.DateCreated + queryDto.Offset).DateTime.Year,
Month = (t.DateCreated + queryDto.Offset).DateTime.Month,
Day = (t.DateCreated + queryDto.Offset).DateTime.Day,
Hour = (t.DateCreated + queryDto.Offset).DateTime.Hour
})
.Select(g => new
{
g.Key.Ip,
// Вытаскиваем компоненты даты из уже смещенного локального времени
LocalYear = g.Key.Year,
LocalMonth = g.Key.Month,
LocalDay = g.Key.Day,
LocalHour = g.Key.Hour,
MaxAllowed = g.Max(t => t.MaxRobots),
MaxTemplates = g.Max(t => t.TemplateRobotsCount),
MaxSchedules = g.Max(t => t.ScheduleRobotsCount),
SumTemplates = g.Sum(t => t.TemplateRobotsCount),
SumSchedules = g.Sum(t => t.ScheduleRobotsCount)
})
.ToListAsync();
if (!rawGroupedData.Any())
return new List<ServerHourlyAnalyticsDto>();
// Получаем информацию о серверах
var serverIps = rawGroupedData.Select(g => g.Ip).ToHashSet();
var serverDictionary = await _userRepository.Get()
.AsNoTracking()
.Where(t => serverIps.Contains(t.Ip))
.ToDictionaryAsync(t => t.Ip);
// Группируем полученные агрегаты по серверам уже в памяти
var dataByServers = rawGroupedData.GroupBy(g => g.Ip);
var result = new List<ServerHourlyAnalyticsDto>();
// Принудительно переводим UTC-границы запроса в локальное время пользователя для корректной генерации "дыр"
var localStart = queryDto.DateStart.ToOffset(queryDto.Offset).DateTime;
var localEnd = queryDto.DateEnd.ToOffset(queryDto.Offset).DateTime;
// Округляем начальный час до ровного значения (00 минут, 00 секунд)
var startHour = new DateTime(localStart.Year, localStart.Month, localStart.Day, localStart.Hour, 0, 0);
foreach (var serverGroup in dataByServers)
{
serverDictionary.TryGetValue(serverGroup.Key, out var robotInfo);
var userDto = new UserBaseDto
{
Ip = serverGroup.Key,
Description = robotInfo?.Description ?? string.Empty,
Name = robotInfo?.Name ?? string.Empty
};
// Формируем точки, которые удалось вытащить из базы данных
var hourlyPoints = serverGroup.Select(g => new RobotHourlyPointDto
{
// Возвращаем дату со смещением пользователя, чтобы Angular сразу отображал правильный час
Hour = new DateTimeOffset(g.LocalYear, g.LocalMonth, g.LocalDay, g.LocalHour, 0, 0, TimeSpan.Zero),
MaxAllowedRobots = g.MaxAllowed,
MaxTemplateRobots = g.MaxTemplates,
MaxScheduleRobots = g.MaxSchedules,
// Считаем среднее количество роботов, деля поминутную сумму строго на 60 минут часа
//todo: тут не всегда нужно делить на 60, если час последний, то там может быть 50 минут или 20 и тп
AvgTemplateRobots = Math.Round(g.SumTemplates / 60.0, 1),
AvgScheduleRobots = Math.Round(g.SumSchedules / 60.0, 1)
})
.ToList();
// Алгоритм заполнения пропусков во времени (если сервер был выключен)
var filledPoints = new List<RobotHourlyPointDto>();
var currentHour = startHour;
int lastKnownMax = hourlyPoints.FirstOrDefault()?.MaxAllowedRobots ?? 0;
while (currentHour <= localEnd)
{
// Создаем временную точку со смещением для точного сопоставления
var targetOffsetDateTime = new DateTimeOffset(currentHour, TimeSpan.Zero);
var point = hourlyPoints.FirstOrDefault(p => p.Hour == targetOffsetDateTime);
if (point != null)
{
filledPoints.Add(point);
lastKnownMax = point.MaxAllowedRobots;
}
else
{
// Если за этот час записей в БД нет — значит, сервер был полностью оффлайн.
// Выводим нули, чтобы график в Angular не прерывался, а плавно падал.
filledPoints.Add(new RobotHourlyPointDto
{
Hour = targetOffsetDateTime,
MaxAllowedRobots = lastKnownMax,
MaxTemplateRobots = 0,
AvgTemplateRobots = 0,
MaxScheduleRobots = 0,
AvgScheduleRobots = 0
});
}
// Шагаем строго на +1 час вперед по локальному времени клиента
currentHour = currentHour.AddHours(1);
}
result.Add(new ServerHourlyAnalyticsDto
{
Robot = userDto,
Snapshots = filledPoints.OrderBy(p => p.Hour).ToList()
});
}
return result
.OrderBy(t => t.Robot.Name)
.ThenBy(t => t.Robot.Ip)
.ToList();
}
public async Task<List<HourlyRobotTypeAnalyticsDto>> GetHourlyAnalyticsByRobotTypeAsync(RobotAnalyticsRobotTypeQuery queryDto)
{
var snapshotsQuery = _robotSnapshotRepository.Get()
.AsNoTracking()
.Where(t => queryDto.DateStart <= t.DateCreated && t.DateCreated <= queryDto.DateEnd)
.GroupBy(t => new
{
// Явно сдвигаем дату
Year = (t.DateCreated + queryDto.Offset).DateTime.Year,
Month = (t.DateCreated + queryDto.Offset).DateTime.Month,
Day = (t.DateCreated + queryDto.Offset).DateTime.Day,
Hour = (t.DateCreated + queryDto.Offset).DateTime.Hour
});
IQueryable<RobotSnapshotRobotTypeDbDto> groupedQuery;
switch (queryDto.RobotType)
{
case Domain.Enums.RobotsEnum.TemplateOrder:
groupedQuery = snapshotsQuery.Select(g => new RobotSnapshotRobotTypeDbDto
{
// Вытаскиваем компоненты даты из уже смещенного локального времени
LocalYear = g.Key.Year,
LocalMonth = g.Key.Month,
LocalDay = g.Key.Day,
LocalHour = g.Key.Hour,
// Макс не правильно.
//MaxAllowed = g.Max(t => t.MaxRobots),
Sum = g.Sum(t => t.TemplateRobotsCount)
});
break;
case Domain.Enums.RobotsEnum.ScheduleOrder:
groupedQuery = snapshotsQuery.Select(g => new RobotSnapshotRobotTypeDbDto
{
// Вытаскиваем компоненты даты из уже смещенного локального времени
LocalYear = g.Key.Year,
LocalMonth = g.Key.Month,
LocalDay = g.Key.Day,
LocalHour = g.Key.Hour,
// Макс не правильно.
//MaxAllowed = g.Max(t => t.MaxRobots),
Sum = g.Sum(t => t.ScheduleRobotsCount)
});
break;
default:
throw new ArgumentException("Не обработано значение параметра", nameof(queryDto.RobotType));
}
var data = await groupedQuery.ToListAsync();
if (!data.Any())
return new List<HourlyRobotTypeAnalyticsDto>();
// Фиксируем текущее время в UTC и «округляем» его до начала часа
var nowLocal = DateTimeOffset.UtcNow.ToOffset(queryDto.Offset);
var currentHourLocal = new DateTime(nowLocal.Year, nowLocal.Month, nowLocal.Day, nowLocal.Hour, 0, 0);
var dbDataDict = data.ToDictionary(
k => new DateTime(k.LocalYear, k.LocalMonth, k.LocalDay, k.LocalHour, 0, 0),
v =>
{
double minutesInHour = 60.0;
var dbLocalHour = new DateTime(v.LocalYear, v.LocalMonth, v.LocalDay, v.LocalHour, 0, 0);
// Строгая проверка: совпадает ли час из базы данных ИМЕННО с текущим часом сегодня?
// Если да, то нужно делить не на 60, а на прошедние минуты
if (dbLocalHour == currentHourLocal)
{
// Если идет первая минута часа, делим на 1, чтобы избежать деления на ноль
minutesInHour = nowLocal.Minute == 0 ? 1.0 : (double)nowLocal.Minute;
}
return new
{
//MaxAllowedRobots = t.MaxAllowed,
AvgRobots = Math.Round(v.Sum / minutesInHour, 1)
};
});
// Заполняем модель
var statistics = new List<HourlyRobotTypeAnalyticsDto>();
#region Алгоритм заполнения пропусков во времени (если сервер был выключен)
// Превращаем границы в локальное время для генерации сетки часов
var currentHour = queryDto.DateStart.ToOffset(queryDto.Offset).DateTime;
var localEnd = queryDto.DateEnd.ToOffset(queryDto.Offset).DateTime;
// Срезаем минуты/секунды для старта
currentHour = new DateTime(currentHour.Year, currentHour.Month, currentHour.Day, currentHour.Hour, 0, 0);
while (currentHour <= localEnd)
{
// Ищем значение в словаре. Если нет — ставим 0
dbDataDict.TryGetValue(currentHour, out var statInfo);
// 1. Создаем DateTimeOffset для локального времени с правильным смещением
var localDateTimeOffset = new DateTimeOffset(currentHour, queryDto.Offset);
statistics.Add(new HourlyRobotTypeAnalyticsDto
{
// Возвращаем DateTimeOffset с нулевым смещением (как в вашем коде) или с queryDto.Offset
Hour = localDateTimeOffset.ToUniversalTime(),
AvgRobots = statInfo?.AvgRobots ?? 0
});
currentHour = currentHour.AddHours(1);
}
#endregion
return statistics;
} }
@@ -79,19 +329,19 @@ namespace PARR.Core.Services.RobotSnapshotServices
Ip = robotSnapshot.Ip Ip = robotSnapshot.Ip
}; };
var createdResult = await robotSnapshotRepository.CreateAsync(snapshot); var createdResult = await _robotSnapshotRepository.CreateAsync(snapshot);
var commitResult = await robotSnapshotRepository.CommitAsync(); var commitResult = await _robotSnapshotRepository.CommitAsync();
if (!createdResult || !commitResult) if (!createdResult || !commitResult)
throw new DbErrorException("Ошибка сохранения в БД"); throw new DbErrorException("Ошибка сохранения в БД");
var user = await userRepository.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Ip == robotSnapshot.Ip); var user = await _userRepository.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Ip == robotSnapshot.Ip);
var userDto = new UserBaseDto { Ip = robotSnapshot.Ip, Description = user?.Description ?? string.Empty, Name = user?.Name ?? string.Empty }; var userDto = new UserBaseDto { Ip = robotSnapshot.Ip, Description = user?.Description ?? string.Empty, Name = user?.Name ?? string.Empty };
var result = new RobotSnapshotItemDto var result = new RobotSnapshotItemDto
{ {
Robot = userDto, Robot = userDto,
Snapshots = mapper.Map<List<RobotSnapshotDto>>(new List<RobotSnapshot> { snapshot }) Snapshots = _mapper.Map<List<RobotSnapshotDto>>(new List<RobotSnapshot> { snapshot })
}; };
return result; return result;

View File

@@ -50,7 +50,6 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler
var unitsList = template.UnitsInTemplate; var unitsList = template.UnitsInTemplate;
if (unitsList.Count == 0) if (unitsList.Count == 0)
{ {
// Данные должны быть загружены оркестратором. Если пусто — значит в БД действительно нет связей.
return input.Replace("%ГРОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase); return input.Replace("%ГРОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase);
} }
@@ -67,12 +66,16 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler
.Where(fv => fieldValueIds.Contains(fv.Id)) .Where(fv => fieldValueIds.Contains(fv.Id))
.ToDictionaryAsync(fv => fv.Id, fv => fv.Value ?? string.Empty, ct); .ToDictionaryAsync(fv => fv.Id, fv => fv.Value ?? string.Empty, ct);
var lines = unitsList.Select((uit, i) => // Сортировка по имени юнита для детерминированного результата
{ var lines = unitsList
var uName = unitNames.GetValueOrDefault(uit.UnitId, $"(UnitId={uit.UnitId})"); .OrderBy(u => unitNames.GetValueOrDefault(u.UnitId, string.Empty), StringComparer.OrdinalIgnoreCase)
var fVal = fieldValueStrings.GetValueOrDefault(uit.UnitFieldValueId, string.Empty); .ThenBy(u => fieldValueStrings.GetValueOrDefault(u.UnitFieldValueId, string.Empty), StringComparer.OrdinalIgnoreCase)
return $"{i + 1}. {uName} ({fVal})"; .Select((uit, i) =>
}); {
var uName = unitNames.GetValueOrDefault(uit.UnitId, $"(UnitId={uit.UnitId})");
var fVal = fieldValueStrings.GetValueOrDefault(uit.UnitFieldValueId, string.Empty);
return $"{i + 1}. {uName} ({fVal})";
});
return input.Replace("%ГРОЛЕ-ПН%", string.Join("\n", lines), StringComparison.OrdinalIgnoreCase); return input.Replace("%ГРОЛЕ-ПН%", string.Join("\n", lines), StringComparison.OrdinalIgnoreCase);
} }

View File

@@ -1,6 +1,6 @@
namespace PARR.Core.Services.UnitFilterService.Models namespace PARR.Core.Services.UnitFilterService.Models
{ {
internal class UnitFilterMatchResult public class UnitFilterMatchResult
{ {
public Guid UnitId { get; set; } public Guid UnitId { get; set; }
public HashSet<Guid> ValidParentIds { get; set; } = new(); public HashSet<Guid> ValidParentIds { get; set; } = new();

View File

@@ -12,6 +12,7 @@ using PARR.Core.Services.UnitService.Interfaces;
using PARR.DAL.Context; using PARR.DAL.Context;
using PARR.Domain.Cache.Models; using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Entities.Unit; using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums; using PARR.Domain.Enums;

View File

@@ -4,6 +4,7 @@ using PARR.Domain.Common.Roles;
using PARR.Domain.Common.Template; using PARR.Domain.Common.Template;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Entities.RobotEntities; using PARR.Domain.Entities.RobotEntities;
using PARR.Domain.Entities.Schedule; using PARR.Domain.Entities.Schedule;
using PARR.Domain.Entities.TaskEntities; using PARR.Domain.Entities.TaskEntities;
@@ -106,16 +107,28 @@ namespace PARR.DAL.Context
public DbSet<JobUnitFilter> JobUnitFilters { get; set; } public DbSet<JobUnitFilter> JobUnitFilters { get; set; }
public DbSet<JobGroup> JobGroups { get; set; }
public DbSet<JobGroupType> JobGroupTypes { get; set; }
public DbSet<JobRelationshipFilter> JobRelationshipFilters { get; set; } public DbSet<JobRelationshipFilter> JobRelationshipFilters { get; set; }
public DbSet<JobAutoControl> JobAutoControls { get; set; } public DbSet<JobAutoControl> JobAutoControls { get; set; }
#endregion
#region JobGroup
public DbSet<JobGroup> JobGroups { get; set; }
public DbSet<JobGroupType> JobGroupTypes { get; set; }
public DbSet<JobGroupDistributionConfig> JobGroupDistributionConfigs { get; set; } public DbSet<JobGroupDistributionConfig> JobGroupDistributionConfigs { get; set; }
public DbSet<JobGroupAutoControl> JobGroupAutoControls { get; set; }
public DbSet<JobGroupUnitFilter> JobGroupUnitFilters { get; set; }
public DbSet<JobGroupFieldFilter> JobGroupFieldFilters { get; set; }
public DbSet<JobGroupRelationshipFilter> JobGroupRelationshipFilters { get; set; }
#endregion #endregion
#region Tasks #region Tasks
@@ -166,9 +179,9 @@ namespace PARR.DAL.Context
modelBuilder.Entity<JobGroupType>(f => modelBuilder.Entity<JobGroupType>(f =>
{ {
f.HasData( f.HasData(
new() { Id = new Guid("4FA62E79-86BB-47C2-BE1A-72A716A170FA"), DateCreated = dateCreated, DateModified = null, Name = JobGroupTypesEnum.Simple.ToString(), Code = JobGroupTypesEnum.Simple, Description = "Обычный" }, new() { Id = new Guid("4FA62E79-86BB-47C2-BE1A-72A716A170FA"), DateCreated = dateCreated, DateModified = null, Name = JobGroupTypesEnum.Simple.ToString(), Code = JobGroupTypesEnum.Simple, Description = "Обычный", IsAllowJobUnitFilter = true, IsJobGroupAutoControl = false },
new() { Id = new Guid("6EC58B1A-5C40-47B9-B036-4FA490E8D503"), DateCreated = dateCreated, DateModified = null, Name = JobGroupTypesEnum.Umbrella.ToString(), Code = JobGroupTypesEnum.Umbrella, Description = "Зонтик" }, new() { Id = new Guid("6EC58B1A-5C40-47B9-B036-4FA490E8D503"), DateCreated = dateCreated, DateModified = null, Name = JobGroupTypesEnum.Umbrella.ToString(), Code = JobGroupTypesEnum.Umbrella, Description = "Зонтик", IsAllowJobUnitFilter = false, IsJobGroupAutoControl = true },
new() { Id = new Guid("318625E5-F833-436A-99DF-2A382A1E71C7"), DateCreated = dateCreated, DateModified = null, Name = JobGroupTypesEnum.Group.ToString(), Code = JobGroupTypesEnum.Group, Description = "Сгруппированный" } new() { Id = new Guid("318625E5-F833-436A-99DF-2A382A1E71C7"), DateCreated = dateCreated, DateModified = null, Name = JobGroupTypesEnum.Group.ToString(), Code = JobGroupTypesEnum.Group, Description = "Сгруппированный", IsAllowJobUnitFilter = false, IsJobGroupAutoControl = true }
); );
}); });
#endregion #endregion
@@ -205,7 +218,7 @@ namespace PARR.DAL.Context
{ {
// При добавлении записей, добавлять тоже в PARR.DAL.Contracts.SettingsFromDb // При добавлении записей, добавлять тоже в PARR.DAL.Contracts.SettingsFromDb
f.HasData( f.HasData(
new { Name = nameof(SettingsFromDb.Initiator), Description = "Инициатор регламентной работы, указывается при создании шаблона в ЕСПП.", Value = "ОВЧАРЕНКО АЛЕКСЕЙ ВИТАЛЬЕВИЧ (OVCHARENKOAV@GVC.OAO.RZD)" }, new { Name = nameof(SettingsFromDb.Initiator), Description = "Инициатор регламентной работы, указывается при создании шаблона в ЕСПП.", Value = "АКСЕНОВ АЛЕКСАНДР ЕВГЕНЬЕВИЧ (AKSENOVAE@GVC.OAO.RZD)" },
new { Name = nameof(SettingsFromDb.ClosingCode), Description = "Код закрытия регламентной работы, указывается при создании шаблона в ЕСПП.", Value = "выполнен" }, new { Name = nameof(SettingsFromDb.ClosingCode), Description = "Код закрытия регламентной работы, указывается при создании шаблона в ЕСПП.", Value = "выполнен" },
new { Name = nameof(SettingsFromDb.Category), Description = "Категория создаваемого объекта в ЕСПП", Value = "регламентная работа" }, new { Name = nameof(SettingsFromDb.Category), Description = "Категория создаваемого объекта в ЕСПП", Value = "регламентная работа" },
new { Name = nameof(SettingsFromDb.TemplatePrefixName), Description = "Префикс имени шаблона в ЕСПП", Value = "%PREFIX%-ЭИТИ-ПТК-ПАРР" }, new { Name = nameof(SettingsFromDb.TemplatePrefixName), Description = "Префикс имени шаблона в ЕСПП", Value = "%PREFIX%-ЭИТИ-ПТК-ПАРР" },

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
using PARR.Core.Repositories.Interfaces.RobotRepositories; using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Core.Repositories.Interfaces.Schedule; using PARR.Core.Repositories.Interfaces.Schedule;
using PARR.Core.Repositories.Interfaces.TaskRepositories; using PARR.Core.Repositories.Interfaces.TaskRepositories;
@@ -12,6 +13,7 @@ using PARR.DAL.Configurations.DbSettings;
using PARR.DAL.Context; using PARR.DAL.Context;
using PARR.DAL.Repositories; using PARR.DAL.Repositories;
using PARR.DAL.Repositories.Job; using PARR.DAL.Repositories.Job;
using PARR.DAL.Repositories.JobGroupRepositories;
using PARR.DAL.Repositories.RobotRepositories; using PARR.DAL.Repositories.RobotRepositories;
using PARR.DAL.Repositories.Schedule; using PARR.DAL.Repositories.Schedule;
using PARR.DAL.Repositories.TaskRepositories; using PARR.DAL.Repositories.TaskRepositories;
@@ -115,6 +117,15 @@ namespace PARR.DAL
#endregion #endregion
#region JobGroup
services.AddScoped<IJobGroupFieldFilterRepository, JobGroupFieldFilterRepository>();
services.AddScoped<IJobGroupRelationshipFilterRepository, JobGroupRelationshipFilterRepository>();
services.AddScoped<IJobGroupUnitFilterRepository, JobGroupUnitFilterRepository>();
services.AddScoped<IJobGroupAutoControlRepository, JobGroupAutoControlRepository>();
#endregion
#region Task #region Task
services.AddScoped<ITaskErrorRepository, TaskErrorRepository>(); services.AddScoped<ITaskErrorRepository, TaskErrorRepository>();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,284 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PARR.DAL.Migrations
{
/// <inheritdoc />
public partial class tblsJobGroups : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "jobGroup");
migrationBuilder.RenameTable(
name: "GroupTypes",
schema: "job",
newName: "GroupTypes",
newSchema: "jobGroup");
migrationBuilder.RenameTable(
name: "Groups",
schema: "job",
newName: "Groups",
newSchema: "jobGroup");
migrationBuilder.RenameTable(
name: "GroupDistributionConfigs",
schema: "job",
newName: "GroupDistributionConfigs",
newSchema: "jobGroup");
migrationBuilder.AddColumn<bool>(
name: "IsAllowJobUnitFilter",
schema: "jobGroup",
table: "GroupTypes",
type: "boolean",
nullable: false,
defaultValue: false,
comment: "Разрешить Job фильтры. Влияет только на интерфейс. Мэтчер собирает все фильтры из группы и работ всегда.");
migrationBuilder.AddColumn<bool>(
name: "IsJobGroupAutoControl",
schema: "jobGroup",
table: "GroupTypes",
type: "boolean",
nullable: false,
defaultValue: false,
comment: "Автоконтролем управляет JobGroup? true - да JobGroup, false - Job. Влияет на интерфейс и на логику работы автоконтроля.");
migrationBuilder.CreateTable(
name: "AutoControls",
schema: "jobGroup",
columns: table => new
{
JobGroupId = table.Column<Guid>(type: "uuid", nullable: false),
IsEnable = table.Column<bool>(type: "boolean", nullable: false),
InitUsedTemplateState = table.Column<bool>(type: "boolean", nullable: false),
InitUsedScheduleState = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AutoControls", x => x.JobGroupId);
table.ForeignKey(
name: "FK_AutoControls_Groups_JobGroupId",
column: x => x.JobGroupId,
principalSchema: "jobGroup",
principalTable: "Groups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
},
comment: "Таблица управления автоконтролем для группы работ");
migrationBuilder.CreateTable(
name: "UnitFilters",
schema: "jobGroup",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
DateCreated = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UnitFilter = table.Column<string>(type: "text", nullable: false),
JobGroupId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UnitFilters", x => x.Id);
table.ForeignKey(
name: "FK_UnitFilters_Groups_JobGroupId",
column: x => x.JobGroupId,
principalSchema: "jobGroup",
principalTable: "Groups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
},
comment: "Таблица описания критериев выборки ЭК, описание полей в АСУ ЕСПП");
migrationBuilder.CreateTable(
name: "FieldFilters",
schema: "jobGroup",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
DateCreated = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UnitFilterId = table.Column<Guid>(type: "uuid", nullable: false),
FieldId = table.Column<Guid>(type: "uuid", nullable: false),
ValueMask = table.Column<string>(type: "text", nullable: false),
IsInverse = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FieldFilters", x => x.Id);
table.ForeignKey(
name: "FK_FieldFilters_Fields_FieldId",
column: x => x.FieldId,
principalSchema: "unit",
principalTable: "Fields",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_FieldFilters_UnitFilters_UnitFilterId",
column: x => x.UnitFilterId,
principalSchema: "jobGroup",
principalTable: "UnitFilters",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
},
comment: "Таблица описания критериев выборки аттрибутов ЭК");
migrationBuilder.CreateTable(
name: "RelationshipFilters",
schema: "jobGroup",
columns: table => new
{
UnitFilterId = table.Column<Guid>(type: "uuid", nullable: false),
FieldId = table.Column<Guid>(type: "uuid", nullable: false),
IsParent = table.Column<bool>(type: "boolean", nullable: false),
ValueMask = table.Column<string>(type: "text", nullable: false),
IsFullMatch = table.Column<bool>(type: "boolean", nullable: false),
IsInverse = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RelationshipFilters", x => new { x.UnitFilterId, x.FieldId });
table.ForeignKey(
name: "FK_RelationshipFilters_Fields_FieldId",
column: x => x.FieldId,
principalSchema: "unit",
principalTable: "Fields",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RelationshipFilters_UnitFilters_UnitFilterId",
column: x => x.UnitFilterId,
principalSchema: "jobGroup",
principalTable: "UnitFilters",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
},
comment: "Таблица фильтров связей ЭК");
migrationBuilder.UpdateData(
schema: "jobGroup",
table: "GroupTypes",
keyColumn: "Id",
keyValue: new Guid("318625e5-f833-436a-99df-2a382a1e71c7"),
columns: new[] { "IsAllowJobUnitFilter", "IsJobGroupAutoControl" },
values: new object[] { false, true });
migrationBuilder.UpdateData(
schema: "jobGroup",
table: "GroupTypes",
keyColumn: "Id",
keyValue: new Guid("4fa62e79-86bb-47c2-be1a-72a716a170fa"),
columns: new[] { "IsAllowJobUnitFilter", "IsJobGroupAutoControl" },
values: new object[] { true, false });
migrationBuilder.UpdateData(
schema: "jobGroup",
table: "GroupTypes",
keyColumn: "Id",
keyValue: new Guid("6ec58b1a-5c40-47b9-b036-4fa490e8d503"),
columns: new[] { "IsAllowJobUnitFilter", "IsJobGroupAutoControl" },
values: new object[] { false, true });
migrationBuilder.UpdateData(
table: "Settings",
keyColumn: "Name",
keyValue: "Initiator",
column: "Value",
value: "АКСЕНОВ АЛЕКСАНДР ЕВГЕНЬЕВИЧ (AKSENOVAE@GVC.OAO.RZD)");
migrationBuilder.CreateIndex(
name: "IX_FieldFilters_FieldId1",
schema: "jobGroup",
table: "FieldFilters",
column: "FieldId");
migrationBuilder.CreateIndex(
name: "IX_FieldFilters_UnitFilterId1",
schema: "jobGroup",
table: "FieldFilters",
column: "UnitFilterId");
migrationBuilder.CreateIndex(
name: "IX_RelationshipFilters_FieldId1",
schema: "jobGroup",
table: "RelationshipFilters",
column: "FieldId");
migrationBuilder.CreateIndex(
name: "IX_UnitFilters_JobGroupId",
schema: "jobGroup",
table: "UnitFilters",
column: "JobGroupId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_FieldFilters_Fields_FieldId",
schema: "job",
table: "FieldFilters");
migrationBuilder.DropForeignKey(
name: "FK_RelationshipFilters_Fields_FieldId",
schema: "job",
table: "RelationshipFilters");
migrationBuilder.DropTable(
name: "AutoControls",
schema: "jobGroup");
migrationBuilder.DropTable(
name: "FieldFilters",
schema: "jobGroup");
migrationBuilder.DropTable(
name: "RelationshipFilters",
schema: "jobGroup");
migrationBuilder.DropTable(
name: "UnitFilters",
schema: "jobGroup");
migrationBuilder.DropColumn(
name: "IsAllowJobUnitFilter",
schema: "jobGroup",
table: "GroupTypes");
migrationBuilder.DropColumn(
name: "IsJobGroupAutoControl",
schema: "jobGroup",
table: "GroupTypes");
migrationBuilder.RenameTable(
name: "GroupTypes",
schema: "jobGroup",
newName: "GroupTypes",
newSchema: "job");
migrationBuilder.RenameTable(
name: "Groups",
schema: "jobGroup",
newName: "Groups",
newSchema: "job");
migrationBuilder.RenameTable(
name: "GroupDistributionConfigs",
schema: "jobGroup",
newName: "GroupDistributionConfigs",
newSchema: "job");
migrationBuilder.UpdateData(
table: "Settings",
keyColumn: "Name",
keyValue: "Initiator",
column: "Value",
value: "ОВЧАРЕНКО АЛЕКСЕЙ ВИТАЛЬЕВИЧ (OVCHARENKOAV@GVC.OAO.RZD)");
}
}
}

View File

@@ -290,7 +290,90 @@ namespace PARR.DAL.Migrations
}); });
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroup", b => modelBuilder.Entity("PARR.Domain.Entities.Job.JobRelationshipFilter", b =>
{
b.Property<Guid>("UnitFilterId")
.HasColumnType("uuid");
b.Property<Guid>("FieldId")
.HasColumnType("uuid");
b.Property<bool>("IsFullMatch")
.HasColumnType("boolean");
b.Property<bool>("IsInverse")
.HasColumnType("boolean");
b.Property<bool>("IsParent")
.HasColumnType("boolean");
b.Property<string>("ValueMask")
.IsRequired()
.HasColumnType("text");
b.HasKey("UnitFilterId", "FieldId");
b.HasIndex("FieldId");
b.ToTable("RelationshipFilters", "job", t =>
{
t.HasComment("Таблица фильтров связей ЭК");
});
});
modelBuilder.Entity("PARR.Domain.Entities.Job.JobUnitFilter", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("JobId")
.HasColumnType("uuid");
b.Property<string>("UnitFilter")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("JobId");
b.ToTable("UnitFilters", "job", t =>
{
t.HasComment("Таблица описания критериев выборки ЭК, описание полей в АСУ ЕСПП");
});
});
modelBuilder.Entity("PARR.Domain.Entities.Job.UnitsInTemplate", b =>
{
b.Property<Guid>("TemplateId")
.HasColumnType("uuid");
b.Property<Guid>("UnitId")
.HasColumnType("uuid");
b.Property<Guid>("UnitFieldValueId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone");
b.HasKey("TemplateId", "UnitId", "UnitFieldValueId");
b.HasIndex("UnitFieldValueId");
b.HasIndex("UnitId");
b.ToTable("UnitsInTemplates", "job", t =>
{
t.HasComment("Таблица связи ЭК в шаблонах");
});
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroup", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -372,13 +455,35 @@ namespace PARR.DAL.Migrations
b.HasIndex("ScheduleExcludeTypeId"); b.HasIndex("ScheduleExcludeTypeId");
b.ToTable("Groups", "job", t => b.ToTable("Groups", "jobGroup", t =>
{ {
t.HasComment("Таблица описания групп работ, для реализации зонтиков"); t.HasComment("Таблица описания групп работ, для реализации зонтиков");
}); });
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroupDistributionConfig", b => modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupAutoControl", b =>
{
b.Property<Guid>("JobGroupId")
.HasColumnType("uuid");
b.Property<bool>("InitUsedScheduleState")
.HasColumnType("boolean");
b.Property<bool>("InitUsedTemplateState")
.HasColumnType("boolean");
b.Property<bool>("IsEnable")
.HasColumnType("boolean");
b.HasKey("JobGroupId");
b.ToTable("AutoControls", "jobGroup", t =>
{
t.HasComment("Таблица управления автоконтролем для группы работ");
});
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupDistributionConfig", b =>
{ {
b.Property<Guid>("GroupId") b.Property<Guid>("GroupId")
.HasColumnType("uuid"); .HasColumnType("uuid");
@@ -396,67 +501,49 @@ namespace PARR.DAL.Migrations
b.HasIndex("DistributionPeriodId"); b.HasIndex("DistributionPeriodId");
b.ToTable("GroupDistributionConfigs", "job", t => b.ToTable("GroupDistributionConfigs", "jobGroup", t =>
{ {
t.HasComment("Настройки автораспределения для группы работ"); t.HasComment("Настройки автораспределения для группы работ");
}); });
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroupType", b => modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupFieldFilter", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<int>("Code")
.HasColumnType("integer");
b.Property<DateTimeOffset>("DateCreated") b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("Description") b.Property<Guid>("FieldId")
.IsRequired() .HasColumnType("uuid");
.HasColumnType("text");
b.Property<string>("Name") b.Property<bool>("IsInverse")
.HasColumnType("boolean");
b.Property<Guid>("UnitFilterId")
.HasColumnType("uuid");
b.Property<string>("ValueMask")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("GroupTypes", "job", t => b.HasIndex("FieldId")
{ .HasDatabaseName("IX_FieldFilters_FieldId1");
t.HasComment("Таблица типов групп работ");
});
b.HasData( b.HasIndex("UnitFilterId")
new .HasDatabaseName("IX_FieldFilters_UnitFilterId1");
b.ToTable("FieldFilters", "jobGroup", t =>
{ {
Id = new Guid("4fa62e79-86bb-47c2-be1a-72a716a170fa"), t.HasComment("Таблица описания критериев выборки аттрибутов ЭК");
Code = 0,
DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "Обычный",
Name = "Simple"
},
new
{
Id = new Guid("6ec58b1a-5c40-47b9-b036-4fa490e8d503"),
Code = 1,
DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "Зонтик",
Name = "Umbrella"
},
new
{
Id = new Guid("318625e5-f833-436a-99df-2a382a1e71c7"),
Code = 2,
DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "Сгруппированный",
Name = "Group"
}); });
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobRelationshipFilter", b => modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupRelationshipFilter", b =>
{ {
b.Property<Guid>("UnitFilterId") b.Property<Guid>("UnitFilterId")
.HasColumnType("uuid"); .HasColumnType("uuid");
@@ -479,15 +566,84 @@ namespace PARR.DAL.Migrations
b.HasKey("UnitFilterId", "FieldId"); b.HasKey("UnitFilterId", "FieldId");
b.HasIndex("FieldId"); b.HasIndex("FieldId")
.HasDatabaseName("IX_RelationshipFilters_FieldId1");
b.ToTable("RelationshipFilters", "job", t => b.ToTable("RelationshipFilters", "jobGroup", t =>
{ {
t.HasComment("Таблица фильтров связей ЭК"); t.HasComment("Таблица фильтров связей ЭК");
}); });
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobUnitFilter", b => modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupType", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("Code")
.HasColumnType("integer");
b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsAllowJobUnitFilter")
.HasColumnType("boolean")
.HasComment("Разрешить Job фильтры. Влияет только на интерфейс. Мэтчер собирает все фильтры из группы и работ всегда.");
b.Property<bool>("IsJobGroupAutoControl")
.HasColumnType("boolean")
.HasComment("Автоконтролем управляет JobGroup? true - да JobGroup, false - Job. Влияет на интерфейс и на логику работы автоконтроля.");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("GroupTypes", "jobGroup", t =>
{
t.HasComment("Таблица типов групп работ");
});
b.HasData(
new
{
Id = new Guid("4fa62e79-86bb-47c2-be1a-72a716a170fa"),
Code = 0,
DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "Обычный",
IsAllowJobUnitFilter = true,
IsJobGroupAutoControl = false,
Name = "Simple"
},
new
{
Id = new Guid("6ec58b1a-5c40-47b9-b036-4fa490e8d503"),
Code = 1,
DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "Зонтик",
IsAllowJobUnitFilter = false,
IsJobGroupAutoControl = true,
Name = "Umbrella"
},
new
{
Id = new Guid("318625e5-f833-436a-99df-2a382a1e71c7"),
Code = 2,
DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "Сгруппированный",
IsAllowJobUnitFilter = false,
IsJobGroupAutoControl = true,
Name = "Group"
});
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupUnitFilter", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -496,7 +652,7 @@ namespace PARR.DAL.Migrations
b.Property<DateTimeOffset>("DateCreated") b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<Guid>("JobId") b.Property<Guid>("JobGroupId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<string>("UnitFilter") b.Property<string>("UnitFilter")
@@ -505,40 +661,14 @@ namespace PARR.DAL.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("JobId"); b.HasIndex("JobGroupId");
b.ToTable("UnitFilters", "job", t => b.ToTable("UnitFilters", "jobGroup", t =>
{ {
t.HasComment("Таблица описания критериев выборки ЭК, описание полей в АСУ ЕСПП"); t.HasComment("Таблица описания критериев выборки ЭК, описание полей в АСУ ЕСПП");
}); });
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.UnitsInTemplate", b =>
{
b.Property<Guid>("TemplateId")
.HasColumnType("uuid");
b.Property<Guid>("UnitId")
.HasColumnType("uuid");
b.Property<Guid>("UnitFieldValueId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone");
b.HasKey("TemplateId", "UnitId", "UnitFieldValueId");
b.HasIndex("UnitFieldValueId");
b.HasIndex("UnitId");
b.ToTable("UnitsInTemplates", "job", t =>
{
t.HasComment("Таблица связи ЭК в шаблонах");
});
});
modelBuilder.Entity("PARR.Domain.Entities.Order", b => modelBuilder.Entity("PARR.Domain.Entities.Order", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -2268,7 +2398,7 @@ namespace PARR.DAL.Migrations
{ {
Name = "Initiator", Name = "Initiator",
Description = "Инициатор регламентной работы, указывается при создании шаблона в ЕСПП.", Description = "Инициатор регламентной работы, указывается при создании шаблона в ЕСПП.",
Value = "ОВЧАРЕНКО АЛЕКСЕЙ ВИТАЛЬЕВИЧ (OVCHARENKOAV@GVC.OAO.RZD)" Value = "АКСЕНОВ АЛЕКСАНДР ЕВГЕНЬЕВИЧ (AKSENOVAE@GVC.OAO.RZD)"
}, },
new new
{ {
@@ -3097,7 +3227,7 @@ namespace PARR.DAL.Migrations
modelBuilder.Entity("PARR.Domain.Entities.Job.Job", b => modelBuilder.Entity("PARR.Domain.Entities.Job.Job", b =>
{ {
b.HasOne("PARR.Domain.Entities.Job.JobGroup", "Group") b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroup", "Group")
.WithMany("Jobs") .WithMany("Jobs")
.HasForeignKey("GroupId") .HasForeignKey("GroupId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -3128,7 +3258,7 @@ namespace PARR.DAL.Migrations
modelBuilder.Entity("PARR.Domain.Entities.Job.JobFieldFilter", b => modelBuilder.Entity("PARR.Domain.Entities.Job.JobFieldFilter", b =>
{ {
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField") b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField")
.WithMany() .WithMany("JobFieldFilters")
.HasForeignKey("FieldId") .HasForeignKey("FieldId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -3144,57 +3274,6 @@ namespace PARR.DAL.Migrations
b.Navigation("UnitFilter"); b.Navigation("UnitFilter");
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroup", b =>
{
b.HasOne("PARR.Domain.Entities.Job.JobGroupType", "GroupType")
.WithMany("JobGroups")
.HasForeignKey("GroupTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "GroupingUnitField")
.WithMany("JobGroupWithGrouping")
.HasForeignKey("GroupingUnitFieldId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PARR.Domain.Entities.Schedule.ScheduleExcludeTypeCalendar", "ScheduleExcludeTypeCalendar")
.WithMany("JobGroups")
.HasForeignKey("ScheduleExcludeTypeCalendarId");
b.HasOne("PARR.Domain.Entities.Schedule.ScheduleExcludeType", "ScheduleExcludeType")
.WithMany("JobGroups")
.HasForeignKey("ScheduleExcludeTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("GroupType");
b.Navigation("GroupingUnitField");
b.Navigation("ScheduleExcludeType");
b.Navigation("ScheduleExcludeTypeCalendar");
});
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroupDistributionConfig", b =>
{
b.HasOne("PARR.Domain.Entities.DistributionPeriod", "DistributionPeriod")
.WithMany("GroupDistributionConfig")
.HasForeignKey("DistributionPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PARR.Domain.Entities.Job.JobGroup", "Group")
.WithOne("DistributionConfig")
.HasForeignKey("PARR.Domain.Entities.Job.JobGroupDistributionConfig", "GroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DistributionPeriod");
b.Navigation("Group");
});
modelBuilder.Entity("PARR.Domain.Entities.Job.JobRelationshipFilter", b => modelBuilder.Entity("PARR.Domain.Entities.Job.JobRelationshipFilter", b =>
{ {
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField") b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField")
@@ -3252,6 +3331,117 @@ namespace PARR.DAL.Migrations
b.Navigation("UnitFieldValue"); b.Navigation("UnitFieldValue");
}); });
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroup", b =>
{
b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroupType", "GroupType")
.WithMany("JobGroups")
.HasForeignKey("GroupTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "GroupingUnitField")
.WithMany("JobGroupWithGrouping")
.HasForeignKey("GroupingUnitFieldId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PARR.Domain.Entities.Schedule.ScheduleExcludeTypeCalendar", "ScheduleExcludeTypeCalendar")
.WithMany("JobGroups")
.HasForeignKey("ScheduleExcludeTypeCalendarId");
b.HasOne("PARR.Domain.Entities.Schedule.ScheduleExcludeType", "ScheduleExcludeType")
.WithMany("JobGroups")
.HasForeignKey("ScheduleExcludeTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("GroupType");
b.Navigation("GroupingUnitField");
b.Navigation("ScheduleExcludeType");
b.Navigation("ScheduleExcludeTypeCalendar");
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupAutoControl", b =>
{
b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroup", "JobGroup")
.WithOne("AutoControl")
.HasForeignKey("PARR.Domain.Entities.JobGroupEntities.JobGroupAutoControl", "JobGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JobGroup");
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupDistributionConfig", b =>
{
b.HasOne("PARR.Domain.Entities.DistributionPeriod", "DistributionPeriod")
.WithMany("GroupDistributionConfig")
.HasForeignKey("DistributionPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroup", "Group")
.WithOne("DistributionConfig")
.HasForeignKey("PARR.Domain.Entities.JobGroupEntities.JobGroupDistributionConfig", "GroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DistributionPeriod");
b.Navigation("Group");
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupFieldFilter", b =>
{
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField")
.WithMany("JobGroupFieldFilters")
.HasForeignKey("FieldId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroupUnitFilter", "UnitFilter")
.WithMany("FieldFilters")
.HasForeignKey("UnitFilterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("UnitField");
b.Navigation("UnitFilter");
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupRelationshipFilter", b =>
{
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField")
.WithMany("JobGroupRelationshipFilters")
.HasForeignKey("FieldId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroupUnitFilter", "UnitFilter")
.WithMany("RelationshipFilters")
.HasForeignKey("UnitFilterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("UnitField");
b.Navigation("UnitFilter");
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupUnitFilter", b =>
{
b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroup", "JobGroup")
.WithMany()
.HasForeignKey("JobGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JobGroup");
});
modelBuilder.Entity("PARR.Domain.Entities.Order", b => modelBuilder.Entity("PARR.Domain.Entities.Order", b =>
{ {
b.HasOne("PARR.Domain.Entities.OrderStatus", "NextStatus") b.HasOne("PARR.Domain.Entities.OrderStatus", "NextStatus")
@@ -3369,7 +3559,7 @@ namespace PARR.DAL.Migrations
modelBuilder.Entity("PARR.Domain.Entities.Schedule.EsppSchValue", b => modelBuilder.Entity("PARR.Domain.Entities.Schedule.EsppSchValue", b =>
{ {
b.HasOne("PARR.Domain.Entities.Job.JobGroup", "JobGroup") b.HasOne("PARR.Domain.Entities.JobGroupEntities.JobGroup", "JobGroup")
.WithMany("EsppSchValues") .WithMany("EsppSchValues")
.HasForeignKey("JobGroupId") .HasForeignKey("JobGroupId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -3614,8 +3804,17 @@ namespace PARR.DAL.Migrations
b.Navigation("UnitFilters"); b.Navigation("UnitFilters");
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroup", b => modelBuilder.Entity("PARR.Domain.Entities.Job.JobUnitFilter", b =>
{ {
b.Navigation("FieldFilters");
b.Navigation("RelationshipFilters");
});
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroup", b =>
{
b.Navigation("AutoControl");
b.Navigation("DistributionConfig"); b.Navigation("DistributionConfig");
b.Navigation("EsppSchValues"); b.Navigation("EsppSchValues");
@@ -3623,12 +3822,12 @@ namespace PARR.DAL.Migrations
b.Navigation("Jobs"); b.Navigation("Jobs");
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroupType", b => modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupType", b =>
{ {
b.Navigation("JobGroups"); b.Navigation("JobGroups");
}); });
modelBuilder.Entity("PARR.Domain.Entities.Job.JobUnitFilter", b => modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupUnitFilter", b =>
{ {
b.Navigation("FieldFilters"); b.Navigation("FieldFilters");
@@ -3776,6 +3975,12 @@ namespace PARR.DAL.Migrations
modelBuilder.Entity("PARR.Domain.Entities.Unit.UnitField", b => modelBuilder.Entity("PARR.Domain.Entities.Unit.UnitField", b =>
{ {
b.Navigation("JobFieldFilters");
b.Navigation("JobGroupFieldFilters");
b.Navigation("JobGroupRelationshipFilters");
b.Navigation("JobGroupWithGrouping"); b.Navigation("JobGroupWithGrouping");
b.Navigation("RelationshipFilters"); b.Navigation("RelationshipFilters");

View File

@@ -2,7 +2,7 @@
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.DAL.Context; using PARR.DAL.Context;
using PARR.DAL.Repositories.Base; using PARR.DAL.Repositories.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.DAL.Repositories.Job namespace PARR.DAL.Repositories.Job
{ {

View File

@@ -2,7 +2,7 @@
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.DAL.Context; using PARR.DAL.Context;
using PARR.DAL.Repositories.Base; using PARR.DAL.Repositories.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.DAL.Repositories.Job namespace PARR.DAL.Repositories.Job
{ {

View File

@@ -0,0 +1,21 @@
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
using PARR.DAL.Context;
using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.DAL.Repositories.JobGroupRepositories
{
internal class JobGroupAutoControlRepository : IJobGroupAutoControlRepository
{
private readonly DataContext _dataContext;
public JobGroupAutoControlRepository(DataContext dataContext)
{
_dataContext = dataContext;
}
public IQueryable<JobGroupAutoControl> Get()
{
return _dataContext.JobGroupAutoControls;
}
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
using PARR.DAL.Context;
using PARR.DAL.Repositories.Base;
using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.DAL.Repositories.JobGroupRepositories
{
internal class JobGroupFieldFilterRepository : BaseRepository<JobGroupFieldFilter>, IJobGroupFieldFilterRepository
{
public JobGroupFieldFilterRepository(ILogger<JobGroupFieldFilterRepository> logger, DataContext dataContext) : base(logger, dataContext) { }
}
}

View File

@@ -0,0 +1,8 @@
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
namespace PARR.DAL.Repositories.JobGroupRepositories
{
internal class JobGroupRelationshipFilterRepository : IJobGroupRelationshipFilterRepository
{
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
using PARR.DAL.Context;
using PARR.DAL.Repositories.Base;
using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.DAL.Repositories.JobGroupRepositories
{
internal class JobGroupUnitFilterRepository : BaseRepository<JobGroupUnitFilter>, IJobGroupUnitFilterRepository
{
public JobGroupUnitFilterRepository(ILogger<JobGroupUnitFilterRepository> logger, DataContext dataContext) : base(logger, dataContext) { }
}
}

View File

@@ -16,6 +16,10 @@
/// </summary> /// </summary>
public const string Job = "job"; public const string Job = "job";
/// <summary>
/// Группы работ
/// </summary>
public const string JobGroup = "jobGroup";
/// <summary> /// <summary>
/// Расписание регламентных работ /// Расписание регламентных работ

View File

@@ -0,0 +1,17 @@
namespace PARR.Domain.DTOs.RobotSnapshotDTO
{
/// <summary>
/// Почасовая статистика по снапшотам роботов
/// </summary>
public record HourlyRobotTypeAnalyticsDto
{
public DateTimeOffset Hour { get; init; }
//public int MaxAllowedRobots { get; init; }
/// <summary>
/// Среднее кол-во роботов работающих в течении часа
/// </summary>
public double AvgRobots { get; init; }
}
}

View File

@@ -0,0 +1,30 @@
using PARR.Domain.Enums;
namespace PARR.Domain.DTOs.RobotSnapshotDTO
{
public record RobotAnalyticsBaseQuery
{
public DateTimeOffset DateStart { get; init; }
public DateTimeOffset DateEnd { get; init; }
/// <summary>
/// Смещение часового пояса
/// </summary>
public TimeSpan Offset { get; init; }
}
public record RobotAnalyticsRobotTypeQuery : RobotAnalyticsBaseQuery
{
/// <summary>
/// Тип робота
/// </summary>
public RobotsEnum RobotType { get; init; }
}
public record RobotAnalyticsQuery : RobotAnalyticsBaseQuery
{
public string? Ip { get; init; }
}
}

View File

@@ -0,0 +1,20 @@
namespace PARR.Domain.DTOs.RobotSnapshotDTO
{
/// <summary>
/// Селект из БД по снапшотам по типам роботов
/// </summary>
public record RobotSnapshotRobotTypeDbDto
{
public int LocalYear { get; init; }
public int LocalMonth { get; init; }
public int LocalDay { get; init; }
public int LocalHour { get; init; }
//public int MaxAllowed { get; init; }
public int Sum { get; init; }
}
}

View File

@@ -0,0 +1,47 @@
using PARR.Domain.DTOs.User;
namespace PARR.Domain.DTOs.RobotSnapshotDTO
{
/// <summary>
/// Аналитика по снапшотам роботов с группировкой по IP робота
/// </summary>
public record ServerHourlyAnalyticsDto
{
public UserBaseDto Robot { get; init; } = null!;
/// <summary>
/// Почасовая статистика для гарфиков
/// </summary>
public List<RobotHourlyPointDto> Snapshots { get; set; } = new();
}
/// <summary>
/// Почасовая статистика по снапшотам роботов
/// </summary>
public record RobotHourlyPointDto
{
public DateTimeOffset Hour { get; set; }
public int MaxAllowedRobots { get; set; }
/// <summary>
/// Максимальное кол-во роботов работающих одновременно по шаблонам
/// </summary>
public int MaxTemplateRobots { get; set; }
/// <summary>
/// Сколько роботов в среднем, работало в течении часа
/// </summary>
public double AvgTemplateRobots { get; set; }
/// <summary>
/// Максимальное кол-во роботов работающих одновременно по расписаниям
/// </summary>
public int MaxScheduleRobots { get; set; }
/// <summary>
/// Сколько роботов в среднем, работало в течении часа
/// </summary>
public double AvgScheduleRobots { get; set; }
}
}

View File

@@ -1,6 +1,6 @@
using PARR.Domain.Constants; using PARR.Domain.Constants;
using PARR.Domain.Entities.Base; using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants; using PARR.Domain.Constants;
using PARR.Domain.Entities.Base; using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.JobGroupEntities;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;

View File

@@ -1,30 +0,0 @@
using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants;
using PARR.Domain.Entities.Base;
using PARR.Domain.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.Job
{
[Table("GroupTypes", Schema = DatabaseSchemas.Job)]
[Comment("Таблица типов групп работ")]
public class JobGroupType : IBaseEntity
{
[Key]
public Guid Id { get; set; }
public DateTimeOffset DateCreated { get; set; }
[NotMapped]
public DateTimeOffset? DateModified { get; set; }
public required JobGroupTypesEnum Code { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
public ICollection<JobGroup> JobGroups { get; set; } = new HashSet<JobGroup>();
}
}

View File

@@ -11,6 +11,7 @@ namespace PARR.Domain.Entities.Job
public class JobRelationshipFilter public class JobRelationshipFilter
{ {
public Guid UnitFilterId { get; set; } public Guid UnitFilterId { get; set; }
/// <summary> /// <summary>
/// Родительская связь - true, /// Родительская связь - true,
/// Дочерняя связь - false /// Дочерняя связь - false

View File

@@ -7,9 +7,9 @@ using PARR.Domain.Settings;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.Job namespace PARR.Domain.Entities.JobGroupEntities
{ {
[Table("Groups", Schema = DatabaseSchemas.Job)] [Table("Groups", Schema = DatabaseSchemas.JobGroup)]
[Comment("Таблица описания групп работ, для реализации зонтиков")] [Comment("Таблица описания групп работ, для реализации зонтиков")]
public class JobGroup : IBaseEntity public class JobGroup : IBaseEntity
{ {
@@ -180,7 +180,7 @@ namespace PARR.Domain.Entities.Job
[ForeignKey(nameof(GroupTypeId))] [ForeignKey(nameof(GroupTypeId))]
public JobGroupType? GroupType { get; set; } public JobGroupType? GroupType { get; set; }
public ICollection<Job> Jobs { get; set; } = new HashSet<Job>(); public ICollection<Job.Job> Jobs { get; set; } = new HashSet<Job.Job>();
public ICollection<EsppSchValue> EsppSchValues { get; set; } = new HashSet<EsppSchValue>(); public ICollection<EsppSchValue> EsppSchValues { get; set; } = new HashSet<EsppSchValue>();
@@ -191,5 +191,7 @@ namespace PARR.Domain.Entities.Job
public ScheduleExcludeTypeCalendar? ScheduleExcludeTypeCalendar { get; set; } public ScheduleExcludeTypeCalendar? ScheduleExcludeTypeCalendar { get; set; }
public JobGroupDistributionConfig? DistributionConfig { get; set; } public JobGroupDistributionConfig? DistributionConfig { get; set; }
public JobGroupAutoControl? AutoControl { get; set; }
} }
} }

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.JobGroupEntities
{
[Table("AutoControls", Schema = DatabaseSchemas.JobGroup)]
[Comment("Таблица управления автоконтролем для группы работ")]
public class JobGroupAutoControl
{
[Key]
public Guid JobGroupId { get; set; }
/// <summary>
/// Включен автоконтроль
/// </summary>
public bool IsEnable { get; set; } = false;
/// <summary>
/// Статус шаблона в момент привязки или создания нового шаблона к Job
/// </summary>
public bool InitUsedTemplateState { get; set; } = false;
/// <summary>
/// Статус расписания в момент привязки или создания нового шаблона к Job
/// </summary>
public bool InitUsedScheduleState { get; set; } = false;
[ForeignKey(nameof(JobGroupId))]
public JobGroup? JobGroup { get; set; }
}
}

View File

@@ -3,12 +3,12 @@ using PARR.Domain.Constants;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.Job namespace PARR.Domain.Entities.JobGroupEntities
{ {
/// <summary> /// <summary>
/// Настройки автораспределения для JobGroup /// Настройки автораспределения для JobGroup
/// </summary> /// </summary>
[Table("GroupDistributionConfigs", Schema = DatabaseSchemas.Job)] [Table("GroupDistributionConfigs", Schema = DatabaseSchemas.JobGroup)]
[Comment("Настройки автораспределения для группы работ")] [Comment("Настройки автораспределения для группы работ")]
public class JobGroupDistributionConfig public class JobGroupDistributionConfig
{ {

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants;
using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.Unit;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.JobGroupEntities
{
[Table("FieldFilters", Schema = DatabaseSchemas.JobGroup)]
[Comment("Таблица описания критериев выборки аттрибутов ЭК")]
public class JobGroupFieldFilter : IBaseEntity
{
[Key]
public Guid Id { get; set; }
public DateTimeOffset DateCreated { get; set; }
[NotMapped]
public DateTimeOffset? DateModified { get; set; }
public Guid UnitFilterId { get; set; }
public Guid FieldId { get; set; }
public required string ValueMask { get; set; }
/// <summary>
/// Отсутствует
/// </summary>
public bool IsInverse { get; set; } = false;
[ForeignKey(nameof(FieldId))]
public UnitField? UnitField { get; set; }
[ForeignKey(nameof(UnitFilterId))]
public JobGroupUnitFilter? UnitFilter { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore;
using PARR.Domain.Cache.Models;
using PARR.Domain.Constants;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PARR.Domain.Entities.JobGroupEntities
{
[Table("RelationshipFilters", Schema = DatabaseSchemas.JobGroup)]
[Comment("Таблица фильтров связей ЭК")]
[PrimaryKey(nameof(UnitFilterId), nameof(FieldId))]
public class JobGroupRelationshipFilter
{
public Guid UnitFilterId { get; set; }
/// <summary>
/// Родительская связь - true,
/// Дочерняя связь - false
/// </summary>
public bool IsParent { get; set; }
public Guid FieldId { get; set; }
public required string ValueMask { get; set; }
/// <summary>
/// Полное совпадение или хотябы одно
/// true - list.All()
/// false - list.Any()
/// </summary>
public bool IsFullMatch { get; set; }
/// <summary>
/// Обратный фильтр, что у связи нет таких значений
/// </summary>
public bool IsInverse { get; set; }
[ForeignKey(nameof(UnitFilterId))]
public JobGroupUnitFilter? UnitFilter { get; set; }
[ForeignKey(nameof(FieldId))]
public UnitField? UnitField { get; set; }
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants;
using PARR.Domain.Entities.Base;
using PARR.Domain.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.JobGroupEntities
{
[Table("GroupTypes", Schema = DatabaseSchemas.JobGroup)]
[Comment("Таблица типов групп работ")]
public class JobGroupType : IBaseEntity
{
[Key]
public Guid Id { get; set; }
public DateTimeOffset DateCreated { get; set; }
[NotMapped]
public DateTimeOffset? DateModified { get; set; }
public required JobGroupTypesEnum Code { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
/// <summary>
/// Разрешить Job фильтры. Влияет только на интерфейс.
/// Мэтчер собирает все фильтры из группы и работ всегда.
/// </summary>
[Comment("Разрешить Job фильтры. Влияет только на интерфейс. Мэтчер собирает все фильтры из группы и работ всегда.")]
public bool IsAllowJobUnitFilter { get; set; }
/// <summary>
/// Автоконтролем управляет JobGroup? true - да JobGroup, false - Job.
/// Влияет на интерфейс и на логику работы автоконтроля.
/// </summary>
[Comment("Автоконтролем управляет JobGroup? true - да JobGroup, false - Job. Влияет на интерфейс и на логику работы автоконтроля.")]
public bool IsJobGroupAutoControl { get; set; } = false;
public ICollection<JobGroup> JobGroups { get; set; } = new HashSet<JobGroup>();
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants;
using PARR.Domain.Entities.Base;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.JobGroupEntities
{
[Table("UnitFilters", Schema = DatabaseSchemas.JobGroup)]
[Comment("Таблица описания критериев выборки ЭК, описание полей в АСУ ЕСПП")]
public class JobGroupUnitFilter : IBaseEntity
{
[Key]
public Guid Id { get; set; }
public DateTimeOffset DateCreated { get; set; }
[NotMapped]
public DateTimeOffset? DateModified { get; set; }
public required string UnitFilter { get; set; }
public Guid JobGroupId { get; set; }
[ForeignKey(nameof(JobGroupId))]
public JobGroup? JobGroup { get; set; }
public ICollection<JobGroupFieldFilter> FieldFilters { get; set; } = new HashSet<JobGroupFieldFilter>();
public ICollection<JobGroupRelationshipFilter> RelationshipFilters { get; set; } = new HashSet<JobGroupRelationshipFilter>();
}
}

View File

@@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants; using PARR.Domain.Constants;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.Schedule namespace PARR.Domain.Entities.Schedule

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants; using PARR.Domain.Constants;
using PARR.Domain.Entities.Base; using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PARR.Domain.Constants; using PARR.Domain.Constants;
using PARR.Domain.Entities.Base; using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;

View File

@@ -2,6 +2,7 @@
using PARR.Domain.Constants; using PARR.Domain.Constants;
using PARR.Domain.Entities.Base; using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@@ -58,10 +59,17 @@ namespace PARR.Domain.Entities.Unit
public ICollection<JobRelationshipFilter> RelationshipFilters { get; set; } = new HashSet<JobRelationshipFilter>(); public ICollection<JobRelationshipFilter> RelationshipFilters { get; set; } = new HashSet<JobRelationshipFilter>();
public ICollection<JobGroupRelationshipFilter> JobGroupRelationshipFilters { get; set; } = new HashSet<JobGroupRelationshipFilter>();
/// <summary> /// <summary>
/// JobGroup которые группируются по этому полю (!!!отключено каскадное удаление!!!) /// JobGroup которые группируются по этому полю (!!!отключено каскадное удаление!!!)
/// </summary> /// </summary>
public ICollection<JobGroup> JobGroupWithGrouping { get; set; } = new HashSet<JobGroup>(); public ICollection<JobGroup> JobGroupWithGrouping { get; set; } = new HashSet<JobGroup>();
public ICollection<JobFieldFilter> JobFieldFilters { get; set; } = new HashSet<JobFieldFilter>();
public ICollection<JobGroupFieldFilter> JobGroupFieldFilters { get; set; } = new HashSet<JobGroupFieldFilter>();
} }
} }

View File

@@ -1,4 +1,6 @@
namespace PARR.EsppSync.Helpers using System.Text.RegularExpressions;
namespace PARR.EsppSync.Helpers
{ {
/// <summary> /// <summary>
/// Хелперы для EsppSync /// Хелперы для EsppSync
@@ -15,11 +17,9 @@
if (str == null) if (str == null)
return string.Empty; return string.Empty;
str = str.Replace("\r", string.Empty); str = Regex.Replace(str, @"\s+", string.Empty);
str = str.Replace("\n", string.Empty);
str = str.Replace(" ", string.Empty);
return str.ToLower(); return str.Trim().ToLowerInvariant();
} }
} }
} }

View File

@@ -6,6 +6,7 @@ using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Base; using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Enums; using PARR.Domain.Enums;
namespace PARR.TemplateActivator; namespace PARR.TemplateActivator;

View File

@@ -2,7 +2,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.TemplateDistributor.Services namespace PARR.TemplateDistributor.Services
{ {

View File

@@ -0,0 +1,25 @@
namespace PARR.TemplateMatcher.Constants
{
/// <summary>
/// Контрактные константы для обработки неиспользуемых шаблонов.
/// Являются соглашением между TemplateMatcher и внешними системами.
/// Изменение требует согласования со всеми участниками контракта.
/// </summary>
public static class UnusedTemplateConstants
{
/// <summary>
/// Системное имя поля зоны ответственности.
/// </summary>
public const string ResponsibilityAreaFieldName = "ЗОНА_ОТВЕТСТВЕННОСТИ";
/// <summary>
/// Системное имя поля тега ПАРР.
/// </summary>
public const string ParrTagFieldName = "ПАРР тег";
/// <summary>
/// Значение тега, обозначающее неиспользуемый юнит.
/// </summary>
public const string NotUsedTagValue = "ПАРР-НЕИСП";
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class BuildGroupsStage : IGroupedSyncStage
{
private readonly IGroupedTemplateBuilder _builder;
private readonly ILogger<BuildGroupsStage> _logger;
public string StageName => "Построение групп";
public BuildGroupsStage(IGroupedTemplateBuilder builder, ILogger<BuildGroupsStage> logger)
{
_builder = builder;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var groups = await _builder.BuildAsync(context.ReverseMapping, context.JobGroup, context.MaxJob);
if (!groups.Any())
throw new GroupedSyncEarlyExitException("Нет данных после построения групп");
context.TemplateGroups = groups;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): построено {Count} групп",
context.JobGroupName, context.JobGroupId, groups.Count);
return context;
}
}

View File

@@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class DeactivateTemplatesStage : IGroupedSyncWriteStage
{
private readonly ITemplateRepository _templateRepository;
private readonly ITemplateDeactivator _deactivator;
private readonly ILogger<DeactivateTemplatesStage> _logger;
public string StageName => "Деактивация шаблонов";
public DeactivateTemplatesStage(
ITemplateRepository templateRepository,
ITemplateDeactivator deactivator,
ILogger<DeactivateTemplatesStage> logger)
{
_templateRepository = templateRepository;
_deactivator = deactivator;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var allJobIds = context.JobsInGroup.Select(j => j.Id).ToHashSet();
var existingTemplates = await _templateRepository.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.Job)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIds.Contains(t.JobId)
&& t.StatusTypeId == TemplateStatusTypeEnum.Used
&& t.Job!.GroupId == context.JobGroupId)
.ToListAsync(ct);
int deactivated = 0;
foreach (var template in existingTemplates)
{
var key = (template.JobId, template.UnitId, template.Index ?? -1);
if (!context.ExpectedTemplateKeys.Contains(key))
{
var unitLogName = context.UnitNames.TryGetValue(template.UnitId, out var unitName)
? $"'{unitName}' ({template.UnitId})"
: template.Unit != null
? $"'{template.Unit.Name}' ({template.UnitId})"
: $"({template.UnitId})";
_logger.LogInformation(
"JobGroup '{JobGroupName}' ({JobGroupId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Job '{JobName}' ({JobId}), Unit {Unit}",
context.JobGroupName, context.JobGroupId,
template.Name, template.Id,
template.Job?.Name ?? string.Empty, template.JobId,
unitLogName);
await _deactivator.DeactivateTemplateAsync(template, context.Initiator);
deactivated++;
}
}
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): деактивировано {Count} шаблонов из {Total}",
context.JobGroupName, context.JobGroupId, deactivated, existingTemplates.Count);
return context;
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Services.UnitFilterService;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class FilterUnitsStage : IGroupedSyncStage
{
private readonly IUnitFilterService _filterService;
private readonly ILogger<FilterUnitsStage> _logger;
public string StageName => "Фильтрация юнитов";
public FilterUnitsStage(IUnitFilterService filterService, ILogger<FilterUnitsStage> logger)
{
_filterService = filterService;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var result = await _filterService.GetUnitsByJobFilterAsync(context.MaxJob.Id, null, ct);
if (result == null || !result.Any())
throw new GroupedSyncEarlyExitException("Фильтры не дали Unit'ов с подходящими связями");
context.FilteredUnits = result.ToList();
context.UnitNames = result.ToDictionary(u => u.Id, u => u.Name);
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): отфильтровано {Count} юнитов",
context.JobGroupName, context.JobGroupId, context.FilteredUnits.Count);
return context;
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class GroupFilterStage : IGroupedSyncStage
{
private readonly IGroupedTemplateUnitFilter _groupedFilter;
private readonly ILogger<GroupFilterStage> _logger;
public string StageName => "Групповая фильтрация";
public GroupFilterStage(IGroupedTemplateUnitFilter groupedFilter, ILogger<GroupFilterStage> logger)
{
_groupedFilter = groupedFilter;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var finalFiltered = await _groupedFilter.FilterAsync(context.FilteredUnits, context.JobGroup);
if (!finalFiltered.Any())
throw new GroupedSyncEarlyExitException("Нет юнитов после групповой фильтрации");
context.FilteredUnits = finalFiltered;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): после групповой фильтрации {Count} юнитов",
context.JobGroupName, context.JobGroupId, finalFiltered.Count);
return context;
}
}

View File

@@ -0,0 +1,39 @@
using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
/// <summary>
/// Контекст групповой синхронизации. Передаётся между этапами.
/// </summary>
public class GroupedSyncContext
{
public Guid JobGroupId { get; init; }
public string JobGroupName { get; set; } = string.Empty;
public HistoryInitiator Initiator { get; init; } = null!;
public JobGroup JobGroup { get; set; } = null!;
public List<Job> JobsInGroup { get; set; } = new();
public Job MaxJob { get; set; } = null!;
public List<UnitFilterResultDto> FilteredUnits { get; set; } = new();
public Dictionary<Guid, List<Guid>> ReverseMapping { get; set; } = new();
public List<GroupedTemplateGroup> TemplateGroups { get; set; } = new();
public HashSet<(Guid JobId, Guid UnitId, int Index)> ExpectedTemplateKeys { get; set; } = new();
/// <summary>
/// Имена юнитов для логирования. Заполняется на этапе фильтрации.
/// </summary>
public Dictionary<Guid, string> UnitNames { get; set; } = new();
/// <summary>
/// Возвращает строку вида 'Имя' (ID) для логирования.
/// </summary>
public string FormatUnit(Guid unitId)
{
return UnitNames.TryGetValue(unitId, out var name)
? $"'{name}' ({unitId})"
: $"({unitId})";
}
}

View File

@@ -0,0 +1,16 @@
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <summary>
/// Штатное прерывание пайплайна (нет данных после этапа).
/// Не является ошибкой — оркестратор перехватывает и логирует как нормальное завершение.
/// </summary>
public class GroupedSyncEarlyExitException : Exception
{
public string Reason { get; }
public GroupedSyncEarlyExitException(string reason) : base(reason)
{
Reason = reason;
}
}
}

View File

@@ -5,10 +5,10 @@ using PARR.Core.Services.Shortcodes;
using PARR.Domain.Constants; using PARR.Domain.Constants;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateBuilder : IGroupedTemplateBuilder internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
{ {

View File

@@ -12,7 +12,7 @@ using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateProcessor : IGroupedTemplateProcessor internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
{ {
@@ -136,7 +136,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate);
if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id); logger.LogInformation("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id);
var updateRequest = new TemplateUpdaterMessage var updateRequest = new TemplateUpdaterMessage
{ {
TemplateId = existingTemplate.Id, TemplateId = existingTemplate.Id,
@@ -160,7 +160,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
} }
else else
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id); logger.LogInformation("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id);
await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator); await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator);
} }
} }

View File

@@ -1,10 +1,9 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateUnitFilter : IGroupedTemplateUnitFilter internal class GroupedTemplateUnitFilter : IGroupedTemplateUnitFilter
{ {

View File

@@ -0,0 +1,14 @@
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <summary>
/// Этап групповой синхронизации, который только читает данные.
/// НЕ выполняет запись в БД, MQ или кэш.
/// </summary>
public interface IGroupedSyncStage
{
string StageName { get; }
Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,8 @@
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <summary>
/// Этап групповой синхронизации с побочными эффектами (запись в БД, MQ).
/// В тестах не подключается — тип системы гарантирует безопасность.
/// </summary>
public interface IGroupedSyncWriteStage : IGroupedSyncStage { }
}

View File

@@ -1,7 +1,8 @@
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces; namespace PARR.TemplateMatcher.Services.GroupedSync;
public interface IGroupedTemplateBuilder public interface IGroupedTemplateBuilder
{ {
@@ -15,4 +16,4 @@ public interface IGroupedTemplateBuilder
JobGroup jobGroup, JobGroup jobGroup,
Job maxJob, Job maxJob,
CancellationToken ct = default); CancellationToken ct = default);
} }

View File

@@ -2,7 +2,7 @@
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces; namespace PARR.TemplateMatcher.Services.GroupedSync;
public interface IGroupedTemplateProcessor public interface IGroupedTemplateProcessor
{ {

View File

@@ -1,7 +1,7 @@
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.JobGroupEntities;
namespace PARR.TemplateMatcher.Services.Interfaces namespace PARR.TemplateMatcher.Services.GroupedSync
{ {
public interface IGroupedTemplateUnitFilter public interface IGroupedTemplateUnitFilter
{ {

View File

@@ -1,7 +1,7 @@
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
namespace PARR.TemplateMatcher.Services.Interfaces namespace PARR.TemplateMatcher.Services.GroupedSync
{ {
/// <summary> /// <summary>
/// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей. /// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей.

View File

@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class LoadJobGroupStage : IGroupedSyncStage
{
private readonly IJobGroupRepository _jobGroupRepository;
private readonly ILogger<LoadJobGroupStage> _logger;
public string StageName => "Загрузка JobGroup";
public LoadJobGroupStage(IJobGroupRepository jobGroupRepository, ILogger<LoadJobGroupStage> logger)
{
_jobGroupRepository = jobGroupRepository;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var jobGroup = await _jobGroupRepository.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs).ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs).ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters).ThenInclude(rf => rf.UnitField)
.Include(jg => jg.Jobs).ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == context.JobGroupId, ct);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
throw new GroupedSyncEarlyExitException("JobGroup не найден или пуст");
var jobsInGroup = jobGroup.Jobs.ToList();
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
throw new GroupedSyncEarlyExitException("Не найден Job с MaxValueRelationships");
context.JobGroup = jobGroup;
context.JobGroupName = jobGroup.GroupName;
context.JobsInGroup = jobsInGroup;
context.MaxJob = maxJob;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): загружено {JobCount} Job'ов, эталонный Job '{MaxJobName}' ({MaxJobId})",
jobGroup.GroupName, jobGroup.Id, jobsInGroup.Count, maxJob.Name, maxJob.Id);
return context;
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class ProcessGroupsStage : IGroupedSyncWriteStage
{
private readonly IGroupedTemplateProcessor _processor;
private readonly ILogger<ProcessGroupsStage> _logger;
public string StageName => "Обработка групп";
public ProcessGroupsStage(IGroupedTemplateProcessor processor, ILogger<ProcessGroupsStage> logger)
{
_processor = processor;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
// Приведение типа обратно из object — пайплайн хранит как object для универсальности контекста
var typedGroups = context.TemplateGroups
.Cast<GroupedTemplateGroup>()
.ToList();
var expectedKeys = await _processor.ProcessAsync(
typedGroups, context.JobsInGroup, context.MaxJob, context.Initiator);
context.ExpectedTemplateKeys = expectedKeys;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): обработано групп, ожидаемых ключей={Count}",
context.JobGroupName, context.JobGroupId, expectedKeys.Count);
return context;
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class ResolveConflictsStage : IGroupedSyncStage
{
private readonly IUnitInTemplateConflictMapper _conflictMapper;
private readonly ILogger<ResolveConflictsStage> _logger;
public string StageName => "Разрешение конфликтов";
public ResolveConflictsStage(IUnitInTemplateConflictMapper conflictMapper, ILogger<ResolveConflictsStage> logger)
{
_conflictMapper = conflictMapper;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var mapping = await _conflictMapper.BuildMappingAsync(context.FilteredUnits, context.MaxJob, ct);
if (!mapping.Any())
throw new GroupedSyncEarlyExitException("Нет связей после разрешения конфликтов");
context.ReverseMapping = mapping;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): разрешено конфликтов, связей={Count}",
context.JobGroupName, context.JobGroupId, mapping.Count);
return context;
}
}

View File

@@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.GroupedSync;
internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
{ {

View File

@@ -1,260 +1,110 @@
using System.Diagnostics; using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.MatchingStatusService; using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models; using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using System.Diagnostics;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{ {
private readonly ILogger<GroupedTemplateSynchronizer> logger; private readonly IEnumerable<IGroupedSyncStage> _readStages;
private readonly IJobGroupRepository jobGroupService; private readonly IEnumerable<IGroupedSyncWriteStage> _writeStages;
private readonly IUnitFilterService unitFilterService; private readonly IMatchingStatusService _matchingStatusService;
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter; private readonly ILogger<GroupedTemplateSynchronizer> _logger;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
private readonly IGroupedTemplateBuilder groupedTemplateBuilder;
private readonly IGroupedTemplateProcessor groupedTemplateProcessor;
private readonly ITemplateRepository templateService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly IMatchingStatusService matchingStatusService;
public GroupedTemplateSynchronizer( public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger, IEnumerable<IGroupedSyncStage> readStages,
IJobGroupRepository jobGroupService, IEnumerable<IGroupedSyncWriteStage> writeStages,
IUnitFilterService unitFilterService, IMatchingStatusService matchingStatusService,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter, ILogger<GroupedTemplateSynchronizer> logger)
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
IGroupedTemplateBuilder groupedTemplateBuilder,
IGroupedTemplateProcessor groupedTemplateProcessor,
ITemplateRepository templateService,
ITemplateDeactivator templateDeactivator,
IMatchingStatusService matchingStatusService)
{ {
this.logger = logger; _readStages = readStages;
this.jobGroupService = jobGroupService; _writeStages = writeStages;
this.unitFilterService = unitFilterService; _matchingStatusService = matchingStatusService;
this.groupedTemplateUnitFilter = groupedTemplateUnitFilter; _logger = logger;
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
this.groupedTemplateBuilder = groupedTemplateBuilder;
this.groupedTemplateProcessor = groupedTemplateProcessor;
this.templateService = templateService;
this.templateDeactivator = templateDeactivator;
this.matchingStatusService = matchingStatusService;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
} }
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
var totalSw = Stopwatch.StartNew(); _logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? === var existingStatus = await _matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true) if (existingStatus.DetailsJobGroups?.Any() == true)
{ {
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId); _logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return; return;
} }
// === Устанавливаем статус "в процессе" === await SetStatusAsync(jobGroupId, "Начало синхронизации");
var initialStatus = new MatchingStatusItemDto
{ var totalSw = Stopwatch.StartNew();
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try try
{ {
// === ЭТАП 1: Загрузка JobGroup === var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator };
var stageSw = Stopwatch.StartNew();
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField)
.Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) foreach (var stage in _readStages)
{ {
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); var stageSw = Stopwatch.StartNew();
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст"); await stage.ExecuteAsync(context);
return; stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
} }
var jobsInGroup = jobGroup.Jobs.ToList(); foreach (var stage in _writeStages)
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Загрузка JobGroup | Время: {Ms} мс | Jobs: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, jobsInGroup.Count);
// === Поиск эталонного Job ===
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{ {
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); var stageSw = Stopwatch.StartNew();
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships"); await stage.ExecuteAsync(context);
return; stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
} }
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships); totalSw.Stop();
_logger.LogInformation("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | ИТОГО: {TotalMs} мс",
context.JobGroupName, jobGroupId, totalSw.ElapsedMilliseconds);
// === ЭТАП 2: Фильтрация юнитов === await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
stageSw.Restart(); await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id); _logger.LogInformation("Синхронизация шаблонов завершена для JobGroup '{JobGroupName}' ({JobGroupId})",
stageSw.Stop(); context.JobGroupName, jobGroupId);
var filterCount = unitFilterResults?.Count() ?? 0; }
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}", catch (GroupedSyncEarlyExitException ex)
jobGroupId, stageSw.ElapsedMilliseconds, filterCount); {
totalSw.Stop();
if (unitFilterResults == null || !unitFilterResults.Any()) _logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)",
{ jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds);
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId); await SetStatusAsync(jobGroupId, ex.Reason);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями"); await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
return;
}
// === ЭТАП 3: Групповая фильтрация ===
stageSw.Restart();
var finalFilteredUnits = await groupedTemplateUnitFilter.FilterAsync(unitFilterResults, jobGroup);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Групповая фильтрация | Время: {Ms} мс | Результат: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, finalFilteredUnits.Count);
if (!finalFilteredUnits.Any())
{
logger.LogInformation("После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return;
}
// === ЭТАП 4: Разрешение конфликтов ===
stageSw.Restart();
var initialReverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnits, maxJob);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Разрешение конфликтов | Время: {Ms} мс | Связей: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, initialReverseMapping.Count);
if (!initialReverseMapping.Any())
{
logger.LogInformation("После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет связей после разрешения конфликтов");
return;
}
// === ЭТАП 5: Построение структуры групп ===
stageSw.Restart();
var templateGroups = await groupedTemplateBuilder.BuildAsync(initialReverseMapping, jobGroup, maxJob);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Построение групп | Время: {Ms} мс | Групп: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, templateGroups.Count);
if (!templateGroups.Any())
{
logger.LogInformation("После построения структуры групп в JobGroup {JobGroupId} не осталось данных.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет данных после построения групп");
return;
}
// === ЭТАП 6: Обработка групп (сравнение, обновление, MQ) ===
stageSw.Restart();
var expectedTemplateKeys = await groupedTemplateProcessor.ProcessAsync(
templateGroups,
jobsInGroup,
maxJob,
initiator);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Обработка групп | Время: {Ms} мс | Ключей: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, expectedTemplateKeys.Count);
// === ЭТАП 7: Деактивация лишних шаблонов ===
stageSw.Restart();
await DeactivateUnusedTemplatesAsync(expectedTemplateKeys, jobGroupId, jobsInGroup, initiator);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Деактивация | Время: {Ms} мс",
jobGroupId, stageSw.ElapsedMilliseconds);
// === ИТОГО === totalSw.Stop();
logger.LogInformation(
"[Perf] JobGroup {JobGroupId} | ИТОГО: {TotalMs} мс",
jobGroupId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
} }
catch (Exception ex) catch (Exception ex)
{ {
totalSw.Stop(); totalSw.Stop();
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds); _logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс",
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}"); jobGroupId, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw; throw;
} }
} }
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); _logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
return Task.CompletedTask;
} }
private async Task DeactivateUnusedTemplatesAsync( public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
Guid jobGroupId,
List<Job> jobsInGroup,
HistoryInitiator initiator)
{ {
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); _logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
var allExistingTemplatesInGroup = await templateService.Get() return Task.CompletedTask;
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) &&
t.StatusTypeId == TemplateStatusTypeEnum.Used &&
t.Job!.GroupId == jobGroupId)
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Unit {UnitId}, Index {Index}).",
existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
} }
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment) private async Task SetStatusAsync(Guid jobGroupId, string comment)
{ {
var status = new MatchingStatusItemDto var status = new MatchingStatusItemDto
{ {
@@ -262,12 +112,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
Action = TemplateMatcherActionEnum.Sync, Action = TemplateMatcherActionEnum.Sync,
Comment = comment Comment = comment
}; };
await _matchingStatusService.SetMatchingStatusAsync(
await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup,
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30) TimeSpan.FromMinutes(30));
);
} }
} }

View File

@@ -2,28 +2,28 @@
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class JobGroupValidatorService : IJobGroupValidatorService internal class JobGroupValidatorService : IJobGroupValidatorService
{ {
private readonly ILogger<IJobValidatorService> logger; private readonly ILogger<IJobValidatorService> _logger;
private readonly IJobGroupRepository jobGroupService; private readonly IJobGroupRepository _jobGroupService;
public JobGroupValidatorService( public JobGroupValidatorService(
ILogger<IJobValidatorService> logger, ILogger<IJobValidatorService> logger,
IJobGroupRepository jobGroupService IJobGroupRepository jobGroupService
) )
{ {
this.logger = logger; _logger = logger;
this.jobGroupService = jobGroupService; _jobGroupService = jobGroupService;
} }
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId) public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
{ {
var isExist = await jobGroupService.GetAsync(jobGroupId); var isExist = await _jobGroupService.GetAsync(jobGroupId);
if (isExist == null) if (isExist == null)
{ {
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}"); _logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
return false; return false;
} }

View File

@@ -2,7 +2,7 @@
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class JobValidatorService : IJobValidatorService internal class JobValidatorService : IJobValidatorService
{ {

View File

@@ -7,7 +7,7 @@ using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class MqTemplateMatcher : IMqTemplateMatcher internal class MqTemplateMatcher : IMqTemplateMatcher
{ {

View File

@@ -15,8 +15,9 @@ using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit; using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.Domain.Settings; using PARR.Domain.Settings;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Constants;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
using System.Diagnostics; using System.Diagnostics;
@@ -31,18 +32,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private const bool DefaultUsedTemplateState = false; private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false; private const bool DefaultUsedScheduleState = false;
// === Константы для логики неиспользуемых шаблонов === private readonly IEnumerable<ISimpleSyncStage> readStages;
private const string FieldNameResponsibilityArea = "ЗОНА_ОТВЕТСТВЕННОСТИ"; private readonly IEnumerable<ISimpleSyncWriteStage> writeStages;
private const string FieldNameParrTag = "ПАРР тег";
private const string TagValueNotWorking = "ПАРР-НЕИСП";
private readonly ILogger<SimpleTemplateSynchronizer> logger; private readonly ILogger<SimpleTemplateSynchronizer> logger;
private readonly IUnitFilterService unitFilterService; private readonly IUnitFilterService unitFilterService;
private readonly MqSettings mqSettings; private readonly MqSettings mqSettings;
private readonly IRabbitService mqService; private readonly IRabbitService mqService;
private readonly ITemplateRepository templateService; private readonly ITemplateRepository templateService;
private readonly IJobRepository jobService; private readonly IJobRepository jobService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateAllocationService templateAllocationService; private readonly ITemplateAllocationService templateAllocationService;
private readonly ITemplateMqPublisher templateMqPublisher; private readonly ITemplateMqPublisher templateMqPublisher;
@@ -54,13 +51,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private readonly IUnitRepository unitRepository; private readonly IUnitRepository unitRepository;
public SimpleTemplateSynchronizer( public SimpleTemplateSynchronizer(
IEnumerable<ISimpleSyncStage> readStages,
IEnumerable<ISimpleSyncWriteStage> writeStages,
ILogger<SimpleTemplateSynchronizer> logger, ILogger<SimpleTemplateSynchronizer> logger,
IUnitFilterService unitFilterService, IUnitFilterService unitFilterService,
MqSettings mqSettings, MqSettings mqSettings,
IRabbitService mqService, IRabbitService mqService,
ITemplateRepository templateService, ITemplateRepository templateService,
IJobRepository jobService, IJobRepository jobService,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer, ITemplateNameNormalizer templateNameNormalizer,
ITemplateAllocationService templateAllocationService, ITemplateAllocationService templateAllocationService,
ITemplateMqPublisher templateMqPublisher, ITemplateMqPublisher templateMqPublisher,
@@ -72,13 +70,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
IUnitRepository unitRepository IUnitRepository unitRepository
) )
{ {
this.readStages = readStages;
this.writeStages = writeStages;
this.logger = logger; this.logger = logger;
this.unitFilterService = unitFilterService; this.unitFilterService = unitFilterService;
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.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer; this.templateNameNormalizer = templateNameNormalizer;
this.templateAllocationService = templateAllocationService; this.templateAllocationService = templateAllocationService;
this.templateMqPublisher = templateMqPublisher; this.templateMqPublisher = templateMqPublisher;
@@ -92,18 +91,15 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
// === Специальная обработка для Job неиспользуемых шаблонов ===
if (jobId == settingsFromDb.JobIdForUnusedTemplates) if (jobId == settingsFromDb.JobIdForUnusedTemplates)
{ {
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId); logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
await SyncUnusedTemplatesAsync(jobId, initiator); await SyncUnusedTemplatesAsync(jobId, initiator);
return; return;
} }
var totalSw = Stopwatch.StartNew();
logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId); logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Any() == true) if (existingStatus.DetailsJobs?.Any() == true)
{ {
@@ -111,7 +107,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return; return;
} }
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto var initialStatus = new MatchingStatusItemDto
{ {
DateStart = DateTimeOffset.UtcNow, DateStart = DateTimeOffset.UtcNow,
@@ -119,169 +114,49 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Comment = "Начало синхронизации" Comment = "Начало синхронизации"
}; };
await matchingStatusService.SetMatchingStatusAsync( await matchingStatusService.SetMatchingStatusAsync(
jobId, jobId, SyncTaskEntityTypeEnum.Job,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(35) TimeSpan.FromMinutes(35));
);
// Таймер запускается ПОСЛЕ инфраструктурных операций (статус, проверка блокировки)
var totalSw = Stopwatch.StartNew();
try try
{ {
// === ЭТАП 1: Загрузка Job === var context = new SimpleSyncContext { JobId = jobId, Initiator = initiator };
var stageSw = Stopwatch.StartNew();
var job = await jobService.Get()
.AsNoTracking()
.Include(j => j.AutoControl)
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g!.GroupType)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId);
if (job == null) foreach (var stage in readStages)
{ {
logger.LogWarning("Job {JobId} не найден.", jobId); var stageSw = Stopwatch.StartNew();
await UpdateMatchingStatusAsync(jobId, "Job не найден"); await stage.ExecuteAsync(context);
return; stageSw.Stop();
} logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
stageSw.Stop(); context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка Job | Время: {Ms} мс",
jobId, stageSw.ElapsedMilliseconds);
// === ЭТАП 2: Фильтрация юнитов ===
stageSw.Restart();
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
stageSw.Stop();
var filterCount = filteredUnits?.Count() ?? 0;
logger.LogDebug("[Perf] Job {JobId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
jobId, stageSw.ElapsedMilliseconds, filterCount);
var unitIds = filteredUnits?.Select(u => u.Id).ToHashSet() ?? new HashSet<Guid>();
// === ЭТАП 3: Загрузка существующих шаблонов ===
stageSw.Restart();
var existingTemplates = await templateService.Get()
.Include(t => t.UnitsInTemplate)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t!.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Include(t => t.Unit)
.Where(t => t.JobId == jobId)
.ToListAsync();
var existingUsedTemplates = existingTemplates
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToList();
var existingUnitIds = existingUsedTemplates.Select(t => t.UnitId).ToHashSet();
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка шаблонов | Время: {Ms} мс | Используется: {Count}",
jobId, stageSw.ElapsedMilliseconds, existingUsedTemplates.Count);
// === ЭТАП 4: Расчёт диффа (создание / деактивация / переименование) ===
stageSw.Restart();
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
var unusedTemplates = existingUsedTemplates
.Where(t => !unitIds.Contains(t.UnitId))
.ToList();
// Проверка имён существующих шаблонов
var templatesToRename = new List<(Template Template, string ExpectedName)>();
foreach (var template in existingUsedTemplates)
{
if (!unitIds.Contains(template.UnitId))
continue;
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
templatesToRename.Add((template, expectedName));
}
} }
stageSw.Stop(); foreach (var stage in writeStages)
logger.LogDebug("[Perf] Job {JobId} | Этап: Расчёт диффа | Время: {Ms} мс | Создать: {Create}, Деактивировать: {Deactivate}, Переименовать: {Rename}",
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count, unusedTemplates.Count, templatesToRename.Count);
// === ЭТАП 5: Создание новых шаблонов ===
stageSw.Restart();
foreach (var unitId in newUnitIds)
{ {
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState; var stageSw = Stopwatch.StartNew();
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState; await stage.ExecuteAsync(context);
stageSw.Stop();
var request = new TemplateAllocationRequest( logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
TargetJob: job, context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
TargetUnitId: unitId,
TargetUnit: null,
Index: null,
UnitsInTemplate: new List<UnitInTemplateMessage>(),
IsActiveTemplate: isActiveTemplate,
IsActiveSchedule: isActiveSchedule,
Initiator: initiator);
await templateAllocationService.AllocateAsync(request);
} }
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Создание шаблонов | Время: {Ms} мс | Количество: {Count}",
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count);
// === ЭТАП 6: Обновление имён существующих шаблонов ===
stageSw.Restart();
foreach (var (template, expectedName) in templatesToRename)
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: '{OldName}' → '{NewName}'",
template.Id, template.Name, expectedName);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Обновление имён | Время: {Ms} мс | Количество: {Count}",
jobId, stageSw.ElapsedMilliseconds, templatesToRename.Count);
// === ЭТАП 7: Деактивация лишних шаблонов ===
stageSw.Restart();
foreach (var unusedTemplate in unusedTemplates)
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).",
unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
}
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Деактивация | Время: {Ms} мс | Количество: {Count}",
jobId, stageSw.ElapsedMilliseconds, unusedTemplates.Count);
// === ИТОГО ===
totalSw.Stop(); totalSw.Stop();
logger.LogInformation("[Perf] Job {JobId} | ИТОГО: {TotalMs} мс", jobId, totalSw.ElapsedMilliseconds); logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
context.JobName, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно"); await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
context.JobName, jobId);
} }
catch (Exception ex) catch (Exception ex)
{ {
totalSw.Stop(); totalSw.Stop();
logger.LogError(ex, "Ошибка при синхронизации Job {JobId} через {ElapsedMs} мс", jobId, totalSw.ElapsedMilliseconds); logger.LogError(ex, "Ошибка при синхронизации Job '{JobName}' ({JobId}) через {ElapsedMs} мс",
string.Empty, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw; throw;
} }
@@ -293,6 +168,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId); logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
} }
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId); logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
@@ -444,30 +320,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
try try
{ {
// 1. Находим ID нужных полей // 1. Находим ID нужных полей
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(FieldNameResponsibilityArea); var responsableAreaField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName);
var tagField = await unitFieldService.GetByAihitNameAsync(FieldNameParrTag); var tagField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName);
if (responsableAreaField == null || tagField == null) if (responsableAreaField == null || tagField == null)
{ {
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", FieldNameResponsibilityArea, FieldNameParrTag); logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue);
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей"); await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
return; return;
} }
var responsableAreaFieldId = responsableAreaField.Id; var responsableAreaFieldId = responsableAreaField.Id;
var tagFieldId = tagField.Id; var tagFieldId = tagField.Id;
const string targetTagValue = TagValueNotWorking;
// 2. Находим ValueId для тега "ПАРР-НЕИСП" // 2. Находим ValueId для тега "ПАРР-НЕИСП"
var targetTagValueId = await unitInValueService.Get() var targetTagValueId = await unitInValueService.Get()
.AsNoTracking() .AsNoTracking()
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == targetTagValue) .Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == UnusedTemplateConstants.NotUsedTagValue)
.Select(uiv => uiv.ValueId) .Select(uiv => uiv.ValueId)
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
if (targetTagValueId == Guid.Empty) if (targetTagValueId == Guid.Empty)
{ {
logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", targetTagValue, FieldNameParrTag); logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", UnusedTemplateConstants.NotUsedTagValue, UnusedTemplateConstants.ParrTagFieldName);
} }
var unusedJob = await jobService.Get().AsNoTracking() var unusedJob = await jobService.Get().AsNoTracking()

View File

@@ -4,17 +4,17 @@ using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Implementations; using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class TemplateMatcher : ITemplateMatcher internal class TemplateMatcher : ITemplateMatcher
{ {
private readonly ILogger<TemplateMatcher> logger; private readonly ILogger<TemplateMatcher> _logger;
private readonly IJobRepository jobService; private readonly IJobRepository _jobService;
private readonly IJobGroupRepository jobGroupService; private readonly IJobGroupRepository _jobGroupService;
private readonly IEnumerable<ITemplateSynchronizer> synchronizers; private readonly IEnumerable<ITemplateSynchronizer> _synchronizers;
public TemplateMatcher( public TemplateMatcher(
ILogger<TemplateMatcher> logger, ILogger<TemplateMatcher> logger,
@@ -23,21 +23,21 @@ namespace PARR.TemplateMatcher
IEnumerable<ITemplateSynchronizer> synchronizers IEnumerable<ITemplateSynchronizer> synchronizers
) )
{ {
this.logger = logger; _logger = logger;
this.jobService = jobService; _jobService = jobService;
this.jobGroupService = jobGroupService; _jobGroupService = jobGroupService;
this.synchronizers = synchronizers; _synchronizers = synchronizers;
} }
public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator) public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); _logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId);
var job = await GetJobWithGroupAndAutoControlAsync(jobId); var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null) if (job == null)
{ {
logger.LogError("Job с Id {JobId} не найден.", jobId); _logger.LogError("Job с Id {JobId} не найден.", jobId);
return; return;
} }
@@ -46,9 +46,9 @@ namespace PARR.TemplateMatcher
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue) if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{ {
logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer.", jobId); _logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
// Так как Job групповой, вызываем синхронизацию для его JobGroup // Так как Job групповой, вызываем синхронизацию для его JobGroup
@@ -56,22 +56,22 @@ namespace PARR.TemplateMatcher
} }
else else
{ {
logger.LogError("GroupedTemplateSynchronizer не найден."); _logger.LogError("GroupedTemplateSynchronizer не найден.");
} }
return; return;
} }
else else
{ {
logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer.", jobId); _logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
await synchronizer.SyncTemplatesForJobAsync(jobId, initiator); await synchronizer.SyncTemplatesForJobAsync(jobId, initiator);
} }
else else
{ {
logger.LogError("SimpleTemplateSynchronizer не найден."); _logger.LogError("SimpleTemplateSynchronizer не найден.");
} }
return; return;
} }
@@ -79,16 +79,16 @@ namespace PARR.TemplateMatcher
public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); _logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
var jobGroup = await jobGroupService.Get() var jobGroup = await _jobGroupService.Get()
.AsNoTracking() .AsNoTracking()
.Include(jg => jg.GroupType) .Include(jg => jg.GroupType)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId); .FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.GroupType == null) if (jobGroup == null || jobGroup.GroupType == null)
{ {
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
return; return;
} }
@@ -99,32 +99,32 @@ namespace PARR.TemplateMatcher
// Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки // Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки
if (jobGroup.GroupingUnitFieldId.HasValue) if (jobGroup.GroupingUnitFieldId.HasValue)
{ {
logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId);
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator); await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator);
} }
else else
{ {
logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId); _logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId);
} }
} }
else else
{ {
logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId);
//await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); //await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
} }
break; break;
case JobGroupTypesEnum.Umbrella: case JobGroupTypesEnum.Umbrella:
logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId);
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
case JobGroupTypesEnum.Simple: case JobGroupTypesEnum.Simple:
default: default:
logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId);
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
} }
@@ -132,16 +132,16 @@ namespace PARR.TemplateMatcher
public async Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) public async Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало обновления шаблонов для JobGroup {JobGroupId}", jobGroupId); _logger.LogDebug("Начало обновления шаблонов для JobGroup {JobGroupId}", jobGroupId);
var jobGroup = await jobGroupService.Get() var jobGroup = await _jobGroupService.Get()
.AsNoTracking() .AsNoTracking()
.Include(jg => jg.GroupType) .Include(jg => jg.GroupType)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId); .FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.GroupType == null) if (jobGroup == null || jobGroup.GroupType == null)
{ {
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
return; return;
} }
@@ -150,23 +150,23 @@ namespace PARR.TemplateMatcher
case JobGroupTypesEnum.Group: case JobGroupTypesEnum.Group:
if (jobGroup.GroupingUnitFieldId.HasValue) if (jobGroup.GroupingUnitFieldId.HasValue)
{ {
logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId); _logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId);
} }
else else
{ {
logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
} }
break; break;
case JobGroupTypesEnum.Umbrella: case JobGroupTypesEnum.Umbrella:
logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
case JobGroupTypesEnum.Simple: case JobGroupTypesEnum.Simple:
default: default:
logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
} }
@@ -175,12 +175,12 @@ namespace PARR.TemplateMatcher
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 job = await GetJobWithGroupAndAutoControlAsync(jobId); var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null) if (job == null)
{ {
logger.LogError("Job с Id {JobId} не найден.", jobId); _logger.LogError("Job с Id {JobId} не найден.", jobId);
return; return;
} }
@@ -189,9 +189,9 @@ namespace PARR.TemplateMatcher
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue) if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{ {
logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer для Update.", jobId); _logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer для Update.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
// Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение) // Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение)
@@ -199,22 +199,22 @@ namespace PARR.TemplateMatcher
} }
else else
{ {
logger.LogError("GroupedTemplateSynchronizer не найден."); _logger.LogError("GroupedTemplateSynchronizer не найден.");
} }
return; return;
} }
else else
{ {
logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer для Update.", jobId); _logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer для Update.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator); await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator);
} }
else else
{ {
logger.LogError("SimpleTemplateSynchronizer не найден."); _logger.LogError("SimpleTemplateSynchronizer не найден.");
} }
return; return;
} }
@@ -223,9 +223,9 @@ namespace PARR.TemplateMatcher
// --- Вспомогательные методы --- // --- Вспомогательные методы ---
private async Task SyncJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator) private async Task SyncJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Синхронизация JobGroup {JobGroupId} как Collection (по каждому Job'у отдельно)", jobGroupId); _logger.LogDebug("Синхронизация JobGroup {JobGroupId} как Collection (по каждому Job'у отдельно)", jobGroupId);
var jobIds = await jobService.Get() var jobIds = await _jobService.Get()
.AsNoTracking() .AsNoTracking()
.Where(j => j.GroupId == jobGroupId) .Where(j => j.GroupId == jobGroupId)
.Select(j => j.Id) .Select(j => j.Id)
@@ -233,35 +233,35 @@ namespace PARR.TemplateMatcher
if (!jobIds.Any()) if (!jobIds.Any())
{ {
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
return; return;
} }
logger.LogDebug("Найдено {Count} Job'ов в JobGroup {JobGroupId}", jobIds.Count, jobGroupId); _logger.LogDebug("Найдено {Count} Job'ов в JobGroup {JobGroupId}", jobIds.Count, jobGroupId);
var simpleSynchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); var simpleSynchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (simpleSynchronizer == null) if (simpleSynchronizer == null)
{ {
logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId); _logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId);
return; return;
} }
foreach (var jobId in jobIds) foreach (var jobId in jobIds)
{ {
logger.LogDebug("Синхронизация Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId); _logger.LogDebug("Синхронизация Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
await simpleSynchronizer.SyncTemplatesForJobAsync(jobId, initiator); await simpleSynchronizer.SyncTemplatesForJobAsync(jobId, initiator);
} }
logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId); _logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId);
} }
private async Task UpdateJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator) private async Task UpdateJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Обновление JobGroup {JobGroupId} как Collection (по каждому Job'у)", jobGroupId); _logger.LogDebug("Обновление JobGroup {JobGroupId} как Collection (по каждому Job'у)", jobGroupId);
var jobIds = await jobService.Get() var jobIds = await _jobService.Get()
.AsNoTracking() .AsNoTracking()
.Where(j => j.GroupId == jobGroupId) .Where(j => j.GroupId == jobGroupId)
.Select(j => j.Id) .Select(j => j.Id)
@@ -269,23 +269,23 @@ namespace PARR.TemplateMatcher
if (!jobIds.Any()) if (!jobIds.Any())
{ {
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
return; return;
} }
foreach (var jobId in jobIds) foreach (var jobId in jobIds)
{ {
logger.LogDebug("Обновление шаблонов для Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId); _logger.LogDebug("Обновление шаблонов для Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
await UpdateTemplatesForJob(jobId, initiator); await UpdateTemplatesForJob(jobId, initiator);
} }
logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId); _logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId);
} }
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId) private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{ {
return await jobService.Get() return await _jobService.Get()
.AsNoTracking() .AsNoTracking()
.Include(j => j.Group) .Include(j => j.Group)
.ThenInclude(j => j!.GroupType) .ThenInclude(j => j!.GroupType)

View File

@@ -1,4 +1,4 @@
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Interfaces
{ {
public interface IMqTemplateMatcher public interface IMqTemplateMatcher
{ {

View File

@@ -1,12 +1,12 @@
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Interfaces
{ {
public 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); Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator); Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
} }
} }

View File

@@ -0,0 +1,52 @@
using Microsoft.Extensions.Logging;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class AllocateTemplatesStage : ISimpleSyncWriteStage
{
private readonly ITemplateAllocationService _allocationService;
private readonly ILogger<AllocateTemplatesStage> _logger;
public string StageName => "Создание шаблонов";
public AllocateTemplatesStage(
ITemplateAllocationService allocationService,
ILogger<AllocateTemplatesStage> logger)
{
_allocationService = allocationService;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
const bool defaultUsedTemplateState = false;
const bool defaultUsedScheduleState = false;
foreach (var unitId in context.NewUnitIds)
{
var isActiveTemplate = context.Job.AutoControl?.InitUsedTemplateState ?? defaultUsedTemplateState;
var isActiveSchedule = context.Job.AutoControl?.InitUsedScheduleState ?? defaultUsedScheduleState;
var request = new TemplateAllocationRequest(
TargetJob: context.Job,
TargetUnitId: unitId,
TargetUnit: null,
Index: null,
UnitsInTemplate: new List<UnitInTemplateMessage>(),
IsActiveTemplate: isActiveTemplate,
IsActiveSchedule: isActiveSchedule,
Initiator: context.Initiator);
await _allocationService.AllocateAsync(request);
_logger.LogDebug("Job '{JobName}' ({JobId}): создан шаблон для Unit {Unit}",
context.JobName, context.JobId, context.FormatUnit(unitId));
}
return context;
}
}

View File

@@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class AnalyzeChangesStage : ISimpleSyncStage
{
private readonly ITemplateRepository _templateRepository;
private readonly ITemplateNameNormalizer _nameNormalizer;
private readonly ILogger<AnalyzeChangesStage> _logger;
public string StageName => "Анализ изменений";
public AnalyzeChangesStage(
ITemplateRepository templateRepository,
ITemplateNameNormalizer nameNormalizer,
ILogger<AnalyzeChangesStage> logger)
{
_templateRepository = templateRepository;
_nameNormalizer = nameNormalizer;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
var existing = await _templateRepository.Get()
.Include(t => t.UnitsInTemplate)
.Include(t => t.Job).ThenInclude(t => t!.Group).ThenInclude(t => t!.GroupType)
.Include(t => t.Job).ThenInclude(t => t!.Tnk)
.Include(t => t.Unit)
.Where(t => t.JobId == context.JobId)
.ToListAsync(ct);
var used = existing.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used).ToList();
var existingUnitIds = used.Select(t => t.UnitId).ToHashSet();
context.ExistingUsedTemplates = used;
context.NewUnitIds = context.FilteredUnitIds.Except(existingUnitIds).ToList();
context.UnusedTemplates = used.Where(t => !context.FilteredUnitIds.Contains(t.UnitId)).ToList();
foreach (var template in used)
{
if (!context.FilteredUnitIds.Contains(template.UnitId)) continue;
var expectedName = await _nameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
context.TemplatesToRename.Add((template, expectedName));
}
_logger.LogDebug(
"Job '{JobName}' ({JobId}): существующих шаблонов={Existing}, создать={Create}, деактивировать={Deactivate}, переименовать={Rename}",
context.JobName, context.JobId,
used.Count, context.NewUnitIds.Count, context.UnusedTemplates.Count, context.TemplatesToRename.Count);
return context;
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class DeactivateTemplatesStage : ISimpleSyncWriteStage
{
private readonly ITemplateDeactivator _deactivator;
private readonly ILogger<DeactivateTemplatesStage> _logger;
public string StageName => "Деактивация шаблонов";
public DeactivateTemplatesStage(ITemplateDeactivator deactivator, ILogger<DeactivateTemplatesStage> logger)
{
_deactivator = deactivator;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
foreach (var template in context.UnusedTemplates)
{
_logger.LogInformation("Job '{JobName}' ({JobId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Unit {Unit}",
context.JobName, context.JobId, template.Name, template.Id,
context.FormatUnit(template.UnitId));
await _deactivator.DeactivateTemplateAsync(template, context.Initiator);
}
return context;
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Services.UnitFilterService;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class FilterUnitsStage : ISimpleSyncStage
{
private readonly IUnitFilterService _filterService;
private readonly ILogger<FilterUnitsStage> _logger;
public string StageName => "Фильтрация юнитов";
public FilterUnitsStage(IUnitFilterService filterService, ILogger<FilterUnitsStage> logger)
{
_filterService = filterService;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
var filtered = await _filterService.GetUnitsByJobFilterAsync(context.JobId, null, ct);
if (filtered != null)
{
context.FilteredUnitIds = filtered.Select(u => u.Id).ToHashSet();
context.UnitNames = filtered.ToDictionary(u => u.Id, u => u.Name);
}
_logger.LogDebug("Job '{JobName}' ({JobId}): отфильтровано {Count} юнитов",
context.JobName, context.JobId, context.FilteredUnitIds.Count);
return context;
}
}

View File

@@ -0,0 +1,12 @@
namespace PARR.TemplateMatcher.Services.SimpleSync
{
/// <summary>
/// Этап синхронизации, который только читает данные и заполняет контекст.
/// НЕ выполняет запись в БД, MQ или кэш.
/// </summary>
public interface ISimpleSyncStage
{
string StageName { get; }
Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,10 @@
namespace PARR.TemplateMatcher.Services.SimpleSync
{
/// <summary>
/// Этап синхронизации, который выполняет побочные эффекты (запись в БД, MQ).
/// В тестах не подключается — тип системы гарантирует безопасность.
/// </summary>
public interface ISimpleSyncWriteStage : ISimpleSyncStage
{
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class LoadJobStage : ISimpleSyncStage
{
private readonly IJobRepository _jobRepository;
private readonly ILogger<LoadJobStage> _logger;
public string StageName => "Загрузка Job";
public LoadJobStage(IJobRepository jobRepository, ILogger<LoadJobStage> logger)
{
_jobRepository = jobRepository;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
var job = await _jobRepository.Get()
.AsNoTracking()
.AsSplitQuery()
.Include(j => j.AutoControl)
.Include(j => j.Tnk)
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == context.JobId, ct);
if (job == null)
throw new InvalidOperationException($"Job '{context.JobId}' не найден");
context.Job = job;
context.JobName = job.Name;
_logger.LogDebug("Job '{JobName}' ({JobId}) загружен", job.Name, job.Id);
return context;
}
}

View File

@@ -0,0 +1,39 @@
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
namespace PARR.TemplateMatcher.Services.SimpleSync
{
/// <summary>
/// Контекст синхронизации простого шаблона. Передаётся между этапами.
/// </summary>
public class SimpleSyncContext
{
public Guid JobId { get; init; }
public string JobName { get; set; } = string.Empty;
public HistoryInitiator Initiator { get; init; } = null!;
public Job Job { get; set; } = null!;
public HashSet<Guid> FilteredUnitIds { get; set; } = new();
/// <summary>
/// Имена юнитов для логирования. Заполняется на этапе фильтрации.
/// </summary>
public Dictionary<Guid, string> UnitNames { get; set; } = new();
public List<Template> ExistingUsedTemplates { get; set; } = new();
public List<Guid> NewUnitIds { get; set; } = new();
public List<Template> UnusedTemplates { get; set; } = new();
public List<(Template Template, string ExpectedName)> TemplatesToRename { get; set; } = new();
/// <summary>
/// Возвращает строку вида 'Имя' (ID) для логирования.
/// Если имя неизвестно — возвращает только ID.
/// </summary>
public string FormatUnit(Guid unitId)
{
return UnitNames.TryGetValue(unitId, out var name)
? $"'{name}' ({unitId})"
: $"({unitId})";
}
}
}

View File

@@ -0,0 +1,50 @@
using Microsoft.Extensions.Logging;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class UpdateNamesStage : ISimpleSyncWriteStage
{
private readonly ITemplateMqPublisher _mqPublisher;
private readonly ILogger<UpdateNamesStage> _logger;
public string StageName => "Обновление имён";
public UpdateNamesStage(ITemplateMqPublisher mqPublisher, ILogger<UpdateNamesStage> logger)
{
_mqPublisher = mqPublisher;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
foreach (var (template, expectedName) in context.TemplatesToRename)
{
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = context.JobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = context.Initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await _mqPublisher.PublishUpdateAsync(updateRequest);
_logger.LogDebug("Job '{JobName}' ({JobId}): шаблон '{TemplateName}' ({TemplateId}) для Unit {Unit} переименован в '{NewName}'",
context.JobName, context.JobId, template.Name, template.Id,
context.FormatUnit(template.UnitId), expectedName);
}
return context;
}
}

View File

@@ -3,9 +3,10 @@ using Microsoft.Extensions.DependencyInjection;
using PARR.Core; using PARR.Core;
using PARR.DAL; using PARR.DAL;
using PARR.Infrastructure; using PARR.Infrastructure;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations; using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Implemetaions;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher
@@ -31,8 +32,31 @@ namespace PARR.TemplateMatcher
// === 2. Scoped: Бизнес-логика и работа с БД (DbContext) === // === 2. Scoped: Бизнес-логика и работа с БД (DbContext) ===
// Создаются заново для каждого сообщения из очереди (внутри CreateAsyncScope) // Создаются заново для каждого сообщения из очереди (внутри CreateAsyncScope)
services.AddScoped<ITemplateMatcher, TemplateMatcher>(); services.AddScoped<ITemplateMatcher, Services.Implementations.TemplateMatcher>();
// Simple pipeline: Read-этапы
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.LoadJobStage>(); //1
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.FilterUnitsStage>(); //2
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.AnalyzeChangesStage>(); //3
// Simple pipeline: Write-этапы
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.AllocateTemplatesStage>(); //4
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.UpdateNamesStage>(); //5
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.DeactivateTemplatesStage>(); //6
services.AddScoped<ITemplateSynchronizer, SimpleTemplateSynchronizer>(); services.AddScoped<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
// Grouped pipeline: Read-этапы
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.LoadJobGroupStage>(); //1
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.FilterUnitsStage>(); //2
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.GroupFilterStage>(); //3
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.ResolveConflictsStage>(); //4
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.BuildGroupsStage>(); //5
// Grouped pipeline: Write-этапы
services.AddScoped<IGroupedSyncWriteStage, Services.Implementations.GroupedSync.ProcessGroupsStage>(); //6
services.AddScoped<IGroupedSyncWriteStage, Services.Implementations.GroupedSync.DeactivateTemplatesStage>();//7
services.AddScoped<ITemplateSynchronizer, GroupedTemplateSynchronizer>(); services.AddScoped<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
// Пайплайн аллокации шаблонов // Пайплайн аллокации шаблонов

View File

@@ -1,4 +1,4 @@
using PARR.TemplateMatcher; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcherWorker namespace PARR.TemplateMatcherWorker
{ {

View File

@@ -25,4 +25,8 @@
<ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" /> <ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" />
<ProjectReference Include="..\PARR.TemplateMatcher\PARR.TemplateMatcher.csproj" /> <ProjectReference Include="..\PARR.TemplateMatcher\PARR.TemplateMatcher.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="log\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PARR.Core.Common.Interfaces; using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache; using PARR.Domain.Cache;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums; using PARR.Domain.Enums;
@@ -8,7 +9,11 @@ using PARR.EsppApi;
using PARR.EsppApi.Constants; using PARR.EsppApi.Constants;
using PARR.EsppApi.Models.Query; using PARR.EsppApi.Models.Query;
using PARR.TemplateMatcher; using PARR.TemplateMatcher;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.Test.NextRun; using PARR.Test.NextRun;
using System.Diagnostics;
namespace PARR.Test namespace PARR.Test
{ {
@@ -41,7 +46,8 @@ namespace PARR.Test
//var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0)); //var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0));
await TemplateMatcherTest(); //await TemplateMatcherTest();
await PreCommitValidationTest();
@@ -161,6 +167,91 @@ namespace PARR.Test
#endregion #endregion
} }
#region PreCommitValidation
private async Task PreCommitValidationTest()
{
_logger.LogInformation("=== НАЧАЛО PRE-COMMIT ВАЛИДАЦИИ ===");
await using var scope = serviceProvider.CreateAsyncScope();
// === 1. Проверка Simple Pipeline ===
_logger.LogInformation("--- Simple Pipeline ---");
var simpleSync = scope.ServiceProvider
.GetRequiredService<IEnumerable<ITemplateSynchronizer>>()
.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (simpleSync == null)
{
_logger.LogError("[FAIL] SimpleTemplateSynchronizer не найден в DI");
return;
}
// Подставьте реальный JobId для Simple
var simpleJobId = Guid.Parse("6ff1de05-80c3-4b38-846b-0c793fd7fc8c");
try
{
var sw = Stopwatch.StartNew();
await simpleSync.SyncTemplatesForJobAsync(simpleJobId, new HistoryInitiator
{
InitiatorIp = "127.0.0.1",
InitiatorParrComponentId = ParrComponentsEnum.Master,
InitiatorComment = "Pre-commit validation: Simple Pipeline"
});
sw.Stop();
_logger.LogInformation("[OK] Simple Pipeline: завершено за {Ms} мс", sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "[FAIL] Simple Pipeline: исключение");
}
// === 2. Проверка Grouped Pipeline ===
_logger.LogInformation("--- Grouped Pipeline ---");
var groupedSync = scope.ServiceProvider
.GetRequiredService<IEnumerable<ITemplateSynchronizer>>()
.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (groupedSync == null)
{
_logger.LogError("[FAIL] GroupedTemplateSynchronizer не найден в DI");
return;
}
// Подставьте реальный JobGroupId для Grouped
var groupedJobGroupId = Guid.Parse("51acaa95-08bf-425f-9a09-87b6a4cbc77e");
try
{
var sw = Stopwatch.StartNew();
await groupedSync.SyncTemplatesForJobGroupAsync(groupedJobGroupId, new HistoryInitiator
{
InitiatorIp = "127.0.0.1",
InitiatorParrComponentId = ParrComponentsEnum.Master,
InitiatorComment = "Pre-commit validation: Grouped Pipeline"
});
sw.Stop();
_logger.LogInformation("[OK] Grouped Pipeline: завершено за {Ms} мс", sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "[FAIL] Grouped Pipeline: исключение");
}
// === 3. Проверка контрактных констант ===
_logger.LogInformation("--- Контрактные константы ---");
var constantsType = typeof(PARR.TemplateMatcher.Constants.UnusedTemplateConstants);
var fields = constantsType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
foreach (var field in fields)
{
var value = field.GetValue(null);
_logger.LogInformation("[OK] Константа {Name} = '{Value}'", field.Name, value);
}
}
#endregion
#region TemplateMatcher #region TemplateMatcher
private async Task TemplateMatcherTest() private async Task TemplateMatcherTest()

View File

@@ -1,23 +1,30 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs" "RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs"
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"PARR.DAL": "Information",
"PARR.Infrastructure.Redis": "Information",
"PARR.TemplateMatcher.Services.GroupedSync.GroupedTemplateProcessor": "Information"
}
}, },
"Serilog": { "WriteTo": [
"MinimumLevel": { {
"Default": "Debug", "Name": "File",
"Override": { "Args": {
"Microsoft": "Warning", "path": "log/log-.txt",
"Microsoft.Hosting.Lifetime": "Debug" "rollingInterval": "Day"
} }
}, }
"WriteTo": [ ]
{ },
"Name": "File", "MqSettings": {
"Args": { "TemplateMatcher": { "HostName": "10.99.253.216" },
"path": "log/log-.txt", "TemplateGenerator": { "HostName": "10.99.253.216" },
"rollingInterval": "Day" "TemplateUpdater": { "HostName": "10.99.253.216" }
} }
} }
]
}
}

View File

@@ -1,41 +1,61 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;", "DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;",
"RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs" "RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log/log-.txt",
"rollingInterval": "Day"
}
}
]
},
"EsppOrderSettings": {
//dev
//"Url": "http://rzd-espp-t-rpa-app-1.gvc.oao.rzd:8080/espp_api/OperationExecutor/",
"Url": "http://espp.gvc.rzd/esppapi_prom/OperationExecutor/",
"UserName": "Auto-PTK-INFO-0002-DVS",
"AccountName": "АВТО ТЕХНОЛОГ ПТК-ИНФО-0002-ДВС (AUTO-PTK-INFO-0002-DVS)",
"Password": "123456789",
"EsppUserTimeZone": 3,
"RobotEk": "РПА-РОБИН-ГВЦ-ЕСПП-ТС-513-ДВС",
"CodeRRO": "ЦТС-РЦТ-9999"
} }
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log/log-.txt",
"rollingInterval": "Day"
}
}
]
},
"EsppOrderSettings": {
//dev
//"Url": "http://rzd-espp-t-rpa-app-1.gvc.oao.rzd:8080/espp_api/OperationExecutor/",
"Url": "http://espp.gvc.rzd/esppapi_prom/OperationExecutor/",
"UserName": "Auto-PTK-INFO-0002-DVS",
"AccountName": "АВТО ТЕХНОЛОГ ПТК-ИНФО-0002-ДВС (AUTO-PTK-INFO-0002-DVS)",
"Password": "123456789",
"EsppUserTimeZone": 3,
"RobotEk": "РПА-РОБИН-ГВЦ-ЕСПП-ТС-513-ДВС",
"CodeRRO": "ЦТС-РЦТ-9999"
},
"MqSettings": {
"TemplateMatcher": {
"HostName": "parr-rabbitmq",
"QueueName": "parr-template-matcher",
"User": "template_matcher_reader",
"Password": "wzqj$Z@3:poasad;lk324@oot"
},
"TemplateGenerator": {
"HostName": "parr-rabbitmq",
"QueueName": "parr-template-generator",
"User": "template_generator_writer",
"Password": "B;6h+yF$zQ0OSkLX"
},
"TemplateUpdater": {
"HostName": "parr-rabbitmq",
"QueueName": "parr-template-updater",
"User": "template_updater_writer",
"Password": "sjdhgfkJHGIUFDi14asd^12"
}
}
} }