diff --git a/AsbCloudApp/Services/IWellOperationImportService.cs b/AsbCloudApp/Services/IWellOperationImportService.cs new file mode 100644 index 00000000..7ef96ab3 --- /dev/null +++ b/AsbCloudApp/Services/IWellOperationImportService.cs @@ -0,0 +1,10 @@ +using System.IO; + +namespace AsbCloudApp.Services +{ + public interface IWellOperationImportService + { + Stream Export(int idWell); + void Import(int idWell, Stream stream, bool deleteWellOperationsBeforeImport = false); + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj index 375a1a5e..0c41213b 100644 --- a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj +++ b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj @@ -8,6 +8,10 @@ 1701;1702;IDE0090;IDE0063;IDE0066 + + + + diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 2989e82d..d4ca1513 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -4,6 +4,7 @@ using AsbCloudDb.Model; using AsbCloudInfrastructure.Services; using AsbCloudInfrastructure.Services.Analysis; using AsbCloudInfrastructure.Services.Cache; +using AsbCloudInfrastructure.Services.WellOperationService; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -39,6 +40,7 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/AsbCloudInfrastructure/Services/WellOperationImportService.cs b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportService.cs similarity index 53% rename from AsbCloudInfrastructure/Services/WellOperationImportService.cs rename to AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportService.cs index 8b2b0f16..87f76289 100644 --- a/AsbCloudInfrastructure/Services/WellOperationImportService.cs +++ b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportService.cs @@ -5,25 +5,43 @@ using System.Linq; using ClosedXML.Excel; using AsbCloudApp.Data; using AsbCloudDb.Model; +using Mapster; +using Microsoft.EntityFrameworkCore; +using AsbCloudApp.Services; -namespace AsbCloudInfrastructure.Services +namespace AsbCloudInfrastructure.Services.WellOperationService { - public class WellOperationImportService + public class WellOperationImportService : IWellOperationImportService { private const string sheetNamePlan = "План"; private const string sheetNameFact = "Факт"; - private static readonly DateTime dateLimitMin = new DateTime(2001,1,1,0,0,0); - private static readonly DateTime dateLimitMax = new DateTime(2099,1,1,0,0,0); + const int headerRowsCount = 1; + + const int columnSection = 1; + const int columnCategory = 2; + const int columnCategoryInfo = 3; + const int columnDepthStart = 4; + const int columnDepthEnd = 5; + const int columnDate = 6; + const int columnDuration = 7; + const int columnComment = 8; + + private static readonly DateTime dateLimitMin = new DateTime(2001, 1, 1, 0, 0, 0); + private static readonly DateTime dateLimitMax = new DateTime(2099, 1, 1, 0, 0, 0); private static readonly TimeSpan drillingDurationLimitMax = TimeSpan.FromDays(366); private readonly IAsbCloudDbContext db; private List categories = null; - public List Categories { - get { + public List Categories + { + get + { if (categories is null) - categories = db.WellOperationCategories.ToList(); + categories = db.WellOperationCategories + .AsNoTracking() + .ToList(); return categories; } } @@ -34,7 +52,9 @@ namespace AsbCloudInfrastructure.Services get { if (sections is null) - sections = db.WellSectionTypes.ToList(); + sections = db.WellSectionTypes + .AsNoTracking() + .ToList(); return sections; } } @@ -44,24 +64,118 @@ namespace AsbCloudInfrastructure.Services this.db = db; } - public IEnumerable ParseFile(string excelFilePath) + public void Import(int idWell, Stream stream, bool deleteWellOperationsBeforeImport = false) { - if (!File.Exists(excelFilePath)) - throw new FileNotFoundException($"Файл {excelFilePath} не найден."); + using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled); + var operations = ParseFileStream(stream); + foreach (var operation in operations) + operation.IdWell = idWell; - return ParseWorkbook(excelFilePath); + SaveOperations(idWell, operations, deleteWellOperationsBeforeImport); } - private IEnumerable ParseWorkbook(string excelFilePath) + public Stream Export(int idWell) { - using var sourceExcelWorkbook = new XLWorkbook(excelFilePath, XLEventTracking.Disabled); - var sheetPlan = sourceExcelWorkbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan); - if (sheetPlan is null) - throw new FileFormatException($"Файл {excelFilePath} не не содержит листа {sheetNamePlan}."); + var operations = db.WellOperations + .Include(o => o.WellSectionType) + .Include(o => o.OperationCategory) + .Where(o => o.IdWell == idWell) + .OrderBy(o => o.DateStart) + .AsNoTracking() + .ToList(); - var sheetFact = sourceExcelWorkbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNameFact); + if (!operations.Any()) + return null; + + return MakeExelFileStream(operations); + } + + private Stream MakeExelFileStream(IEnumerable operations) + { + using Stream ecxelTemplateStream = System.Reflection.Assembly.GetExecutingAssembly() + .GetManifestResourceStream("AsbCloudInfrastructure.Services.WellOperationService.WellOperationImportTemplate.xltx"); + + using var workbook = new XLWorkbook(ecxelTemplateStream, XLEventTracking.Disabled); + AddOperationsToWorkbook(workbook, operations); + + MemoryStream memoryStream = new MemoryStream(); + workbook.SaveAs(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + return memoryStream; + } + + private void AddOperationsToWorkbook(XLWorkbook workbook, IEnumerable operations) + { + var planOperations = operations.Where(o => o.IdType == 0); + if (planOperations.Any()) + { + var sheetPlan = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan); + AddOperationsToSheet(sheetPlan, planOperations); + } + + var factOperations = operations.Where(o => o.IdType == 1); + if (factOperations.Any()) + { + var sheetFact = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNameFact); + AddOperationsToSheet(sheetFact, factOperations); + } + } + + private void AddOperationsToSheet(IXLWorksheet sheet, IEnumerable operations) + { + var operationsList = operations.ToList(); + for (int i = 0; i < operationsList.Count(); i++) + { + var row = sheet.Row(1 + i + headerRowsCount); + AddOperationToRow(row, operationsList[i]); + } + } + + private void AddOperationToRow(IXLRow row, WellOperation operation) + { + row.Cell(columnSection).Value = operation.WellSectionType?.Caption; + row.Cell(columnCategory).Value = operation.OperationCategory?.Name; + row.Cell(columnCategoryInfo).Value = operation.CategoryInfo; + row.Cell(columnDepthStart).Value = operation.DepthStart; + row.Cell(columnDepthEnd).Value = operation.DepthEnd; + row.Cell(columnDate).Value = operation.DateStart; + row.Cell(columnDuration).Value = operation.DurationHours; + row.Cell(columnComment).Value = operation.Comment; + } + + private void SaveOperations(int idWell, IEnumerable operations, bool deleteWellOperationsBeforeImport = false) + { + var transaction = db.Database.BeginTransaction(); + try + { + if (deleteWellOperationsBeforeImport) + db.WellOperations.RemoveRange(db.WellOperations.Where(o => o.IdWell == idWell)); + db.WellOperations.AddRange(operations.Adapt()); + db.SaveChanges(); + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + private IEnumerable ParseFileStream(Stream stream) + { + using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled); + return ParseWorkbook(workbook); + } + + private IEnumerable ParseWorkbook(IXLWorkbook workbook) + { + var sheetPlan = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan); + if (sheetPlan is null) + throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}."); + + var sheetFact = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNameFact); if (sheetFact is null) - throw new FileFormatException($"Файл {excelFilePath} не не содержит листа {sheetNameFact}."); + throw new FileFormatException($"Книга excel не содержит листа {sheetNameFact}."); //sheetPlan.RangeUsed().RangeAddress.LastAddress.ColumnNumber var wellOperations = new List(); @@ -77,13 +191,12 @@ namespace AsbCloudInfrastructure.Services private IEnumerable ParseSheet(IXLWorksheet sheet, int idType) { - const int headerRowsCount = 1; if (sheet.RangeUsed().RangeAddress.LastAddress.ColumnNumber < 7) throw new FileFormatException($"Лист {sheet.Name} содержит меньшее количество столбцев."); var count = sheet.RowsUsed().Count() - headerRowsCount; - + if (count > 1024) throw new FileFormatException($"Лист {sheet.Name} содержит слишком большое количество операций."); @@ -100,20 +213,20 @@ namespace AsbCloudInfrastructure.Services { var operation = ParseRow(row, idType); operations.Add(operation); - + if (lastOperationDateStart > operation.DateStart) parseErrors.Add($"Лист {sheet.Name} строка {row.RowNumber()} дата позднее даты предыдущей операции."); lastOperationDateStart = operation.DateStart; } - catch(FileFormatException ex) + catch (FileFormatException ex) { parseErrors.Add(ex.Message); - } + } }; // проверка диапазона дат - if(operations.Min(o => o.DateStart) - operations.Max(o => o.DateStart) > drillingDurationLimitMax) + if (operations.Min(o => o.DateStart) - operations.Max(o => o.DateStart) > drillingDurationLimitMax) parseErrors.Add($"Лист {sheet.Name} содержит диапазон дат больше {drillingDurationLimitMax}"); if (parseErrors.Any()) @@ -124,15 +237,6 @@ namespace AsbCloudInfrastructure.Services private WellOperationDto ParseRow(IXLRow row, int idType) { - const int columnSection = 1; - const int columnCategory = 2; - const int columnCategoryInfo = 3; - const int columnDepthStart = 4; - const int columnDepthEnd = 5; - const int columnDate = 6; - const int columnDuration = 7; - const int columnComment = 8; - var vSection = row.Cell(columnSection).Value; var vCategory = row.Cell(columnCategory).Value; var vCategoryInfo = row.Cell(columnCategoryInfo).Value; @@ -142,14 +246,14 @@ namespace AsbCloudInfrastructure.Services var vDuration = row.Cell(columnDuration).Value; var vComment = row.Cell(columnComment).Value; - var operation = new WellOperationDto{IdType = idType}; + var operation = new WellOperationDto { IdType = idType }; if (vSection is string sectionName) { var section = Sections.Find(c => c.Caption.ToLower() == sectionName.ToLower()); if (section is null) throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} указана некорректная секция"); - + operation.IdWellSectionType = section.Id; operation.WellSectionTypeName = section.Caption; } @@ -159,7 +263,7 @@ namespace AsbCloudInfrastructure.Services if (vCategory is string categoryName) { var category = Categories.Find(c => c.Name.ToLower() == categoryName.ToLower()); - if(category is null) + if (category is null) throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} указана некорректная операция"); operation.IdCategory = category.Id; diff --git a/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportTemplate.xltx b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportTemplate.xltx new file mode 100644 index 00000000..0197218c Binary files /dev/null and b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportTemplate.xltx differ diff --git a/AsbCloudInfrastructure/Services/WellOperationService.cs b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationService.cs similarity index 98% rename from AsbCloudInfrastructure/Services/WellOperationService.cs rename to AsbCloudInfrastructure/Services/WellOperationService/WellOperationService.cs index 51e63e9c..865e95f2 100644 --- a/AsbCloudInfrastructure/Services/WellOperationService.cs +++ b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationService.cs @@ -10,7 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace AsbCloudInfrastructure.Services +namespace AsbCloudInfrastructure.Services.WellOperationService { public class WellOperationService : IWellOperationService { diff --git a/AsbCloudInfrastructure/Services/WellOperationsStatService.cs b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationsStatService.cs similarity index 99% rename from AsbCloudInfrastructure/Services/WellOperationsStatService.cs rename to AsbCloudInfrastructure/Services/WellOperationService/WellOperationsStatService.cs index 1aaba862..9b273efe 100644 --- a/AsbCloudInfrastructure/Services/WellOperationsStatService.cs +++ b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationsStatService.cs @@ -10,7 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace AsbCloudInfrastructure.Services +namespace AsbCloudInfrastructure.Services.WellOperationService { class Race { diff --git a/AsbCloudWebApi/Controllers/WellOperationController.cs b/AsbCloudWebApi/Controllers/WellOperationController.cs index e6684ba1..9c218049 100644 --- a/AsbCloudWebApi/Controllers/WellOperationController.cs +++ b/AsbCloudWebApi/Controllers/WellOperationController.cs @@ -1,9 +1,11 @@ using AsbCloudApp.Data; using AsbCloudApp.Services; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -19,11 +21,13 @@ namespace AsbCloudWebApi.Controllers { private readonly IWellOperationService operationService; private readonly IWellService wellService; + private readonly IWellOperationImportService wellOperationImportService; - public WellOperationController(IWellOperationService operationService, IWellService wellService) + public WellOperationController(IWellOperationService operationService, IWellService wellService, IWellOperationImportService wellOperationImportService) { this.operationService = operationService; this.wellService = wellService; + this.wellOperationImportService = wellOperationImportService; } /// @@ -170,6 +174,78 @@ namespace AsbCloudWebApi.Controllers return Ok(result); } + + /// + /// Импортирует операции из excel (xlsx) файла + /// + /// id скважины + /// Коллекция файлов - 1 файл xlsx + /// Удалить операции перед импортом, если фал валидный + /// Токен отмены задачи + /// + [HttpPost] + [Route("import")] + [ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)] + public async Task ImportAsync(int idWell, + [FromForm] IFormFileCollection files, + bool deleteWellOperationsBeforeImport = false, + CancellationToken token = default) + { + int? idCompany = User.GetCompanyId(); + int? idUser = User.GetUserId(); + + if (idCompany is null || idUser is null) + return Forbid(); + + if (!await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, + idWell, token).ConfigureAwait(false)) + return Forbid(); + + if (files.Count < 1) + return BadRequest("нет файла"); + + var file = files[0]; + if(Path.GetExtension( file.FileName).ToLower() != "*.xlsx") + return BadRequest("Требуется xlsx файл."); + using Stream stream = file.OpenReadStream(); + + try + { + wellOperationImportService.Import(idWell, stream, deleteWellOperationsBeforeImport); + } + catch(FileFormatException ex) + { + return BadRequest(ex.Message); + } + + return Ok(); + } + + /// + /// Возвращает файл с диска на сервере + /// + /// id скважины + /// Токен отмены задачи + /// Запрашиваемый файл + [HttpGet] + [Route("export")] + [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] + public async Task ExportAsync([FromRoute] int idWell, CancellationToken token = default) + { + int? idCompany = User.GetCompanyId(); + + if (idCompany is null) + return Forbid(); + + if (!await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, + idWell, token).ConfigureAwait(false)) + return Forbid(); + + var stream = wellOperationImportService.Export(idWell); + var fileName = await wellService.GetWellCaptionByIdAsync(idWell, token) + "_operations.xlsx"; + return File(stream, "application/octet-stream", fileName); + } + private async Task CanUserAccessToWellAsync(int idWell, CancellationToken token = default) { int? idCompany = User.GetCompanyId(); diff --git a/ConsoleApp1/DebugWellOperationImportService.cs b/ConsoleApp1/DebugWellOperationImportService.cs index 4fde35ce..d2fe92c8 100644 --- a/ConsoleApp1/DebugWellOperationImportService.cs +++ b/ConsoleApp1/DebugWellOperationImportService.cs @@ -1,6 +1,7 @@ using AsbCloudDb.Model; using AsbCloudInfrastructure.Services; using AsbCloudInfrastructure.Services.Cache; +using AsbCloudInfrastructure.Services.WellOperationService; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; @@ -22,8 +23,14 @@ namespace ConsoleApp1 var wellOperationImportService = new WellOperationImportService(db); - var ops = wellOperationImportService.ParseFile(@"C:\temp\Миграция.xlsx"); - + //var inStream = System.IO.File.OpenRead(@"C:\temp\Миграция.xlsx"); + //wellOperationImportService.Import(1, inStream); + var stream = wellOperationImportService.Export(1); + var fs = System.IO.File.Create(@"C:\temp\2.xlsx"); + stream.CopyTo(fs); + fs.Flush(); + fs.Close(); + fs.Dispose(); Console.WriteLine("_"); } } diff --git a/ConsoleApp1/DebugWellOperationsStatService.cs b/ConsoleApp1/DebugWellOperationsStatService.cs index 35a6ee1e..bd938666 100644 --- a/ConsoleApp1/DebugWellOperationsStatService.cs +++ b/ConsoleApp1/DebugWellOperationsStatService.cs @@ -2,6 +2,7 @@ using AsbCloudDb.Model; using AsbCloudInfrastructure.Services; using AsbCloudInfrastructure.Services.Cache; +using AsbCloudInfrastructure.Services.WellOperationService; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic;