feat(api,core,dal,domain): Сохранение снапшотов состояния роботов.

This commit is contained in:
Mikhail Trubnikov
2026-06-10 16:48:16 +10:00
parent a64ab59de1
commit d736208a46
22 changed files with 4194 additions and 1 deletions

View File

@@ -256,6 +256,11 @@
public const string GetStatusesLastDay = BaseStat + "/orders/statuses/last-day/{statusCode}"; public const string GetStatusesLastDay = BaseStat + "/orders/statuses/last-day/{statusCode}";
} }
public static class StatRobotSnapshot
{
public const string Create = BaseStat + "/robot-snapshots/";
}
public static class StatRabbitMq public static class StatRabbitMq
{ {
public const string GetQueueStats = BaseStat + "/rabbitmq-queues/"; public const string GetQueueStats = BaseStat + "/rabbitmq-queues/";

View File

@@ -0,0 +1,11 @@
namespace PARR.API.Contracts.V1.Requests
{
public record StatRobotSnapshotCreateRequest
{
public int TemplateRobotsCount { get; set; }
public int ScheduleRobotsCount { get; set; }
public int MaxRobots { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
namespace PARR.API.Contracts.V1.Responses.Statistics
{
public record StatRobotSnapshotItemResponse
{
public Guid Id { get; init; }
public DateTimeOffset DateCreated { get; init; }
public int TemplateRobotsCount { get; init; }
public int ScheduleRobotsCount { get; init; }
public int MaxRobots { get; init; }
public required string Ip { get; init; }
}
}

View File

@@ -0,0 +1,52 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PARR.API.Contracts.V1;
using PARR.API.Contracts.V1.Requests;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base;
using PARR.API.Services.Interfaces;
using PARR.Core.Services.RobotSnapshotServices;
using PARR.Domain.Common.Roles;
using PARR.Domain.DTOs.RobotSnapshotDTO;
namespace PARR.API.Controllers.V1.Statistics
{
[Authorize(Roles = ParrRoles.Administrator.Role)]
public class StatRobotSnapshotController : BaseApiController
{
private readonly IRobotSnapshotService robotSnapshotService;
private readonly IMapper mapper;
private readonly IClientService clientService;
public StatRobotSnapshotController(
IRobotSnapshotService robotSnapshotService,
IMapper mapper,
IClientService clientService
)
{
this.robotSnapshotService = robotSnapshotService;
this.mapper = mapper;
this.clientService = clientService;
}
/// <summary>
/// Отправить снапшот роботов.
/// Доступ только с ролью "Робот ЕСПП"
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[Authorize(Roles = ParrRoles.EsppRobot.Role)]
[HttpPost(ApiRoutes.StatRobotSnapshot.Create)]
public async Task<IActionResult> Create([FromBody] StatRobotSnapshotCreateRequest request)
{
var obj = new CreateRobotSnapshot(request.TemplateRobotsCount, request.ScheduleRobotsCount, request.MaxRobots, clientService.GetClientIp()?.ToString() ?? "");
var snapshot = await robotSnapshotService.CreateAsync(obj);
return Created("", new Response<StatRobotSnapshotItemResponse>(mapper.Map<StatRobotSnapshotItemResponse>(snapshot), true));
}
}
}

View File

@@ -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.RobotSnapshotDTO;
using PARR.Domain.DTOs.RobotTask; using PARR.Domain.DTOs.RobotTask;
using PARR.Domain.DTOs.Shortcode; using PARR.Domain.DTOs.Shortcode;
using PARR.Domain.DTOs.TaskDTO; using PARR.Domain.DTOs.TaskDTO;
@@ -485,6 +486,9 @@ namespace PARR.API.MappingProfiles
#endregion #endregion
CreateMap<RobotSnapshotItem, StatRobotSnapshotItemResponse>();
} }
} }

View File

@@ -0,0 +1,23 @@
using FluentValidation;
using PARR.API.Contracts.V1.Requests;
namespace PARR.API.Validators
{
public class StatRobotSnapshotCreateRequestValidator: AbstractValidator<StatRobotSnapshotCreateRequest>
{
public StatRobotSnapshotCreateRequestValidator()
{
RuleFor(t => t.TemplateRobotsCount)
.GreaterThanOrEqualTo(0)
.WithMessage("Значение должно быть больше или равно нулю");
RuleFor(t => t.ScheduleRobotsCount)
.GreaterThanOrEqualTo(0)
.WithMessage("Значение должно быть больше или равно нулю");
RuleFor(t => t.MaxRobots)
.GreaterThanOrEqualTo(0)
.WithMessage("Значение должно быть больше или равно нулю");
}
}
}

View File

@@ -7,6 +7,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.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;
using PARR.Core.Services.Shortcodes; using PARR.Core.Services.Shortcodes;
@@ -105,6 +106,7 @@ namespace PARR.Core
services.AddTransient<IMatchingStatusService, MatchingStatusService>(); services.AddTransient<IMatchingStatusService, MatchingStatusService>();
services.AddScoped<IRobotTaskService, RobotTaskService>(); services.AddScoped<IRobotTaskService, RobotTaskService>();
services.AddScoped<IRobotSnapshotService, RobotSnapshotService>();
services.AddScoped<IUnitService, UnitService>(); services.AddScoped<IUnitService, UnitService>();
services.AddScoped<UnitCacheService>(); services.AddScoped<UnitCacheService>();

View File

@@ -0,0 +1,13 @@
using AutoMapper;
using PARR.Domain.DTOs.RobotSnapshotDTO;
namespace PARR.Core.Infrastructure.Mapping.RobotSnapshot
{
public class RobotSnapshotMappingProfile : Profile
{
public RobotSnapshotMappingProfile()
{
CreateMap<PARR.Domain.Entities.RobotEntities.RobotSnapshot, RobotSnapshotItem>();
}
}
}

View File

@@ -0,0 +1,9 @@
using PARR.Core.Repositories.Base;
using PARR.Domain.Entities.RobotEntities;
namespace PARR.Core.Repositories.Interfaces.RobotRepositories
{
public interface IRobotSnapshotRepository : IBaseRepository<RobotSnapshot>
{
}
}

View File

@@ -0,0 +1,9 @@
using PARR.Domain.DTOs.RobotSnapshotDTO;
namespace PARR.Core.Services.RobotSnapshotServices
{
public interface IRobotSnapshotService
{
Task<RobotSnapshotItem> CreateAsync(CreateRobotSnapshot robotSnapshot);
}
}

View File

@@ -0,0 +1,46 @@
using AutoMapper;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.Entities.RobotEntities;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotSnapshotServices
{
internal class RobotSnapshotService : IRobotSnapshotService
{
private readonly IRobotSnapshotRepository robotSnapshotRepository;
private readonly IMapper mapper;
public RobotSnapshotService(
IRobotSnapshotRepository robotSnapshotRepository,
IMapper mapper
)
{
this.robotSnapshotRepository = robotSnapshotRepository;
this.mapper = mapper;
}
public async Task<RobotSnapshotItem> CreateAsync(CreateRobotSnapshot robotSnapshot)
{
var snapshot = new RobotSnapshot
{
Id = Guid.NewGuid(),
TemplateRobotsCount = robotSnapshot.TemplateRobotsCount,
ScheduleRobotsCount = robotSnapshot.ScheduleRobotsCount,
MaxRobots = robotSnapshot.MaxRobots,
Ip = robotSnapshot.Ip
};
var createdResult = await robotSnapshotRepository.CreateAsync(snapshot);
var commitResult = await robotSnapshotRepository.CommitAsync();
if (!createdResult || !commitResult)
throw new DbErrorException("Ошибка сохранения в БД");
return mapper.Map<RobotSnapshotItem>(snapshot);
}
}
}

View File

@@ -4,6 +4,7 @@ using PARR.Domain.Common.Roles;
using PARR.Domain.Common.Template; using PARR.Domain.Common.Template;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.RobotEntities;
using PARR.Domain.Entities.Schedule; using PARR.Domain.Entities.Schedule;
using PARR.Domain.Entities.TaskEntities; using PARR.Domain.Entities.TaskEntities;
using PARR.Domain.Entities.Unit; using PARR.Domain.Entities.Unit;
@@ -126,6 +127,12 @@ namespace PARR.DAL.Context
#endregion #endregion
#region Robot
public DbSet<RobotSnapshot> RobotSnapshots { get; set; }
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Core.Repositories.Interfaces.Schedule; using PARR.Core.Repositories.Interfaces.Schedule;
using PARR.Core.Repositories.Interfaces.TaskRepositories; using PARR.Core.Repositories.Interfaces.TaskRepositories;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
@@ -11,6 +12,7 @@ using PARR.DAL.Configurations.DbSettings;
using PARR.DAL.Context; using PARR.DAL.Context;
using PARR.DAL.Repositories; using PARR.DAL.Repositories;
using PARR.DAL.Repositories.Job; using PARR.DAL.Repositories.Job;
using PARR.DAL.Repositories.RobotRepositories;
using PARR.DAL.Repositories.Schedule; using PARR.DAL.Repositories.Schedule;
using PARR.DAL.Repositories.TaskRepositories; using PARR.DAL.Repositories.TaskRepositories;
using PARR.DAL.Repositories.Unit; using PARR.DAL.Repositories.Unit;
@@ -79,6 +81,9 @@ namespace PARR.DAL
services.AddTransient<IParrComponentRepository, ParrComponentRepository>(); services.AddTransient<IParrComponentRepository, ParrComponentRepository>();
services.AddTransient<ITemplateStatusTypeRepository, TemplateStatusTypeRepository>(); services.AddTransient<ITemplateStatusTypeRepository, TemplateStatusTypeRepository>();
services.AddScoped<IRobotSnapshotRepository, RobotSnapshotRepository>();
#region Schedule #region Schedule
services.AddTransient<IScheduleExcludeTypeRepository, ScheduleExcludeTypeRepository>(); services.AddTransient<IScheduleExcludeTypeRepository, ScheduleExcludeTypeRepository>();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PARR.DAL.Migrations
{
/// <inheritdoc />
public partial class tblRobotSnapshot : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "robot");
migrationBuilder.CreateTable(
name: "Snapshots",
schema: "robot",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
DateCreated = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
TemplateRobotsCount = table.Column<int>(type: "integer", nullable: false, comment: "Количество запущенных роботов по шаблонам"),
ScheduleRobotsCount = table.Column<int>(type: "integer", nullable: false, comment: "Количество запущенных роботов по расписаниям"),
MaxRobots = table.Column<int>(type: "integer", nullable: false, comment: "Максимально разрешенное количество роботов на сервере"),
Ip = table.Column<string>(type: "character varying(45)", maxLength: 45, nullable: false, comment: "Текущий IP-адрес сервера")
},
constraints: table =>
{
table.PrimaryKey("PK_Snapshots", x => x.Id);
},
comment: "Снимки роботов");
migrationBuilder.UpdateData(
table: "Settings",
keyColumn: "Name",
keyValue: "RobotWaitTime",
column: "Value",
value: "00:10:00");
migrationBuilder.CreateIndex(
name: "IX_Snapshots_Ip_DateCreated",
schema: "robot",
table: "Snapshots",
columns: new[] { "Ip", "DateCreated" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Snapshots",
schema: "robot");
migrationBuilder.UpdateData(
table: "Settings",
keyColumn: "Name",
keyValue: "RobotWaitTime",
column: "Value",
value: "00:15:00");
}
}
}

View File

@@ -867,6 +867,43 @@ namespace PARR.DAL.Migrations
b.ToTable("RobotConfigurations"); b.ToTable("RobotConfigurations");
}); });
modelBuilder.Entity("PARR.Domain.Entities.RobotEntities.RobotSnapshot", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Ip")
.IsRequired()
.HasMaxLength(45)
.HasColumnType("character varying(45)")
.HasComment("Текущий IP-адрес сервера");
b.Property<int>("MaxRobots")
.HasColumnType("integer")
.HasComment("Максимально разрешенное количество роботов на сервере");
b.Property<int>("ScheduleRobotsCount")
.HasColumnType("integer")
.HasComment("Количество запущенных роботов по расписаниям");
b.Property<int>("TemplateRobotsCount")
.HasColumnType("integer")
.HasComment("Количество запущенных роботов по шаблонам");
b.HasKey("Id");
b.HasIndex("Ip", "DateCreated");
b.ToTable("Snapshots", "robot", t =>
{
t.HasComment("Снимки роботов");
});
});
modelBuilder.Entity("PARR.Domain.Entities.RobotHistory", b => modelBuilder.Entity("PARR.Domain.Entities.RobotHistory", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -2261,7 +2298,7 @@ namespace PARR.DAL.Migrations
{ {
Name = "RobotWaitTime", Name = "RobotWaitTime",
Description = "Время ожидания выполнения роботом задания", Description = "Время ожидания выполнения роботом задания",
Value = "00:15:00" Value = "00:10:00"
}, },
new new
{ {

View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.DAL.Context;
using PARR.DAL.Repositories.Base;
using PARR.Domain.Entities.RobotEntities;
namespace PARR.DAL.Repositories.RobotRepositories
{
internal class RobotSnapshotRepository : BaseRepository<RobotSnapshot>, IRobotSnapshotRepository
{
public RobotSnapshotRepository(ILogger<RobotSnapshotRepository> logger, DataContext dataContext) : base(logger, dataContext) { }
}
}

View File

@@ -27,5 +27,11 @@
/// Управление очередями /// Управление очередями
/// </summary> /// </summary>
public const string Task = "task"; public const string Task = "task";
/// <summary>
/// Робот
/// </summary>
public const string Robot = "robot";
} }
} }

View File

@@ -0,0 +1,5 @@
namespace PARR.Domain.DTOs.RobotSnapshotDTO
{
public record CreateRobotSnapshot(int TemplateRobotsCount, int ScheduleRobotsCount, int MaxRobots, string Ip);
}

View File

@@ -0,0 +1,17 @@
namespace PARR.Domain.DTOs.RobotSnapshotDTO
{
public record RobotSnapshotItem
{
public Guid Id { get; init; }
public DateTimeOffset DateCreated { get; init; }
public int TemplateRobotsCount { get; init; }
public int ScheduleRobotsCount { get; init; }
public int MaxRobots { get; init; }
public required string Ip { get; init; }
}
}

View File

@@ -0,0 +1,38 @@
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.RobotEntities
{
/// <summary>
/// Снимок роботов с одного сервера
/// </summary>
[Table("Snapshots", Schema = DatabaseSchemas.Robot)]
[Index(nameof(Ip), nameof(DateCreated))]
[Comment("Снимки роботов")]
public class RobotSnapshot : IBaseEntity
{
[Key]
public Guid Id { get; set; }
public DateTimeOffset DateCreated { get; set; }
[NotMapped]
public DateTimeOffset? DateModified { get; set; }
[Comment("Количество запущенных роботов по шаблонам")]
public int TemplateRobotsCount { get; set; }
[Comment("Количество запущенных роботов по расписаниям")]
public int ScheduleRobotsCount { get; set; }
[Comment("Максимально разрешенное количество роботов на сервере")]
public int MaxRobots { get; set; }
[MaxLength(45)]
[Comment("Текущий IP-адрес сервера")]
public required string Ip { get; set; }
}
}

View File

@@ -23,5 +23,6 @@
<ProjectReference Include="..\PARR.EsppApi\PARR.EsppApi.csproj" /> <ProjectReference Include="..\PARR.EsppApi\PARR.EsppApi.csproj" />
<ProjectReference Include="..\PARR.Infrastructure\PARR.Infrastructure.csproj" /> <ProjectReference Include="..\PARR.Infrastructure\PARR.Infrastructure.csproj" />
<ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" /> <ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" />
<ProjectReference Include="..\PARR.TemplateMatcher\PARR.TemplateMatcher.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>