From d2b98d3a774df9ee49fa8fe0e665e4ca558860bc Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Thu, 4 Aug 2022 15:06:17 +0500 Subject: [PATCH] Add internal DetectedOperationExportService. Add DetectedOperation statistics --- .../DetectedOperationDrillersStatDto.cs} | 19 +-- .../DetectedOperationDto.cs | 2 +- .../DetectedOperationListDto.cs | 22 ++++ .../DetectedOperationStatDto.cs | 60 +++++++++ .../Requests/DetectedOperationRequest.cs | 6 +- .../Services/IDetectedOperationService.cs | 10 +- .../DetectedOperationExportService.cs | 117 +++++++++++++++++ .../DetectedOperationService.cs | 122 +++++++++++++++--- .../SAUB/DetectedOperationController.cs | 62 +++++++++ .../Controllers/WellOperationController.cs | 1 - 10 files changed, 376 insertions(+), 45 deletions(-) rename AsbCloudApp/Data/{DetectedOperationListDto.cs => DetectedOperation/DetectedOperationDrillersStatDto.cs} (66%) rename AsbCloudApp/Data/{ => DetectedOperation}/DetectedOperationDto.cs (98%) create mode 100644 AsbCloudApp/Data/DetectedOperation/DetectedOperationListDto.cs create mode 100644 AsbCloudApp/Data/DetectedOperation/DetectedOperationStatDto.cs create mode 100644 AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationExportService.cs diff --git a/AsbCloudApp/Data/DetectedOperationListDto.cs b/AsbCloudApp/Data/DetectedOperation/DetectedOperationDrillersStatDto.cs similarity index 66% rename from AsbCloudApp/Data/DetectedOperationListDto.cs rename to AsbCloudApp/Data/DetectedOperation/DetectedOperationDrillersStatDto.cs index a18f6e87..65cce4aa 100644 --- a/AsbCloudApp/Data/DetectedOperationListDto.cs +++ b/AsbCloudApp/Data/DetectedOperation/DetectedOperationDrillersStatDto.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; - -namespace AsbCloudApp.Data +namespace AsbCloudApp.Data.DetectedOperation { #nullable enable /// /// Статистика по операциям бурильщика /// - public class DetectedOperationStatDto + public class DetectedOperationDrillersStatDto { /// /// Бурильщик @@ -38,18 +36,5 @@ namespace AsbCloudApp.Data /// public double? Loss { get; set; } } - - /// - /// Автоматически определяемая операция - /// - public class DetectedOperationListDto - { - /// - /// Список всех операций - /// - public IEnumerable Operations { get; set; } - - public IEnumerable Stats { get; set; } - } #nullable disable } diff --git a/AsbCloudApp/Data/DetectedOperationDto.cs b/AsbCloudApp/Data/DetectedOperation/DetectedOperationDto.cs similarity index 98% rename from AsbCloudApp/Data/DetectedOperationDto.cs rename to AsbCloudApp/Data/DetectedOperation/DetectedOperationDto.cs index 59b28043..add539e4 100644 --- a/AsbCloudApp/Data/DetectedOperationDto.cs +++ b/AsbCloudApp/Data/DetectedOperation/DetectedOperationDto.cs @@ -1,6 +1,6 @@ using System; -namespace AsbCloudApp.Data +namespace AsbCloudApp.Data.DetectedOperation { #nullable enable /// diff --git a/AsbCloudApp/Data/DetectedOperation/DetectedOperationListDto.cs b/AsbCloudApp/Data/DetectedOperation/DetectedOperationListDto.cs new file mode 100644 index 00000000..c52b2860 --- /dev/null +++ b/AsbCloudApp/Data/DetectedOperation/DetectedOperationListDto.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace AsbCloudApp.Data.DetectedOperation +{ + + /// + /// Автоматически определяемая операция + /// + public class DetectedOperationListDto + { + /// + /// Список всех операций + /// + public IEnumerable Operations { get; set; } + + /// + /// Статистика по бурильщикам + /// + public IEnumerable Stats { get; set; } + } +#nullable disable +} diff --git a/AsbCloudApp/Data/DetectedOperation/DetectedOperationStatDto.cs b/AsbCloudApp/Data/DetectedOperation/DetectedOperationStatDto.cs new file mode 100644 index 00000000..aebe2f46 --- /dev/null +++ b/AsbCloudApp/Data/DetectedOperation/DetectedOperationStatDto.cs @@ -0,0 +1,60 @@ +namespace AsbCloudApp.Data.DetectedOperation +{ +#nullable enable + /// + /// Статистика по операциям например за период. + /// + public class DetectedOperationStatDto + { + /// + /// Id названия/описания операции + /// + public int IdCategory { get; set; } + + /// + /// Название операции + /// + public string Category { get; set; } = string.Empty; + + /// + /// Количество операций + /// + public int Count { get; set; } + + /// + /// Среднее по ключевому показателю + /// + public double ValueAverage { get; set; } + + /// + /// Мин по ключевому показателю + /// + public double ValueMin { get; set; } + + /// + /// Макс по ключевому показателю + /// + public double ValueMax { get; set; } + + /// + /// Суммарное время операций, мин + /// + public double MinutesTotal { get; set; } + + /// + /// Мин продолжительность операции, мин + /// + public double MinutesMin { get; set; } + + /// + /// Макс продолжительность операции, мин + /// + public double MinutesMax { get; set; } + + /// + /// Средняя продолжительность операции, мин + /// + public double MinutesAverage { get; set; } + } +#nullable disable +} diff --git a/AsbCloudApp/Requests/DetectedOperationRequest.cs b/AsbCloudApp/Requests/DetectedOperationRequest.cs index 6d54a801..a6097b9c 100644 --- a/AsbCloudApp/Requests/DetectedOperationRequest.cs +++ b/AsbCloudApp/Requests/DetectedOperationRequest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace AsbCloudApp.Requests @@ -15,10 +16,9 @@ namespace AsbCloudApp.Requests public int IdWell { get; set; } /// - /// категория операций + /// категории операций /// - [Required] - public int IdCategory { get; set; } + public IEnumerable IdsCategories { get; set; } /// /// Больше или равно дате diff --git a/AsbCloudApp/Services/IDetectedOperationService.cs b/AsbCloudApp/Services/IDetectedOperationService.cs index eb932c53..3d077443 100644 --- a/AsbCloudApp/Services/IDetectedOperationService.cs +++ b/AsbCloudApp/Services/IDetectedOperationService.cs @@ -1,15 +1,21 @@ using AsbCloudApp.Data; +using AsbCloudApp.Data.DetectedOperation; using AsbCloudApp.Requests; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; namespace AsbCloudApp.Services { +#nullable enable public interface IDetectedOperationService { - Task> GetCategoriesAsync(int? idWell, CancellationToken token); - Task GetAsync(DetectedOperationRequest request, CancellationToken token); + Task?> GetCategoriesAsync(int? idWell, CancellationToken token); + Task GetAsync(DetectedOperationRequest request, CancellationToken token); Task DeleteAsync(DetectedOperationRequest request, CancellationToken token); + Task?> GetOperationsStatAsync(DetectedOperationRequest request, CancellationToken token); + Task ExportAsync(IEnumerable idsWells, CancellationToken token); } +#nullable disable } diff --git a/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationExportService.cs b/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationExportService.cs new file mode 100644 index 00000000..ffca6b9f --- /dev/null +++ b/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationExportService.cs @@ -0,0 +1,117 @@ +using AsbCloudApp.Data; +using AsbCloudApp.Services; +using AsbCloudDb.Model; +using ClosedXML.Excel; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Services.DetectOperations +{ + internal class DetectedOperationExportService + { + private readonly IAsbCloudDbContext db; + private readonly IWellService wellService; + + public DetectedOperationExportService(IAsbCloudDbContext db, IWellService wellService) + { + this.db = db; + this.wellService = wellService; + } + + public async Task ExportAsync(IEnumerable idsWells, CancellationToken token) + { + using var workbook = new XLWorkbook(XLEventTracking.Disabled); + + await AddSheetsAsync(workbook, idsWells, token); + + MemoryStream memoryStream = new MemoryStream(); + workbook.SaveAs(memoryStream, new SaveOptions { }); + memoryStream.Seek(0, SeekOrigin.Begin); + return memoryStream; + } + + private async Task AddSheetsAsync(XLWorkbook workbook, IEnumerable idsWells, CancellationToken token) + { + if(!idsWells.Any()) + return; + + var wells = idsWells.Select(i => wellService.GetOrDefault(i)) + .Where(w => w is not null && w.IdTelemetry is not null); + + if (!wells.Any()) + return; + + var idsTelemetries = wells.Select(w => w.IdTelemetry); + if (!idsTelemetries.Any()) + return; + + var operations = await db.DetectedOperations + .Include(o => o.OperationCategory) + .AsNoTracking() + .Where(o => idsTelemetries.Contains(o.IdTelemetry)) + .OrderBy(o => o.IdTelemetry) + .ThenBy(o => o.DateStart) + .ToListAsync(token); + + var groups = operations.GroupBy(o => o.IdTelemetry); + + foreach (var well in wells) + { + var ops = groups.FirstOrDefault(g => g.Key == well.IdTelemetry) + ?.ToList(); + + if(ops?.Any() != true) + continue; + + var sheetName = $"{well.Cluster}_{well.Caption}" + .Replace('.','_'); + + var sheet = workbook.AddWorksheet(sheetName); + AddHeader(sheet); + const int headerHeight = 1; + for(var i = 0; i< ops.Count; i++ ) + AddRow(sheet, ops[i], well, i + 1 + headerHeight); + } + } + + private static void AddHeader(IXLWorksheet sheet) + { + var rowNumber = 1; + sheet.Cell(rowNumber, 1).Value = "Name"; + sheet.Column(1).Width = 34; + + sheet.Cell(rowNumber, 2).Value = "DateStart"; + sheet.Column(2).Width = 17; + + sheet.Cell(rowNumber, 3).Value = "DateEnd"; + sheet.Column(3).Width = 17; + + sheet.Cell(rowNumber, 4).Value = "DepthStart"; + sheet.Column(4).Width = 9; + + sheet.Cell(rowNumber, 5).Value = "DepthEnd"; + sheet.Column(5).Width = 9; + + sheet.Cell(rowNumber, 6).Value = "KeyValue"; + sheet.Column(6).Width = 9; + + sheet.SheetView.FreezeRows(rowNumber); + } + + private static void AddRow(IXLWorksheet sheet, DetectedOperation operation, WellDto well, int rowNumber) + { + var timezoneoffsetHours = well.Timezone.Hours; + sheet.Cell(rowNumber, 1).Value = operation.OperationCategory.Name; + sheet.Cell(rowNumber, 2).Value = operation.DateStart.ToRemoteDateTime(timezoneoffsetHours); + sheet.Cell(rowNumber, 3).Value = operation.DateEnd.ToRemoteDateTime(timezoneoffsetHours); + sheet.Cell(rowNumber, 4).Value = operation.DepthStart; + sheet.Cell(rowNumber, 5).Value = operation.DepthEnd; + sheet.Cell(rowNumber, 6).Value = operation.Value; + } + } +} diff --git a/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs b/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs index 60ea4fcc..067d7e73 100644 --- a/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs +++ b/AsbCloudInfrastructure/Services/DetectOperations/DetectedOperationService.cs @@ -1,4 +1,5 @@ using AsbCloudApp.Data; +using AsbCloudApp.Data.DetectedOperation; using AsbCloudApp.Requests; using AsbCloudApp.Services; using AsbCloudDb; @@ -7,12 +8,14 @@ using Mapster; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Services.DetectOperations { +#nullable enable public class DetectedOperationService : IDetectedOperationService { public const int IdOperationRotor = 1; @@ -33,27 +36,50 @@ namespace AsbCloudInfrastructure.Services.DetectOperations this.scheduleService = scheduleService; } - public async Task GetAsync(DetectedOperationRequest request, CancellationToken token) + public async Task GetAsync(DetectedOperationRequest request, CancellationToken token) + { + var dtos = await GetOperationsAsync(request, token); + if (dtos?.Any() != true) + return null; + + var stats = GetOperationsDrillersStat(dtos); + var result = new DetectedOperationListDto + { + Operations = dtos, + Stats = stats + }; + return result; + } + + private async Task?> GetOperationsAsync(DetectedOperationRequest request, CancellationToken token) { var well = await wellService.GetOrDefaultAsync(request.IdWell, token); if (well?.IdTelemetry is null || well.Timezone is null) return null; var query = BuildQuery(well, request) - .AsNoTracking(); + ?.AsNoTracking(); + + if (query is null) + return null; var data = await query.ToListAsync(token); var operationValues = await operationValueService.GetByIdWellAsync(request.IdWell, token); var schedules = await scheduleService.GetByIdWellAsync(request.IdWell, token); var dtos = data.Select(o => Convert(o, well, operationValues, schedules)); - var groups = dtos.GroupBy(o => o.Driller); + return dtos; + } - var stats = new List(groups.Count()); + private static IEnumerable GetOperationsDrillersStat(IEnumerable operations) + { + var groups = operations.GroupBy(o => o.Driller); + + var stats = new List(groups.Count()); foreach (var group in groups) { var itemsWithTarget = group.Where(i => i.OperationValue is not null); - var stat = new DetectedOperationStatDto + var stat = new DetectedOperationDrillersStatDto { Driller = group.Key, AverageValue = group.Sum(e => e.Value) / group.Count(), @@ -62,20 +88,62 @@ namespace AsbCloudInfrastructure.Services.DetectOperations if (itemsWithTarget.Any()) { var itemsOutOfTarget = itemsWithTarget.Where(o => !IsTargetOk(o)); - stat.AverageTargetValue = itemsWithTarget.Average(e => e.OperationValue.TargetValue); + stat.AverageTargetValue = itemsWithTarget.Average(e => e.OperationValue?.TargetValue); stat.Efficiency = 100d * itemsOutOfTarget.Count() / itemsWithTarget.Count(); stat.Loss = itemsOutOfTarget.Sum(DeltaToTarget); } stats.Add(stat); } + return stats; + } - var result = new DetectedOperationListDto - { - Operations = dtos, - Stats = stats - }; - return result; + public async Task?> GetOperationsStatAsync(DetectedOperationRequest request, CancellationToken token) + { + var well = await wellService.GetOrDefaultAsync(request.IdWell, token); + if (well?.IdTelemetry is null || well.Timezone is null) + return null; + + var query = BuildQuery(well, request) + ?.AsNoTracking(); + + if (query is null) + return null; + + var entities = await query + .Select(o => new { + o.IdCategory, + DurationMinutes = (o.DateEnd - o.DateStart).TotalMinutes, + o.Value, + }) + .ToListAsync(token); + + if (!entities.Any()) + return null; + + var operationValues = await operationValueService.GetByIdWellAsync(request.IdWell, token); + var categories = await query + .Select(o => new {o.IdCategory, o.OperationCategory.Name }) + .Distinct() + .ToDictionaryAsync(c=>c.IdCategory, c=>c.Name, token); + + var dtos = entities + .GroupBy(o => o.IdCategory) + .OrderBy(g => g.Key) + .Select(g => new DetectedOperationStatDto{ + IdCategory = g.Key, + Category = categories[g.Key], + Count = g.Count(), + MinutesAverage = g.Average(o => o.DurationMinutes), + MinutesMin = g.Min(o => o.DurationMinutes), + MinutesMax = g.Max(o => o.DurationMinutes), + MinutesTotal = g.Sum(o => o.DurationMinutes), + ValueAverage = g.Average(o => o.Value), + ValueMax = g.Max(o => o.Value), + ValueMin = g.Min(o => o.Value), + }); + + return dtos; } public async Task DeleteAsync(DetectedOperationRequest request, CancellationToken token) @@ -85,6 +153,10 @@ namespace AsbCloudInfrastructure.Services.DetectOperations return 0; var query = BuildQuery(well, request); + + if (query is null) + return 0; + db.DetectedOperations.RemoveRange(query); return await db.SaveChangesAsync(token); } @@ -93,10 +165,10 @@ namespace AsbCloudInfrastructure.Services.DetectOperations { return (op.IdCategory) switch { - IdOperationRotor => op.Value > op.OperationValue.TargetValue, - IdOperationSlide => op.Value > op.OperationValue.TargetValue, - IdOperationSlipsTime => op.Value > op.OperationValue.TargetValue, - _ => op.Value > op.OperationValue.TargetValue, + IdOperationRotor => op.Value > op.OperationValue?.TargetValue, + IdOperationSlide => op.Value > op.OperationValue?.TargetValue, + IdOperationSlipsTime => op.Value > op.OperationValue?.TargetValue, + _ => op.Value > op.OperationValue?.TargetValue, }; } @@ -106,12 +178,12 @@ namespace AsbCloudInfrastructure.Services.DetectOperations { IdOperationRotor => 0, IdOperationSlide => 0, - IdOperationSlipsTime => op.Value - op.OperationValue.TargetValue, + IdOperationSlipsTime => op.Value - op.OperationValue?.TargetValue??0, _ => 0, }; } - private IQueryable BuildQuery(WellDto well, DetectedOperationRequest request) + private IQueryable? BuildQuery(WellDto well, DetectedOperationRequest request) { if (well?.IdTelemetry is null || well.Timezone is null) return null; @@ -122,7 +194,8 @@ namespace AsbCloudInfrastructure.Services.DetectOperations if (request is not null) { - query = query.Where(o => request.IdCategory == o.IdCategory); + if (request.IdsCategories?.Any() == true) + query = query.Where(o => request.IdsCategories.Contains(o.IdCategory)); if (request.GtDate is not null) query = query.Where(o => o.DateStart >= request.GtDate.Value.ToUtcDateTimeOffset(well.Timezone.Hours)); @@ -182,9 +255,9 @@ namespace AsbCloudInfrastructure.Services.DetectOperations return dto; } - public async Task> GetCategoriesAsync(int? idWell, CancellationToken token) + public async Task?> GetCategoriesAsync(int? idWell, CancellationToken token) { - IQueryable query = null; + IQueryable query; if(idWell is null) { query = db.WellOperationCategories; @@ -209,5 +282,12 @@ namespace AsbCloudInfrastructure.Services.DetectOperations .ToArrayAsync(token); return result; } + + public Task ExportAsync(IEnumerable idsWells, CancellationToken token) + { + var exportService = new DetectedOperationExportService(db, wellService); + return exportService.ExportAsync(idsWells, token); + } } +#nullable disable } diff --git a/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs b/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs index d077c837..b4b7c098 100644 --- a/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs +++ b/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs @@ -1,10 +1,12 @@ using AsbCloudApp.Data; +using AsbCloudApp.Data.DetectedOperation; using AsbCloudApp.Requests; using AsbCloudApp.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -61,6 +63,25 @@ namespace AsbCloudWebApi.Controllers.SAUB return Ok(result); } + /// + /// Получить статистику по фильтрованному списку операций по телеметрии САУБ + /// + /// + /// + /// + [HttpGet("stat")] + [ProducesResponseType(typeof(IEnumerable), (int)System.Net.HttpStatusCode.OK)] + public async Task GetStatAsync( + [FromQuery] DetectedOperationRequest request, + CancellationToken token = default) + { + if (!await UserHasAccesToWellAsync(request.IdWell, token)) + return Forbid(); + + var result = await detectedOperationService.GetOperationsStatAsync(request, token); + return Ok(result); + } + /// /// Удалить операции. /// Удаленные операции будут определены повторно сервисом автоматизированного определения операций. @@ -92,5 +113,46 @@ namespace AsbCloudWebApi.Controllers.SAUB return true; return false; } + + /// + /// Создает excel файл с операциями по скважине + /// + /// id скважины + /// + /// Токен отмены задачи + /// Запрашиваемый файл + [HttpGet] + [Route("export")] + [Permission] + [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] + public async Task ExportAsync(int? idWell, int? idCluster, CancellationToken token = default) + { + if (idCluster is null && idWell is null) + return this.MakeBadRequest(nameof(idWell), $"One of {nameof(idWell)} or {nameof(idCluster)} mast be set."); + + int? idCompany = User.GetCompanyId(); + + if (idCompany is null) + return Forbid(); + + IEnumerable idsWells; + if (idCluster is not null) + { + var companyWells = await wellService.GetWellsByCompanyAsync((int)idCompany, token); + idsWells = companyWells.Where(w => w.IdCluster == idCluster) + .Select(w=>w.Id); + } + else + { + if (!await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, + (int)idWell, token).ConfigureAwait(false)) + return Forbid(); + idsWells = new List { (int)idWell }; + } + + var stream = await detectedOperationService.ExportAsync(idsWells, token); + var fileName = "operations.xlsx"; + return File(stream, "application/octet-stream", fileName); + } } } diff --git a/AsbCloudWebApi/Controllers/WellOperationController.cs b/AsbCloudWebApi/Controllers/WellOperationController.cs index e73bb7f0..96f01ea8 100644 --- a/AsbCloudWebApi/Controllers/WellOperationController.cs +++ b/AsbCloudWebApi/Controllers/WellOperationController.cs @@ -268,7 +268,6 @@ namespace AsbCloudWebApi.Controllers return File(stream, "application/octet-stream", fileName); } - /// /// Создает excel файл с "сетевым графиком" ///