using AsbCloudApp.Data; using AsbCloudApp.Services; using AsbCloudDb.Model; using ClosedXML.Excel; using Mapster; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace AsbCloudInfrastructure.Services.WellOperationService { #nullable enable /* * password for WellOperationImportTemplate.xlsx is ASB2020! */ public class WellOperationImportService : IWellOperationImportService { private const string sheetNamePlan = "План"; private const string sheetNameFact = "Факт"; private const int headerRowsCount = 1; private const int columnSection = 1; private const int columnCategory = 2; private const int columnCategoryInfo = 3; private const int columnDepthStart = 4; private const int columnDepthEnd = 5; private const int columnDate = 6; private const int columnDuration = 7; private 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 readonly IWellService wellService; private List categories = null!; public List Categories { get { if (categories is null) { categories = db.WellOperationCategories .AsNoTracking() .ToList(); } return categories; } } private List sections = null!; public List Sections { get { if (sections is null) sections = db.WellSectionTypes .AsNoTracking() .ToList(); return sections; } } // TODO: use WellOperationRepository instead of DB public WellOperationImportService(IAsbCloudDbContext db, IWellService wellService) { this.db = db; this.wellService = wellService; } public void Import(int idWell, Stream stream, bool deleteWellOperationsBeforeImport = false) { using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled); var operations = ParseFileStream(stream); foreach (var operation in operations) operation.IdWell = idWell; SaveOperations(idWell, operations, deleteWellOperationsBeforeImport); } public Stream Export(int idWell) { var operations = db.WellOperations .Include(o => o.WellSectionType) .Include(o => o.OperationCategory) .Where(o => o.IdWell == idWell) .OrderBy(o => o.DateStart) .AsNoTracking() .ToList(); var timezone = wellService.GetTimezone(idWell); return MakeExelFileStream(operations, timezone.Hours); } public Stream GetExcelTemplateStream() { var resourceName = System.Reflection.Assembly.GetExecutingAssembly() .GetManifestResourceNames() .FirstOrDefault(n => n.EndsWith("WellOperationImportTemplate.xlsx"))!; var stream = System.Reflection.Assembly.GetExecutingAssembly() .GetManifestResourceStream(resourceName)!; return stream; } private Stream MakeExelFileStream(IEnumerable operations, double timezoneOffset) { using Stream ecxelTemplateStream = GetExcelTemplateStream(); using var workbook = new XLWorkbook(ecxelTemplateStream, XLEventTracking.Disabled); AddOperationsToWorkbook(workbook, operations, timezoneOffset); MemoryStream memoryStream = new MemoryStream(); workbook.SaveAs(memoryStream, new SaveOptions { }); memoryStream.Seek(0, SeekOrigin.Begin); return memoryStream; } private static void AddOperationsToWorkbook(XLWorkbook workbook, IEnumerable operations, double timezoneOffset) { var planOperations = operations.Where(o => o.IdType == 0); if (planOperations.Any()) { var sheetPlan = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan); if (sheetPlan is not null) AddOperationsToSheet(sheetPlan, planOperations, timezoneOffset); } var factOperations = operations.Where(o => o.IdType == 1); if (factOperations.Any()) { var sheetFact = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNameFact); if (sheetFact is not null) AddOperationsToSheet(sheetFact, factOperations, timezoneOffset); } } private static void AddOperationsToSheet(IXLWorksheet sheet, IEnumerable operations, double timezoneOffset) { var operationsList = operations.ToList(); for (int i = 0; i < operationsList.Count; i++) { var row = sheet.Row(1 + i + headerRowsCount); AddOperationToRow(row, operationsList[i], timezoneOffset); } } private static void AddOperationToRow(IXLRow row, WellOperation operation, double timezoneOffset) { 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.ToRemoteDateTime(timezoneOffset); row.Cell(columnDuration).Value = operation.DurationHours; row.Cell(columnComment).Value = operation.Comment; } private void SaveOperations(int idWell, IEnumerable operations, bool deleteWellOperationsBeforeImport = false) { var timezone = wellService.GetTimezone(idWell); var transaction = db.Database.BeginTransaction(); try { if (deleteWellOperationsBeforeImport) db.WellOperations.RemoveRange(db.WellOperations.Where(o => o.IdWell == idWell)); var entities = operations.Select(o => { var entity = o.Adapt(); entity.IdWell = idWell; entity.DateStart = o.DateStart.ToUtcDateTimeOffset(timezone.Hours); return entity; }); db.WellOperations.AddRange(entities); 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($"Книга excel не содержит листа {sheetNameFact}."); //sheetPlan.RangeUsed().RangeAddress.LastAddress.ColumnNumber var wellOperations = new List(); var wellOperationsPlan = ParseSheet(sheetPlan, 0); wellOperations.AddRange(wellOperationsPlan); var wellOperationsFact = ParseSheet(sheetFact, 1); wellOperations.AddRange(wellOperationsFact); return wellOperations; } private IEnumerable ParseSheet(IXLWorksheet sheet, int idType) { 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} содержит слишком большое количество операций."); if (count <= 0) return new List(); var operations = new List(count); var parseErrors = new List(); DateTime lastOperationDateStart = new DateTime(); for (int i = 0; i < count; i++) { var row = sheet.Row(1 + i + headerRowsCount); try { var operation = ParseRow(row, idType); operations.Add(operation); if (lastOperationDateStart > operation.DateStart) parseErrors.Add($"Лист {sheet.Name} строка {row.RowNumber()} дата позднее даты предыдущей операции."); lastOperationDateStart = operation.DateStart; } catch (FileFormatException ex) { parseErrors.Add(ex.Message); } }; if (parseErrors.Any()) throw new FileFormatException(string.Join("\r\n", parseErrors)); else { if (operations.Any()) if (operations.Min(o => o.DateStart) - operations.Max(o => o.DateStart) > drillingDurationLimitMax) parseErrors.Add($"Лист {sheet.Name} содержит диапазон дат больше {drillingDurationLimitMax}"); } return operations; } private WellOperationDto ParseRow(IXLRow row, int idType) { var vSection = row.Cell(columnSection).Value; var vCategory = row.Cell(columnCategory).Value; var vCategoryInfo = row.Cell(columnCategoryInfo).Value; var vDepthStart = row.Cell(columnDepthStart).Value; var vDepthEnd = row.Cell(columnDepthEnd).Value; var vDate = row.Cell(columnDate).Value; var vDuration = row.Cell(columnDuration).Value; var vComment = row.Cell(columnComment).Value; 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; } else throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана секция"); if (vCategory is string categoryName) { var category = Categories.Find(c => c.Name.ToLower() == categoryName.ToLower()); if (category is null) throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} указана некорректная операция"); operation.IdCategory = category.Id; operation.CategoryName = category.Name; } else throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана операция"); if (vCategoryInfo is not null) operation.CategoryInfo = vCategoryInfo.ToString(); if (vDepthStart is double depthStart && depthStart >= 0d && depthStart <= 20_000d) operation.DepthStart = depthStart; else throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана глубина на начало операции"); if (vDepthEnd is double depthEnd && depthEnd >= 0d && depthEnd <= 20_000d) operation.DepthEnd = depthEnd; else throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана глубина при завершении операции"); if (vDate is DateTime date && date > dateLimitMin && date < dateLimitMax) operation.DateStart = date; else throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} неправильно указана дата/время начала операции"); if (vDuration is double duration && duration >= 0d && duration <= 240d) operation.DurationHours = duration; else throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана длительность операции"); if (vComment is not null) operation.Comment = vComment.ToString(); return operation; } } #nullable disable }