diff --git a/Persistence.API/Controllers/DataSaubController.cs b/Persistence.API/Controllers/DataSaubController.cs index 63069a9..202e527 100644 --- a/Persistence.API/Controllers/DataSaubController.cs +++ b/Persistence.API/Controllers/DataSaubController.cs @@ -4,6 +4,10 @@ using Persistence.Repositories; using Persistence.Repository.Data; namespace Persistence.API.Controllers; + +/// +/// Работа с временными данными +/// [ApiController] [Authorize] [Route("api/[controller]")] diff --git a/Persistence.API/Controllers/SetpointController.cs b/Persistence.API/Controllers/SetpointController.cs index 9a6bd61..42a5ff5 100644 --- a/Persistence.API/Controllers/SetpointController.cs +++ b/Persistence.API/Controllers/SetpointController.cs @@ -1,53 +1,112 @@ -using Microsoft.AspNetCore.Authorization; +using System.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Models; using Persistence.Repositories; -namespace Persistence.API.Controllers +namespace Persistence.API.Controllers; + +/// +/// Работа с уставками +/// +[ApiController] +[Authorize] +[Route("api/[controller]")] +public class SetpointController : ControllerBase, ISetpointApi { - [ApiController] - [Authorize] - [Route("api/[controller]")] - public class SetpointController : ControllerBase, ISetpointApi + private readonly ISetpointRepository setpointRepository; + + public SetpointController(ISetpointRepository setpointRepository) { - private readonly ISetpointRepository setpointRepository; + this.setpointRepository = setpointRepository; + } - public SetpointController(ISetpointRepository setpointRepository) - { - this.setpointRepository = setpointRepository; - } + /// + /// Получить актуальные значения уставок + /// + /// + /// + /// + [HttpGet("current")] + public async Task>> GetCurrent([FromQuery] IEnumerable setpointKeys, CancellationToken token) + { + var result = await setpointRepository.GetCurrent(setpointKeys, token); - [HttpGet("current")] - public async Task>> GetCurrent([FromQuery] IEnumerable setpointKeys, CancellationToken token) - { - var result = await setpointRepository.GetCurrent(setpointKeys, token); + return Ok(result); + } - return Ok(result); - } + /// + /// Получить значения уставок за определенный момент времени + /// + /// + /// + /// + /// + [HttpGet("history")] + public async Task>> GetHistory([FromQuery] IEnumerable setpointKeys, [FromQuery] DateTimeOffset historyMoment, CancellationToken token) + { + var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token); - [HttpGet("history")] - public async Task>> GetHistory([FromQuery] IEnumerable setpointKeys, [FromQuery] DateTimeOffset historyMoment, CancellationToken token) - { - var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token); + return Ok(result); + } - return Ok(result); - } + /// + /// Получить историю изменений значений уставок + /// + /// + /// + /// + [HttpGet("log")] + public async Task>>> GetLog([FromQuery] IEnumerable setpointKeys, CancellationToken token) + { + var result = await setpointRepository.GetLog(setpointKeys, token); - [HttpGet("log")] - public async Task>>> GetLog([FromQuery] IEnumerable setpointKeys, CancellationToken token) - { - var result = await setpointRepository.GetLog(setpointKeys, token); + return Ok(result); + } - return Ok(result); - } + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + [HttpGet("range")] + public async Task> GetDatesRangeAsync(CancellationToken token) + { + var result = await setpointRepository.GetDatesRangeAsync(token); - [HttpPost] - public async Task> Save(Guid setpointKey, object newValue, CancellationToken token) - { - // ToDo: вычитка idUser - await setpointRepository.Save(setpointKey, newValue, 0, token); + return Ok(result); + } - return Ok(); - } + /// + /// Получить порцию записей, начиная с заданной даты + /// + /// + /// + /// + /// + [HttpGet("part")] + public async Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) + { + var result = await setpointRepository.GetPart(dateBegin, take, token); + + return Ok(result); + } + + /// + /// Сохранить уставку + /// + /// + /// + /// + /// + /// + [HttpPost] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] + public async Task Add(Guid setpointKey, object newValue, CancellationToken token) + { + var userId = User.GetUserId(); + await setpointRepository.Add(setpointKey, newValue, userId, token); + + return CreatedAtAction(nameof(Add), true); } } diff --git a/Persistence.API/Controllers/TechMessagesController.cs b/Persistence.API/Controllers/TechMessagesController.cs new file mode 100644 index 0000000..d2691c1 --- /dev/null +++ b/Persistence.API/Controllers/TechMessagesController.cs @@ -0,0 +1,128 @@ +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Persistence.Models; +using Persistence.Repositories; + +namespace Persistence.API.Controllers; + +/// +/// Работа с состояниями систем автобурения (АБ) +/// +[ApiController] +[Authorize] +[Route("api/[controller]")] +public class TechMessagesController : ControllerBase +{ + private readonly ITechMessagesRepository techMessagesRepository; + private static readonly Dictionary categories = new Dictionary() + { + { 0, "System" }, + { 1, "Авария" }, + { 2, "Предупреждение" }, + { 3, "Инфо" }, + { 4, "Прочее" } + }; + + public TechMessagesController(ITechMessagesRepository techMessagesRepository) + { + this.techMessagesRepository = techMessagesRepository; + } + + /// + /// Получить список технологических сообщений в виде страницы + /// + /// + /// + /// + [HttpGet] + public async Task>> GetPage([FromQuery] RequestDto request, CancellationToken token) + { + var result = await techMessagesRepository.GetPage(request, token); + + return Ok(result); + } + + /// + /// Получить статистику по системам + /// + /// + /// + /// + /// + [HttpGet("statistics")] + public async Task>> GetStatistics([FromQuery] IEnumerable autoDrillingSystem, [FromQuery] IEnumerable categoryIds, CancellationToken token) + { + var result = await techMessagesRepository.GetStatistics(autoDrillingSystem, categoryIds, token); + + return Ok(result); + } + + /// + /// Получить список всех систем + /// + /// + /// + [HttpGet("systems")] + public async Task>> GetSystems(CancellationToken token) + { + var result = await techMessagesRepository.GetSystems(token); + + return Ok(result); + } + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + [HttpGet("range")] + public async Task> GetDatesRangeAsync(CancellationToken token) + { + var result = await techMessagesRepository.GetDatesRangeAsync(token); + + return Ok(result); + } + + /// + /// Получить порцию записей, начиная с заданной даты + /// + /// + /// + /// + /// + [HttpGet("part")] + public async Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) + { + var result = await techMessagesRepository.GetPart(dateBegin, take, token); + + return Ok(result); + } + + /// + /// Добавить новые технологические сообщения + /// + /// + /// + /// + [HttpPost] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] + public async Task AddRange([FromBody] IEnumerable dtos, CancellationToken token) + { + var userId = User.GetUserId(); + + var result = await techMessagesRepository.AddRange(dtos, userId, token); + + return CreatedAtAction(nameof(AddRange), result); + } + + /// + /// Получить словарь категорий + /// + /// + [HttpGet("categories")] + public ActionResult> GetImportantCategories() + { + return Ok(categories); + } +} \ No newline at end of file diff --git a/Persistence.API/Controllers/TimeSeriesController.cs b/Persistence.API/Controllers/TimeSeriesController.cs index a4a860a..6991759 100644 --- a/Persistence.API/Controllers/TimeSeriesController.cs +++ b/Persistence.API/Controllers/TimeSeriesController.cs @@ -4,6 +4,7 @@ using Persistence.Models; using Persistence.Repositories; namespace Persistence.API.Controllers; + [ApiController] [Authorize] [Route("api/[controller]")] @@ -12,37 +13,62 @@ public class TimeSeriesController : ControllerBase, ITimeSeriesDataApi timeSeriesDataRepository; - public TimeSeriesController(ITimeSeriesDataRepository timeSeriesDataRepository) - { - this.timeSeriesDataRepository = timeSeriesDataRepository; + public TimeSeriesController(ITimeSeriesDataRepository timeSeriesDataRepository) + { + this.timeSeriesDataRepository = timeSeriesDataRepository; } - [HttpGet] + /// + /// Получить список объектов, удовлетворяющий диапазону дат + /// + /// + /// + /// + [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task Get(DateTimeOffset dateBegin, CancellationToken token) { - var result = await this.timeSeriesDataRepository.GetGtDate(dateBegin, token); + var result = await timeSeriesDataRepository.GetGtDate(dateBegin, token); return Ok(result); } - [HttpGet("datesRange")] + /// + /// Получить диапазон дат, для которых есть данные в репозиторие + /// + /// + /// + [HttpGet("datesRange")] public async Task GetDatesRange(CancellationToken token) { - var result = await this.timeSeriesDataRepository.GetDatesRange(token); + var result = await timeSeriesDataRepository.GetDatesRange(token); return Ok(result); } - [HttpGet("resampled")] + /// + /// Получить список объектов с прореживанием, удовлетворяющий диапазону дат + /// + /// + /// + /// + /// + /// + [HttpGet("resampled")] public async Task GetResampledData(DateTimeOffset dateBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default) { - var result = await this.timeSeriesDataRepository.GetResampledData(dateBegin, intervalSec, approxPointsCount, token); + var result = await timeSeriesDataRepository.GetResampledData(dateBegin, intervalSec, approxPointsCount, token); return Ok(result); } - [HttpPost] - public async Task InsertRange(IEnumerable dtos, CancellationToken token) + /// + /// Добавить записи + /// + /// + /// + /// + [HttpPost] + public async Task AddRange(IEnumerable dtos, CancellationToken token) { - var result = await this.timeSeriesDataRepository.InsertRange(dtos, token); + var result = await timeSeriesDataRepository.AddRange(dtos, token); return Ok(result); } diff --git a/Persistence.API/Controllers/TimestampedSetController.cs b/Persistence.API/Controllers/TimestampedSetController.cs index f18e4c8..bd0e97e 100644 --- a/Persistence.API/Controllers/TimestampedSetController.cs +++ b/Persistence.API/Controllers/TimestampedSetController.cs @@ -32,9 +32,9 @@ public class TimestampedSetController : ControllerBase /// кол-во затронутых записей [HttpPost] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] - public async Task InsertRange([FromRoute]Guid idDiscriminator, [FromBody]IEnumerable sets, CancellationToken token) + public async Task AddRange([FromRoute]Guid idDiscriminator, [FromBody]IEnumerable sets, CancellationToken token) { - var result = await repository.InsertRange(idDiscriminator, sets, token); + var result = await repository.AddRange(idDiscriminator, sets, token); return Ok(result); } diff --git a/Persistence.API/DependencyInjection.cs b/Persistence.API/DependencyInjection.cs index cdfca4c..19cedc9 100644 --- a/Persistence.API/DependencyInjection.cs +++ b/Persistence.API/DependencyInjection.cs @@ -1,8 +1,12 @@ +using System.Reflection; using System.Text.Json.Nodes; +using Mapster; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; +using Persistence.Database.Entity; +using Persistence.Models; using Persistence.Models.Configurations; using Swashbuckle.AspNetCore.SwaggerGen; @@ -10,6 +14,12 @@ namespace Persistence.API; public static class DependencyInjection { + public static void MapsterSetup() + { + TypeAdapterConfig.GlobalSettings.Default.Config + .ForType() + .Ignore(dest => dest.System, dest => dest.SystemId); + } public static void AddSwagger(this IServiceCollection services, IConfiguration configuration) { services.AddSwaggerGen(c => @@ -38,11 +48,10 @@ public static class DependencyInjection c.AddKeycloackSecurity(configuration); else c.AddDefaultSecurity(configuration); - //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - //var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - //var includeControllerXmlComment = true; - //options.IncludeXmlComments(xmlPath, includeControllerXmlComment); - //options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment); + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + var includeControllerXmlComment = true; + c.IncludeXmlComments(xmlPath, includeControllerXmlComment); }); } diff --git a/Persistence.API/Persistence.API.csproj b/Persistence.API/Persistence.API.csproj index 2b8cb73..dfff363 100644 --- a/Persistence.API/Persistence.API.csproj +++ b/Persistence.API/Persistence.API.csproj @@ -5,10 +5,13 @@ enable enable Linux + True + $(NoWarn);1591 - + + diff --git a/Persistence.API/Startup.cs b/Persistence.API/Startup.cs index e074845..98ad4aa 100644 --- a/Persistence.API/Startup.cs +++ b/Persistence.API/Startup.cs @@ -23,6 +23,9 @@ public class Startup services.AddInfrastructure(); services.AddPersistenceDbContext(Configuration); services.AddJWTAuthentication(Configuration); + services.AddMemoryCache(); + + DependencyInjection.MapsterSetup(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/Persistence.Client/Clients/ISetpointClient.cs b/Persistence.Client/Clients/ISetpointClient.cs index 49733f0..0b81ffa 100644 --- a/Persistence.Client/Clients/ISetpointClient.cs +++ b/Persistence.Client/Clients/ISetpointClient.cs @@ -4,7 +4,7 @@ using Refit; namespace Persistence.Client.Clients; /// -/// Интерфейс для тестирования API, предназначенного для работы с уставками +/// Интерфейс клиента для работы с уставками /// public interface ISetpointClient { @@ -19,6 +19,12 @@ public interface ISetpointClient [Get($"{BaseRoute}/log")] Task>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); + [Get($"{BaseRoute}/range")] + Task> GetDatesRangeAsync(CancellationToken token); + + [Get($"{BaseRoute}/part")] + Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); + [Post($"{BaseRoute}/")] - Task Save(Guid setpointKey, object newValue); + Task Add(Guid setpointKey, object newValue); } diff --git a/Persistence.Client/Clients/ITechMessagesClient.cs b/Persistence.Client/Clients/ITechMessagesClient.cs new file mode 100644 index 0000000..878c6cf --- /dev/null +++ b/Persistence.Client/Clients/ITechMessagesClient.cs @@ -0,0 +1,31 @@ +using Persistence.Models; +using Refit; + +namespace Persistence.Client.Clients +{ + /// + /// Интерфейс клиента для хранения технологических сообщений + /// + public interface ITechMessagesClient + { + private const string BaseRoute = "/api/techMessages"; + + [Get($"{BaseRoute}")] + Task>> GetPage([Query] RequestDto request, CancellationToken token); + + [Post($"{BaseRoute}")] + Task> AddRange([Body] IEnumerable dtos, CancellationToken token); + + [Get($"{BaseRoute}/systems")] + Task>> GetSystems(CancellationToken token); + + [Get($"{BaseRoute}/range")] + Task> GetDatesRangeAsync(CancellationToken token); + + [Get($"{BaseRoute}/part")] + Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); + + [Get($"{BaseRoute}/statistics")] + Task>> GetStatistics([Query] string autoDrillingSystem, [Query] int categoryId, CancellationToken token); + } +} diff --git a/Persistence.Client/Clients/ITimeSeriesClient.cs b/Persistence.Client/Clients/ITimeSeriesClient.cs index 8f7ef0e..8e97836 100644 --- a/Persistence.Client/Clients/ITimeSeriesClient.cs +++ b/Persistence.Client/Clients/ITimeSeriesClient.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using Persistence.Models; +using Persistence.Models; using Refit; namespace Persistence.Client.Clients; @@ -9,7 +8,7 @@ public interface ITimeSeriesClient private const string BaseRoute = "/api/dataSaub"; [Post($"{BaseRoute}")] - Task> InsertRange(IEnumerable dtos); + Task> AddRange(IEnumerable dtos); [Get($"{BaseRoute}")] Task>> Get(DateTimeOffset dateBegin, DateTimeOffset dateEnd); diff --git a/Persistence.Client/Clients/ITimestampedSetClient.cs b/Persistence.Client/Clients/ITimestampedSetClient.cs index 95e8bd1..bbff603 100644 --- a/Persistence.Client/Clients/ITimestampedSetClient.cs +++ b/Persistence.Client/Clients/ITimestampedSetClient.cs @@ -20,7 +20,7 @@ public interface ITimestampedSetClient /// /// [Post(baseUrl)] - Task> InsertRange(Guid idDiscriminator, IEnumerable sets); + Task> AddRange(Guid idDiscriminator, IEnumerable sets); /// /// Получение данных с фильтрацией. Значение фильтра null - отключен diff --git a/Persistence.Client/Helpers/ApiTokenHelper.cs b/Persistence.Client/Helpers/ApiTokenHelper.cs index e508922..5eed66e 100644 --- a/Persistence.Client/Helpers/ApiTokenHelper.cs +++ b/Persistence.Client/Helpers/ApiTokenHelper.cs @@ -29,8 +29,10 @@ public static class ApiTokenHelper private static string CreateDefaultJwtToken(this AuthUser authUser) { + var nameIdetifier = Guid.NewGuid().ToString(); var claims = new List() { + new(ClaimTypes.NameIdentifier, nameIdetifier), new("client_id", authUser.ClientId), new("username", authUser.Username), new("password", authUser.Password), diff --git a/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.cs b/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.cs index 49e438a..ea6fccf 100644 --- a/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.cs +++ b/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.cs @@ -18,7 +18,7 @@ namespace Persistence.Database.Postgres.Migrations Key = table.Column(type: "uuid", nullable: false, comment: "Ключ"), Created = table.Column(type: "timestamp with time zone", nullable: false, comment: "Дата изменения уставки"), Value = table.Column(type: "jsonb", nullable: false, comment: "Значение уставки"), - IdUser = table.Column(type: "integer", nullable: false, comment: "Id автора последнего изменения") + IdUser = table.Column(type: "uuid", nullable: false, comment: "Id автора последнего изменения") }, constraints: table => { diff --git a/Persistence.Database.Postgres/Migrations/20241202072250_TechMessageMigration.Designer.cs b/Persistence.Database.Postgres/Migrations/20241202072250_TechMessageMigration.Designer.cs new file mode 100644 index 0000000..6ed33f7 --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241202072250_TechMessageMigration.Designer.cs @@ -0,0 +1,233 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Persistence.Database.Model; + +#nullable disable + +namespace Persistence.Database.Postgres.Migrations +{ + [DbContext(typeof(PersistenceDbContext))] + [Migration("20241202072250_TechMessageMigration")] + partial class TechMessageMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("Russian_Russia.1251") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Persistence.Database.Entity.DrillingSystem", b => + { + b.Property("SystemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Id системы автобурения"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("Описание системы автобурения"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(256)") + .HasComment("Наименование системы автобурения"); + + b.HasKey("SystemId"); + + b.ToTable("DrillingSystem"); + }); + + modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Id события"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasComment("Id Категории важности"); + + b.Property("Depth") + .HasColumnType("double precision") + .HasComment("Глубина забоя"); + + b.Property("MessageText") + .IsRequired() + .HasColumnType("varchar(512)") + .HasComment("Текст сообщения"); + + b.Property("SystemId") + .HasColumnType("uuid") + .HasComment("Id системы автобурения, к которой относится сообщение"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasComment("Дата возникновения"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("Id пользователя за пультом бурильщика"); + + b.HasKey("EventId"); + + b.HasIndex("SystemId"); + + b.ToTable("TechMessage"); + }); + + modelBuilder.Entity("Persistence.Database.Entity.TimestampedSet", b => + { + b.Property("IdDiscriminator") + .HasColumnType("uuid") + .HasComment("Дискриминатор ссылка на тип сохраняемых данных"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasComment("Отметка времени, строго в UTC"); + + b.Property("Set") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Набор сохраняемых данных"); + + b.HasKey("IdDiscriminator", "Timestamp"); + + b.ToTable("TimestampedSets", t => + { + t.HasComment("Общая таблица данных временных рядов"); + }); + }); + + modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => + { + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("AxialLoad") + .HasColumnType("double precision") + .HasColumnName("axialLoad"); + + b.Property("BitDepth") + .HasColumnType("double precision") + .HasColumnName("bitDepth"); + + b.Property("BlockPosition") + .HasColumnType("double precision") + .HasColumnName("blockPosition"); + + b.Property("BlockSpeed") + .HasColumnType("double precision") + .HasColumnName("blockSpeed"); + + b.Property("Flow") + .HasColumnType("double precision") + .HasColumnName("flow"); + + b.Property("HookWeight") + .HasColumnType("double precision") + .HasColumnName("hookWeight"); + + b.Property("IdFeedRegulator") + .HasColumnType("integer") + .HasColumnName("idFeedRegulator"); + + b.Property("Mode") + .HasColumnType("integer") + .HasColumnName("mode"); + + b.Property("Mse") + .HasColumnType("double precision") + .HasColumnName("mse"); + + b.Property("MseState") + .HasColumnType("smallint") + .HasColumnName("mseState"); + + b.Property("Pressure") + .HasColumnType("double precision") + .HasColumnName("pressure"); + + b.Property("Pump0Flow") + .HasColumnType("double precision") + .HasColumnName("pump0Flow"); + + b.Property("Pump1Flow") + .HasColumnType("double precision") + .HasColumnName("pump1Flow"); + + b.Property("Pump2Flow") + .HasColumnType("double precision") + .HasColumnName("pump2Flow"); + + b.Property("RotorSpeed") + .HasColumnType("double precision") + .HasColumnName("rotorSpeed"); + + b.Property("RotorTorque") + .HasColumnType("double precision") + .HasColumnName("rotorTorque"); + + b.Property("User") + .HasColumnType("text") + .HasColumnName("user"); + + b.Property("WellDepth") + .HasColumnType("double precision") + .HasColumnName("wellDepth"); + + b.HasKey("Date"); + + b.ToTable("DataSaub"); + }); + + modelBuilder.Entity("Persistence.Database.Model.Setpoint", b => + { + b.Property("Key") + .HasColumnType("uuid") + .HasComment("Ключ"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasComment("Дата создания уставки"); + + b.Property("IdUser") + .HasColumnType("uuid") + .HasComment("Id автора последнего изменения"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Значение уставки"); + + b.HasKey("Key", "Created"); + + b.ToTable("Setpoint"); + }); + + modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b => + { + b.HasOne("Persistence.Database.Entity.DrillingSystem", "System") + .WithMany() + .HasForeignKey("SystemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("System"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/20241202072250_TechMessageMigration.cs b/Persistence.Database.Postgres/Migrations/20241202072250_TechMessageMigration.cs new file mode 100644 index 0000000..ccf18d4 --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241202072250_TechMessageMigration.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Database.Postgres.Migrations +{ + /// + public partial class TechMessageMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DrillingSystem", + columns: table => new + { + SystemId = table.Column(type: "uuid", nullable: false, comment: "Id системы автобурения"), + Name = table.Column(type: "varchar(256)", nullable: false, comment: "Наименование системы автобурения"), + Description = table.Column(type: "text", nullable: true, comment: "Описание системы автобурения") + }, + constraints: table => + { + table.PrimaryKey("PK_DrillingSystem", x => x.SystemId); + }); + + migrationBuilder.CreateTable( + name: "TechMessage", + columns: table => new + { + EventId = table.Column(type: "uuid", nullable: false, comment: "Id события"), + CategoryId = table.Column(type: "integer", nullable: false, comment: "Id Категории важности"), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false, comment: "Дата возникновения"), + Depth = table.Column(type: "double precision", nullable: true, comment: "Глубина забоя"), + MessageText = table.Column(type: "varchar(512)", nullable: false, comment: "Текст сообщения"), + SystemId = table.Column(type: "uuid", nullable: false, comment: "Id системы автобурения, к которой относится сообщение"), + UserId = table.Column(type: "uuid", nullable: false, comment: "Id пользователя за пультом бурильщика") + }, + constraints: table => + { + table.PrimaryKey("PK_TechMessage", x => x.EventId); + table.ForeignKey( + name: "FK_TechMessage_DrillingSystem_SystemId", + column: x => x.SystemId, + principalTable: "DrillingSystem", + principalColumn: "SystemId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TechMessage_SystemId", + table: "TechMessage", + column: "SystemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TechMessage"); + + migrationBuilder.DropTable( + name: "DrillingSystem"); + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs index f41f669..e53c81a 100644 --- a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs +++ b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs @@ -24,6 +24,89 @@ namespace Persistence.Database.Postgres.Migrations NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Persistence.Database.Entity.DrillingSystem", b => + { + b.Property("SystemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Id системы автобурения"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("Описание системы автобурения"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(256)") + .HasComment("Наименование системы автобурения"); + + b.HasKey("SystemId"); + + b.ToTable("DrillingSystem"); + }); + + modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Id события"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasComment("Id Категории важности"); + + b.Property("Depth") + .HasColumnType("double precision") + .HasComment("Глубина забоя"); + + b.Property("MessageText") + .IsRequired() + .HasColumnType("varchar(512)") + .HasComment("Текст сообщения"); + + b.Property("SystemId") + .HasColumnType("uuid") + .HasComment("Id системы автобурения, к которой относится сообщение"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasComment("Дата возникновения"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("Id пользователя за пультом бурильщика"); + + b.HasKey("EventId"); + + b.HasIndex("SystemId"); + + b.ToTable("TechMessage"); + }); + + modelBuilder.Entity("Persistence.Database.Entity.TimestampedSet", b => + { + b.Property("IdDiscriminator") + .HasColumnType("uuid") + .HasComment("Дискриминатор ссылка на тип сохраняемых данных"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasComment("Отметка времени, строго в UTC"); + + b.Property("Set") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Набор сохраняемых данных"); + + b.HasKey("IdDiscriminator", "Timestamp"); + + b.ToTable("TimestampedSets", t => + { + t.HasComment("Общая таблица данных временных рядов"); + }); + }); + modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => { b.Property("Date") @@ -115,10 +198,10 @@ namespace Persistence.Database.Postgres.Migrations b.Property("Created") .HasColumnType("timestamp with time zone") - .HasComment("Дата изменения уставки"); + .HasComment("Дата создания уставки"); - b.Property("IdUser") - .HasColumnType("integer") + b.Property("IdUser") + .HasColumnType("uuid") .HasComment("Id автора последнего изменения"); b.Property("Value") @@ -130,6 +213,17 @@ namespace Persistence.Database.Postgres.Migrations b.ToTable("Setpoint"); }); + + modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b => + { + b.HasOne("Persistence.Database.Entity.DrillingSystem", "System") + .WithMany() + .HasForeignKey("SystemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("System"); + }); #pragma warning restore 612, 618 } } diff --git a/Persistence.Database.Postgres/PersistenceDbContext.cs b/Persistence.Database.Postgres/PersistenceDbContext.cs index a0cae6a..89b09db 100644 --- a/Persistence.Database.Postgres/PersistenceDbContext.cs +++ b/Persistence.Database.Postgres/PersistenceDbContext.cs @@ -10,7 +10,9 @@ public partial class PersistenceDbContext : DbContext public DbSet Setpoint => Set(); - public DbSet TimestampedSets => Set(); + public DbSet TechMessage => Set(); + + public DbSet TimestampedSets => Set(); public PersistenceDbContext() : base() @@ -40,5 +42,14 @@ public partial class PersistenceDbContext : DbContext modelBuilder.Entity() .Property(e => e.Set) .HasJsonConversion(); - } + + modelBuilder.Entity(entity => + { + entity.HasOne(t => t.System) + .WithMany() + .HasForeignKey(t => t.SystemId) + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + } } diff --git a/Persistence.Database/Entity/DrillingSystem.cs b/Persistence.Database/Entity/DrillingSystem.cs new file mode 100644 index 0000000..6588fb0 --- /dev/null +++ b/Persistence.Database/Entity/DrillingSystem.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Persistence.Database.Entity; +public class DrillingSystem +{ + [Key, Comment("Id системы автобурения")] + public Guid SystemId { get; set; } + + [Required, Column(TypeName = "varchar(256)"), Comment("Наименование системы автобурения")] + public required string Name { get; set; } + + [Comment("Описание системы автобурения")] + public string? Description { get; set; } +} diff --git a/Persistence.Database/Entity/Setpoint.cs b/Persistence.Database/Entity/Setpoint.cs index ef6b5dc..6ca2c27 100644 --- a/Persistence.Database/Entity/Setpoint.cs +++ b/Persistence.Database/Entity/Setpoint.cs @@ -16,6 +16,6 @@ namespace Persistence.Database.Model public DateTimeOffset Created { get; set; } [Comment("Id автора последнего изменения")] - public int IdUser { get; set; } + public Guid IdUser { get; set; } } } diff --git a/Persistence.Database/Entity/TechMessage.cs b/Persistence.Database/Entity/TechMessage.cs new file mode 100644 index 0000000..ea29cc2 --- /dev/null +++ b/Persistence.Database/Entity/TechMessage.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Persistence.Database.Entity +{ + public class TechMessage + { + [Key, Comment("Id события")] + public Guid EventId { get; set; } + + [Comment("Id Категории важности")] + public int CategoryId { get; set; } + + [Comment("Дата возникновения")] + public DateTimeOffset Timestamp { get; set; } + + [Comment("Глубина забоя")] + public double? Depth { get; set; } + + [Column(TypeName = "varchar(512)"), Comment("Текст сообщения")] + public required string MessageText { get; set; } + + [Required, Comment("Id системы автобурения, к которой относится сообщение")] + public required Guid SystemId { get; set; } + + [Required, ForeignKey(nameof(SystemId)), Comment("Система автобурения, к которой относится сообщение")] + public virtual required DrillingSystem System { get; set; } + + [Comment("Id пользователя за пультом бурильщика")] + public Guid UserId { get; set; } + } +} diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index faa0147..2c455a0 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -1,7 +1,9 @@ using System.Net; +using Microsoft.AspNetCore.Mvc.TagHelpers.Cache; using Microsoft.Extensions.DependencyInjection; using Persistence.Client; using Persistence.Client.Clients; +using Persistence.Database.Model; using Xunit; namespace Persistence.IntegrationTests.Controllers @@ -46,7 +48,7 @@ namespace Persistence.IntegrationTests.Controllers public async Task GetCurrent_AfterSave_returns_success() { //arrange - var setpointKey = await Save(); + var setpointKey = await Add(); //act var response = await setpointClient.GetCurrent([setpointKey]); @@ -82,7 +84,7 @@ namespace Persistence.IntegrationTests.Controllers public async Task GetHistory_AfterSave_returns_success() { //arrange - var setpointKey = await Save(); + var setpointKey = await Add(); var historyMoment = DateTimeOffset.UtcNow; historyMoment = historyMoment.AddDays(1); @@ -119,7 +121,7 @@ namespace Persistence.IntegrationTests.Controllers public async Task GetLog_AfterSave_returns_success() { //arrange - var setpointKey = await Save(); + var setpointKey = await Add(); //act var response = await setpointClient.GetLog([setpointKey]); @@ -132,12 +134,92 @@ namespace Persistence.IntegrationTests.Controllers } [Fact] - public async Task Save_returns_success() + public async Task GetDatesRange_returns_success() { - await Save(); + //arrange + dbContext.CleanupDbSet(); + + //act + var response = await setpointClient.GetDatesRangeAsync(new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Equal(DateTimeOffset.MinValue, response.Content!.From); + Assert.Equal(DateTimeOffset.MaxValue, response.Content!.To); } - private async Task Save() + [Fact] + public async Task GetDatesRange_AfterSave_returns_success() + { + //arrange + dbContext.CleanupDbSet(); + + await Add(); + + var dateBegin = DateTimeOffset.MinValue; + var take = 1; + var part = await setpointClient.GetPart(dateBegin, take, new CancellationToken()); + + //act + var response = await setpointClient.GetDatesRangeAsync(new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var expectedValue = part.Content! + .FirstOrDefault()!.Created + .ToString("dd.MM.yyyy-HH:mm:ss"); + var actualValueFrom = response.Content.From + .ToString("dd.MM.yyyy-HH:mm:ss"); + Assert.Equal(expectedValue, actualValueFrom); + + var actualValueTo = response.Content.To + .ToString("dd.MM.yyyy-HH:mm:ss"); + Assert.Equal(expectedValue, actualValueTo); + } + + [Fact] + public async Task GetPart_returns_success() + { + //arrange + var dateBegin = DateTimeOffset.UtcNow; + var take = 2; + + //act + var response = await setpointClient.GetPart(dateBegin, take, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetPart_AfterSave_returns_success() + { + //arrange + var dateBegin = DateTimeOffset.UtcNow; + var take = 1; + await Add(); + + //act + var response = await setpointClient.GetPart(dateBegin, take, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + } + + [Fact] + public async Task Save_returns_success() + { + await Add(); + } + + private async Task Add() { //arrange var setpointKey = Guid.NewGuid(); @@ -148,10 +230,10 @@ namespace Persistence.IntegrationTests.Controllers }; //act - var response = await setpointClient.Save(setpointKey, setpointValue); + var response = await setpointClient.Add(setpointKey, setpointValue); //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); return setpointKey; } diff --git a/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs b/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs new file mode 100644 index 0000000..1f194ad --- /dev/null +++ b/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs @@ -0,0 +1,288 @@ +using System.Net; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Persistence.Client; +using Persistence.Client.Clients; +using Persistence.Database.Entity; +using Persistence.Models; +using Xunit; + +namespace Persistence.IntegrationTests.Controllers +{ + public class TechMessagesControllerTest : BaseIntegrationTest + { + private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; + private readonly ITechMessagesClient techMessagesClient; + private readonly IMemoryCache memoryCache; + public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory) + { + var scope = factory.Services.CreateScope(); + var persistenceClientFactory = scope.ServiceProvider + .GetRequiredService(); + + techMessagesClient = persistenceClientFactory.GetClient(); + memoryCache = scope.ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task GetPage_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); + + var requestDto = new RequestDto() + { + Skip = 1, + Take = 2, + SortSettings = nameof(TechMessage.CategoryId) + }; + + //act + var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content.Items); + Assert.Equal(requestDto.Skip, response.Content.Skip); + Assert.Equal(requestDto.Take, response.Content.Take); + } + + [Fact] + public async Task GetPage_AfterSave_returns_success() + { + //arrange + var dtos = await InsertRange(); + var dtosCount = dtos.Count(); + var requestDto = new RequestDto() + { + Skip = 0, + Take = 2, + SortSettings = nameof(TechMessage.CategoryId) + }; + + //act + var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Equal(dtosCount, response.Content.Count); + } + + [Fact] + public async Task InsertRange_returns_success() + { + await InsertRange(); + } + + [Fact] + public async Task InsertRange_returns_BadRequest() + { + //arrange + var dtos = new List() + { + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = -1, // < 0 + Timestamp = DateTimeOffset.UtcNow, + Depth = -1, // < 0 + MessageText = string.Empty, // length < 0 + System = string.Concat(Enumerable.Repeat(nameof(TechMessageDto.System), 100)), // length > 256 + UserId = Guid.NewGuid() + } + }; + + //act + var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task GetSystems_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); + + //act + var response = await techMessagesClient.GetSystems(new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetSystems_AfterSave_returns_success() + { + //arrange + var dtos = await InsertRange(); + var systems = dtos + .Select(e => e.System) + .Distinct() + .ToArray(); + + //act + var response = await techMessagesClient.GetSystems(new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + string?[]? content = response.Content?.ToArray(); + Assert.Equal(systems, content); + } + + [Fact] + public async Task GetStatistics_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); + + var imortantId = 1; + var autoDrillingSystem = nameof(TechMessageDto.System); + + //act + var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetStatistics_AfterSave_returns_success() + { + //arrange + var imortantId = 0; + var autoDrillingSystem = nameof(TechMessageDto.System); + var dtos = await InsertRange(); + var filteredDtos = dtos.Where(e => e.CategoryId == imortantId && e.System == autoDrillingSystem); + + //act + var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var categories = response.Content + .FirstOrDefault()?.Categories + .FirstOrDefault(e => e.Key == 0).Value; + Assert.Equal(filteredDtos.Count(), categories); + } + + [Fact] + public async Task GetDatesRange_returns_success() + { + //act + var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + //Assert.Equal(DateTimeOffset.MinValue, response.Content?.From); + //Assert.Equal(DateTimeOffset.MaxValue, response.Content?.To); + } + + [Fact] + public async Task GetDatesRange_AfterSave_returns_success() + { + //arrange + await InsertRange(); + + //act + var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content?.From); + Assert.NotNull(response.Content?.To); + } + + [Fact] + public async Task GetPart_returns_success() + { + //arrange + var dateBegin = DateTimeOffset.UtcNow; + var take = 2; + + //act + var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetPart_AfterSave_returns_success() + { + //arrange + var dateBegin = DateTimeOffset.UtcNow; + var take = 1; + await InsertRange(); + + //act + var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + } + + private async Task> InsertRange() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); + + var dtos = new List() + { + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = 1, + Timestamp = DateTimeOffset.UtcNow, + Depth = 1.11, + MessageText = nameof(TechMessageDto.MessageText), + System = nameof(TechMessageDto.System).ToLower(), + UserId = Guid.NewGuid() + }, + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = 2, + Timestamp = DateTimeOffset.UtcNow, + Depth = 2.22, + MessageText = nameof(TechMessageDto.MessageText), + System = nameof(TechMessageDto.System).ToLower(), + UserId = Guid.NewGuid() + } + }; + + + //act + var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); + + //assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal(dtos.Count, response.Content); + + return dtos; + } + } +} diff --git a/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs b/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs index 87efa43..08bb11e 100644 --- a/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs @@ -30,7 +30,7 @@ public abstract class TimeSeriesBaseControllerTest : BaseIntegrat var expected = dto.Adapt(); //act - var response = await timeSeriesClient.InsertRange(new TDto[] { expected }); + var response = await timeSeriesClient.AddRange(new TDto[] { expected }); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs index aa33e1b..3fc7dff 100644 --- a/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs @@ -25,7 +25,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest IEnumerable testSets = Generate(10, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); // act - var response = await client.InsertRange(idDiscriminator, testSets); + var response = await client.AddRange(idDiscriminator, testSets); // assert Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); @@ -39,7 +39,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest Guid idDiscriminator = Guid.NewGuid(); int count = 10; IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); // act var response = await client.Get(idDiscriminator, null, null, 0, int.MaxValue); @@ -58,7 +58,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest Guid idDiscriminator = Guid.NewGuid(); int count = 10; IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); string[] props = ["A"]; // act @@ -86,7 +86,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest var dateMin = DateTimeOffset.Now; var dateMax = DateTimeOffset.Now.AddSeconds(count); IEnumerable testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); var tail = testSets.OrderBy(t => t.Timestamp).Skip(count / 2).Take(int.MaxValue); var geDate = tail.First().Timestamp; var tolerance = TimeSpan.FromSeconds(1); @@ -111,7 +111,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest Guid idDiscriminator = Guid.NewGuid(); int count = 10; IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); var expectedCount = count / 2; // act @@ -133,7 +133,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest var expectedCount = 1; int count = 10 + expectedCount; IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); // act var response = await client.Get(idDiscriminator, null, null, count - expectedCount, count); @@ -152,7 +152,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest Guid idDiscriminator = Guid.NewGuid(); int count = 10; IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); var expectedCount = 8; // act @@ -174,7 +174,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest var dateMin = DateTimeOffset.Now; var dateMax = DateTimeOffset.Now.AddSeconds(count-1); IEnumerable testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); var tolerance = TimeSpan.FromSeconds(1); // act @@ -195,7 +195,7 @@ public class TimestampedSetControllerTest : BaseIntegrationTest Guid idDiscriminator = Guid.NewGuid(); int count = 144; IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var insertResponse = await client.AddRange(idDiscriminator, testSets); // act var response = await client.Count(idDiscriminator); diff --git a/Persistence.IntegrationTests/Extensions/EFCoreExtensions.cs b/Persistence.IntegrationTests/Extensions/EFCoreExtensions.cs new file mode 100644 index 0000000..6b09587 --- /dev/null +++ b/Persistence.IntegrationTests/Extensions/EFCoreExtensions.cs @@ -0,0 +1,14 @@ +using Persistence.Database.Model; + +namespace Persistence.IntegrationTests.Extensions; + +public static class EFCoreExtensions +{ + public static void CleanupDbSet(this PersistenceDbContext dbContext) + where T : class + { + var dbset = dbContext.Set(); + dbset.RemoveRange(dbset); + dbContext.SaveChanges(); + } +} diff --git a/Persistence.Repository/Data/SetpointDto.cs b/Persistence.Repository/Data/SetpointDto.cs deleted file mode 100644 index 4a20aa4..0000000 --- a/Persistence.Repository/Data/SetpointDto.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Persistence.Repository.Data -{ - /// - /// Модель для работы с уставкой - /// - public class SetpointDto - { - /// - /// Идентификатор уставки - /// - public int Id { get; set; } - - /// - /// Значение уставки - /// - public required object Value { get; set; } - - /// - /// Дата сохранения уставки - /// - public DateTimeOffset Edit { get; set; } - - /// - /// Ключ пользователя - /// - public int IdUser { get; set; } - } -} diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index e79c555..e353d30 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -19,6 +19,7 @@ public static class DependencyInjection services.AddTransient(); services.AddTransient, TimeSeriesDataCachedRepository>(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/Persistence.Repository/Extensions/EFExtensionsSortBy.cs b/Persistence.Repository/Extensions/EFExtensionsSortBy.cs new file mode 100644 index 0000000..03b9c65 --- /dev/null +++ b/Persistence.Repository/Extensions/EFExtensionsSortBy.cs @@ -0,0 +1,267 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +namespace Persistence.Repository.Extensions; + +public static class EFExtensionsSortBy +{ + struct TypeAccessor + { + public LambdaExpression KeySelector { get; set; } + public MethodInfo OrderBy { get; set; } + public MethodInfo OrderByDescending { get; set; } + public MethodInfo ThenBy { get; set; } + public MethodInfo ThenByDescending { get; set; } + } + + private static ConcurrentDictionary> TypePropSelectors { get; set; } = + new(); + + private static readonly MethodInfo methodOrderBy = GetExtOrderMethod("OrderBy"); + + private static readonly MethodInfo methodOrderByDescending = GetExtOrderMethod("OrderByDescending"); + + private static readonly MethodInfo methodThenBy = GetExtOrderMethod("ThenBy"); + + private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending"); + + private static MethodInfo GetExtOrderMethod(string methodName) + => typeof(Queryable) + .GetMethods() + .Where(m => m.Name == methodName && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[1].ParameterType.IsAssignableTo(typeof(LambdaExpression))) + .Single(); + + private static Dictionary MakeTypeAccessors(Type type) + { + var propContainer = new Dictionary(); + var properties = type.GetProperties(); + foreach (var propertyInfo in properties) + { + var name = propertyInfo.Name.ToLower(); + ParameterExpression arg = Expression.Parameter(type, "x"); + MemberExpression property = Expression.Property(arg, propertyInfo.Name); + var selector = Expression.Lambda(property, new ParameterExpression[] { arg }); + var typeAccessor = new TypeAccessor + { + KeySelector = selector, + OrderBy = methodOrderBy.MakeGenericMethod(type, propertyInfo.PropertyType), + OrderByDescending = methodOrderByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), + ThenBy = methodThenBy.MakeGenericMethod(type, propertyInfo.PropertyType), + ThenByDescending = methodThenByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), + }; + + propContainer.Add(name, typeAccessor); + } + + return propContainer; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию. + /// + /// + /// + /// + /// Свойство сортировки. + /// Состоит из названия свойства (в любом регистре) + /// и опционально указания направления сортировки "asc" или "desc" + /// + /// + /// var query = query("Date desc"); + /// + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + IEnumerable propertySorts) + { + if (propertySorts?.Any() != true) + return (IOrderedQueryable)query; + + var sortEnum = propertySorts.GetEnumerator(); + sortEnum.MoveNext(); + var orderedQuery = query.SortBy(sortEnum.Current); + + while (sortEnum.MoveNext()) + orderedQuery = orderedQuery.ThenSortBy(sortEnum.Current); + + return orderedQuery; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию. + /// Этот метод сбросит ранее наложенные сортировки. + /// + /// + /// + /// + /// Свойство сортировки. + /// Состоит из названия свойства (в любом регистре) + /// и опционально указания направления сортировки "asc" или "desc" + /// + /// + /// var query = query("Date desc"); + /// + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + string propertySort) + { + var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); + var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; + var propertyName = parts[0]; + + var newQuery = query.SortBy(propertyName, isDesc); + return newQuery; + } + + /// + /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию. + /// + /// + /// + /// + /// Свойство сортировки. + /// Состоит из названия свойства (в любом регистре) + /// и опционально указания направления сортировки "asc" или "desc" + /// + /// + /// var query = query("Date desc"); + /// + /// Запрос с примененной сортировкой + public static IOrderedQueryable ThenSortBy( + this IOrderedQueryable query, + string propertySort) + { + var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); + var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; + var propertyName = parts[0]; + + var newQuery = query.ThenSortBy(propertyName, isDesc); + return newQuery; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию + /// + /// + /// + /// Название свойства (в любом регистре) + /// Сортировать по убыванию + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + string propertyName, + bool isDesc) + { + Type rootType = typeof(TSource); + var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); + var propertyNameLower = propertyName.ToLower(); + + MethodInfo orderByDescending; + MethodInfo orderByAscending; + + LambdaExpression? lambdaExpression = null; + + if (propertyName.Contains('.')) + { + Type type = rootType; + ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); + Expression expr = rootExpression; + + var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < propertyPath.Length; i++) + { + PropertyInfo pi = type.GetProperty(propertyPath[i])!; + expr = Expression.Property(expr, pi); + type = pi.PropertyType; + } + + Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); + lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); + + orderByAscending = methodOrderBy.MakeGenericMethod(rootType, type); + orderByDescending = methodOrderByDescending.MakeGenericMethod(rootType, type); + } + else + { + var rootTypeAccessor = typePropSelector[propertyNameLower]; + orderByAscending = rootTypeAccessor.OrderBy; + orderByDescending = rootTypeAccessor.OrderByDescending; + lambdaExpression = rootTypeAccessor.KeySelector; + } + + var genericMethod = isDesc + ? orderByDescending + : orderByAscending; + + var newQuery = (IOrderedQueryable)genericMethod + .Invoke(genericMethod, new object[] { query, lambdaExpression })!; + return newQuery; + } + + /// + /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию + /// + /// + /// + /// Название свойства (в любом регистре) + /// Сортировать по убыванию + /// Запрос с примененной сортировкой + public static IOrderedQueryable ThenSortBy( + this IOrderedQueryable query, + string propertyName, + bool isDesc) + { + Type rootType = typeof(TSource); + var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); + var propertyNameLower = propertyName.ToLower(); + + MethodInfo orderByDescending; + MethodInfo orderByAscending; + + LambdaExpression? lambdaExpression = null; + + // TODO: Устранить дублирование кода + if (propertyName.Contains('.')) + { + Type type = rootType; + ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); + Expression expr = rootExpression; + + var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < propertyPath.Length; i++) + { + PropertyInfo pi = type.GetProperty(propertyPath[i])!; + expr = Expression.Property(expr, pi); + type = pi.PropertyType; + } + + Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); + lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); + + orderByAscending = methodThenBy.MakeGenericMethod(rootType, type); + orderByDescending = methodThenByDescending.MakeGenericMethod(rootType, type); + } + else + { + var rootTypeAccessor = typePropSelector[propertyNameLower]; + orderByAscending = rootTypeAccessor.ThenBy; + orderByDescending = rootTypeAccessor.ThenByDescending; + lambdaExpression = rootTypeAccessor.KeySelector; + } + + var genericMethod = isDesc + ? orderByDescending + : orderByAscending; + + var newQuery = (IOrderedQueryable)genericMethod + .Invoke(genericMethod, new object[] { query, lambdaExpression })!; + return newQuery; + } +} \ No newline at end of file diff --git a/Persistence.Repository/Repositories/SetpointRepository.cs b/Persistence.Repository/Repositories/SetpointRepository.cs index 7f81c29..f0e921f 100644 --- a/Persistence.Repository/Repositories/SetpointRepository.cs +++ b/Persistence.Repository/Repositories/SetpointRepository.cs @@ -43,6 +43,38 @@ namespace Persistence.Repository.Repositories return dtos; } + public async Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => e.Created >= dateBegin) + .Take(take) + .ToArrayAsync(token); + var dtos = entities + .Select(e => e.Adapt()); + + return dtos; + } + + public async Task GetDatesRangeAsync(CancellationToken token) + { + var query = GetQueryReadOnly() + .GroupBy(e => 1) + .Select(group => new + { + Min = group.Min(e => e.Created), + Max = group.Max(e => e.Created), + }); + var values = await query.FirstOrDefaultAsync(token); + var result = new DatesRangeDto() + { + From = values?.Min ?? DateTimeOffset.MinValue, + To = values?.Max ?? DateTimeOffset.MaxValue + }; + + return result; + } + public async Task>> GetLog(IEnumerable setpointKeys, CancellationToken token) { var query = GetQueryReadOnly(); @@ -56,7 +88,7 @@ namespace Persistence.Repository.Repositories return dtos; } - public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token) + public async Task Add(Guid setpointKey, object newValue, Guid idUser, CancellationToken token) { var entity = new Setpoint() { diff --git a/Persistence.Repository/Repositories/TechMessagesRepository.cs b/Persistence.Repository/Repositories/TechMessagesRepository.cs new file mode 100644 index 0000000..c838619 --- /dev/null +++ b/Persistence.Repository/Repositories/TechMessagesRepository.cs @@ -0,0 +1,177 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Persistence.Database.Entity; +using Persistence.Models; +using Persistence.Repositories; +using Persistence.Repository.Extensions; + +namespace Persistence.Repository.Repositories +{ + public class TechMessagesRepository : ITechMessagesRepository + { + private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; + private const int CacheExpirationInMinutes = 60; + private readonly IMemoryCache memoryCache; + private DbContext db; + + public TechMessagesRepository(DbContext db, IMemoryCache memoryCache) + { + this.memoryCache = memoryCache; + this.db = db; + } + + protected virtual IQueryable GetQueryReadOnly() => db.Set() + .Include(e => e.System); + + public async Task> GetPage(RequestDto request, CancellationToken token) + { + var query = GetQueryReadOnly(); + var count = await query.CountAsync(token); + + var sort = request.SortSettings != string.Empty + ? request.SortSettings + : nameof(TechMessage.Timestamp); + var entities = await query + .SortBy(request.SortSettings) + .Skip(request.Skip) + .Take(request.Take) + .ToArrayAsync(token); + + var dto = new PaginationContainer() + { + Skip = request.Skip, + Take = request.Take, + Count = count, + Items = entities.Select(e => e.Adapt()) + }; + + return dto; + } + + public async Task> GetStatistics(IEnumerable autoDrillingSystem, IEnumerable categoryIds, CancellationToken token) + { + var query = GetQueryReadOnly(); + var systems = autoDrillingSystem.Select(s => s.ToLower().Trim()); + var result = await query + .Where(e => systems.Count() == 0 || systems.Contains(e.System.Name.ToLower().Trim())) + .GroupBy(e => e.System.Name, (key, group) => new + { + System = key, + Categories = group + .Where(g => categoryIds.Count() == 0 || categoryIds.Contains(g.CategoryId)) + }) + .ToArrayAsync(token); + + var entities = new List(); + foreach (var e in result) + { + var categories = e.Categories + .GroupBy(g => g.CategoryId) + .ToDictionary(c => c.Key, v => v.Count()); + var entity = new MessagesStatisticDto() + { + System = e.System, + Categories = categories + }; + entities.Add(entity); + } + + return entities; + } + + public async Task> GetSystems(CancellationToken token) + { + var entities = await GetDrillingSystems(token); + var result = entities.Select(e => e.Name); + + return result; + } + + public async Task AddRange(IEnumerable dtos, Guid userId, CancellationToken token) + { + + var entities = new List(); + foreach (var dto in dtos) + { + var entity = dto.Adapt(); + var systems = await GetDrillingSystems(token); + var systemId = systems.FirstOrDefault(e => e.Name.ToLower().Trim() == dto.System.ToLower().Trim())?.SystemId + ?? await CreateDrillingSystem(dto.System, token); + + entity.SystemId = systemId; + entity.UserId = userId; + + entities.Add(entity); + } + + await db.Set().AddRangeAsync(entities, token); + var result = await db.SaveChangesAsync(token); + + return result; + } + + public async Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => e.Timestamp >= dateBegin) + .Take(take) + .ToArrayAsync(token); + var dtos = entities + .Select(e => e.Adapt()); + + return dtos; + } + + public async Task GetDatesRangeAsync(CancellationToken token) + { + var query = GetQueryReadOnly() + .GroupBy(e => 1) + .Select(group => new + { + Min = group.Min(e => e.Timestamp), + Max = group.Max(e => e.Timestamp), + }); + var values = await query.FirstOrDefaultAsync(token); + var result = new DatesRangeDto() + { + From = values?.Min ?? DateTimeOffset.MinValue, + To = values?.Max ?? DateTimeOffset.MaxValue + }; + + return result; + } + + private async Task> GetDrillingSystems(CancellationToken token) + { + var systems = await memoryCache.GetOrCreateAsync(SystemCacheKey, async f => + { + f.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationInMinutes); + + var query = db.Set(); + var entities = await query.ToListAsync(token); + var dtos = entities.Select(e => e.Adapt()); + + return dtos; + }); + + return systems!; + } + private async Task CreateDrillingSystem(string name, CancellationToken token) + { + memoryCache.Remove(SystemCacheKey); + + var entity = new Database.Entity.DrillingSystem() + { + SystemId = default, + Name = name.ToLower().Trim() + }; + + await db.Set().AddAsync(entity); + await db.SaveChangesAsync(token); + + return entity.SystemId; + } + } +} diff --git a/Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs b/Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs index 20dbe00..2037f09 100644 --- a/Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs +++ b/Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs @@ -48,9 +48,9 @@ public class TimeSeriesDataCachedRepository : TimeSeriesDataRepos return items; } - public override async Task InsertRange(IEnumerable dtos, CancellationToken token) + public override async Task AddRange(IEnumerable dtos, CancellationToken token) { - var result = await base.InsertRange(dtos, token); + var result = await base.AddRange(dtos, token); if (result > 0) { diff --git a/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs b/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs index d73caf7..0e58c76 100644 --- a/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs +++ b/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs @@ -41,7 +41,7 @@ public class TimeSeriesDataRepository : ITimeSeriesDataRepository return dtos; } - public virtual async Task InsertRange(IEnumerable dtos, CancellationToken token) + public virtual async Task AddRange(IEnumerable dtos, CancellationToken token) { var entities = dtos.Select(d => d.Adapt()); diff --git a/Persistence.Repository/Repositories/TimestampedSetRepository.cs b/Persistence.Repository/Repositories/TimestampedSetRepository.cs index ad9a6cf..a67b823 100644 --- a/Persistence.Repository/Repositories/TimestampedSetRepository.cs +++ b/Persistence.Repository/Repositories/TimestampedSetRepository.cs @@ -20,7 +20,7 @@ public class TimestampedSetRepository : ITimestampedSetRepository this.db = db; } - public Task InsertRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token) + public Task AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token) { var entities = sets.Select(set => new TimestampedSet(idDiscriminator, set.Timestamp.ToUniversalTime(), set.Set)); var dbSet = db.Set(); diff --git a/Persistence/API/ISetpointApi.cs b/Persistence/API/ISetpointApi.cs index 7af0895..b1504a2 100644 --- a/Persistence/API/ISetpointApi.cs +++ b/Persistence/API/ISetpointApi.cs @@ -6,7 +6,7 @@ namespace Persistence.API; /// /// Интерфейс для API, предназначенного для работы с уставками /// -public interface ISetpointApi +public interface ISetpointApi : ISyncApi { /// /// Получить актуальные значения уставок @@ -33,12 +33,12 @@ public interface ISetpointApi /// Task>>> GetLog(IEnumerable setpoitKeys, CancellationToken token); - /// - /// Метод сохранения уставки - /// - /// ключ уставки - /// значение - /// - /// - Task> Save(Guid setpointKey, object newValue, CancellationToken token); + /// + /// Метод сохранения уставки + /// + /// ключ уставки + /// значение + /// + /// + Task Add(Guid setpointKey, object newValue, CancellationToken token); } diff --git a/Persistence/API/ISyncApi.cs b/Persistence/API/ISyncApi.cs index 7f72812..e630ee7 100644 --- a/Persistence/API/ISyncApi.cs +++ b/Persistence/API/ISyncApi.cs @@ -6,7 +6,7 @@ namespace Persistence.API; /// /// Интерфейс для API, предназначенного для синхронизации данных /// -public interface ISyncApi where TDto : class, new() +public interface ISyncApi { /// /// Получить порцию записей, начиная с заданной даты diff --git a/Persistence/API/ITimeSeriesDataApi.cs b/Persistence/API/ITimeSeriesDataApi.cs index a2406aa..51b9332 100644 --- a/Persistence/API/ITimeSeriesDataApi.cs +++ b/Persistence/API/ITimeSeriesDataApi.cs @@ -28,7 +28,7 @@ public interface ITimeSeriesDataApi : ITimeSeriesBaseDataApi /// /// /// - Task InsertRange(IEnumerable dtos, CancellationToken token); + Task AddRange(IEnumerable dtos, CancellationToken token); } diff --git a/Persistence/Models/DrillingSystemDto.cs b/Persistence/Models/DrillingSystemDto.cs new file mode 100644 index 0000000..c2e7abc --- /dev/null +++ b/Persistence/Models/DrillingSystemDto.cs @@ -0,0 +1,22 @@ +namespace Persistence.Models; + +/// +/// Модель системы автобурения +/// +public class DrillingSystemDto +{ + /// + /// Ключ + /// + public Guid SystemId { get; set; } + + /// + /// Наименование + /// + public required string Name { get; set; } + + /// + /// Описание + /// + public string? Description { get; set; } +} diff --git a/Persistence/Models/MessagesStatisticDto.cs b/Persistence/Models/MessagesStatisticDto.cs new file mode 100644 index 0000000..08f0edd --- /dev/null +++ b/Persistence/Models/MessagesStatisticDto.cs @@ -0,0 +1,17 @@ +namespace Persistence.Models; + +/// +/// Статистика сообщений по системам бурения +/// +public class MessagesStatisticDto +{ + /// + /// Система бурения + /// + public required string System { get; set; } + + /// + /// Количество сообщений в соответствии с категориями важности + /// + public required Dictionary Categories { get; set; } +} diff --git a/Persistence/Models/SetpointLogDto.cs b/Persistence/Models/SetpointLogDto.cs index 484be7a..4aa61b5 100644 --- a/Persistence/Models/SetpointLogDto.cs +++ b/Persistence/Models/SetpointLogDto.cs @@ -13,5 +13,5 @@ public class SetpointLogDto : SetpointValueDto /// /// Ключ пользователя /// - public int IdUser { get; set; } + public Guid IdUser { get; set; } } diff --git a/Persistence/Models/TechMessageDto.cs b/Persistence/Models/TechMessageDto.cs new file mode 100644 index 0000000..84da656 --- /dev/null +++ b/Persistence/Models/TechMessageDto.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; + +namespace Persistence.Models +{ + /// + /// Модель технологического сообщения + /// + public class TechMessageDto + { + /// + /// Id события + /// + [Required] + public Guid EventId { get; set; } + + /// + /// Id Категории важности + /// + [Range(0, int.MaxValue, ErrorMessage = "Id Категории важности не может быть меньше 0")] + public int CategoryId { get; set; } + + /// + /// Дата возникновения + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// Глубина забоя + /// + [Range(0, double.MaxValue, ErrorMessage = "Глубина забоя не может быть меньше 0")] + public double? Depth { get; set; } + + /// + /// Текст сообщения + /// + [Required] + [StringLength(512, MinimumLength = 1, ErrorMessage = "Допустимая длина текста сообщения от 1 до 512 символов")] + public required string MessageText { get; set; } + + /// + /// Система автобурения, к которой относится сообщение + /// + [Required] + [StringLength(256, MinimumLength = 1, ErrorMessage = "Допустимая длина наименования системы АБ от 1 до 256 символов")] + public required string System { get; set; } + + /// + /// Id пользователя за пультом бурильщика + /// + public Guid UserId { get; set; } + } +} diff --git a/Persistence/Repositories/AbstractChangeLogRepository.cs b/Persistence/Repositories/AbstractChangeLogRepository.cs index 88cf511..d7b1cbc 100644 --- a/Persistence/Repositories/AbstractChangeLogRepository.cs +++ b/Persistence/Repositories/AbstractChangeLogRepository.cs @@ -74,7 +74,7 @@ namespace Persistence.Repositories; // throw new NotImplementedException(); // } -// public async Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token) +// public async Task AddRange(int idUser, IEnumerable dtos, CancellationToken token) // { // using var transaction = dbContext.Database.BeginTransaction(); // try diff --git a/Persistence/Repositories/ISetpointRepository.cs b/Persistence/Repositories/ISetpointRepository.cs index 1d82b16..502b44a 100644 --- a/Persistence/Repositories/ISetpointRepository.cs +++ b/Persistence/Repositories/ISetpointRepository.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Mvc; using Persistence.Models; namespace Persistence.Repositories; @@ -7,13 +8,13 @@ namespace Persistence.Repositories; /// public interface ISetpointRepository { - /// - /// Получить значения уставок по набору ключей - /// - /// - /// - /// - Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); + /// + /// Получить значения уставок по набору ключей + /// + /// + /// + /// + Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); /// /// Получить значения уставок за определенный момент времени @@ -32,15 +33,31 @@ public interface ISetpointRepository /// Task>> GetLog(IEnumerable setpointKeys, CancellationToken token); - /// - /// Метод сохранения уставки - /// - /// ключ операции - /// ключ пользователя - /// значение - /// - /// - /// to do - /// id User учесть в соответствующем методе репозитория - Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token); + /// + /// Получить порцию записей, начиная с заданной даты + /// + /// + /// + /// + /// + Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + Task GetDatesRangeAsync(CancellationToken token); + + /// + /// Метод сохранения уставки + /// + /// ключ операции + /// ключ пользователя + /// значение + /// + /// + /// to do + /// id User учесть в соответствующем методе репозитория + Task Add(Guid setpointKey, object newValue, Guid idUser, CancellationToken token); } diff --git a/Persistence/Repositories/ITechMessagesRepository.cs b/Persistence/Repositories/ITechMessagesRepository.cs new file mode 100644 index 0000000..92e8f70 --- /dev/null +++ b/Persistence/Repositories/ITechMessagesRepository.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Persistence.Models; + +namespace Persistence.Repositories +{ + /// + /// Интерфейс по работе с технологическими сообщениями + /// + public interface ITechMessagesRepository + { + /// + /// Получить страницу списка объектов + /// + /// + /// + /// + Task> GetPage(RequestDto request, CancellationToken token); + + /// + /// Добавление новых сообщений + /// + /// + /// + /// + Task AddRange(IEnumerable dtos, Guid userId, CancellationToken token); + + /// + /// Получение списка уникальных названий систем АБ + /// + /// + /// + Task> GetSystems(CancellationToken token); + + /// + /// Получение количества сообщений по категориям и системам автобурения + /// + /// Id Категории важности + /// Система автобурения + /// + /// + Task> GetStatistics(IEnumerable autoDrillingSystem, IEnumerable categoryIds, CancellationToken token); + + /// + /// Получить порцию записей, начиная с заданной даты + /// + /// + /// + /// + /// + Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + Task GetDatesRangeAsync(CancellationToken token); + } +} diff --git a/Persistence/Repositories/ITimeSeriesDataRepository.cs b/Persistence/Repositories/ITimeSeriesDataRepository.cs index 9de7fc7..aa2c9ff 100644 --- a/Persistence/Repositories/ITimeSeriesDataRepository.cs +++ b/Persistence/Repositories/ITimeSeriesDataRepository.cs @@ -15,5 +15,5 @@ public interface ITimeSeriesDataRepository : ISyncRepository, ITimeS /// /// /// - Task InsertRange(IEnumerable dtos, CancellationToken token); + Task AddRange(IEnumerable dtos, CancellationToken token); } diff --git a/Persistence/Repositories/ITimestampedSetRepository.cs b/Persistence/Repositories/ITimestampedSetRepository.cs index 27627c3..c350739 100644 --- a/Persistence/Repositories/ITimestampedSetRepository.cs +++ b/Persistence/Repositories/ITimestampedSetRepository.cs @@ -55,5 +55,5 @@ public interface ITimestampedSetRepository /// /// /// - Task InsertRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); + Task AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); } \ No newline at end of file