Compare commits
10 Commits
3b6df1f617
...
fe4461ee4e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4461ee4e | ||
|
|
108eb05853 | ||
|
|
1fb5cbd2e8 | ||
|
|
c0720b38de | ||
|
|
db270dbb1c | ||
|
|
dd206b44e9 | ||
|
|
21e00ce42e | ||
|
|
1b21ee4a79 | ||
|
|
a438f18480 | ||
|
|
19cb6b50bf |
@@ -50,6 +50,8 @@
|
||||
public const string GetNextRun = Base + "/tests/next-run";
|
||||
|
||||
public const string CreateCache = Base + "/tests/cache/";
|
||||
|
||||
public const string TestHandler = Base + "/tests/test/";
|
||||
}
|
||||
|
||||
public static class Template
|
||||
|
||||
@@ -23,5 +23,10 @@
|
||||
/// Ошибок
|
||||
/// </summary>
|
||||
public int Errors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Среднее кол-во роботов работающих в течении часа
|
||||
/// </summary>
|
||||
public double AvgRobots { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ using PARR.Domain.Common.Pagination;
|
||||
using PARR.Domain.Common.Rabbit.Messages;
|
||||
using PARR.Domain.Common.Roles;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ using PARR.API.Contracts.V1.Responses.Base;
|
||||
using PARR.API.Contracts.V1.Responses.Statistics;
|
||||
using PARR.API.Controllers.V1.Base;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Services.RobotSnapshotServices;
|
||||
using PARR.Domain.Common.Roles;
|
||||
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.API.Controllers.V1.Statistics
|
||||
@@ -24,13 +26,15 @@ namespace PARR.API.Controllers.V1.Statistics
|
||||
private readonly ITaskStatusRepository taskStatusService;
|
||||
private readonly IRobotHistoryRepository robotHistoryService;
|
||||
private readonly IMapper mapper;
|
||||
private readonly IRobotSnapshotService _robotSnapshotService;
|
||||
|
||||
public StatRobotTaskController(
|
||||
IRobotRepository robotService,
|
||||
IRobotConfigurationRepository robotConfigurationService,
|
||||
ITaskStatusRepository taskStatusService,
|
||||
IRobotHistoryRepository robotHistoryService,
|
||||
IMapper mapper
|
||||
IMapper mapper,
|
||||
IRobotSnapshotService robotSnapshotService
|
||||
)
|
||||
{
|
||||
this.robotService = robotService;
|
||||
@@ -38,6 +42,7 @@ namespace PARR.API.Controllers.V1.Statistics
|
||||
this.taskStatusService = taskStatusService;
|
||||
this.robotHistoryService = robotHistoryService;
|
||||
this.mapper = mapper;
|
||||
_robotSnapshotService = robotSnapshotService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,11 +92,11 @@ namespace PARR.API.Controllers.V1.Statistics
|
||||
[HttpGet(ApiRoutes.StatRobotTask.GetPeriodStatistics)]
|
||||
public async Task<IActionResult> GetPeriodStatistics([FromRoute] RobotsEnum robot)
|
||||
{
|
||||
var offset = TimeSpan.FromHours(0);
|
||||
|
||||
int minusHour = 24;
|
||||
var queryDate = DateTimeOffset.UtcNow.AddHours(-minusHour);
|
||||
queryDate = new DateTimeOffset(queryDate.Year, queryDate.Month, queryDate.Day, queryDate.Hour, 0, 0, new TimeSpan(0));
|
||||
//queryDate = queryDate.Date + new TimeSpan(queryDate.Hour, 0, 0);
|
||||
//queryDate=queryDate.ToOffset(TimeSpan.Zero);
|
||||
queryDate = new DateTimeOffset(queryDate.Year, queryDate.Month, queryDate.Day, queryDate.Hour, 0, 0, offset);
|
||||
|
||||
|
||||
var query = robotHistoryService.Get()
|
||||
@@ -110,16 +115,24 @@ namespace PARR.API.Controllers.V1.Statistics
|
||||
|
||||
var statistics = await query.ToListAsync();
|
||||
|
||||
//if (!statistics.Any())
|
||||
// return NoContent();
|
||||
|
||||
var periodList = GetPeriodList(queryDate, DateTimeOffset.UtcNow);
|
||||
|
||||
if (!periodList.Any())
|
||||
return NoContent();
|
||||
|
||||
// Статистика по роботам (кол-во роботов)
|
||||
var robotStats = await _robotSnapshotService.GetHourlyAnalyticsByRobotTypeAsync(new RobotAnalyticsRobotTypeQuery
|
||||
{
|
||||
DateStart = queryDate,
|
||||
DateEnd = DateTimeOffset.UtcNow,
|
||||
Offset = offset,
|
||||
RobotType = robot
|
||||
});
|
||||
|
||||
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);
|
||||
if (statItem != null)
|
||||
{
|
||||
@@ -127,6 +140,7 @@ namespace PARR.API.Controllers.V1.Statistics
|
||||
item.Creating = statItem.Creating;
|
||||
item.Updating = statItem.Updating;
|
||||
item.Ok = statItem.Ok;
|
||||
item.AvgRobots = robotCount?.AvgRobots ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -150,6 +164,7 @@ namespace PARR.API.Controllers.V1.Statistics
|
||||
Errors = 0,
|
||||
Ok = 0,
|
||||
Updating = 0,
|
||||
AvgRobots = 0,
|
||||
Date = new DateTimeOffset(periodDate.Date.Year, periodDate.Date.Month, periodDate.Date.Day, periodDate.Hour, 0, 0, new TimeSpan(0)),
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ using PARR.Core.Services.NextRunServices;
|
||||
using PARR.Core.Services.Shortcodes;
|
||||
using PARR.Domain.Common.Roles;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.API.Controllers.V1.Statistics
|
||||
|
||||
@@ -10,9 +10,11 @@ using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.NextRunServices;
|
||||
using PARR.Core.Services.RobotSnapshotServices;
|
||||
using PARR.Core.Services.UnitService.Interfaces;
|
||||
using PARR.Core.Services.Workload.Implementations;
|
||||
using PARR.Domain.Cache;
|
||||
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
||||
|
||||
namespace PARR.API.Controllers.V1
|
||||
{
|
||||
@@ -27,6 +29,7 @@ namespace PARR.API.Controllers.V1
|
||||
private readonly ILogger<TestController> logger;
|
||||
private readonly IUnitService unitService;
|
||||
private readonly IUnitRepository unitRepository;
|
||||
private readonly IRobotSnapshotService _robotSnapshotService;
|
||||
|
||||
public TestController(
|
||||
IClientService clientService,
|
||||
@@ -37,7 +40,8 @@ namespace PARR.API.Controllers.V1
|
||||
WorkloadCacheService workloadCacheService,
|
||||
ILogger<TestController> logger,
|
||||
IUnitService unitService,
|
||||
IUnitRepository unitRepository
|
||||
IUnitRepository unitRepository,
|
||||
IRobotSnapshotService robotSnapshotService
|
||||
)
|
||||
{
|
||||
this.clientService = clientService;
|
||||
@@ -48,6 +52,7 @@ namespace PARR.API.Controllers.V1
|
||||
this.logger = logger;
|
||||
this.unitService = unitService;
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using PARR.Domain.DTOs.Workload;
|
||||
using PARR.Domain.Entities;
|
||||
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.Unit;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using AutoMapper;
|
||||
using PARR.API.Contracts.V1.Responses;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Settings;
|
||||
|
||||
namespace PARR.API.MappingProfiles.Resolvers
|
||||
|
||||
@@ -3,7 +3,7 @@ using PARR.API.Contracts.V1.Requests;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.API.Validators
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
|
||||
namespace PARR.Core.Common.Helpers;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using PARR.Core.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.Core.Repositories.Interfaces.Job
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using PARR.Core.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.Core.Repositories.Interfaces.Job
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.Core.Repositories.Interfaces.JobGroupRepositories
|
||||
{
|
||||
public interface IJobGroupAutoControlRepository
|
||||
{
|
||||
IQueryable<JobGroupAutoControl> Get();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PARR.Core.Repositories.Interfaces.JobGroupRepositories
|
||||
{
|
||||
public interface IJobGroupRelationshipFilterRepository
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using PARR.Core.Services.NextRunServices.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.Core.Services.NextRunServices
|
||||
|
||||
@@ -7,7 +7,7 @@ using PARR.Core.Services.NextRunServices.Models;
|
||||
using PARR.Core.Services.NextRunServices.Subservices;
|
||||
using PARR.Core.Services.Shortcodes;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Settings;
|
||||
|
||||
|
||||
@@ -4,8 +4,34 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
||||
{
|
||||
public interface IRobotSnapshotService
|
||||
{
|
||||
/// <summary>
|
||||
/// Получить статистику
|
||||
/// </summary>
|
||||
/// <param name="queryDto"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<RobotSnapshotItemDto>> GetAsync(RobotSnapshotQuery queryDto);
|
||||
|
||||
/// <summary>
|
||||
/// Записать статистику
|
||||
/// </summary>
|
||||
/// <param name="robotSnapshot"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
||||
{
|
||||
internal class RobotSnapshotService : IRobotSnapshotService
|
||||
{
|
||||
private readonly IRobotSnapshotRepository robotSnapshotRepository;
|
||||
private readonly IUserRepository userRepository;
|
||||
private readonly IMapper mapper;
|
||||
private readonly IRobotSnapshotRepository _robotSnapshotRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public RobotSnapshotService(
|
||||
IRobotSnapshotRepository robotSnapshotRepository,
|
||||
@@ -21,9 +21,9 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
||||
IMapper mapper
|
||||
)
|
||||
{
|
||||
this.robotSnapshotRepository = robotSnapshotRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.mapper = mapper;
|
||||
_robotSnapshotRepository = robotSnapshotRepository;
|
||||
_userRepository = userRepository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
||||
var dateEnd = DateTimeOffset.UtcNow;
|
||||
var dateStart = dateEnd.AddMinutes(-minutes);
|
||||
|
||||
var query = robotSnapshotRepository.Get().AsNoTracking()
|
||||
var query = _robotSnapshotRepository.Get().AsNoTracking()
|
||||
.Where(t => dateStart <= t.DateCreated && t.DateCreated <= dateEnd);
|
||||
|
||||
if (!string.IsNullOrEmpty(queryDto.Ip))
|
||||
@@ -47,7 +47,7 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
||||
|
||||
|
||||
var robotIps = groupingSnapshots.Select(t => t.Key).ToHashSet();
|
||||
var robotList = await userRepository.Get()
|
||||
var robotList = await _userRepository.Get()
|
||||
.AsNoTracking()
|
||||
.Where(t => robotIps.Contains(t.Ip))
|
||||
.ToDictionaryAsync(t => t.Ip);
|
||||
@@ -59,12 +59,262 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
||||
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 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 });
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
var createdResult = await robotSnapshotRepository.CreateAsync(snapshot);
|
||||
var commitResult = await robotSnapshotRepository.CommitAsync();
|
||||
var createdResult = await _robotSnapshotRepository.CreateAsync(snapshot);
|
||||
var commitResult = await _robotSnapshotRepository.CommitAsync();
|
||||
|
||||
if (!createdResult || !commitResult)
|
||||
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 result = new RobotSnapshotItemDto
|
||||
{
|
||||
Robot = userDto,
|
||||
Snapshots = mapper.Map<List<RobotSnapshotDto>>(new List<RobotSnapshot> { snapshot })
|
||||
Snapshots = _mapper.Map<List<RobotSnapshotDto>>(new List<RobotSnapshot> { snapshot })
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
@@ -50,7 +50,6 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler
|
||||
var unitsList = template.UnitsInTemplate;
|
||||
if (unitsList.Count == 0)
|
||||
{
|
||||
// Данные должны быть загружены оркестратором. Если пусто — значит в БД действительно нет связей.
|
||||
return input.Replace("%ГР_ПОЛЕ-ПН%", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -67,12 +66,16 @@ internal class GroupedFieldShortcodeHandler : IShortcodeHandler
|
||||
.Where(fv => fieldValueIds.Contains(fv.Id))
|
||||
.ToDictionaryAsync(fv => fv.Id, fv => fv.Value ?? string.Empty, ct);
|
||||
|
||||
var lines = unitsList.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})";
|
||||
});
|
||||
// Сортировка по имени юнита для детерминированного результата
|
||||
var lines = unitsList
|
||||
.OrderBy(u => unitNames.GetValueOrDefault(u.UnitId, string.Empty), StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(u => fieldValueStrings.GetValueOrDefault(u.UnitFieldValueId, string.Empty), StringComparer.OrdinalIgnoreCase)
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace PARR.Core.Services.UnitFilterService.Models
|
||||
{
|
||||
internal class UnitFilterMatchResult
|
||||
public class UnitFilterMatchResult
|
||||
{
|
||||
public Guid UnitId { get; set; }
|
||||
public HashSet<Guid> ValidParentIds { get; set; } = new();
|
||||
|
||||
@@ -12,6 +12,7 @@ using PARR.Core.Services.UnitService.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Cache.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using PARR.Domain.Common.Roles;
|
||||
using PARR.Domain.Common.Template;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Entities.RobotEntities;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
using PARR.Domain.Entities.TaskEntities;
|
||||
@@ -106,16 +107,28 @@ namespace PARR.DAL.Context
|
||||
|
||||
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<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<JobGroupAutoControl> JobGroupAutoControls { get; set; }
|
||||
|
||||
public DbSet<JobGroupUnitFilter> JobGroupUnitFilters { get; set; }
|
||||
|
||||
public DbSet<JobGroupFieldFilter> JobGroupFieldFilters { get; set; }
|
||||
|
||||
public DbSet<JobGroupRelationshipFilter> JobGroupRelationshipFilters { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tasks
|
||||
@@ -166,9 +179,9 @@ namespace PARR.DAL.Context
|
||||
modelBuilder.Entity<JobGroupType>(f =>
|
||||
{
|
||||
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("6EC58B1A-5C40-47B9-B036-4FA490E8D503"), DateCreated = dateCreated, DateModified = null, Name = JobGroupTypesEnum.Umbrella.ToString(), Code = JobGroupTypesEnum.Umbrella, Description = "Зонтик" },
|
||||
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("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 = "Зонтик", 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 = "Сгруппированный", IsAllowJobUnitFilter = false, IsJobGroupAutoControl = true }
|
||||
);
|
||||
});
|
||||
#endregion
|
||||
@@ -205,7 +218,7 @@ namespace PARR.DAL.Context
|
||||
{
|
||||
// При добавлении записей, добавлять тоже в PARR.DAL.Contracts.SettingsFromDb
|
||||
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.Category), Description = "Категория создаваемого объекта в ЕСПП", Value = "регламентная работа" },
|
||||
new { Name = nameof(SettingsFromDb.TemplatePrefixName), Description = "Префикс имени шаблона в ЕСПП", Value = "%PREFIX%-ЭИТИ-ПТК-ПАРР" },
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
|
||||
using PARR.Core.Repositories.Interfaces.RobotRepositories;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.Core.Repositories.Interfaces.TaskRepositories;
|
||||
@@ -12,6 +13,7 @@ using PARR.DAL.Configurations.DbSettings;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories;
|
||||
using PARR.DAL.Repositories.Job;
|
||||
using PARR.DAL.Repositories.JobGroupRepositories;
|
||||
using PARR.DAL.Repositories.RobotRepositories;
|
||||
using PARR.DAL.Repositories.Schedule;
|
||||
using PARR.DAL.Repositories.TaskRepositories;
|
||||
@@ -115,6 +117,15 @@ namespace PARR.DAL
|
||||
|
||||
#endregion
|
||||
|
||||
#region JobGroup
|
||||
|
||||
services.AddScoped<IJobGroupFieldFilterRepository, JobGroupFieldFilterRepository>();
|
||||
services.AddScoped<IJobGroupRelationshipFilterRepository, JobGroupRelationshipFilterRepository>();
|
||||
services.AddScoped<IJobGroupUnitFilterRepository, JobGroupUnitFilterRepository>();
|
||||
services.AddScoped<IJobGroupAutoControlRepository, JobGroupAutoControlRepository>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task
|
||||
|
||||
services.AddScoped<ITaskErrorRepository, TaskErrorRepository>();
|
||||
|
||||
4014
PARR.DAL/Migrations/20260615065234_tblsJobGroups.Designer.cs
generated
Normal file
4014
PARR.DAL/Migrations/20260615065234_tblsJobGroups.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
284
PARR.DAL/Migrations/20260615065234_tblsJobGroups.cs
Normal file
284
PARR.DAL/Migrations/20260615065234_tblsJobGroups.cs
Normal 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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -372,13 +455,35 @@ namespace PARR.DAL.Migrations
|
||||
|
||||
b.HasIndex("ScheduleExcludeTypeId");
|
||||
|
||||
b.ToTable("Groups", "job", t =>
|
||||
b.ToTable("Groups", "jobGroup", t =>
|
||||
{
|
||||
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")
|
||||
.HasColumnType("uuid");
|
||||
@@ -396,67 +501,49 @@ namespace PARR.DAL.Migrations
|
||||
|
||||
b.HasIndex("DistributionPeriodId");
|
||||
|
||||
b.ToTable("GroupDistributionConfigs", "job", t =>
|
||||
b.ToTable("GroupDistributionConfigs", "jobGroup", t =>
|
||||
{
|
||||
t.HasComment("Настройки автораспределения для группы работ");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroupType", b =>
|
||||
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupFieldFilter", 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<Guid>("FieldId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
b.Property<bool>("IsInverse")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("UnitFilterId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ValueMask")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("GroupTypes", "job", t =>
|
||||
{
|
||||
t.HasComment("Таблица типов групп работ");
|
||||
});
|
||||
b.HasIndex("FieldId")
|
||||
.HasDatabaseName("IX_FieldFilters_FieldId1");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
b.HasIndex("UnitFilterId")
|
||||
.HasDatabaseName("IX_FieldFilters_UnitFilterId1");
|
||||
|
||||
b.ToTable("FieldFilters", "jobGroup", t =>
|
||||
{
|
||||
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 = "Обычный",
|
||||
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"
|
||||
t.HasComment("Таблица описания критериев выборки аттрибутов ЭК");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PARR.Domain.Entities.Job.JobRelationshipFilter", b =>
|
||||
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupRelationshipFilter", b =>
|
||||
{
|
||||
b.Property<Guid>("UnitFilterId")
|
||||
.HasColumnType("uuid");
|
||||
@@ -479,15 +566,84 @@ namespace PARR.DAL.Migrations
|
||||
|
||||
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("Таблица фильтров связей ЭК");
|
||||
});
|
||||
});
|
||||
|
||||
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")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -496,7 +652,7 @@ namespace PARR.DAL.Migrations
|
||||
b.Property<DateTimeOffset>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("JobId")
|
||||
b.Property<Guid>("JobGroupId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("UnitFilter")
|
||||
@@ -505,40 +661,14 @@ namespace PARR.DAL.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
b.HasIndex("JobGroupId");
|
||||
|
||||
b.ToTable("UnitFilters", "job", t =>
|
||||
b.ToTable("UnitFilters", "jobGroup", 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.Order", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2268,7 +2398,7 @@ namespace PARR.DAL.Migrations
|
||||
{
|
||||
Name = "Initiator",
|
||||
Description = "Инициатор регламентной работы, указывается при создании шаблона в ЕСПП.",
|
||||
Value = "ОВЧАРЕНКО АЛЕКСЕЙ ВИТАЛЬЕВИЧ (OVCHARENKOAV@GVC.OAO.RZD)"
|
||||
Value = "АКСЕНОВ АЛЕКСАНДР ЕВГЕНЬЕВИЧ (AKSENOVAE@GVC.OAO.RZD)"
|
||||
},
|
||||
new
|
||||
{
|
||||
@@ -3097,7 +3227,7 @@ namespace PARR.DAL.Migrations
|
||||
|
||||
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")
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -3128,7 +3258,7 @@ namespace PARR.DAL.Migrations
|
||||
modelBuilder.Entity("PARR.Domain.Entities.Job.JobFieldFilter", b =>
|
||||
{
|
||||
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField")
|
||||
.WithMany()
|
||||
.WithMany("JobFieldFilters")
|
||||
.HasForeignKey("FieldId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
@@ -3144,57 +3274,6 @@ namespace PARR.DAL.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("PARR.Domain.Entities.Unit.UnitField", "UnitField")
|
||||
@@ -3252,6 +3331,117 @@ namespace PARR.DAL.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("PARR.Domain.Entities.OrderStatus", "NextStatus")
|
||||
@@ -3369,7 +3559,7 @@ namespace PARR.DAL.Migrations
|
||||
|
||||
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")
|
||||
.HasForeignKey("JobGroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -3614,8 +3804,17 @@ namespace PARR.DAL.Migrations
|
||||
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("EsppSchValues");
|
||||
@@ -3623,12 +3822,12 @@ namespace PARR.DAL.Migrations
|
||||
b.Navigation("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PARR.Domain.Entities.Job.JobGroupType", b =>
|
||||
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupType", b =>
|
||||
{
|
||||
b.Navigation("JobGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PARR.Domain.Entities.Job.JobUnitFilter", b =>
|
||||
modelBuilder.Entity("PARR.Domain.Entities.JobGroupEntities.JobGroupUnitFilter", b =>
|
||||
{
|
||||
b.Navigation("FieldFilters");
|
||||
|
||||
@@ -3776,6 +3975,12 @@ namespace PARR.DAL.Migrations
|
||||
|
||||
modelBuilder.Entity("PARR.Domain.Entities.Unit.UnitField", b =>
|
||||
{
|
||||
b.Navigation("JobFieldFilters");
|
||||
|
||||
b.Navigation("JobGroupFieldFilters");
|
||||
|
||||
b.Navigation("JobGroupRelationshipFilters");
|
||||
|
||||
b.Navigation("JobGroupWithGrouping");
|
||||
|
||||
b.Navigation("RelationshipFilters");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
|
||||
|
||||
namespace PARR.DAL.Repositories.JobGroupRepositories
|
||||
{
|
||||
internal class JobGroupRelationshipFilterRepository : IJobGroupRelationshipFilterRepository
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,10 @@
|
||||
/// </summary>
|
||||
public const string Job = "job";
|
||||
|
||||
/// <summary>
|
||||
/// Группы работ
|
||||
/// </summary>
|
||||
public const string JobGroup = "jobGroup";
|
||||
|
||||
/// <summary>
|
||||
/// Расписание регламентных работ
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
30
PARR.Domain/DTOs/RobotSnapshotDTO/RobotAnalyticsQuery.cs
Normal file
30
PARR.Domain/DTOs/RobotSnapshotDTO/RobotAnalyticsQuery.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities.Base;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace PARR.Domain.Entities.Job
|
||||
public class JobRelationshipFilter
|
||||
{
|
||||
public Guid UnitFilterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Родительская связь - true,
|
||||
/// Дочерняя связь - false
|
||||
|
||||
@@ -7,9 +7,9 @@ using PARR.Domain.Settings;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
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("Таблица описания групп работ, для реализации зонтиков")]
|
||||
public class JobGroup : IBaseEntity
|
||||
{
|
||||
@@ -180,7 +180,7 @@ namespace PARR.Domain.Entities.Job
|
||||
[ForeignKey(nameof(GroupTypeId))]
|
||||
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>();
|
||||
|
||||
@@ -191,5 +191,7 @@ namespace PARR.Domain.Entities.Job
|
||||
public ScheduleExcludeTypeCalendar? ScheduleExcludeTypeCalendar { get; set; }
|
||||
|
||||
public JobGroupDistributionConfig? DistributionConfig { get; set; }
|
||||
|
||||
public JobGroupAutoControl? AutoControl { get; set; }
|
||||
}
|
||||
}
|
||||
33
PARR.Domain/Entities/JobGroupEntities/JobGroupAutoControl.cs
Normal file
33
PARR.Domain/Entities/JobGroupEntities/JobGroupAutoControl.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ using PARR.Domain.Constants;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace PARR.Domain.Entities.Job
|
||||
namespace PARR.Domain.Entities.JobGroupEntities
|
||||
{
|
||||
/// <summary>
|
||||
/// Настройки автораспределения для JobGroup
|
||||
/// </summary>
|
||||
[Table("GroupDistributionConfigs", Schema = DatabaseSchemas.Job)]
|
||||
[Table("GroupDistributionConfigs", Schema = DatabaseSchemas.JobGroup)]
|
||||
[Comment("Настройки автораспределения для группы работ")]
|
||||
public class JobGroupDistributionConfig
|
||||
{
|
||||
39
PARR.Domain/Entities/JobGroupEntities/JobGroupFieldFilter.cs
Normal file
39
PARR.Domain/Entities/JobGroupEntities/JobGroupFieldFilter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
44
PARR.Domain/Entities/JobGroupEntities/JobGroupType.cs
Normal file
44
PARR.Domain/Entities/JobGroupEntities/JobGroupType.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
32
PARR.Domain/Entities/JobGroupEntities/JobGroupUnitFilter.cs
Normal file
32
PARR.Domain/Entities/JobGroupEntities/JobGroupUnitFilter.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace PARR.Domain.Entities.Schedule
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
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<JobGroupRelationshipFilter> JobGroupRelationshipFilters { get; set; } = new HashSet<JobGroupRelationshipFilter>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// JobGroup которые группируются по этому полю (!!!отключено каскадное удаление!!!)
|
||||
/// </summary>
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace PARR.EsppSync.Helpers
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PARR.EsppSync.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Хелперы для EsppSync
|
||||
@@ -15,11 +17,9 @@
|
||||
if (str == null)
|
||||
return string.Empty;
|
||||
|
||||
str = str.Replace("\r", string.Empty);
|
||||
str = str.Replace("\n", string.Empty);
|
||||
str = str.Replace(" ", string.Empty);
|
||||
str = Regex.Replace(str, @"\s+", string.Empty);
|
||||
|
||||
return str.ToLower();
|
||||
return str.Trim().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.TemplateActivator;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.TemplateDistributor.Services
|
||||
{
|
||||
|
||||
25
PARR.TemplateMatcher/Constants/UnusedTemplateConstants.cs
Normal file
25
PARR.TemplateMatcher/Constants/UnusedTemplateConstants.cs
Normal 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 = "ПАРР-НЕИСП";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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})";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ using PARR.Core.Services.Shortcodes;
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
|
||||
{
|
||||
@@ -12,7 +12,7 @@ using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
{
|
||||
@@ -136,7 +136,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate);
|
||||
if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id);
|
||||
logger.LogInformation("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id);
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = existingTemplate.Id,
|
||||
@@ -160,7 +160,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id);
|
||||
logger.LogInformation("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id);
|
||||
await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.UnitFilterService.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
internal class GroupedTemplateUnitFilter : IGroupedTemplateUnitFilter
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync
|
||||
{
|
||||
/// <summary>
|
||||
/// Этап групповой синхронизации с побочными эффектами (запись в БД, MQ).
|
||||
/// В тестах не подключается — тип системы гарантирует безопасность.
|
||||
/// </summary>
|
||||
public interface IGroupedSyncWriteStage : IGroupedSyncStage { }
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.JobGroupEntities;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces;
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
public interface IGroupedTemplateBuilder
|
||||
{
|
||||
@@ -15,4 +16,4 @@ public interface IGroupedTemplateBuilder
|
||||
JobGroup jobGroup,
|
||||
Job maxJob,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces;
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
public interface IGroupedTemplateProcessor
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using PARR.Core.Services.UnitFilterService.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync
|
||||
{
|
||||
/// <summary>
|
||||
/// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.UnitFilterService.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
|
||||
{
|
||||
@@ -1,260 +1,110 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Services.MatchingStatusService;
|
||||
using PARR.Core.Services.UnitFilterService;
|
||||
using PARR.Domain.Cache.Models;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
{
|
||||
private readonly ILogger<GroupedTemplateSynchronizer> logger;
|
||||
private readonly IJobGroupRepository jobGroupService;
|
||||
private readonly IUnitFilterService unitFilterService;
|
||||
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter;
|
||||
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
|
||||
private readonly IGroupedTemplateBuilder groupedTemplateBuilder;
|
||||
private readonly IGroupedTemplateProcessor groupedTemplateProcessor;
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly ITemplateDeactivator templateDeactivator;
|
||||
private readonly IMatchingStatusService matchingStatusService;
|
||||
private readonly IEnumerable<IGroupedSyncStage> _readStages;
|
||||
private readonly IEnumerable<IGroupedSyncWriteStage> _writeStages;
|
||||
private readonly IMatchingStatusService _matchingStatusService;
|
||||
private readonly ILogger<GroupedTemplateSynchronizer> _logger;
|
||||
|
||||
public GroupedTemplateSynchronizer(
|
||||
ILogger<GroupedTemplateSynchronizer> logger,
|
||||
IJobGroupRepository jobGroupService,
|
||||
IUnitFilterService unitFilterService,
|
||||
IGroupedTemplateUnitFilter groupedTemplateUnitFilter,
|
||||
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
|
||||
IGroupedTemplateBuilder groupedTemplateBuilder,
|
||||
IGroupedTemplateProcessor groupedTemplateProcessor,
|
||||
ITemplateRepository templateService,
|
||||
ITemplateDeactivator templateDeactivator,
|
||||
IMatchingStatusService matchingStatusService)
|
||||
IEnumerable<IGroupedSyncStage> readStages,
|
||||
IEnumerable<IGroupedSyncWriteStage> writeStages,
|
||||
IMatchingStatusService matchingStatusService,
|
||||
ILogger<GroupedTemplateSynchronizer> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.jobGroupService = jobGroupService;
|
||||
this.unitFilterService = unitFilterService;
|
||||
this.groupedTemplateUnitFilter = groupedTemplateUnitFilter;
|
||||
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);
|
||||
_readStages = readStages;
|
||||
_writeStages = writeStages;
|
||||
_matchingStatusService = matchingStatusService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
|
||||
_logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
|
||||
return;
|
||||
}
|
||||
|
||||
// === Устанавливаем статус "в процессе" ===
|
||||
var initialStatus = new MatchingStatusItemDto
|
||||
{
|
||||
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)
|
||||
);
|
||||
await SetStatusAsync(jobGroupId, "Начало синхронизации");
|
||||
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// === ЭТАП 1: Загрузка JobGroup ===
|
||||
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);
|
||||
var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator };
|
||||
|
||||
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
|
||||
foreach (var stage in _readStages)
|
||||
{
|
||||
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
|
||||
return;
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var jobsInGroup = jobGroup.Jobs.ToList();
|
||||
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)
|
||||
foreach (var stage in _writeStages)
|
||||
{
|
||||
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
|
||||
return;
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
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: Фильтрация юнитов ===
|
||||
stageSw.Restart();
|
||||
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
|
||||
stageSw.Stop();
|
||||
var filterCount = unitFilterResults?.Count() ?? 0;
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds, filterCount);
|
||||
|
||||
if (unitFilterResults == null || !unitFilterResults.Any())
|
||||
{
|
||||
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
|
||||
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);
|
||||
await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
|
||||
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
_logger.LogInformation("Синхронизация шаблонов завершена для JobGroup '{JobGroupName}' ({JobGroupId})",
|
||||
context.JobGroupName, jobGroupId);
|
||||
}
|
||||
catch (GroupedSyncEarlyExitException ex)
|
||||
{
|
||||
totalSw.Stop();
|
||||
_logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)",
|
||||
jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds);
|
||||
await SetStatusAsync(jobGroupId, ex.Reason);
|
||||
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
totalSw.Stop();
|
||||
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
|
||||
_logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс",
|
||||
jobGroupId, totalSw.ElapsedMilliseconds);
|
||||
await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
|
||||
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(
|
||||
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
|
||||
Guid jobGroupId,
|
||||
List<Job> jobsInGroup,
|
||||
HistoryInitiator initiator)
|
||||
public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
|
||||
var allExistingTemplatesInGroup = await templateService.Get()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
_logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
|
||||
private async Task SetStatusAsync(Guid jobGroupId, string comment)
|
||||
{
|
||||
var status = new MatchingStatusItemDto
|
||||
{
|
||||
@@ -262,12 +112,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
Action = TemplateMatcherActionEnum.Sync,
|
||||
Comment = comment
|
||||
};
|
||||
|
||||
await matchingStatusService.SetMatchingStatusAsync(
|
||||
jobGroupId,
|
||||
SyncTaskEntityTypeEnum.JobGroup,
|
||||
await _matchingStatusService.SetMatchingStatusAsync(
|
||||
jobGroupId, SyncTaskEntityTypeEnum.JobGroup,
|
||||
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
|
||||
TimeSpan.FromMinutes(30)
|
||||
);
|
||||
TimeSpan.FromMinutes(30));
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,28 @@
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implemetaions
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class JobGroupValidatorService : IJobGroupValidatorService
|
||||
{
|
||||
private readonly ILogger<IJobValidatorService> logger;
|
||||
private readonly IJobGroupRepository jobGroupService;
|
||||
private readonly ILogger<IJobValidatorService> _logger;
|
||||
private readonly IJobGroupRepository _jobGroupService;
|
||||
|
||||
public JobGroupValidatorService(
|
||||
ILogger<IJobValidatorService> logger,
|
||||
IJobGroupRepository jobGroupService
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.jobGroupService = jobGroupService;
|
||||
_logger = logger;
|
||||
_jobGroupService = jobGroupService;
|
||||
}
|
||||
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
|
||||
{
|
||||
var isExist = await jobGroupService.GetAsync(jobGroupId);
|
||||
var isExist = await _jobGroupService.GetAsync(jobGroupId);
|
||||
|
||||
if (isExist == null)
|
||||
{
|
||||
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
|
||||
_logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implemetaions
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class JobValidatorService : IJobValidatorService
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
namespace PARR.TemplateMatcher
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class MqTemplateMatcher : IMqTemplateMatcher
|
||||
{
|
||||
@@ -15,8 +15,9 @@ using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Settings;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Constants;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -31,18 +32,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
private const bool DefaultUsedTemplateState = false;
|
||||
private const bool DefaultUsedScheduleState = false;
|
||||
|
||||
// === Константы для логики неиспользуемых шаблонов ===
|
||||
private const string FieldNameResponsibilityArea = "ЗОНА_ОТВЕТСТВЕННОСТИ";
|
||||
private const string FieldNameParrTag = "ПАРР тег";
|
||||
private const string TagValueNotWorking = "ПАРР-НЕИСП";
|
||||
|
||||
private readonly IEnumerable<ISimpleSyncStage> readStages;
|
||||
private readonly IEnumerable<ISimpleSyncWriteStage> writeStages;
|
||||
private readonly ILogger<SimpleTemplateSynchronizer> logger;
|
||||
private readonly IUnitFilterService unitFilterService;
|
||||
private readonly MqSettings mqSettings;
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly IJobRepository jobService;
|
||||
private readonly ITemplateDeactivator templateDeactivator;
|
||||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||||
private readonly ITemplateAllocationService templateAllocationService;
|
||||
private readonly ITemplateMqPublisher templateMqPublisher;
|
||||
@@ -54,13 +51,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
private readonly IUnitRepository unitRepository;
|
||||
|
||||
public SimpleTemplateSynchronizer(
|
||||
IEnumerable<ISimpleSyncStage> readStages,
|
||||
IEnumerable<ISimpleSyncWriteStage> writeStages,
|
||||
ILogger<SimpleTemplateSynchronizer> logger,
|
||||
IUnitFilterService unitFilterService,
|
||||
MqSettings mqSettings,
|
||||
IRabbitService mqService,
|
||||
ITemplateRepository templateService,
|
||||
IJobRepository jobService,
|
||||
ITemplateDeactivator templateDeactivator,
|
||||
ITemplateNameNormalizer templateNameNormalizer,
|
||||
ITemplateAllocationService templateAllocationService,
|
||||
ITemplateMqPublisher templateMqPublisher,
|
||||
@@ -72,13 +70,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
IUnitRepository unitRepository
|
||||
)
|
||||
{
|
||||
this.readStages = readStages;
|
||||
this.writeStages = writeStages;
|
||||
this.logger = logger;
|
||||
this.unitFilterService = unitFilterService;
|
||||
this.mqSettings = mqSettings;
|
||||
this.mqService = mqService;
|
||||
this.templateService = templateService;
|
||||
this.jobService = jobService;
|
||||
this.templateDeactivator = templateDeactivator;
|
||||
this.templateNameNormalizer = templateNameNormalizer;
|
||||
this.templateAllocationService = templateAllocationService;
|
||||
this.templateMqPublisher = templateMqPublisher;
|
||||
@@ -92,18 +91,15 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
|
||||
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
// === Специальная обработка для Job неиспользуемых шаблонов ===
|
||||
if (jobId == settingsFromDb.JobIdForUnusedTemplates)
|
||||
{
|
||||
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId);
|
||||
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
|
||||
await SyncUnusedTemplatesAsync(jobId, initiator);
|
||||
return;
|
||||
}
|
||||
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
|
||||
|
||||
// === Проверка: уже запущена? ===
|
||||
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||
if (existingStatus.DetailsJobs?.Any() == true)
|
||||
{
|
||||
@@ -111,7 +107,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
return;
|
||||
}
|
||||
|
||||
// === Устанавливаем статус "в процессе" ===
|
||||
var initialStatus = new MatchingStatusItemDto
|
||||
{
|
||||
DateStart = DateTimeOffset.UtcNow,
|
||||
@@ -119,169 +114,49 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
Comment = "Начало синхронизации"
|
||||
};
|
||||
await matchingStatusService.SetMatchingStatusAsync(
|
||||
jobId,
|
||||
SyncTaskEntityTypeEnum.Job,
|
||||
jobId, SyncTaskEntityTypeEnum.Job,
|
||||
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
|
||||
TimeSpan.FromMinutes(35)
|
||||
);
|
||||
TimeSpan.FromMinutes(35));
|
||||
|
||||
// Таймер запускается ПОСЛЕ инфраструктурных операций (статус, проверка блокировки)
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// === ЭТАП 1: Загрузка Job ===
|
||||
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);
|
||||
var context = new SimpleSyncContext { JobId = jobId, Initiator = initiator };
|
||||
|
||||
if (job == null)
|
||||
foreach (var stage in readStages)
|
||||
{
|
||||
logger.LogWarning("Job {JobId} не найден.", jobId);
|
||||
await UpdateMatchingStatusAsync(jobId, "Job не найден");
|
||||
return;
|
||||
}
|
||||
stageSw.Stop();
|
||||
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));
|
||||
}
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
stageSw.Stop();
|
||||
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)
|
||||
foreach (var stage in writeStages)
|
||||
{
|
||||
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
|
||||
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
|
||||
|
||||
var request = new TemplateAllocationRequest(
|
||||
TargetJob: job,
|
||||
TargetUnitId: unitId,
|
||||
TargetUnit: null,
|
||||
Index: null,
|
||||
UnitsInTemplate: new List<UnitInTemplateMessage>(),
|
||||
IsActiveTemplate: isActiveTemplate,
|
||||
IsActiveSchedule: isActiveSchedule,
|
||||
Initiator: initiator);
|
||||
|
||||
await templateAllocationService.AllocateAsync(request);
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
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();
|
||||
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 matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
|
||||
context.JobName, jobId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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}");
|
||||
throw;
|
||||
}
|
||||
@@ -293,6 +168,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
|
||||
@@ -444,30 +320,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
try
|
||||
{
|
||||
// 1. Находим ID нужных полей
|
||||
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(FieldNameResponsibilityArea);
|
||||
var tagField = await unitFieldService.GetByAihitNameAsync(FieldNameParrTag);
|
||||
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName);
|
||||
var tagField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName);
|
||||
|
||||
if (responsableAreaField == null || tagField == null)
|
||||
{
|
||||
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", FieldNameResponsibilityArea, FieldNameParrTag);
|
||||
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue);
|
||||
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
|
||||
return;
|
||||
}
|
||||
|
||||
var responsableAreaFieldId = responsableAreaField.Id;
|
||||
var tagFieldId = tagField.Id;
|
||||
const string targetTagValue = TagValueNotWorking;
|
||||
|
||||
// 2. Находим ValueId для тега "ПАРР-НЕИСП"
|
||||
var targetTagValueId = await unitInValueService.Get()
|
||||
.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)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
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()
|
||||
|
||||
@@ -4,17 +4,17 @@ using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Services.Implementations;
|
||||
using PARR.TemplateMatcher.Services.GroupedSync;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class TemplateMatcher : ITemplateMatcher
|
||||
{
|
||||
private readonly ILogger<TemplateMatcher> logger;
|
||||
private readonly IJobRepository jobService;
|
||||
private readonly IJobGroupRepository jobGroupService;
|
||||
private readonly IEnumerable<ITemplateSynchronizer> synchronizers;
|
||||
private readonly ILogger<TemplateMatcher> _logger;
|
||||
private readonly IJobRepository _jobService;
|
||||
private readonly IJobGroupRepository _jobGroupService;
|
||||
private readonly IEnumerable<ITemplateSynchronizer> _synchronizers;
|
||||
|
||||
public TemplateMatcher(
|
||||
ILogger<TemplateMatcher> logger,
|
||||
@@ -23,21 +23,21 @@ namespace PARR.TemplateMatcher
|
||||
IEnumerable<ITemplateSynchronizer> synchronizers
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.jobService = jobService;
|
||||
this.jobGroupService = jobGroupService;
|
||||
this.synchronizers = synchronizers;
|
||||
_logger = logger;
|
||||
_jobService = jobService;
|
||||
_jobGroupService = jobGroupService;
|
||||
_synchronizers = synchronizers;
|
||||
|
||||
}
|
||||
|
||||
public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId);
|
||||
_logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId);
|
||||
|
||||
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
|
||||
if (job == null)
|
||||
{
|
||||
logger.LogError("Job с Id {JobId} не найден.", jobId);
|
||||
_logger.LogError("Job с Id {JobId} не найден.", jobId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
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)
|
||||
{
|
||||
// Так как Job групповой, вызываем синхронизацию для его JobGroup
|
||||
@@ -56,22 +56,22 @@ namespace PARR.TemplateMatcher
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("GroupedTemplateSynchronizer не найден.");
|
||||
_logger.LogError("GroupedTemplateSynchronizer не найден.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
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)
|
||||
{
|
||||
await synchronizer.SyncTemplatesForJobAsync(jobId, initiator);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("SimpleTemplateSynchronizer не найден.");
|
||||
_logger.LogError("SimpleTemplateSynchronizer не найден.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -79,16 +79,16 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
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()
|
||||
.Include(jg => jg.GroupType)
|
||||
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
|
||||
|
||||
if (jobGroup == null || jobGroup.GroupType == null)
|
||||
{
|
||||
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
|
||||
_logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,32 +99,32 @@ namespace PARR.TemplateMatcher
|
||||
// Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки
|
||||
if (jobGroup.GroupingUnitFieldId.HasValue)
|
||||
{
|
||||
logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId);
|
||||
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
|
||||
_logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId);
|
||||
var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
|
||||
if (synchronizer != null)
|
||||
{
|
||||
await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId);
|
||||
_logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId);
|
||||
_logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId);
|
||||
//await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||
}
|
||||
break;
|
||||
|
||||
case JobGroupTypesEnum.Umbrella:
|
||||
logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId);
|
||||
_logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId);
|
||||
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||
break;
|
||||
|
||||
case JobGroupTypesEnum.Simple:
|
||||
default:
|
||||
logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId);
|
||||
_logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId);
|
||||
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||
break;
|
||||
}
|
||||
@@ -132,16 +132,16 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
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()
|
||||
.Include(jg => jg.GroupType)
|
||||
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
|
||||
|
||||
if (jobGroup == null || jobGroup.GroupType == null)
|
||||
{
|
||||
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
|
||||
_logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,23 +150,23 @@ namespace PARR.TemplateMatcher
|
||||
case JobGroupTypesEnum.Group:
|
||||
if (jobGroup.GroupingUnitFieldId.HasValue)
|
||||
{
|
||||
logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId);
|
||||
_logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId);
|
||||
_logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId);
|
||||
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||
}
|
||||
break;
|
||||
|
||||
case JobGroupTypesEnum.Umbrella:
|
||||
logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId);
|
||||
_logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId);
|
||||
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||
break;
|
||||
|
||||
case JobGroupTypesEnum.Simple:
|
||||
default:
|
||||
logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId);
|
||||
_logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId);
|
||||
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||
break;
|
||||
}
|
||||
@@ -175,12 +175,12 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
|
||||
_logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
|
||||
|
||||
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
|
||||
if (job == null)
|
||||
{
|
||||
logger.LogError("Job с Id {JobId} не найден.", jobId);
|
||||
_logger.LogError("Job с Id {JobId} не найден.", jobId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,9 +189,9 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
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)
|
||||
{
|
||||
// Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение)
|
||||
@@ -199,22 +199,22 @@ namespace PARR.TemplateMatcher
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("GroupedTemplateSynchronizer не найден.");
|
||||
_logger.LogError("GroupedTemplateSynchronizer не найден.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
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)
|
||||
{
|
||||
await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("SimpleTemplateSynchronizer не найден.");
|
||||
_logger.LogError("SimpleTemplateSynchronizer не найден.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -223,9 +223,9 @@ namespace PARR.TemplateMatcher
|
||||
// --- Вспомогательные методы ---
|
||||
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()
|
||||
.Where(j => j.GroupId == jobGroupId)
|
||||
.Select(j => j.Id)
|
||||
@@ -233,35 +233,35 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
if (!jobIds.Any())
|
||||
{
|
||||
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
|
||||
_logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
|
||||
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)
|
||||
{
|
||||
logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId);
|
||||
_logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId);
|
||||
_logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId);
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
.Where(j => j.GroupId == jobGroupId)
|
||||
.Select(j => j.Id)
|
||||
@@ -269,23 +269,23 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
if (!jobIds.Any())
|
||||
{
|
||||
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
|
||||
_logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId);
|
||||
_logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId);
|
||||
}
|
||||
|
||||
|
||||
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
|
||||
{
|
||||
return await jobService.Get()
|
||||
return await _jobService.Get()
|
||||
.AsNoTracking()
|
||||
.Include(j => j.Group)
|
||||
.ThenInclude(j => j!.GroupType)
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace PARR.TemplateMatcher
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
{
|
||||
public interface IMqTemplateMatcher
|
||||
{
|
||||
@@ -1,12 +1,12 @@
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
|
||||
namespace PARR.TemplateMatcher
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
{
|
||||
public interface ITemplateMatcher
|
||||
{
|
||||
Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator);
|
||||
Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator);
|
||||
Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
|
||||
Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
|
||||
Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
36
PARR.TemplateMatcher/Services/SimpleSync/FilterUnitsStage.cs
Normal file
36
PARR.TemplateMatcher/Services/SimpleSync/FilterUnitsStage.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
12
PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncStage.cs
Normal file
12
PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncStage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PARR.TemplateMatcher.Services.SimpleSync
|
||||
{
|
||||
/// <summary>
|
||||
/// Этап синхронизации, который выполняет побочные эффекты (запись в БД, MQ).
|
||||
/// В тестах не подключается — тип системы гарантирует безопасность.
|
||||
/// </summary>
|
||||
public interface ISimpleSyncWriteStage : ISimpleSyncStage
|
||||
{
|
||||
}
|
||||
}
|
||||
42
PARR.TemplateMatcher/Services/SimpleSync/LoadJobStage.cs
Normal file
42
PARR.TemplateMatcher/Services/SimpleSync/LoadJobStage.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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})";
|
||||
}
|
||||
}
|
||||
}
|
||||
50
PARR.TemplateMatcher/Services/SimpleSync/UpdateNamesStage.cs
Normal file
50
PARR.TemplateMatcher/Services/SimpleSync/UpdateNamesStage.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using PARR.Core;
|
||||
using PARR.DAL;
|
||||
using PARR.Infrastructure;
|
||||
using PARR.TemplateMatcher.Services.GroupedSync;
|
||||
using PARR.TemplateMatcher.Services.Implementations;
|
||||
using PARR.TemplateMatcher.Services.Implemetaions;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
namespace PARR.TemplateMatcher
|
||||
@@ -31,8 +32,31 @@ namespace PARR.TemplateMatcher
|
||||
|
||||
// === 2. Scoped: Бизнес-логика и работа с БД (DbContext) ===
|
||||
// Создаются заново для каждого сообщения из очереди (внутри 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>();
|
||||
|
||||
// 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>();
|
||||
|
||||
// Пайплайн аллокации шаблонов
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using PARR.TemplateMatcher;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcherWorker
|
||||
{
|
||||
|
||||
@@ -25,4 +25,8 @@
|
||||
<ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" />
|
||||
<ProjectReference Include="..\PARR.TemplateMatcher\PARR.TemplateMatcher.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="log\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Core.Services.UnitFilterService;
|
||||
using PARR.Domain.Cache;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Enums;
|
||||
@@ -8,7 +9,11 @@ using PARR.EsppApi;
|
||||
using PARR.EsppApi.Constants;
|
||||
using PARR.EsppApi.Models.Query;
|
||||
using PARR.TemplateMatcher;
|
||||
using PARR.TemplateMatcher.Services.GroupedSync;
|
||||
using PARR.TemplateMatcher.Services.Implementations;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.Test.NextRun;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PARR.Test
|
||||
{
|
||||
@@ -41,7 +46,8 @@ namespace PARR.Test
|
||||
//var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0));
|
||||
|
||||
|
||||
await TemplateMatcherTest();
|
||||
//await TemplateMatcherTest();
|
||||
await PreCommitValidationTest();
|
||||
|
||||
|
||||
|
||||
@@ -161,6 +167,91 @@ namespace PARR.Test
|
||||
#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
|
||||
private async Task TemplateMatcherTest()
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs"
|
||||
"ConnectionStrings": {
|
||||
"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": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Debug"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "log/log-.txt",
|
||||
"rollingInterval": "Day"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "log/log-.txt",
|
||||
"rollingInterval": "Day"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MqSettings": {
|
||||
"TemplateMatcher": { "HostName": "10.99.253.216" },
|
||||
"TemplateGenerator": { "HostName": "10.99.253.216" },
|
||||
"TemplateUpdater": { "HostName": "10.99.253.216" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,61 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;",
|
||||
"RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "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"
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;",
|
||||
"RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user