feat(core,api): Детали по заданиями роботов

This commit is contained in:
Mikhail Trubnikov
2026-07-29 09:50:21 +10:00
parent 56cdca0502
commit 7d5fb23daf
18 changed files with 4435 additions and 36 deletions

View File

@@ -1,12 +1,17 @@
namespace PARR.API.Contracts.V1.Responses namespace PARR.API.Contracts.V1.Responses
{ {
public class JobGroupBaseResponse public class JobGroupShortResponse
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
}
//public bool? IsUmbrella { get; set; } public class JobGroupBaseResponse : JobGroupShortResponse
{
//public Guid Id { get; set; }
//public required string Name { get; set; }
public required string ShortDescription { get; set; } public required string ShortDescription { get; set; }

View File

@@ -0,0 +1,16 @@
namespace PARR.API.Contracts.V1.Responses.Statistics
{
public record StatRobotTaskDetailsResponse
{
public RobotResponse Robot { get; init; } = null!;
public TaskStatusResponse Task { get; init; } = null!;
public List<StatRobotTaskGroupDetailsResponse> Details { get; init; } = null!;
}
public record StatRobotTaskGroupDetailsResponse
{
public JobGroupShortResponse JobGroup { get; init; } = null!;
public int TemplatesCount { get; init; }
}
}

View File

@@ -2,7 +2,9 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PARR.API.Contracts.V1; using PARR.API.Contracts.V1;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Controllers.V1.Base; using PARR.API.Controllers.V1.Base;
using PARR.Core.Services.RobotStatusDetails.Interfaces;
using PARR.Domain.Common.Roles; using PARR.Domain.Common.Roles;
using PARR.Domain.Enums; using PARR.Domain.Enums;
@@ -15,12 +17,15 @@ namespace PARR.API.Controllers.V1.Statistics
public class StatRobotStatusDetailsController : BaseApiController public class StatRobotStatusDetailsController : BaseApiController
{ {
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly IRobotStatusDetailsService _robotStatusDetailsService;
public StatRobotStatusDetailsController( public StatRobotStatusDetailsController(
IMapper mapper IMapper mapper,
IRobotStatusDetailsService robotStatusDetailsService
) )
{ {
_mapper = mapper; _mapper = mapper;
_robotStatusDetailsService = robotStatusDetailsService;
} }
@@ -33,7 +38,10 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatRobotStatusDetails.Details)] [HttpGet(ApiRoutes.StatRobotStatusDetails.Details)]
public async Task<IActionResult> Details([FromRoute] RobotsEnum robot, [FromRoute] RobotStatusEnum status) public async Task<IActionResult> Details([FromRoute] RobotsEnum robot, [FromRoute] RobotStatusEnum status)
{ {
return Ok(); var details = await _robotStatusDetailsService.GetDetailsAsync(robot, status);
var response = _mapper.Map<StatRobotStatusDetailsResponse>(details);
return Ok(new Response<StatRobotStatusDetailsResponse>(response, true));
} }
} }

View File

@@ -0,0 +1,19 @@
using PARR.API.Contracts.V1.Responses;
namespace PARR.API.Controllers.V1.Statistics
{
public record StatRobotStatusDetailsResponse
{
public RobotResponse Robot { get; init; } = null!;
public RobotStatusResponse Status { get; init; } = null!;
public List<StatRobotStatusGroupDetailsResponse> Details { get; init; } = null!;
}
public record StatRobotStatusGroupDetailsResponse
{
public JobGroupShortResponse JobGroup { get; init; } = null!;
public int TemplatesCount { get; init; }
}
}

View File

@@ -2,6 +2,8 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PARR.API.Contracts.V1; using PARR.API.Contracts.V1;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base; using PARR.API.Controllers.V1.Base;
using PARR.Core.Services.RobotTaskDetailsServices.Interfaces; using PARR.Core.Services.RobotTaskDetailsServices.Interfaces;
using PARR.Domain.Common.Roles; using PARR.Domain.Common.Roles;
@@ -38,8 +40,9 @@ namespace PARR.API.Controllers.V1.Statistics
public async Task<IActionResult> Details([FromRoute] RobotsEnum robot, [FromRoute] TaskStatusEnum task) public async Task<IActionResult> Details([FromRoute] RobotsEnum robot, [FromRoute] TaskStatusEnum task)
{ {
var details = await _robotTaskDetailsService.GetDetailsAsync(robot, task); var details = await _robotTaskDetailsService.GetDetailsAsync(robot, task);
var response = _mapper.Map<StatRobotTaskDetailsResponse>(details);
return Ok(); return Ok(new Response<StatRobotTaskDetailsResponse>(response, true));
} }
} }

View File

@@ -2,12 +2,15 @@
using PARR.API.Authentication.Models; using PARR.API.Authentication.Models;
using PARR.API.Contracts.V1.Responses; using PARR.API.Contracts.V1.Responses;
using PARR.API.Contracts.V1.Responses.Statistics; using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.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.RobotMetrics;
using PARR.Domain.DTOs.RobotSnapshotDTO; using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.DTOs.RobotStatusDetails;
using PARR.Domain.DTOs.RobotTask; using PARR.Domain.DTOs.RobotTask;
using PARR.Domain.DTOs.RobotTaskDetails;
using PARR.Domain.DTOs.RobotTaskRobotStatus; using PARR.Domain.DTOs.RobotTaskRobotStatus;
using PARR.Domain.DTOs.Shared; using PARR.Domain.DTOs.Shared;
using PARR.Domain.DTOs.Shortcode; using PARR.Domain.DTOs.Shortcode;
@@ -376,6 +379,9 @@ namespace PARR.API.MappingProfiles
#region JobGroup #region JobGroup
CreateMap<JobGroupShortResult, JobGroupShortResponse>()
.ForMember(d => d.Name, o => o.MapFrom(s => s.GroupName));
CreateMap<JobGroup, JobGroupBaseResponse>() CreateMap<JobGroup, JobGroupBaseResponse>()
.Include<JobGroup, JobGroupResponse>() .Include<JobGroup, JobGroupResponse>()
.Include<JobGroup, JobGroupWithDistributionConfigResponse>() .Include<JobGroup, JobGroupWithDistributionConfigResponse>()
@@ -521,6 +527,27 @@ namespace PARR.API.MappingProfiles
#endregion #endregion
#region StatRobotTaskDetailsResponse
CreateMap<RobotTaskGroupDetailsResult, StatRobotTaskGroupDetailsResponse>();
//todo: ForMember не нужен?
//.ForMember(d => d.JobGroup, o => o.MapFrom(s => s.JobGroup));
CreateMap<RobotTaskDetailsResult, StatRobotTaskDetailsResponse>();
//todo: ForMember не нужен?
//.ForMember(d => d.Details, o => o.MapFrom(s => s.Details));
#endregion
#region StatRobotStatusDetailsResponse
CreateMap<RobotStatusDetailsResult, StatRobotStatusDetailsResponse>();
CreateMap<RobotStatusGroupDetailsResult, StatRobotStatusGroupDetailsResponse>();
#endregion
} }
} }

View File

@@ -10,6 +10,8 @@ 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.RobotMetrics;
using PARR.Core.Services.RobotSnapshotServices; using PARR.Core.Services.RobotSnapshotServices;
using PARR.Core.Services.RobotStatusDetails.Implementations;
using PARR.Core.Services.RobotStatusDetails.Interfaces;
using PARR.Core.Services.RobotTask.Implementations; using PARR.Core.Services.RobotTask.Implementations;
using PARR.Core.Services.RobotTask.Interfaces; using PARR.Core.Services.RobotTask.Interfaces;
using PARR.Core.Services.RobotTaskDetailsServices.Implementations; using PARR.Core.Services.RobotTaskDetailsServices.Implementations;
@@ -117,6 +119,7 @@ namespace PARR.Core
services.AddScoped<IRobotSnapshotService, RobotSnapshotService>(); services.AddScoped<IRobotSnapshotService, RobotSnapshotService>();
services.AddScoped<IRobotTaskRobotStatusService, RobotTaskRobotStatusService>(); services.AddScoped<IRobotTaskRobotStatusService, RobotTaskRobotStatusService>();
services.AddScoped<IRobotTaskDetailsService, RobotTaskDetailsService>(); services.AddScoped<IRobotTaskDetailsService, RobotTaskDetailsService>();
services.AddScoped<IRobotStatusDetailsService, RobotStatusDetailsService>();
services.AddScoped<IUnitService, UnitService>(); services.AddScoped<IUnitService, UnitService>();
services.AddScoped<UnitCacheService>(); services.AddScoped<UnitCacheService>();

View File

@@ -25,8 +25,4 @@
<!-- Разрешаем Castle DynamicProxy (Moq / NSubstitute) видеть internal классы PARR.Core --> <!-- Разрешаем Castle DynamicProxy (Moq / NSubstitute) видеть internal классы PARR.Core -->
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" /> <InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Services\RobotStatusDetails\" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,88 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.RobotStatusDetails.Interfaces;
using PARR.Domain.DTOs.RobotStatusDetails;
using PARR.Domain.DTOs.Shared;
using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotStatusDetails.Implementations
{
internal class RobotStatusDetailsService : IRobotStatusDetailsService
{
private readonly ILogger<RobotStatusDetailsService> _logger;
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
private readonly IMapper _mapper;
private readonly IRobotRepository _robotRepository;
private readonly IRobotStatusRepository _robotStatusRepository;
public RobotStatusDetailsService(
ILogger<RobotStatusDetailsService> logger,
IRobotConfigurationRepository robotConfigurationRepository,
IMapper mapper,
IRobotRepository robotRepository,
IRobotStatusRepository robotStatusRepository
)
{
_logger = logger;
_robotConfigurationRepository = robotConfigurationRepository;
_mapper = mapper;
_robotRepository = robotRepository;
_robotStatusRepository = robotStatusRepository;
}
public async Task<RobotStatusDetailsResult> GetDetailsAsync(RobotsEnum robot, RobotStatusEnum status, CancellationToken cancellationToken = default)
{
var groupedDetails = await _robotConfigurationRepository.Get()
.AsNoTracking()
.Where(config => config.RobotCode == (int)robot && config.RobotStatusCode == (int)status)
.GroupBy(config => config.Template!.Job!.Group)
.Select(t => new
{
JobGroup = t.Key,
TemplatesCount = t.Count()
})
.ToListAsync(cancellationToken);
var robotEntity = await _robotRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(r => r.Code == (int)robot, cancellationToken);
var robotStatusEntity = await _robotStatusRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Code == (int)status, cancellationToken);
if (robotEntity == null)
{
_logger.LogWarning("Робот с кодом {RobotCode} не найден в БД", robot);
throw new AppValidationException($"Робот с кодом {(int)robot} не найден");
}
if (robotStatusEntity == null)
{
_logger.LogWarning("Статус робота с кодом {StatusCode} не найден в БД", status);
throw new AppValidationException($"Статус робота с кодом {(int)status} не найден");
}
var result = new RobotStatusDetailsResult
{
Robot = _mapper.Map<RobotResult>(robotEntity),
Status = _mapper.Map<RobotStatusResult>(robotStatusEntity),
Details = groupedDetails
.Select(t => new RobotStatusGroupDetailsResult
{
JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup),
TemplatesCount = t.TemplatesCount
}).OrderBy(t => t.JobGroup.GroupName)
.ToList()
};
return result;
}
}
}

View File

@@ -0,0 +1,10 @@
using PARR.Domain.DTOs.RobotStatusDetails;
using PARR.Domain.Enums;
namespace PARR.Core.Services.RobotStatusDetails.Interfaces
{
public interface IRobotStatusDetailsService
{
Task<RobotStatusDetailsResult> GetDetailsAsync(RobotsEnum robot, RobotStatusEnum status, CancellationToken cancellationToken = default);
}
}

View File

@@ -6,6 +6,7 @@ using PARR.Core.Services.RobotTaskDetailsServices.Interfaces;
using PARR.Domain.DTOs.RobotTaskDetails; using PARR.Domain.DTOs.RobotTaskDetails;
using PARR.Domain.DTOs.Shared; using PARR.Domain.DTOs.Shared;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotTaskDetailsServices.Implementations namespace PARR.Core.Services.RobotTaskDetailsServices.Implementations
{ {
@@ -32,34 +33,55 @@ namespace PARR.Core.Services.RobotTaskDetailsServices.Implementations
_taskStatusRepository = taskStatusRepository; _taskStatusRepository = taskStatusRepository;
} }
public async Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task) public async Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task, CancellationToken cancellationToken = default)
{ {
var details = await _robotConfigurationRepository.Get() // 1. Получаем группировку конфигураций
.Where(t => t.RobotCode == (int)robot && t.TaskStatusCode == (int)task) var groupedDetails = await _robotConfigurationRepository.Get()
.GroupBy(t => t.Template!.Job!.Group) .AsNoTracking()
.Where(config => config.RobotCode == (int)robot && config.TaskStatusCode == (int)task)
.GroupBy(config => config.Template!.Job!.Group)
.Select(t => new .Select(t => new
{ {
JobGroup = t.Key, JobGroup = t.Key,
TemplatesCount = t.Count() TemplatesCount = t.Count()
}).ToListAsync(); })
.ToListAsync(cancellationToken);
var robotObj = await _robotRepository.Get() // 2. Получаем сущности робота и статуса задачи)
var robotEntity = await _robotRepository.Get()
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(t => t.Code == (int)robot); .FirstOrDefaultAsync(r => r.Code == (int)robot, cancellationToken);
var taskObj = await _taskStatusRepository.Get() var taskStatusEntity = await _taskStatusRepository.Get()
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(t => t.Code == (int)task); .FirstOrDefaultAsync(t => t.Code == (int)task, cancellationToken);
if (robotEntity == null)
{
_logger.LogWarning("Робот с кодом {RobotCode} не найден в БД", robot);
throw new AppValidationException($"Робот с кодом {(int)robot} не найден");
}
if (taskStatusEntity == null)
{
_logger.LogWarning("Статус задачи с кодом {TaskCode} не найден в БД", task);
throw new AppValidationException($"Статус задачи с кодом {(int)task} не найден");
}
// 3. Маппинг и сборка результирующего DTO
var result = new RobotTaskDetailsResult var result = new RobotTaskDetailsResult
{ {
Robot = _mapper.Map<RobotResult>(robotObj), Robot = _mapper.Map<RobotResult>(robotEntity),
Task = _mapper.Map<RobotTaskStatusResult>(taskObj), Task = _mapper.Map<RobotTaskStatusResult>(taskStatusEntity),
Details = details.Select(t => new RobotTaskGroupDetailsResult Details = groupedDetails
.Select(t => new RobotTaskGroupDetailsResult
{ {
JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup), JobGroup = _mapper.Map<JobGroupShortResult>(t.JobGroup),
TemplatesCount = t.TemplatesCount TemplatesCount = t.TemplatesCount
}).OrderBy(t => t.JobGroup.GroupName).ToList() })
.OrderBy(d => d.JobGroup.GroupName)
.ToList()
}; };
return result; return result;

View File

@@ -11,6 +11,6 @@ namespace PARR.Core.Services.RobotTaskDetailsServices.Interfaces
/// <param name="robot"></param> /// <param name="robot"></param>
/// <param name="task"></param> /// <param name="task"></param>
/// <returns></returns> /// <returns></returns>
Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task); Task<RobotTaskDetailsResult> GetDetailsAsync(RobotsEnum robot, TaskStatusEnum task, CancellationToken cancellationToken = default);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PARR.DAL.Migrations
{
/// <inheritdoc />
public partial class tblRobotConfigurationsAddIndexes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_RobotConfigurations_RobotCode",
table: "RobotConfigurations");
migrationBuilder.CreateIndex(
name: "IX_RobotConfigurations_RobotCode_RobotStatusCode",
table: "RobotConfigurations",
columns: new[] { "RobotCode", "RobotStatusCode" });
migrationBuilder.CreateIndex(
name: "IX_RobotConfigurations_RobotCode_TaskStatusCode",
table: "RobotConfigurations",
columns: new[] { "RobotCode", "TaskStatusCode" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_RobotConfigurations_RobotCode_RobotStatusCode",
table: "RobotConfigurations");
migrationBuilder.DropIndex(
name: "IX_RobotConfigurations_RobotCode_TaskStatusCode",
table: "RobotConfigurations");
migrationBuilder.CreateIndex(
name: "IX_RobotConfigurations_RobotCode",
table: "RobotConfigurations",
column: "RobotCode");
}
}
}

View File

@@ -950,12 +950,14 @@ namespace PARR.DAL.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("RobotCode");
b.HasIndex("RobotStatusCode"); b.HasIndex("RobotStatusCode");
b.HasIndex("TaskStatusCode"); b.HasIndex("TaskStatusCode");
b.HasIndex("RobotCode", "RobotStatusCode");
b.HasIndex("RobotCode", "TaskStatusCode");
b.HasIndex("TemplateId", "RobotCode") b.HasIndex("TemplateId", "RobotCode")
.IsUnique(); .IsUnique();

View File

@@ -0,0 +1,18 @@
using PARR.Domain.DTOs.Shared;
namespace PARR.Domain.DTOs.RobotStatusDetails
{
public record RobotStatusDetailsResult
{
public RobotResult Robot { get; init; } = null!;
public RobotStatusResult Status { get; init; } = null!;
public List<RobotStatusGroupDetailsResult> Details { get; init; } = null!;
}
public record RobotStatusGroupDetailsResult
{
public JobGroupShortResult JobGroup { get; init; } = null!;
public int TemplatesCount { get; init; }
}
}

View File

@@ -4,15 +4,15 @@ namespace PARR.Domain.DTOs.RobotTaskDetails
{ {
public record RobotTaskDetailsResult public record RobotTaskDetailsResult
{ {
public RobotResult Robot { get; init; } public RobotResult Robot { get; init; } = null!;
public RobotTaskStatusResult Task { get; init; } public RobotTaskStatusResult Task { get; init; } = null!;
public List<RobotTaskGroupDetailsResult> Details { get; init; } public List<RobotTaskGroupDetailsResult> Details { get; init; } = null!;
} }
public record RobotTaskGroupDetailsResult public record RobotTaskGroupDetailsResult
{ {
public JobGroupShortResult JobGroup { get; init; } public JobGroupShortResult JobGroup { get; init; } = null!;
public int TemplatesCount { get; init; } public int TemplatesCount { get; init; }
} }
} }

View File

@@ -10,6 +10,8 @@ namespace PARR.Domain.Entities
[Index(nameof(TemplateId), nameof(RobotCode), IsUnique = true)] [Index(nameof(TemplateId), nameof(RobotCode), IsUnique = true)]
[Index(nameof(TemplateId), nameof(TaskStatusCode))] [Index(nameof(TemplateId), nameof(TaskStatusCode))]
[Index(nameof(TemplateId), nameof(RobotStatusCode))] [Index(nameof(TemplateId), nameof(RobotStatusCode))]
[Index(nameof(RobotCode), nameof(TaskStatusCode))]
[Index(nameof(RobotCode), nameof(RobotStatusCode))]
public class RobotConfiguration : IBaseEntity public class RobotConfiguration : IBaseEntity
{ {
[Key] [Key]