Compare commits
9 Commits
min-relati
...
98245e73e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98245e73e6 | ||
|
|
cdcb4fd9bc | ||
|
|
bbb14ee4ea | ||
|
|
5edbcff35b | ||
|
|
6c93e1971f | ||
|
|
68696a2fdc | ||
|
|
751b693e72 | ||
|
|
6ca6bfbc2e | ||
|
|
178a991d8b |
@@ -1,4 +1,6 @@
|
|||||||
namespace PARR.API.Contracts.V1
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
|
||||||
|
|
||||||
|
namespace PARR.API.Contracts.V1
|
||||||
{
|
{
|
||||||
// https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/
|
// https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/
|
||||||
|
|
||||||
@@ -224,6 +226,7 @@
|
|||||||
{
|
{
|
||||||
public const string Get = BaseStat + "/templates/";
|
public const string Get = BaseStat + "/templates/";
|
||||||
public const string GetForPeriod = BaseStat + "/templates/period";
|
public const string GetForPeriod = BaseStat + "/templates/period";
|
||||||
|
public const string GetTemplatesWithoutScheduleAndTaskCount = BaseStat + "/templates/without-schedule";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class StatStatusTypeTemplates
|
public static class StatStatusTypeTemplates
|
||||||
@@ -324,6 +327,16 @@
|
|||||||
public const string GetWorkloadTemplateReport = BaseStat + "/workload/templates/{reportType}/{filter}/{state}/{date}";
|
public const string GetWorkloadTemplateReport = BaseStat + "/workload/templates/{reportType}/{filter}/{state}/{date}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class StatRobotMetrics
|
||||||
|
{
|
||||||
|
public const string GetRobotStatusMetrics = BaseStat + "/robot-metrics/robot-status/{robotCode}/{period}";
|
||||||
|
|
||||||
|
public const string GetTaskStatusMetrics = BaseStat + "/robot-metrics/task-status/{robotCode}/{period}";
|
||||||
|
|
||||||
|
public const string GetFilteredMetrics = BaseStat + "/robot-metrics/filtered";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Наряды
|
#region Наряды
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.API.Contracts.V1.Requests.Queries
|
||||||
|
{
|
||||||
|
public record RobotFilteredMetricsQuery
|
||||||
|
{
|
||||||
|
public RobotsEnum? RobotCode { get; init; }
|
||||||
|
|
||||||
|
public RobotStatusEnum? RobotStatusCode { get; init; }
|
||||||
|
|
||||||
|
public TaskStatusEnum? TaskStatusCode { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset? DateFrom { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset? DateTo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Шаг группировки в минутах (например, 2, 30, 60, 1440)
|
||||||
|
/// </summary>
|
||||||
|
public int IntervalMinutes { get; set; } = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatFilteredChartPoint
|
||||||
|
{
|
||||||
|
public DateTimeOffset Timestamp { get; init; }
|
||||||
|
public int Count { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatRobotStatusChartPoint
|
||||||
|
{
|
||||||
|
public DateTimeOffset Timestamp { get; init; }
|
||||||
|
public int Wait { get; init; }
|
||||||
|
public int InProgress { get; init; }
|
||||||
|
public int Error { get; init; }
|
||||||
|
public int Complete { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatTaskStatusChartPoint
|
||||||
|
{
|
||||||
|
public DateTimeOffset Timestamp { get; init; }
|
||||||
|
public int Creating { get; init; }
|
||||||
|
public int Updating { get; init; }
|
||||||
|
public int Ok { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace PARR.API.Contracts.V1.Responses.Statistics
|
||||||
|
{
|
||||||
|
public record StatTemplatesWithoutScheduleResponse(int Count);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PARR.API.Contracts.V1;
|
||||||
|
using PARR.API.Contracts.V1.Requests.Queries;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Statistics;
|
||||||
|
using PARR.API.Controllers.V1.Base;
|
||||||
|
using PARR.Core.Services.RobotMetrics;
|
||||||
|
using PARR.Domain.Common.Roles;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Метрики заданий роботам
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = ParrRoles.Administrator.Role)]
|
||||||
|
public class StatRobotMetricsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IRobotMetricsService _robotMetricsService;
|
||||||
|
|
||||||
|
public StatRobotMetricsController(
|
||||||
|
IMapper mapper,
|
||||||
|
IRobotMetricsService robotMetricsService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_robotMetricsService = robotMetricsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Заданиям Роботу, график
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotMetrics.GetRobotStatusMetrics)]
|
||||||
|
public async Task<IActionResult> GetRobotStatusMetrics([FromRoute] RobotsEnum robotCode, [FromRoute] ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await _robotMetricsService.GetRobotStatusMetricsAsync(robotCode, period, cancellationToken);
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<StatRobotStatusChartPoint>>(data);
|
||||||
|
|
||||||
|
return Ok(new Response<List<StatRobotStatusChartPoint>>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Статусам Заданий, график
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotMetrics.GetTaskStatusMetrics)]
|
||||||
|
public async Task<IActionResult> GetTaskStatusMetrics([FromRoute] RobotsEnum robotCode, [FromRoute] ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await _robotMetricsService.GetTaskStatusMetricsAsync(robotCode, period, cancellationToken);
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<StatTaskStatusChartPoint>>(data);
|
||||||
|
|
||||||
|
return Ok(new Response<List<StatTaskStatusChartPoint>>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по статусам заданий, гибкий фильтр
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatRobotMetrics.GetFilteredMetrics)]
|
||||||
|
public async Task<IActionResult> GetFilteredMetrics([FromQuery] RobotFilteredMetricsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// ------- Правильность расчетов этого метода доконца не проверена -------
|
||||||
|
|
||||||
|
var filter = _mapper.Map<MetricFilter>(request);
|
||||||
|
|
||||||
|
var data = await _robotMetricsService.GetFilteredMetricsAsync(filter, cancellationToken);
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<StatFilteredChartPoint>>(data);
|
||||||
|
|
||||||
|
return Ok(new Response<List<StatFilteredChartPoint>>(response, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ using PARR.API.Helpers;
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
using PARR.Domain.Common.Roles;
|
using PARR.Domain.Common.Roles;
|
||||||
|
using PARR.Domain.Entities;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
namespace PARR.API.Controllers.V1.Statistics
|
namespace PARR.API.Controllers.V1.Statistics
|
||||||
@@ -20,16 +21,16 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
[Authorize(Roles = ParrRoles.Administrator.Role)]
|
[Authorize(Roles = ParrRoles.Administrator.Role)]
|
||||||
public class StatTemplateController : BaseApiController
|
public class StatTemplateController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly ITemplateRepository templateService;
|
private readonly ITemplateRepository _templateRepository;
|
||||||
private readonly INextRunService nextRunService;
|
private readonly INextRunService _nextRunService;
|
||||||
|
|
||||||
public StatTemplateController(
|
public StatTemplateController(
|
||||||
ITemplateRepository templateService,
|
ITemplateRepository templateRepository,
|
||||||
INextRunService nextRunService
|
INextRunService nextRunService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.templateService = templateService;
|
_templateRepository = templateRepository;
|
||||||
this.nextRunService = nextRunService;
|
_nextRunService = nextRunService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,12 +42,12 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
{
|
{
|
||||||
var response = new StatTemplateResponse
|
var response = new StatTemplateResponse
|
||||||
{
|
{
|
||||||
ActivateScheduleCount = await templateService.Get().AsNoTracking().CountAsync(t => t.IsActiveSchedule),
|
ActivateScheduleCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.IsActiveSchedule),
|
||||||
ActivateTemplateCount = await templateService.Get().AsNoTracking().CountAsync(t => t.IsActiveTemplate),
|
ActivateTemplateCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.IsActiveTemplate),
|
||||||
TemplateAgentCount = await templateService.Get().AsNoTracking().CountAsync(t => t.Job!.Group!.IsAgent),
|
TemplateAgentCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.Job!.Group!.IsAgent),
|
||||||
TemplateCount = await templateService.Get().AsNoTracking().CountAsync(),
|
TemplateCount = await _templateRepository.Get().AsNoTracking().CountAsync(),
|
||||||
SyncEsppScheduleCount = await templateService.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.ScheduleOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok)),
|
SyncEsppScheduleCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.ScheduleOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok)),
|
||||||
SyncEsppTemplatesCount = await templateService.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.TemplateOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok))
|
SyncEsppTemplatesCount = await _templateRepository.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.TemplateOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok))
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(new Response<StatTemplateResponse>(response, true));
|
return Ok(new Response<StatTemplateResponse>(response, true));
|
||||||
@@ -71,7 +72,7 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
userEnd.AddDays(1),
|
userEnd.AddDays(1),
|
||||||
timeZoneQuery.TimeZoneOffset);
|
timeZoneQuery.TimeZoneOffset);
|
||||||
|
|
||||||
var allRecords = await templateService.Get()
|
var allRecords = await _templateRepository.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
|
.FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
|
||||||
.Select(t => new { t.Id, t.DateCreated })
|
.Select(t => new { t.Id, t.DateCreated })
|
||||||
@@ -80,7 +81,7 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
var resultDict = allRecords.GroupByUserDate(t => t.DateCreated, timeZoneQuery.TimeZoneOffset);
|
var resultDict = allRecords.GroupByUserDate(t => t.DateCreated, timeZoneQuery.TimeZoneOffset);
|
||||||
|
|
||||||
|
|
||||||
var daysList = await nextRunService.GetWorkDaysAsync(userStart, userEnd, false);
|
var daysList = await _nextRunService.GetWorkDaysAsync(userStart, userEnd, false);
|
||||||
|
|
||||||
var response = daysList.Select(date => new StatTemplatePeriodResponse
|
var response = daysList.Select(date => new StatTemplatePeriodResponse
|
||||||
{
|
{
|
||||||
@@ -92,5 +93,27 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить кол-во шаблонов у которых ИД расписания null и нет задания на создание расписания
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.StatTemplate.GetTemplatesWithoutScheduleAndTaskCount)]
|
||||||
|
public async Task<IActionResult> GetTemplatesWithoutScheduleAndTaskCount()
|
||||||
|
{
|
||||||
|
var count = await _templateRepository.Get()
|
||||||
|
.CountAsync(t =>
|
||||||
|
t.ScheduleEsppId == null
|
||||||
|
&& !t.RobotConfigurations.Any(x =>
|
||||||
|
x.RobotCode == (int)RobotsEnum.ScheduleOrder
|
||||||
|
&& x.TaskStatusCode == (int)TaskStatusEnum.Creating
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var resposne = new StatTemplatesWithoutScheduleResponse(count);
|
||||||
|
|
||||||
|
return Ok(new Response<StatTemplatesWithoutScheduleResponse>(resposne, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using PARR.API.Contracts.V1.Responses.Statistics;
|
|||||||
using PARR.API.MappingProfiles.Resolvers;
|
using PARR.API.MappingProfiles.Resolvers;
|
||||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||||
using PARR.Domain.DTOs.Matching;
|
using PARR.Domain.DTOs.Matching;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
||||||
using PARR.Domain.DTOs.RobotTask;
|
using PARR.Domain.DTOs.RobotTask;
|
||||||
using PARR.Domain.DTOs.Shortcode;
|
using PARR.Domain.DTOs.Shortcode;
|
||||||
@@ -500,6 +501,14 @@ namespace PARR.API.MappingProfiles
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region StatRobotMetrics
|
||||||
|
|
||||||
|
CreateMap<TaskStatusChartPoint, StatTaskStatusChartPoint>();
|
||||||
|
CreateMap<RobotStatusChartPoint, StatRobotStatusChartPoint>();
|
||||||
|
CreateMap<FilteredChartPoint, StatFilteredChartPoint>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using PARR.API.Contracts.V1.Requests;
|
using PARR.API.Contracts.V1.Requests;
|
||||||
using PARR.API.Contracts.V1.Requests.Queries;
|
using PARR.API.Contracts.V1.Requests.Queries;
|
||||||
using PARR.Domain.Common.Pagination;
|
using PARR.Domain.Common.Pagination;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
||||||
using PARR.Domain.Entities.JobEntities;
|
using PARR.Domain.Entities.JobEntities;
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ namespace PARR.API.MappingProfiles
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
CreateMap<StatRobotSnapshotQuery, RobotSnapshotQuery>();
|
CreateMap<StatRobotSnapshotQuery, RobotSnapshotQuery>();
|
||||||
|
|
||||||
|
|
||||||
|
CreateMap<RobotFilteredMetricsQuery, MetricFilter>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using PARR.Core.Common.Interfaces;
|
|||||||
using PARR.Core.Services.MatchingStatusService;
|
using PARR.Core.Services.MatchingStatusService;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
using PARR.Core.Services.NextRunServices.Subservices;
|
using PARR.Core.Services.NextRunServices.Subservices;
|
||||||
|
using PARR.Core.Services.RobotMetrics;
|
||||||
using PARR.Core.Services.RobotSnapshotServices;
|
using PARR.Core.Services.RobotSnapshotServices;
|
||||||
using PARR.Core.Services.RobotTask.Implementations;
|
using PARR.Core.Services.RobotTask.Implementations;
|
||||||
using PARR.Core.Services.RobotTask.Interfaces;
|
using PARR.Core.Services.RobotTask.Interfaces;
|
||||||
@@ -28,7 +29,6 @@ using PARR.Core.Services.UnitService.Implementations;
|
|||||||
using PARR.Core.Services.UnitService.Interfaces;
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
using PARR.Core.Services.Workload.Implementations;
|
using PARR.Core.Services.Workload.Implementations;
|
||||||
using PARR.Core.Services.Workload.Interfaces;
|
using PARR.Core.Services.Workload.Interfaces;
|
||||||
using PARR.Domain.Entities.RobotEntities;
|
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
|
|
||||||
@@ -115,6 +115,8 @@ namespace PARR.Core
|
|||||||
services.AddScoped<IUnitService, UnitService>();
|
services.AddScoped<IUnitService, UnitService>();
|
||||||
services.AddScoped<UnitCacheService>();
|
services.AddScoped<UnitCacheService>();
|
||||||
|
|
||||||
|
services.AddScoped<IRobotMetricsService, RobotMetricsService>();
|
||||||
|
|
||||||
//services.AddScoped<IUserService, UserService>();
|
//services.AddScoped<IUserService, UserService>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
10
PARR.Core/Extensions/EnumerableExtensions.cs
Normal file
10
PARR.Core/Extensions/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace PARR.Core.Extensions
|
||||||
|
{
|
||||||
|
public static class EnumerableExtensions
|
||||||
|
{
|
||||||
|
public static int MaxOrDefault(this IEnumerable<int> source)
|
||||||
|
{
|
||||||
|
return source.Any() ? source.Max() : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
PARR.Core/Services/RobotMetrics/IRobotMetricsService.cs
Normal file
37
PARR.Core/Services/RobotMetrics/IRobotMetricsService.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Отчетность по метрикам заданий и работы роботов.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRobotMetricsService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Заданиям Роботу
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robotCode"></param>
|
||||||
|
/// <param name="period"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<RobotStatusChartPoint>> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Статусам Заданий
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robotCode"></param>
|
||||||
|
/// <param name="period"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<TaskStatusChartPoint>> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика с применением гибких фильтров
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<FilteredChartPoint>> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
260
PARR.Core/Services/RobotMetrics/RobotMetricsService.cs
Normal file
260
PARR.Core/Services/RobotMetrics/RobotMetricsService.cs
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Extensions;
|
||||||
|
using PARR.Core.Repositories.Interfaces;
|
||||||
|
using PARR.Core.Repositories.Interfaces.RobotRepositories;
|
||||||
|
using PARR.Domain.DTOs.RobotMetrics;
|
||||||
|
using PARR.Domain.Enums;
|
||||||
|
using PARR.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace PARR.Core.Services.RobotMetrics
|
||||||
|
{
|
||||||
|
internal class RobotMetricsService : IRobotMetricsService
|
||||||
|
{
|
||||||
|
private readonly IRobotConfigurationSnapshotRepository _snapshotRepository;
|
||||||
|
private readonly IRobotConfigurationRepository _configurationRepository;
|
||||||
|
private readonly ILogger<RobotMetricsService> _logger;
|
||||||
|
|
||||||
|
public RobotMetricsService(
|
||||||
|
IRobotConfigurationSnapshotRepository snapshotRepository,
|
||||||
|
IRobotConfigurationRepository configurationRepository,
|
||||||
|
ILogger<RobotMetricsService> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_snapshotRepository = snapshotRepository;
|
||||||
|
_configurationRepository = configurationRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<FilteredChartPoint>> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// ------- Правильность расчетов этого метода доконца не проверена -------
|
||||||
|
|
||||||
|
if (filter == null)
|
||||||
|
throw new AppValidationException("Фильтр не может быть пустым.");
|
||||||
|
|
||||||
|
if (filter.IntervalMinutes < 1)
|
||||||
|
throw new AppValidationException("Интервал группировки не может быть меньше 1 минуты.");
|
||||||
|
|
||||||
|
var query = _snapshotRepository.Get().AsNoTracking();
|
||||||
|
|
||||||
|
if (filter.RobotCode.HasValue)
|
||||||
|
query = query.Where(t => t.RobotCode == (int)filter.RobotCode.Value);
|
||||||
|
|
||||||
|
if (filter.RobotStatusCode.HasValue)
|
||||||
|
query = query.Where(t => t.RobotStatusCode == (int)filter.RobotStatusCode.Value);
|
||||||
|
|
||||||
|
if (filter.TaskStatusCode.HasValue)
|
||||||
|
query = query.Where(t => t.TaskStatusCode == (int)filter.TaskStatusCode);
|
||||||
|
|
||||||
|
// Если даты не переданы, берем последние 24 часа по умолчанию
|
||||||
|
var dateFrom = filter.DateFrom ?? DateTimeOffset.UtcNow.AddDays(-1);
|
||||||
|
query = query.Where(s => s.DateCreated >= dateFrom);
|
||||||
|
|
||||||
|
if (filter.DateTo.HasValue)
|
||||||
|
query = query.Where(s => s.DateCreated <= filter.DateTo.Value);
|
||||||
|
|
||||||
|
var dbData = await query.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var dbGrouped = dbData
|
||||||
|
.GroupBy(s => RoundToInterval(s.DateCreated, filter.IntervalMinutes))
|
||||||
|
.ToDictionary(t => t.Key, t => t.ToList());
|
||||||
|
|
||||||
|
var dateTo = filter.DateTo ?? DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var result = GenerateTimeGrid(dateFrom, dateTo, filter.IntervalMinutes)
|
||||||
|
.Select(time => new FilteredChartPoint(
|
||||||
|
Timestamp: time,
|
||||||
|
//Count: dbGrouped.TryGetValue(time, out var points) ? points.Max(x => x.Count) : 0
|
||||||
|
Count: dbGrouped.TryGetValue(time, out var points)
|
||||||
|
? points.GroupBy(x => x.DateCreated) // Группируем по точной минуте снапшота
|
||||||
|
.Select(g => g.Sum(x => x.Count)) // Складываем всё, что подошли под фильтр в эту минуту
|
||||||
|
.MaxOrDefault() // Берем максимальный пик за весь интервал (например, за час)
|
||||||
|
: 0 // Если снапшотов не было — честный ноль
|
||||||
|
)).ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<RobotStatusChartPoint>> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateRobot(robotCode);
|
||||||
|
CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
|
||||||
|
|
||||||
|
// История из снапшотов
|
||||||
|
var snapshots = await _snapshotRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var dbGrouped = snapshots
|
||||||
|
.GroupBy(t => RoundToInterval(t.DateCreated, intervalMinutes))
|
||||||
|
.ToDictionary(t => t.Key, t => t.ToList());
|
||||||
|
|
||||||
|
// Генерим сетку значений, если значений нет, вставляем нули
|
||||||
|
var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
|
||||||
|
.Select(time => dbGrouped.TryGetValue(time, out var points)
|
||||||
|
? new RobotStatusChartPoint(
|
||||||
|
Timestamp: time,
|
||||||
|
// группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
|
||||||
|
Wait: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(t => t.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
InProgress: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Error: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).MaxOrDefault(x => x.Count)
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Complete:
|
||||||
|
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
|
||||||
|
.MaxOrDefault()
|
||||||
|
)
|
||||||
|
: new RobotStatusChartPoint(time, Wait: 0, InProgress: 0, Error: 0, Complete: 0)
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
// Последнее значение в конце графика из реальной таблицы
|
||||||
|
var liveRaw = await _configurationRepository
|
||||||
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.RobotCode == (int)robotCode)
|
||||||
|
.GroupBy(t => t.RobotStatusCode)
|
||||||
|
.Select(g => new { RobotStatusCode = g.Key, Count = g.Count() })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
history.Add(new RobotStatusChartPoint(
|
||||||
|
Timestamp: DateTimeOffset.UtcNow,
|
||||||
|
Wait: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).Sum(x => x.Count),
|
||||||
|
InProgress: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).Sum(x => x.Count),
|
||||||
|
Error: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).Sum(x => x.Count),
|
||||||
|
Complete: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete).Sum(x => x.Count)
|
||||||
|
));
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<TaskStatusChartPoint>> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateRobot(robotCode);
|
||||||
|
CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
|
||||||
|
|
||||||
|
// История из снапшотов
|
||||||
|
var snapshots = await _snapshotRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var dbGrouped = snapshots
|
||||||
|
.GroupBy(s => RoundToInterval(s.DateCreated, intervalMinutes))
|
||||||
|
.ToDictionary(t => t.Key, t => t.ToList());
|
||||||
|
|
||||||
|
var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
|
||||||
|
.Select(time => dbGrouped.TryGetValue(time, out var points)
|
||||||
|
? new TaskStatusChartPoint(
|
||||||
|
Timestamp: time,
|
||||||
|
// группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
|
||||||
|
Creating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Updating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).MaxOrDefault(x => x.Count),
|
||||||
|
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault(),
|
||||||
|
Ok: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).MaxOrDefault(x => x.Count)
|
||||||
|
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok)
|
||||||
|
.GroupBy(x => x.DateCreated)
|
||||||
|
.Select(g => g.Sum(x => x.Count))
|
||||||
|
.MaxOrDefault()
|
||||||
|
)
|
||||||
|
: new TaskStatusChartPoint(time, Creating: 0, Updating: 0, Ok: 0)
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
// Живой текущий кадр в конец графика
|
||||||
|
var liveRaw = await _configurationRepository
|
||||||
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.RobotCode == (int)robotCode)
|
||||||
|
.GroupBy(t => t.TaskStatusCode)
|
||||||
|
.Select(g => new { TaskStatusCode = g.Key, Count = g.Count() })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
history.Add(new TaskStatusChartPoint(
|
||||||
|
Timestamp: DateTimeOffset.UtcNow,
|
||||||
|
Creating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).Sum(x => x.Count),
|
||||||
|
Updating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).Sum(x => x.Count),
|
||||||
|
Ok: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).Sum(x => x.Count)
|
||||||
|
));
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ValidateRobot(RobotsEnum robotCode)
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(typeof(RobotsEnum), robotCode))
|
||||||
|
throw new NotFoundException($"Робот с кодом {robotCode} не найден в системе.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void CalculatePeriodDates(ChartPeriod period, out DateTimeOffset fromDate, out int intervalMinutes)
|
||||||
|
{
|
||||||
|
switch (period)
|
||||||
|
{
|
||||||
|
case ChartPeriod.TwoHours:
|
||||||
|
fromDate = DateTimeOffset.UtcNow.AddHours(-2);
|
||||||
|
intervalMinutes = 2;
|
||||||
|
break;
|
||||||
|
case ChartPeriod.TwentyFourHours:
|
||||||
|
fromDate = DateTimeOffset.UtcNow.AddDays(-1);
|
||||||
|
intervalMinutes = 30;
|
||||||
|
break;
|
||||||
|
case ChartPeriod.SevenDays:
|
||||||
|
fromDate = DateTimeOffset.UtcNow.AddDays(-7);
|
||||||
|
intervalMinutes = 60;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AppValidationException("Указан неподдерживаемый период времени.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private DateTimeOffset RoundToInterval(DateTimeOffset dt, int intervalMinutes)
|
||||||
|
{
|
||||||
|
var minutes = (dt.Minute / intervalMinutes) * intervalMinutes;
|
||||||
|
return new DateTimeOffset(dt.Year, dt.Month, dt.Day, dt.Hour, minutes, 0, dt.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Генератор сетки времени
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fromDate"></param>
|
||||||
|
/// <param name="toDate"></param>
|
||||||
|
/// <param name="intervalMinutes"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private IEnumerable<DateTimeOffset> GenerateTimeGrid(DateTimeOffset fromDate, DateTimeOffset toDate, int intervalMinutes)
|
||||||
|
{
|
||||||
|
var startTime = RoundToInterval(fromDate, intervalMinutes);
|
||||||
|
var endTime = RoundToInterval(toDate, intervalMinutes);
|
||||||
|
|
||||||
|
for (var time = startTime; time <= endTime; time = time.AddMinutes(intervalMinutes))
|
||||||
|
{
|
||||||
|
yield return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ namespace PARR.Core.Services.UnitFilterService.Matchers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class UnitFieldMatcher : IUnitFieldMatcher
|
internal class UnitFieldMatcher : IUnitFieldMatcher
|
||||||
{
|
{
|
||||||
private const int chunkSize = 1000;
|
private const int chunkSize = 200;
|
||||||
private readonly IUnitRepository unitRepository;
|
private readonly IUnitRepository unitRepository;
|
||||||
private readonly ILogger<UnitFieldMatcher> logger;
|
private readonly ILogger<UnitFieldMatcher> logger;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace PARR.DAL.Repositories.Unit
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IQueryable<PARR.Domain.Entities.Unit.Unit> GetUnitByFieldAndValue(IQueryable<PARR.Domain.Entities.Unit.Unit> query, Guid fieldId, string valueMask, bool isInverse = false)
|
public IQueryable<Domain.Entities.Unit.Unit> GetUnitByFieldAndValue(IQueryable<Domain.Entities.Unit.Unit> query, Guid fieldId, string valueMask, bool isInverse = false)
|
||||||
{
|
{
|
||||||
//TODO: вынесено из UnitFilterService
|
//TODO: вынесено из UnitFilterService
|
||||||
|
|
||||||
|
|||||||
9
PARR.Domain/DTOs/RobotMetrics/FilteredChartPoint.cs
Normal file
9
PARR.Domain/DTOs/RobotMetrics/FilteredChartPoint.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по роботам, согласно гибким фильтрам.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Timestamp"></param>
|
||||||
|
/// <param name="Count"></param>
|
||||||
|
public record FilteredChartPoint(DateTimeOffset Timestamp, int Count);
|
||||||
|
}
|
||||||
16
PARR.Domain/DTOs/RobotMetrics/MetricFilter.cs
Normal file
16
PARR.Domain/DTOs/RobotMetrics/MetricFilter.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using PARR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
public record MetricFilter
|
||||||
|
{
|
||||||
|
public RobotsEnum? RobotCode { get; init; }
|
||||||
|
public RobotStatusEnum? RobotStatusCode { get; init; }
|
||||||
|
public TaskStatusEnum? TaskStatusCode { get; init; }
|
||||||
|
public DateTimeOffset? DateFrom { get; init; }
|
||||||
|
public DateTimeOffset? DateTo { get; init; }
|
||||||
|
|
||||||
|
// Шаг группировки в минутах (например, 2, 30, 60, 1440)
|
||||||
|
public int IntervalMinutes { get; set; } = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
PARR.Domain/DTOs/RobotMetrics/RobotStatusChartPoint.cs
Normal file
7
PARR.Domain/DTOs/RobotMetrics/RobotStatusChartPoint.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Робот Сатусам, на графике
|
||||||
|
/// </summary>
|
||||||
|
public record RobotStatusChartPoint(DateTimeOffset Timestamp, int Wait, int InProgress, int Error, int Complete);
|
||||||
|
}
|
||||||
11
PARR.Domain/DTOs/RobotMetrics/TaskStatusChartPoint.cs
Normal file
11
PARR.Domain/DTOs/RobotMetrics/TaskStatusChartPoint.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PARR.Domain.DTOs.RobotMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика по Статусам заданий роботам, на графике
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Timestamp"></param>
|
||||||
|
/// <param name="Creating"></param>
|
||||||
|
/// <param name="Updating"></param>
|
||||||
|
/// <param name="Ok"></param>
|
||||||
|
public record TaskStatusChartPoint(DateTimeOffset Timestamp, int Creating, int Updating, int Ok);
|
||||||
|
}
|
||||||
26
PARR.Domain/Enums/ChartPeriod.cs
Normal file
26
PARR.Domain/Enums/ChartPeriod.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace PARR.Domain.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Периоды для графиков
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum ChartPeriod
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Последние 2 часа
|
||||||
|
/// </summary>
|
||||||
|
TwoHours = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Последние 24 часа
|
||||||
|
/// </summary>
|
||||||
|
TwentyFourHours = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Последняя неделя
|
||||||
|
/// </summary>
|
||||||
|
SevenDays = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,26 +16,53 @@ namespace PARR.TemplateMatcher.Services.Implementations
|
|||||||
bool defaultTemplateState = false,
|
bool defaultTemplateState = false,
|
||||||
bool defaultScheduleState = false)
|
bool defaultScheduleState = false)
|
||||||
{
|
{
|
||||||
// Тип группы определяет источник настроек
|
// Защита от оптимизации: если группу забыли подгрузить, метод честно падает,
|
||||||
var isGroupLevel = jobGroup?.GroupType?.IsJobGroupAutoControl == true;
|
// потому что без GroupType бизнес-логика не может определить уровень управления
|
||||||
|
if (jobGroup == null)
|
||||||
if (isGroupLevel && jobGroup!.AutoControl != null)
|
|
||||||
{
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Критическая ошибка бизнес-логики: Для работы '{job.Name}' (ID: {job.Id}) " +
|
||||||
|
$"не передана группа (null). Нужно добавить '.Include(j => j.Group)'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Проверяем, что разработчики подгрузили GroupType из базы данных
|
||||||
|
if (jobGroup.GroupType == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Для работы '{job.Name}' (ID: {job.Id}) передана группа, " +
|
||||||
|
$"но её GroupType = null. Нужно добавить '.ThenInclude(g => g.GroupType)' в запрос.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Теперь компилятор знает, что jobGroup и GroupType гарантированно не null
|
||||||
|
var isGroupLevel = jobGroup.GroupType.IsJobGroupAutoControl;
|
||||||
|
|
||||||
|
// 2. Сценарий: Управление на уровне Группы Работ
|
||||||
|
if (isGroupLevel)
|
||||||
|
{
|
||||||
|
if (jobGroup.AutoControl == null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"В типе группы '{jobGroup.GroupType.Id}' указано управление " +
|
||||||
|
$"на уровне ГРУППЫ, но у группы '{jobGroup.GroupName}' (ID: {jobGroup.Id}) " +
|
||||||
|
$"отсутствуют настройки автоконтроля (JobGroup.AutoControl равен null)!");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jobGroup.AutoControl.InitUsedTemplateState,
|
jobGroup.AutoControl.InitUsedTemplateState,
|
||||||
jobGroup.AutoControl.InitUsedScheduleState
|
jobGroup.AutoControl.InitUsedScheduleState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGroupLevel && job.AutoControl != null)
|
// 3. Сценарий: Управление на уровне конкретной Работы
|
||||||
{
|
if (job.AutoControl == null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"В типе группы '{jobGroup.GroupType.Id}' указано управление " +
|
||||||
|
$"на уровне РАБОТЫ, но у работы '{job.Name}' (ID: {job.Id}) " +
|
||||||
|
$"отсутствуют настройки автоконтроля (Job.AutoControl равен null)!");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
job.AutoControl.InitUsedTemplateState,
|
job.AutoControl.InitUsedTemplateState,
|
||||||
job.AutoControl.InitUsedScheduleState
|
job.AutoControl.InitUsedScheduleState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (defaultTemplateState, defaultScheduleState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ internal class LoadJobStage : ISimpleSyncStage
|
|||||||
.Include(j => j.AutoControl)
|
.Include(j => j.AutoControl)
|
||||||
.Include(j => j.Tnk)
|
.Include(j => j.Tnk)
|
||||||
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
|
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
|
||||||
.Include(j => j.AutoControl)
|
.Include(j => j.Group).ThenInclude(g => g!.AutoControl)
|
||||||
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
|
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
|
||||||
.FirstOrDefaultAsync(j => j.Id == context.JobId, ct);
|
.FirstOrDefaultAsync(j => j.Id == context.JobId, ct);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user