diff --git a/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs b/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs index 29a2a63e..c35d4135 100644 --- a/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs +++ b/AsbCloudApp/Data/ProcessMap/ProcessMapPlanDto.cs @@ -87,5 +87,10 @@ namespace AsbCloudApp.Data.ProcessMap /// Плановый процент использования spin master /// public double UsageSpin { get; set; } + + /// + /// DTO типа секции + /// + public WellSectionTypeDto WellSectionType { get; set; } = null!; } } diff --git a/AsbCloudApp/Data/WellSectionTypeDto.cs b/AsbCloudApp/Data/WellSectionTypeDto.cs new file mode 100644 index 00000000..4e89779a --- /dev/null +++ b/AsbCloudApp/Data/WellSectionTypeDto.cs @@ -0,0 +1,17 @@ +namespace AsbCloudApp.Data; + +/// +/// Тип секции +/// +public class WellSectionTypeDto : IId +{ + /// + /// Id секции + /// + public int Id { get; set; } + + /// + /// Название типа секции + /// + public string Caption { get; set; } = null!; +} \ No newline at end of file diff --git a/AsbCloudApp/Services/IProcessMapPlanImportService.cs b/AsbCloudApp/Services/IProcessMapPlanImportService.cs new file mode 100644 index 00000000..84442df1 --- /dev/null +++ b/AsbCloudApp/Services/IProcessMapPlanImportService.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudApp.Services; + +/// +/// Сервис импорта/экспорта для РТК вводимых вручную +/// +public interface IProcessMapPlanImportService +{ + /// + /// Загрузить данные из файла + /// + /// + /// + /// + /// + /// + Task ImportAsync(int idWell, int idUser, Stream stream, CancellationToken cancellationToken); + + /// + /// Сформировать файл с данными + /// + /// + /// + /// + Task ExportAsync(int idWell, CancellationToken cancellationToken); + + /// + /// Получение шаблона для заполнения + /// + /// + Task GetExcelTemplateStreamAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj index 86ae7c27..e6a5edcb 100644 --- a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj +++ b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj @@ -35,6 +35,7 @@ + diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 6ddc0248..ed88ec5f 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -128,6 +128,7 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -195,6 +196,8 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient, CrudCacheRepositoryBase>(); // Subsystem service services.AddTransient, CrudCacheRepositoryBase>(); diff --git a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs new file mode 100644 index 00000000..60fbc9bd --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs @@ -0,0 +1,352 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data.ProcessMap; +using AsbCloudApp.Repositories; +using AsbCloudApp.Services; +using ClosedXML.Excel; +using System; +using AsbCloudApp.Data; + +namespace AsbCloudInfrastructure.Services.ProcessMap; + +/* +* password for ProcessMapImportTemplate.xlsx is ASB2020! +*/ +public class ProcessMapPlanImportService : IProcessMapPlanImportService +{ + private readonly IProcessMapPlanRepository processMapPlanRepository; + private readonly ICrudRepository wellSectionTypeRepository; + + private const string sheetNamePlan = "План"; + + private const int headerRowsCount = 2; + + private const int columnWellSectionType = 1; + private const int columnMode = 2; + private const int columnDepthStart = 3; + private const int columnDepthEnd = 4; + private const int columnPressurePlan = 5; + private const int columnPressureLimitMax = 6; + private const int columnAxialLoadPlan = 7; + private const int columnAxialLoadLimitMax = 8; + private const int columnTopDriveTorquePlan = 9; + private const int columnTopDriveTorqueLimitMax = 10; + private const int columnTopDriveSpeedPlan = 11; + private const int columnTopDriveSpeedLimitMax = 12; + private const int columnFlowPlan = 13; + private const int columnFlowLimitMax = 14; + private const int columnRopPlan = 15; + private const int columnUsageSaub = 16; + private const int columnUsageSpin = 17; + + private WellSectionTypeDto[] sections = null!; + + public ProcessMapPlanImportService(IProcessMapPlanRepository processMapPlanRepository, + ICrudRepository wellSectionTypeRepository) + { + this.processMapPlanRepository = processMapPlanRepository; + this.wellSectionTypeRepository = wellSectionTypeRepository; + } + + public async Task ImportAsync(int idWell, int idUser, Stream stream, CancellationToken cancellationToken) + { + sections = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)).ToArray(); + + using var workBook = new XLWorkbook(stream); + + var processPlanMaps = ParseWorkBook(workBook); + + foreach (var processPlanMap in processPlanMaps) + { + processPlanMap.IdWell = idWell; + processPlanMap.IdUser = idUser; + } + + await processMapPlanRepository.InsertRangeAsync(processPlanMaps, cancellationToken); + } + + public async Task ExportAsync(int idWell, CancellationToken cancellationToken) + { + var processMapPlans = (await processMapPlanRepository.GetByIdWellAsync(idWell, + cancellationToken)).ToArray(); + + return await GenerateExcelFileStreamAsync(processMapPlans, + cancellationToken); + } + + public async Task GetExcelTemplateStreamAsync(CancellationToken cancellationToken) + { + var resourceName = Assembly.GetExecutingAssembly() + .GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("ProcessMapPlanTemplate.xlsx"))!; + + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(resourceName)!; + + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + return memoryStream; + } + + private void AddToWorkbook(XLWorkbook workbook, ProcessMapPlanDto[] processMapPlans) + { + if (!processMapPlans.Any()) + return; + + var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan) + ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}."); + + AddToSheet(sheet, processMapPlans); + } + + private void AddToSheet(IXLWorksheet sheet, ProcessMapPlanDto[] processMapPlans) + { + for (int i = 0; i < processMapPlans.Length; i++) + { + var row = sheet.Row(1 + i + headerRowsCount); + AddToRow(row, processMapPlans[i]); + } + } + + private void AddToRow(IXLRow row, ProcessMapPlanDto processMap) + { + row.Cell(columnWellSectionType).Value = processMap.WellSectionType.Caption; + row.Cell(columnMode).Value = GetModeCaption(processMap.IdMode); + row.Cell(columnDepthStart).Value = processMap.DepthStart; + row.Cell(columnDepthEnd).Value = processMap.DepthEnd; + row.Cell(columnPressurePlan).Value = processMap.Pressure.Plan; + row.Cell(columnPressureLimitMax).Value = processMap.Pressure.LimitMax; + row.Cell(columnAxialLoadPlan).Value = processMap.AxialLoad.Plan; + row.Cell(columnAxialLoadLimitMax).Value = processMap.AxialLoad.LimitMax; + row.Cell(columnTopDriveTorquePlan).Value = processMap.TopDriveTorque.Plan; + row.Cell(columnTopDriveTorqueLimitMax).Value = processMap.TopDriveTorque.LimitMax; + row.Cell(columnTopDriveSpeedPlan).Value = processMap.TopDriveSpeed.Plan; + row.Cell(columnTopDriveSpeedLimitMax).Value = processMap.TopDriveSpeed.LimitMax; + row.Cell(columnFlowPlan).Value = processMap.Flow.Plan; + row.Cell(columnFlowLimitMax).Value = processMap.Flow.LimitMax; + row.Cell(columnRopPlan).Value = processMap.RopPlan; + row.Cell(columnUsageSaub).Value = processMap.UsageSaub; + row.Cell(columnUsageSpin).Value = processMap.UsageSpin; + } + + private ProcessMapPlanDto[] ParseWorkBook(IXLWorkbook workbook) + { + var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan) + ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}."); + + return ParseSheet(sheet); + } + + private ProcessMapPlanDto[] ParseSheet(IXLWorksheet sheet) + { + const int columnsCount = 17; + + if (sheet.RangeUsed().RangeAddress.LastAddress.ColumnNumber < columnsCount) + throw new FileFormatException($"Лист {sheet.Name} содержит меньшее количество столбцов."); + + var rowsCount = sheet.RowsUsed().Count() - headerRowsCount; + + if (rowsCount <= 0) + return Array.Empty(); + + var processMapPlans = new ProcessMapPlanDto[rowsCount]; + + var parseErrors = new List(); + + for (int i = 0; i < processMapPlans.Length; i++) + { + var row = sheet.Row(1 + i + headerRowsCount); + + try + { + processMapPlans[i] = ParseRow(row); + } + catch (FileFormatException ex) + { + parseErrors.Add(ex.Message); + } + } + + if (parseErrors.Any()) + throw new FileFormatException(string.Join("\r\n", parseErrors)); + + return processMapPlans; + } + + private ProcessMapPlanDto ParseRow(IXLRow row) + { + var wellSectionTypeCaption = GetCellValue(row, columnWellSectionType).Trim().ToLower(); + var modeName = GetCellValue(row, columnMode).Trim().ToLower(); + var depthStart = GetCellValue(row, columnDepthStart); + var depthEnd = GetCellValue(row, columnDepthEnd); + var pressurePlan = GetCellValue(row, columnPressurePlan); + var pressureLimitMax = GetCellValue(row, columnPressureLimitMax); + var axialLoadPlan = GetCellValue(row, columnAxialLoadPlan); + var axialLoadLimitMax = GetCellValue(row, columnAxialLoadLimitMax); + var topDriveTorquePlan = GetCellValue(row, columnTopDriveTorquePlan); + var topDriveTorqueLimitMax = GetCellValue(row, columnTopDriveTorqueLimitMax); + var topDriveSpeedPlan = GetCellValue(row, columnTopDriveSpeedPlan); + var topDriveSpeedLimitMax = GetCellValue(row, columnTopDriveSpeedLimitMax); + var flowPlan = GetCellValue(row, columnFlowPlan); + var flowLimitMax = GetCellValue(row, columnFlowLimitMax); + var ropPlan = GetCellValue(row, columnRopPlan); + var usageSaub = GetCellValue(row, columnUsageSaub); + var usageSpin = GetCellValue(row, columnUsageSpin); + + var wellSection = sections.FirstOrDefault(s => s.Caption.Trim().ToLower() == wellSectionTypeCaption) + ?? throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная секция"); + + var idMode = GetIdMode(modeName) + ?? throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный режим"); + + if (depthStart is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная стартовая глубина"); + + if (depthEnd is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная конечная глубина"); + + if (pressurePlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение перепада давления"); + + if (pressureLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение перепада давления"); + + if (axialLoadPlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение нагрузки"); + + if (axialLoadLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение нагрузки"); + + if (topDriveTorquePlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение момента на ВСП"); + + if (topDriveTorqueLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение момента на ВСП"); + + if (topDriveSpeedPlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение оборотов на ВСП"); + + if (topDriveSpeedLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничения оборота на ВСП"); + + if (flowPlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение расхода"); + + if (flowLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение расхода"); + + if (ropPlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение механической скорости"); + + if (usageSaub is < 0 or > 100) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный плановый процент использования АКБ"); + + if (usageSpin is < 0 or > 100) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректные плановый процент использования spin master"); + + return new() + { + IdWellSectionType = wellSection.Id, + IdMode = idMode, + DepthStart = depthStart, + LastUpdate = DateTime.UtcNow, + DepthEnd = depthEnd, + Pressure = new() + { + Plan = pressurePlan, + LimitMax = pressureLimitMax + }, + AxialLoad = new() + { + Plan = axialLoadPlan, + LimitMax = axialLoadLimitMax + }, + TopDriveTorque = new() + { + Plan = topDriveTorquePlan, + LimitMax = topDriveTorqueLimitMax + }, + TopDriveSpeed = new() + { + Plan = topDriveSpeedPlan, + LimitMax = topDriveSpeedLimitMax + }, + Flow = new() + { + Plan = flowPlan, + LimitMax = flowLimitMax + }, + RopPlan = ropPlan, + UsageSaub = usageSaub, + UsageSpin = usageSpin + }; + } + + private async Task GenerateExcelFileStreamAsync(ProcessMapPlanDto[] processMapPlans, + CancellationToken cancellationToken) + { + using var excelTemplateStream = await GetExcelTemplateStreamAsync(cancellationToken); + + using var workbook = new XLWorkbook(excelTemplateStream, XLEventTracking.Disabled); + + AddToWorkbook(workbook, processMapPlans); + + MemoryStream memoryStream = new MemoryStream(); + workbook.SaveAs(memoryStream, new SaveOptions { }); + memoryStream.Seek(0, SeekOrigin.Begin); + return memoryStream; + } + + private static int? GetIdMode(string modeName) => + modeName switch + { + "ручной" => 0, + "ротор" => 1, + "слайд" => 2, + _ => null + }; + + private static string GetModeCaption(int idMode) + => idMode switch + { + 1 => "Ротор", + 2 => "Слайд", + _ => "Ручной", + }; + + private static T GetCellValue(IXLRow row, int columnNumber) + { + var cell = row.Cell(columnNumber); + + if (cell.Value is T cellValue) + { + return cellValue; + } + + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. Ячейка:{columnNumber},{row.RowNumber()} содержит некорректное значение"); + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanTemplate.xlsx b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanTemplate.xlsx new file mode 100644 index 00000000..48d2708e Binary files /dev/null and b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanTemplate.xlsx differ diff --git a/AsbCloudWebApi/Controllers/ProcessMapController.cs b/AsbCloudWebApi/Controllers/ProcessMapController.cs index a8fbd303..474c260c 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapController.cs @@ -7,9 +7,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; namespace AsbCloudWebApi.Controllers { @@ -25,6 +28,7 @@ namespace AsbCloudWebApi.Controllers private readonly IHubContext telemetryHubContext; private readonly IProcessMapReportMakerService processMapReportService; private readonly IProcessMapReportService processMapService; + private readonly IProcessMapPlanImportService processMapPlanImportService; private const string SirnalRMethodGetDataName = "UpdateProcessMap"; @@ -34,13 +38,15 @@ namespace AsbCloudWebApi.Controllers IProcessMapReportMakerService processMapReportService, IProcessMapReportService processMapService, ITelemetryService telemetryService, - IHubContext telemetryHubContext) + IHubContext telemetryHubContext, + IProcessMapPlanImportService processMapPlanImportService) : base(wellService, repository) { this.telemetryService = telemetryService; this.telemetryHubContext = telemetryHubContext; this.processMapReportService = processMapReportService; this.processMapService = processMapService; + this.processMapPlanImportService = processMapPlanImportService; } @@ -61,7 +67,6 @@ namespace AsbCloudWebApi.Controllers var idWell = telemetryService.GetIdWellByTelemetryUid(uid); if (idWell is null) return BadRequest($"Wrong uid {uid}"); -#warning implement Process map get method return Ok(Enumerable.Empty()); } @@ -94,7 +99,6 @@ namespace AsbCloudWebApi.Controllers /// /// /// /// - /// [HttpGet] [Route("getReportFile/{wellId}")] [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] @@ -132,7 +136,7 @@ namespace AsbCloudWebApi.Controllers } /// - /// Добавить запись + /// Добавить запись плановой РТК /// /// /// @@ -147,7 +151,7 @@ namespace AsbCloudWebApi.Controllers } /// - /// Редактировать запись по id + /// Редактировать запись по id плановой РТК /// /// запись /// @@ -161,6 +165,77 @@ namespace AsbCloudWebApi.Controllers return result; } + /// + /// Возвращает шаблон файла импорта плановой РТК + /// + /// Запрашиваемый файл + [HttpGet] + [Route("template")] + [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] + public async Task GetTemplateAsync(CancellationToken cancellationToken) + { + var stream = await processMapPlanImportService.GetExcelTemplateStreamAsync(cancellationToken); + var fileName = "ЕЦП_шаблон_файла_РТК.xlsx"; + return File(stream, "application/octet-stream", fileName); + } + + /// + /// Импортирует плановой РТК из excel (xlsx) файла + /// + /// Id скважины + /// Загружаемый файл + /// + /// + [HttpPost] + [Route("import")] + public async Task ImportAsync(int idWell, + [Required] IFormFile file, + CancellationToken cancellationToken) + { + int? idUser = User.GetUserId(); + + if (idUser is null) + return Forbid(); + + if (Path.GetExtension(file.FileName).ToLower() != ".xlsx") + return BadRequest("Требуется xlsx файл."); + + using Stream stream = file.OpenReadStream(); + + await processMapPlanImportService.ImportAsync(idWell, + idUser.Value, + stream, + cancellationToken); + + return Ok(); + } + + /// + /// Экспорт плановой РТК в excel + /// + /// Id скважины + /// + /// + [HttpGet] + [Route("export")] + [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] + public async Task ExportAsync([FromQuery] int idWell, CancellationToken cancellationToken) + { + int? idUser = User.GetUserId(); + + if (idUser is null) + return Forbid(); + + var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken); + + if (well is null) + return NoContent(); + + var stream = await processMapPlanImportService.ExportAsync(idWell, cancellationToken); + var fileName = $"РТК-план по скважине {well.Caption} куст {well.Cluster}.xlsx"; + return File(stream, "application/octet-stream", fileName); + } + private async Task NotifyUsersBySignalR(int idWell, CancellationToken token) { var dtos = await service.GetAllAsync(idWell, null, token);