diff --git a/AsbCloudApp/Data/WellboreDto.cs b/AsbCloudApp/Data/WellboreDto.cs
new file mode 100644
index 00000000..e64a188f
--- /dev/null
+++ b/AsbCloudApp/Data/WellboreDto.cs
@@ -0,0 +1,64 @@
+using System;
+
+namespace AsbCloudApp.Data;
+
+///
+/// Ствол скважины
+///
+public class WellboreDto
+{
+ ///
+ /// Идентификатор
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Название
+ ///
+ public string Name { get; set; } = null!;
+
+ ///
+ /// Идентификатор скважины
+ ///
+ public int IdWell { get; set; }
+
+ ///
+ /// Состояние скважины
+ ///
+ public int IdWellState { get; set; }
+
+ ///
+ /// Идентификатор телеметрии
+ ///
+ public int? IdWellTelemetry { get; set; }
+
+ ///
+ /// Временная зона скважины
+ ///
+ public SimpleTimezoneDto? WellTimezone { get; set; }
+
+ ///
+ /// Название скважины
+ ///
+ public string WellName { get; set; } = null!;
+
+ ///
+ /// Начальная глубина ствола
+ ///
+ public double DepthStart { get; set; }
+
+ ///
+ /// Конечная глубина скважины
+ ///
+ public double DepthEnd { get; set; }
+
+ ///
+ /// Дата начала первой операции
+ ///
+ public DateTimeOffset DateStart { get; set; }
+
+ ///
+ /// Дата завершения последней операции
+ ///
+ public DateTimeOffset DateEnd { get; set; }
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Requests/WellboreRequest.cs b/AsbCloudApp/Requests/WellboreRequest.cs
new file mode 100644
index 00000000..32b4cbe2
--- /dev/null
+++ b/AsbCloudApp/Requests/WellboreRequest.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace AsbCloudApp.Requests;
+
+///
+/// Параметры запроса для ствола скважины
+///
+public class WellboreRequest : RequestBase
+{
+ ///
+ /// Пары идентификаторов скважины и секции
+ ///
+ public IEnumerable<(int idWell, int? idSection)> Ids { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Services/IWellboreService.cs b/AsbCloudApp/Services/IWellboreService.cs
new file mode 100644
index 00000000..f42485ae
--- /dev/null
+++ b/AsbCloudApp/Services/IWellboreService.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data;
+using AsbCloudApp.Requests;
+
+namespace AsbCloudApp.Services;
+
+///
+/// Сервис для ствола скважины
+///
+public interface IWellboreService
+{
+ ///
+ /// Получение ствола скважины
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task GetWellboreAsync(int idWell, int idSection, CancellationToken cancellationToken);
+
+ ///
+ /// Получение стволов скважин
+ ///
+ ///
+ ///
+ ///
+ Task> GetWellboresAsync(WellboreRequest request, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs
index 601d05e7..bb894d63 100644
--- a/AsbCloudInfrastructure/DependencyInjection.cs
+++ b/AsbCloudInfrastructure/DependencyInjection.cs
@@ -222,6 +222,8 @@ namespace AsbCloudInfrastructure
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
+
return services;
}
diff --git a/AsbCloudInfrastructure/Services/WellboreService.cs b/AsbCloudInfrastructure/Services/WellboreService.cs
new file mode 100644
index 00000000..727878dc
--- /dev/null
+++ b/AsbCloudInfrastructure/Services/WellboreService.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Requests;
+using AsbCloudApp.Services;
+using AsbCloudDb.Model;
+
+namespace AsbCloudInfrastructure.Services;
+
+public class WellboreService : IWellboreService
+{
+ private readonly IWellService wellService;
+ private readonly IWellOperationRepository wellOperationRepository;
+
+ public WellboreService(IWellService wellService, IWellOperationRepository wellOperationRepository)
+ {
+ this.wellService = wellService;
+ this.wellOperationRepository = wellOperationRepository;
+ }
+
+ public async Task GetWellboreAsync(int idWell, int idSection, CancellationToken cancellationToken)
+ {
+ var request = new WellboreRequest
+ {
+ Ids = new (int, int?)[] { (idWell, idSection) },
+ Take = 1,
+ };
+ var data = await GetWellboresAsync(request, cancellationToken);
+ return data.FirstOrDefault();
+ }
+
+ public async Task> GetWellboresAsync(WellboreRequest request,
+ CancellationToken cancellationToken)
+ {
+ var wellbores = new List(request.Ids.Count());
+ var skip = request.Skip ?? 0;
+ var take = request.Take ?? 10;
+
+ var sections = wellOperationRepository.GetSectionTypes()
+ .ToDictionary(w => w.Id, w => w);
+
+ var ids = request.Ids.GroupBy(i => i.idWell);
+
+ foreach (var id in ids)
+ {
+ var well = await wellService.GetOrDefaultAsync(id.Key, cancellationToken);
+
+ if (well is null)
+ continue;
+
+ var wellOperations = await GetFactOperationsAsync(well.Id, id.Select(i => i.idSection), cancellationToken);
+ var groupedOperations = wellOperations.GroupBy(o => o.IdWellSectionType);
+ var wellWellbores = groupedOperations.Select(group => new WellboreDto {
+ Id = group.Key,
+ IdWell = well.Id,
+ IdWellState = well.IdState,
+ IdWellTelemetry = well.IdTelemetry,
+ Name = sections[group.Key].Caption,
+ WellName = well.Caption,
+ WellTimezone = well.Timezone,
+
+ DateStart = group.Min(operation => operation.DateStart),
+ DateEnd = group.Max(operation => operation.DateStart.AddHours(operation.DurationHours)),
+ DepthStart = group.Min(operation => operation.DepthStart),
+ DepthEnd = group.Max(operation => operation.DepthEnd),
+ });
+ wellbores.AddRange(wellWellbores);
+ }
+
+ return wellbores
+ .OrderBy(w =>w.IdWell).ThenBy(w=>w.Id)
+ .Skip(skip).Take(take);
+ }
+
+ private async Task> GetFactOperationsAsync(int idWell, IEnumerable idsSections,
+ CancellationToken cancellationToken)
+ {
+ var request = new WellOperationRequest
+ {
+ IdWell = idWell,
+ OperationType = WellOperation.IdOperationTypeFact,
+ SortFields = new[] { "DateStart asc" },
+ };
+
+ request.SectionTypeIds = idsSections.All(i => i.HasValue)
+ ? idsSections.Select(i => i!.Value)
+ : null;
+
+ return (await wellOperationRepository.GetAsync(request, cancellationToken))
+ .OrderBy(o => o.DateStart);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi/Controllers/WellboreController.cs b/AsbCloudWebApi/Controllers/WellboreController.cs
new file mode 100644
index 00000000..7c8bdaf3
--- /dev/null
+++ b/AsbCloudWebApi/Controllers/WellboreController.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data;
+using AsbCloudApp.Exceptions;
+using AsbCloudApp.Requests;
+using AsbCloudApp.Services;
+using AsbCloudDb.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace AsbCloudWebApi.Controllers;
+
+///
+/// Ствол скважины
+///
+[Authorize]
+[ApiController]
+[Route("api/well/[controller]")]
+public class WellboreController : ControllerBase
+{
+ private readonly IWellboreService wellboreService;
+
+ public WellboreController(IWellboreService wellboreService)
+ {
+ this.wellboreService = wellboreService;
+ }
+
+ ///
+ /// Получение ствола скважины
+ ///
+ /// Id скважины
+ /// Id типа секции скважины
+ ///
+ ///
+
+ [HttpGet("{idWell:int}/{idSection:int}")]
+ [ProducesResponseType(typeof(WellboreDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task GetAsync(int idWell, int idSection, CancellationToken cancellationToken)
+ {
+ var wellbore = await wellboreService.GetWellboreAsync(idWell, idSection, cancellationToken);
+
+ if (wellbore is null)
+ return NoContent();
+
+ return Ok(wellbore);
+ }
+
+ ///
+ /// Получение списка стволов скважин
+ ///
+ /// Пары идентификаторов скважины и секции
+ /// Опциональный параметр. Количество пропускаемых записей
+ /// Опциональный параметр. Количество получаемых записей
+ ///
+ ///
+ [HttpGet]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public async Task GetAllAsync([FromQuery] IEnumerable ids,
+ int? skip,
+ int? take,
+ CancellationToken cancellationToken)
+ {
+ var request = new WellboreRequest
+ {
+ Ids = ids.Select(id => ParseId(id)),
+ Skip = skip,
+ Take = take
+ };
+
+ return Ok(await wellboreService.GetWellboresAsync(request, cancellationToken));
+ }
+
+ private static (int, int?) ParseId(string id)
+ {
+ var idPair = id.Split(',');
+ if (!int.TryParse(idPair[0], out var idWell))
+ throw new ArgumentInvalidException($"Не удалось получить Id скважины \"{idPair[0]}\"", nameof(id));
+
+ if (idPair.Length > 1)
+ {
+ if (int.TryParse(idPair[1], out int idWellSectionType))
+ return (idWell, idWellSectionType);
+ else
+ throw new ArgumentInvalidException($"Не удалось получить Id ствола \"{idPair[1]}\"", nameof(id));
+ }
+ return (idWell, null);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi/Rest/wellbore.http b/AsbCloudWebApi/Rest/wellbore.http
new file mode 100644
index 00000000..9a7918ac
--- /dev/null
+++ b/AsbCloudWebApi/Rest/wellbore.http
@@ -0,0 +1,15 @@
+@baseUrl = http://127.0.0.1:5000
+@contentType = application/json
+@auth = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjI1NDgxNjIsImV4cCI6MTY5NDEwNTc2MiwiaXNzIjoiYSIsImF1ZCI6ImEifQ.OEAlNzxi7Jat6pzDBTAjTbChskc-tdJthJexyWwwUKE
+
+@uid = 20210101_000000000
+@idCluster = 1
+@idWell = 1
+
+# https://marketplace.visualstudio.com/items?itemName=humao.rest-client
+
+###
+GET {{baseUrl}}/api/well/wellbore?ids=1,2
+Content-Type: {{contentType}}
+accept: */*
+Authorization: {{auth}}