diff --git a/AsbCloudApp/Data/WellOperationImport/RowDto.cs b/AsbCloudApp/Data/WellOperationImport/RowDto.cs new file mode 100644 index 00000000..5b6691f4 --- /dev/null +++ b/AsbCloudApp/Data/WellOperationImport/RowDto.cs @@ -0,0 +1,54 @@ +using System; + +namespace AsbCloudApp.Data.WellOperationImport; + +/// +/// Объект строки полученный из файла excel +/// +public class RowDto +{ + /// + /// Номер строки + /// + public int Number { get; set; } + + /// + /// Название секции + /// + public string? Section { get; set; } + + /// + /// Категория + /// + public string? Category { get; set; } + + /// + /// Описание категории + /// + public string CategoryInfo { get; set; } = null!; + + /// + /// Начальная глубина операции + /// + public double DepthStart { get; set; } + + /// + /// Конечная глубина операции + /// + public double DepthEnd { get; set; } + + /// + /// Дата начала операции + /// + public DateTime Date { get; set; } + + /// + /// Длительность операции + /// + public double Duration { get; set; } + + /// + /// Комментарий + /// + public string? Comment { get; set; } +} \ No newline at end of file diff --git a/AsbCloudApp/Data/WellOperationImport/WellOperationImportOptionsDto.cs b/AsbCloudApp/Data/WellOperationImport/WellOperationImportOptionsDto.cs new file mode 100644 index 00000000..5a1efc22 --- /dev/null +++ b/AsbCloudApp/Data/WellOperationImport/WellOperationImportOptionsDto.cs @@ -0,0 +1,29 @@ +namespace AsbCloudApp.Data.WellOperationImport; + +/// +/// Опции для настройки парсинга документа +/// +public class WellOperationParserOptionsDto +{ + /// + /// Название листа + /// + public string? SheetName { get; set; } + + /// + /// Id шаблона + /// 0 - Дефолтный шаблон + /// 1 - Газпром хантос + /// + public int IdTemplate { get; set; } + + /// + /// Начальная строка + /// + public int? StartRow { get; set; } + + /// + /// Конечная строка + /// + public int? EndRow { get; set; } +} \ No newline at end of file diff --git a/AsbCloudApp/Services/IWellOperationImportService.cs b/AsbCloudApp/Services/IWellOperationImportService.cs deleted file mode 100644 index f8c5fac9..00000000 --- a/AsbCloudApp/Services/IWellOperationImportService.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.IO; - -namespace AsbCloudApp.Services -{ - /// - /// сервис импорта/экспорта операций по скважине вводимых вручную - /// - public interface IWellOperationImportService - { - /// - /// скачать в excel - /// - /// - /// - Stream Export(int idWell); - - /// - /// скачать шаблон для заполнения - /// - /// - Stream GetExcelTemplateStream(); - - /// - /// закгрузить из excel список операций - /// - /// - /// - /// - /// Очистить старые перед импортом (если файл проходит валидацию) - void Import(int idWell, Stream stream, int idUser, bool deleteWellOperationsBeforeImport = false); - } -} \ No newline at end of file diff --git a/AsbCloudApp/Services/WellOperationImport/IWellOperationExcelParser.cs b/AsbCloudApp/Services/WellOperationImport/IWellOperationExcelParser.cs new file mode 100644 index 00000000..c8c8042a --- /dev/null +++ b/AsbCloudApp/Services/WellOperationImport/IWellOperationExcelParser.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.IO; +using AsbCloudApp.Data.WellOperationImport; + +namespace AsbCloudApp.Services.WellOperationImport; + +/// +/// Парсинг операций из excel файла +/// +public interface IWellOperationExcelParser +{ + /// + /// Id шаблона + /// + int IdTemplate { get; } + + /// + /// Типы операций, которые можно получить из файла + /// + IEnumerable IdTypes { get; } + + /// + /// Метод парсинга документа + /// + /// + /// + /// + IEnumerable Parse(Stream stream, WellOperationParserOptionsDto options); +} \ No newline at end of file diff --git a/AsbCloudApp/Services/WellOperationImport/IWellOperationExportService.cs b/AsbCloudApp/Services/WellOperationImport/IWellOperationExportService.cs new file mode 100644 index 00000000..9b0a96ac --- /dev/null +++ b/AsbCloudApp/Services/WellOperationImport/IWellOperationExportService.cs @@ -0,0 +1,19 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudApp.Services.WellOperationImport; + +/// +/// Экспорт ГГД +/// +public interface IWellOperationExportService +{ + /// + /// Скачать в excel + /// + /// + /// + /// + Task ExportAsync(int idWell, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudApp/Services/WellOperationImport/IWellOperationImportService.cs b/AsbCloudApp/Services/WellOperationImport/IWellOperationImportService.cs new file mode 100644 index 00000000..1da68cf8 --- /dev/null +++ b/AsbCloudApp/Services/WellOperationImport/IWellOperationImportService.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data.WellOperationImport; + +namespace AsbCloudApp.Services.WellOperationImport; + +/// +/// Импорт ГГД +/// +public interface IWellOperationImportService +{ + /// + /// Загрузить из excel список операций + /// + /// + /// + /// + /// + /// + /// + /// + Task ImportAsync(int idWell, int idUser, int idType, Stream stream, WellOperationParserOptionsDto options, + bool deleteWellOperationsBeforeImport, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudApp/Services/WellOperationImport/IWellOperationImportTemplateService.cs b/AsbCloudApp/Services/WellOperationImport/IWellOperationImportTemplateService.cs new file mode 100644 index 00000000..817f3ae5 --- /dev/null +++ b/AsbCloudApp/Services/WellOperationImport/IWellOperationImportTemplateService.cs @@ -0,0 +1,15 @@ +using System.IO; + +namespace AsbCloudApp.Services.WellOperationImport; + +/// +/// Сервис для получения шаблонов ГГД +/// +public interface IWellOperationImportTemplateService +{ + /// + /// Скачать шаблон для заполнения + /// + /// + Stream GetExcelTemplateStream(); +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj index 72ed00c1..e14fb012 100644 --- a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj +++ b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj @@ -34,9 +34,12 @@ - + + + + diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 3bff8ee2..9f845fff 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -26,8 +26,11 @@ using System; using AsbCloudApp.Data.Manuals; using AsbCloudApp.Services.AutoGeneratedDailyReports; using AsbCloudApp.Services.Notifications; +using AsbCloudApp.Services.WellOperationImport; using AsbCloudDb.Model.Manuals; using AsbCloudInfrastructure.Services.AutoGeneratedDailyReports; +using AsbCloudInfrastructure.Services.WellOperationImport; +using AsbCloudInfrastructure.Services.WellOperationImport.FileParser; using AsbCloudInfrastructure.Services.ProcessMap.ProcessMapWellboreDevelopment; namespace AsbCloudInfrastructure @@ -133,7 +136,6 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -233,6 +235,13 @@ namespace AsbCloudInfrastructure services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + return services; } diff --git a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs index 21f118a8..e8185a34 100644 --- a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs +++ b/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapPlanImportService.cs @@ -342,7 +342,8 @@ public class ProcessMapPlanImportService : IProcessMapPlanImportService 2 => "Слайд", _ => "Ручной", }; - + + //TODO: вынести в метод расширения private static T GetCellValue(IXLRow row, int columnNumber) { try diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/Constants/DefaultTemplateInfo.cs b/AsbCloudInfrastructure/Services/WellOperationImport/Constants/DefaultTemplateInfo.cs new file mode 100644 index 00000000..96f3272b --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/Constants/DefaultTemplateInfo.cs @@ -0,0 +1,17 @@ +namespace AsbCloudInfrastructure.Services.WellOperationImport.Constants; + +public static class DefaultTemplateInfo +{ + public const string SheetNamePlan = "План"; + public const string SheetNameFact = "Факт"; + + public const int HeaderRowsCount = 1; + public const int ColumnSection = 1; + public const int ColumnCategory = 2; + public const int ColumnCategoryInfo = 3; + public const int ColumnDepthStart = 4; + public const int ColumnDepthEnd = 5; + public const int ColumnDate = 6; + public const int ColumnDuration = 7; + public const int ColumnComment = 8; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/Constants/OperationAttributes.cs b/AsbCloudInfrastructure/Services/WellOperationImport/Constants/OperationAttributes.cs new file mode 100644 index 00000000..c4049d03 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/Constants/OperationAttributes.cs @@ -0,0 +1,10 @@ +namespace AsbCloudInfrastructure.Services.WellOperationImport.Constants; + +public static class OperationAttributes +{ + public const string CategoryInfo = "Описание"; + public const string SectionDiameter = "ОК"; + public const string Depth = "Забой"; + public const string Duration = "Время операции"; + public const string Date = "Дата окончания операции"; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/Constants/Templates.cs b/AsbCloudInfrastructure/Services/WellOperationImport/Constants/Templates.cs new file mode 100644 index 00000000..631dc170 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/Constants/Templates.cs @@ -0,0 +1,7 @@ +namespace AsbCloudInfrastructure.Services.WellOperationImport.Constants; + +public static class Templates +{ + public const int IdDefaultTemplate = 0; + public const int IdGazpromKhantosTemplate = 1; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/StringSimilarity/CosineSimilarity.cs b/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/StringSimilarity/CosineSimilarity.cs new file mode 100644 index 00000000..ae69f7f9 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/StringSimilarity/CosineSimilarity.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace AsbCloudInfrastructure.Services.WellOperationImport.FileParser.StringSimilarity; + +public class CosineSimilarity +{ + private const int DefaultK = 2; + + protected int K { get; } + + public CosineSimilarity(int k) + { + if (k <= 0) + { + throw new ArgumentOutOfRangeException(nameof(k), "k should be positive!"); + } + + K = k; + } + + public CosineSimilarity() : this(DefaultK) { } + + public double Similarity(IDictionary profile1, IDictionary profile2) + => DotProduct(profile1, profile2) + / (Norm(profile1) * Norm(profile2)); + + public Dictionary GetProfile(string s) + { + var shingles = new Dictionary(); + + if (string.IsNullOrWhiteSpace(s)) + return shingles; + + var cleanString = Stemming(s); + + for (int i = 0; i < (cleanString.Length - K + 1); i++) + { + var shingle = cleanString.Substring(i, K); + + if (shingles.TryGetValue(shingle, out var old)) + { + shingles[shingle] = old + 1; + } + else + { + shingles[shingle] = 1; + } + } + + return shingles; + } + + private static string Stemming(string s) + { + var cleaned = Regex.Replace(s.ToLower(), "[^a-zа-я0-9]", ""); + var words = cleaned.Split(' '); + var filteredWords = words.Where(word => word.Length > 1).ToArray(); + return string.Concat(filteredWords); + } + + private static double Norm(IDictionary profile) + { + double agg = 0; + + foreach (var entry in profile) + { + agg += 1.0 * entry.Value * entry.Value; + } + + return Math.Sqrt(agg); + } + + private static double DotProduct(IDictionary profile1, IDictionary profile2) + { + var smallProfile = profile2; + var largeProfile = profile1; + + if (profile1.Count < profile2.Count) + { + smallProfile = profile1; + largeProfile = profile2; + } + + double agg = 0; + foreach (var entry in smallProfile) + { + if (!largeProfile.TryGetValue(entry.Key, out var i)) + continue; + + agg += 1.0 * entry.Value * i; + } + + return agg; + } +} diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/WellOperationDefaultExcelParser.cs b/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/WellOperationDefaultExcelParser.cs new file mode 100644 index 00000000..7d79e243 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/WellOperationDefaultExcelParser.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AsbCloudApp.Data.WellOperationImport; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Services.WellOperationImport; +using AsbCloudDb.Model; +using AsbCloudInfrastructure.Services.WellOperationImport.Constants; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.WellOperationImport.FileParser; + +public class WellOperationDefaultExcelParser : IWellOperationExcelParser +{ + public int IdTemplate => Templates.IdDefaultTemplate; + public IEnumerable IdTypes => new[] { WellOperation.IdOperationTypePlan, WellOperation.IdOperationTypeFact }; + + public IEnumerable Parse(Stream stream, WellOperationParserOptionsDto options) + { + using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled); + + return ParseWorkbook(workbook, options); + } + + private static IEnumerable ParseWorkbook(IXLWorkbook workbook, WellOperationParserOptionsDto options) + { + if (string.IsNullOrWhiteSpace(options.SheetName)) + throw new ArgumentInvalidException("Не указано название листа", nameof(options.SheetName)); + + var sheet = workbook.Worksheets.FirstOrDefault(ws => + string.Equals(ws.Name, options.SheetName, StringComparison.CurrentCultureIgnoreCase)) + ?? throw new FileFormatException($"Книга excel не содержит листа '{options.SheetName}'"); + + return ParseSheet(sheet); + } + + private static IEnumerable ParseSheet(IXLWorksheet sheet) + { + if (sheet.RangeUsed().RangeAddress.LastAddress.ColumnNumber < 7) + throw new FileFormatException($"Лист {sheet.Name} содержит меньшее количество столбцов."); + + var count = sheet.RowsUsed().Count() - DefaultTemplateInfo.HeaderRowsCount; + + switch (count) + { + case > 1024: + throw new FileFormatException($"Лист {sheet.Name} содержит слишком большое количество операций."); + case <= 0: + return Enumerable.Empty(); + } + + var rows = new RowDto[count]; + + var cellValuesErrors = new List(); + + for (int i = 0; i < rows.Length; i++) + { + try + { + var xlRow = sheet.Row(1 + i + DefaultTemplateInfo.HeaderRowsCount); + + rows[i] = ParseRow(xlRow); + } + catch (FileFormatException ex) + { + cellValuesErrors.Add(ex.Message); + } + } + + if (cellValuesErrors.Any()) + throw new FileFormatException(string.Join("\r\n", cellValuesErrors)); + + return rows; + } + + private static RowDto ParseRow(IXLRow xlRow) + { + return new RowDto + { + Number = xlRow.RowNumber(), + Section = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnSection)), + Category = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnCategory)), + CategoryInfo = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnCategoryInfo)), + DepthStart = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnDepthStart)), + DepthEnd = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnDepthEnd)), + Date = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnDate)), + Duration = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnDuration)), + Comment = GetCellValue(xlRow.Cell(DefaultTemplateInfo.ColumnComment)) + }; + } + + //TODO: вынести в метод расширения + private static T GetCellValue(IXLCell cell) + { + try + { + return (T)Convert.ChangeType(cell.Value, typeof(T)); + } + catch + { + throw new FileFormatException( + $"Лист '{cell.Worksheet.Name}'. Ячейка: ({cell.Address.RowNumber},{cell.Address.ColumnNumber}) содержит некорректное значение"); + } + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/WellOperationGazpromKhantosExcelParser.cs b/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/WellOperationGazpromKhantosExcelParser.cs new file mode 100644 index 00000000..4de99d77 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/FileParser/WellOperationGazpromKhantosExcelParser.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using AsbCloudApp.Data.WellOperationImport; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Services.WellOperationImport; +using AsbCloudDb.Model; +using AsbCloudInfrastructure.Services.WellOperationImport.Constants; +using AsbCloudInfrastructure.Services.WellOperationImport.FileParser.StringSimilarity; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.WellOperationImport.FileParser; + +public class WellOperationGazpromKhantosExcelParser : IWellOperationExcelParser +{ + private class Operation + { + public int RowNumber { get; set; } + + public string CategoryInfo { get; set; } = null!; + + public double SectionDiameter { get; set; } + + public double Depth { get; set; } + + public double Duration { get; set; } + + public DateTime Date { get; set; } + } + + private readonly CosineSimilarity cosineSimilarity; + + private readonly Dictionary operationDict = InitDict("Operations.txt", '='); + private readonly Dictionary sectionDict = InitDict("Sections.txt", '='); + private readonly Dictionary operationAttributesDict = InitDict("OperationAttributes.txt", '='); + + + public WellOperationGazpromKhantosExcelParser() + { + cosineSimilarity = new CosineSimilarity(); + } + + public int IdTemplate => Templates.IdGazpromKhantosTemplate; + + public IEnumerable IdTypes => new[] { WellOperation.IdOperationTypePlan }; + + public IEnumerable Parse(Stream stream, WellOperationParserOptionsDto options) + { + using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled); + + return ParseWorkBook(workbook, options); + } + + private IEnumerable ParseWorkBook(IXLWorkbook workbook, WellOperationParserOptionsDto options) + { + if (string.IsNullOrWhiteSpace(options.SheetName)) + throw new ArgumentInvalidException("Не указано название листа", nameof(options.SheetName)); + + if (options.StartRow is null or < 1 or > 1048576) + throw new ArgumentInvalidException("Некорректное значение начальной строки", nameof(options.StartRow)); + + if (options.EndRow is null or < 1 or > 1048576) + throw new ArgumentInvalidException("Некорректное значение конечной строки", nameof(options.EndRow)); + + if (options.EndRow < options.StartRow) + throw new ArgumentInvalidException("Конечный номер строки не может быть больше начального", nameof(options.EndRow)); + + var sheet = workbook.Worksheets.FirstOrDefault(ws => + string.Equals(ws.Name, options.SheetName, StringComparison.CurrentCultureIgnoreCase)) + ?? throw new FileFormatException($"Книга excel не содержит листа '{options.SheetName}'"); + + return ParseSheet(sheet, options.StartRow.Value, options.EndRow.Value); + } + + private IEnumerable ParseSheet(IXLWorksheet sheet, int startRow, int endRow) + { + var operationAttributes = GetOperationAttributes(sheet.RowsUsed()); + + if (operationAttributes is null) + return Enumerable.Empty(); + + var rowsCount = endRow - startRow + 1; + + var operations = new List(); + + var cellValuesErrors = new List(); + + for (int i = 0; i < rowsCount; i++) + { + var xlRow = sheet.Row(startRow + i); + + try + { + operations.Add(new Operation + { + RowNumber = xlRow.RowNumber(), + CategoryInfo = GetCellValue(xlRow.Cell(operationAttributes[OperationAttributes.CategoryInfo])), + SectionDiameter = GetCellValue(xlRow.Cell(operationAttributes[OperationAttributes.SectionDiameter])), + Depth = GetCellValue(xlRow.Cell(operationAttributes[OperationAttributes.Depth])), + Duration = GetCellValue(xlRow.Cell(operationAttributes[OperationAttributes.Duration])), + Date = GetCellValue(xlRow.Cell(operationAttributes[OperationAttributes.Date])) + }); + } + catch (FileFormatException ex) + { + cellValuesErrors.Add(ex.Message); + } + } + + if (cellValuesErrors.Any()) + throw new FileFormatException(string.Join("\r\n", cellValuesErrors)); + + return BuildRows(); + + IEnumerable<(double Diameter, string Name)> BuildSections() + { + var groupedOperations = operations.GroupBy(o => o.SectionDiameter) + .Select(s => new + { + Diameter = s.Key, + CategoryInfo = string.Concat(s.Select(o => o.CategoryInfo)) + }); + + var repeatedSections = new[] { "xвостовик" }; + + var sections = new List<(double diameter, string section)>(); + + foreach (var groupedOperation in groupedOperations) + { + var sectionNamesSet = new HashSet(sections.Select(s => s.section)); + + sections.Add(new ValueTuple(groupedOperation.Diameter, sectionDict.FirstOrDefault(item => + groupedOperation.CategoryInfo.Contains(item.Key) && + (!sectionNamesSet.Contains(item.Value) || repeatedSections.Contains(item.Value.ToLowerInvariant()))).Value)); + } + + return sections; + } + + IEnumerable BuildRows() + { + if (!operations.Any()) + return Enumerable.Empty(); + + var rows = new List(); + + for (int i = 0; i < operations.Count; i++) + { + var currentOperation = operations[i]; + var nextOperation = i + 1 < operations.Count ? operations[i + 1] : currentOperation; + + rows.Add(new RowDto + { + Number = currentOperation.RowNumber, + Section = BuildSections().FirstOrDefault(s => Math.Abs(s.Diameter - currentOperation.SectionDiameter) < 0.1).Name, + Category = GetValueDictionary(operationDict, currentOperation.CategoryInfo, 0.3), + CategoryInfo = currentOperation.CategoryInfo, + DepthStart = currentOperation.Depth, + DepthEnd = nextOperation.Depth, + Duration = currentOperation.Duration, + Date = currentOperation.Date.AddHours(-currentOperation.Duration) + }); + } + + return rows; + } + } + + private IDictionary? GetOperationAttributes(IXLRows xlRows) + { + const int countOperationAttributes = 5; + + IDictionary? operationAttributes = null; + + foreach (var xlRow in xlRows) + { + operationAttributes = new Dictionary(); + + var cells = xlRow.CellsUsed().ToArray(); + + foreach (var cell in cells) + { + var operationAttribute = GetValueDictionary(operationAttributesDict, GetCellValue(cell), 0.7); + + if (operationAttribute is null || operationAttributes.Any(a => a.Key == operationAttribute)) + continue; + + operationAttributes.Add(operationAttribute, cell.Address.ColumnNumber); + } + + if (operationAttributes.Count >= countOperationAttributes) + break; + } + + return operationAttributes is not null && operationAttributes.Count == countOperationAttributes ? operationAttributes : null; + } + + private string? GetValueDictionary(IDictionary dict, string cellValue, double? minSimilarity) + { + var similarValues = new List<(double similarity, string value)>(); + + var profile1 = cosineSimilarity.GetProfile(cellValue); + + foreach (var item in dict) + { + var profile2 = cosineSimilarity.GetProfile(item.Key); + + var similarity = cosineSimilarity.Similarity(profile1, profile2); + + similarValues.Add((similarity, item.Value)); + } + + var mostSimilarValue = similarValues.MaxBy(v => v.similarity); + + return minSimilarity.HasValue && mostSimilarValue.similarity >= minSimilarity ? mostSimilarValue.value : null; + } + + private static Dictionary InitDict(string fileName, char separator) + { + var resourceName = Assembly.GetExecutingAssembly() + .GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith(fileName))!; + + var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(resourceName)!; + + using var reader = new StreamReader(stream); + + return reader.ReadToEnd().Split('\r') + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(line => line.Split(separator)) + .ToDictionary(parts => parts[0].Trim(), parts => parts[1].Trim()); + } + + //TODO: вынести в метод расширения + private static T GetCellValue(IXLCell cell) + { + try + { + if (typeof(T) != typeof(DateTime)) + return (T)Convert.ChangeType(cell.GetFormattedString(), typeof(T), CultureInfo.InvariantCulture); + + return (T)(object)DateTime.FromOADate((double)cell.Value); + } + catch + { + throw new FileFormatException( + $"Лист '{cell.Worksheet.Name}'. Ячейка: ({cell.Address.RowNumber},{cell.Address.ColumnNumber}) содержит некорректное значение"); + } + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/OperationAttributes.txt b/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/OperationAttributes.txt new file mode 100644 index 00000000..7127ce99 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/OperationAttributes.txt @@ -0,0 +1,8 @@ +Описание=Описание +ОК=ОК +Секция=ОК +Забой, м=Забой +Время=Время операции +Плановое время бурения, сут=Время операции +Окончание=Дата окончания операции +Дата окончания План РГ=Дата окончания операции \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/Operations.txt b/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/Operations.txt new file mode 100644 index 00000000..7bad4814 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/Operations.txt @@ -0,0 +1,190 @@ +Сборка КНБК=Сборка КНБК +Сборка роторной КНБК=Сборка КНБК +Шаблонирование спуск КНБК=Шаблонирование перед спуском +Бурение под направлением=Бурение ротором +Шаблонирование перед спуском=Шаблонирование перед спуском +Шаблонировка пробуренного интервала + промывка на забое+ подъем КНБК=Шаблонирование перед спуском +Разборка КНБК=Разборка КНБК +ПР к спуску направления 324мм=ПЗР при спуске ОК +Спуск направления=Спуск ОК +Спуск направления 324мм=Спуск ОК +Цементаж направления 324мм=Цементирование +ОЗЦ. Оборудование устья.=ОЗЦ +ОЗЦ. Чистка забурочной ямы. Чистка ВШН. Отворот доп. патрубка. ЗГР=ОЗЦ +Перетяжка талевого каната / замена.=Перетяжка талевого каната +Шаблонирование подъём КНБК=Шаблонировка подъем БИ, продувка +Сборка СБТ 127мм-300м=Сборка БИ с мостков на подсвечник +Сборка КНБК для бурения кондуктора=Сборка КНБК +Сборка КНБК для бурения. Компоновка БК согласно собранного БИ в п.10=Сборка КНБК +Cпуск КНБК=Спуск КНБК +Cпуск КНБК со сборкой БИ с мостков=Спуск бурильного инструмента со сборкой с мостков +Разбурка оснастки (ЦКОД, цем.стакан, БК), замена раствора=Разбуривание тех.оснастки +Бурение под кондуктор. Наращивание св.=Бурение ротором +Промывка, ОБР, МBТ БР<70 кг/м3=Промывка +Промывка на забое=Промывка +Шаблонирование (подъем)=Шаблонировка во время бурения +Шаблонирование (спуск)=Шаблонировка во время бурения +Промывка на забое. Прокачка ВУС, ОБР, МBТ БР <70 кг/м3=Промывка +Подъем=Подъем КНБК +Разборка КНБК с телесистемой=Разборка КНБК +ПЗР к спуску ОК 245мм=ПЗР при спуске ОК +Спуск ОК 245мм с промежуточными промывками (500 м, 1000м). Вывоз БР с БДЕ=Спуск ОК +Промывка перед цементажем=Промывка при спуске ОК +Цементаж кондуктора 245мм=Цементирование +Монтаж ОУС. Вывоз БР, Чистка емкостей=Чистка ЦСГО/емкостного блока +Монтаж ОУС=Монтаж ПВО +Заготовка бурового раствора, чистка емкостей.=Опрессовка ПВО +Монтаж ПВО, монтаж разрезной воронки и устьевого желоба. Вывоз БР, заготовка БР=Монтаж ПВО +Опрессовка глухих плашек ППГ, БГ, БД , выкидных линий, крестовины с коренными задвижками. ЗБР=Опрессовка ПВО +Сборка КНБК на бурение=Сборка КНБК +Сборка СБТ 127мм-465м=Сборка БИ с мостков на подсвечник +Спуск КНБК со сборкой с мостков СБТ -127 (1700м)=Спуск КНБК +Сборка КНБК на бурение транспортного ствола=Сборка КНБК +Опрессовка трубных плашек, ПУГ=Опрессовка ПВО +Разбурка оснастки (ЦКОД, цем.стакан, БК, углубление на 2 метра ниже БК, опрессовка цементного кольца)=Разбуривание тех.оснастки +Разбурка БК, ЦКОДа и цем.стакана=Разбуривание тех.оснастки +Перевод скважины на новый раствор, чистка ЦСГО=Промывка - перевод скважины на новый раствор +Перевод скважины на новый буровой раствор=Промывка - перевод скважины на новый раствор +Бурение транспортного ствола наращ.св. (прокачка укрепляющих пачек ч/з каждые 150-200м)=Бурение ротором +Промывка после ХМ св TVD - 1660 м (ниже на 50 м)=Промывка +Чистка ЦСГО (опрессовка цем. кольца кондуктора во время чистки ЦСГО)=Чистка ЦСГО/емкостного блока +Промывка после Алымской св TVD - 2140 м (ниже на 50 м)=Промывка +Бурение транспортного ствола наращ. cв. (прокачка укрепляющих пачек ч/з каждые 150-200м).=Бурение ротором +Бурение транспортного ствола (1000м первые сутки бурения)=Бурение ротором +Подъем КНБК шаблонировка ствола скважины=Шаблонировка подъем БИ, продувка +Промывка (по согласованию с ЦУСС)=Промывка +Шаблонировка. Подъем КНБК (по согласованию с ЦУСС)=Шаблонировка во время бурения +Шаблонировка.Спуск КНБК со сборкой БИ 300м (по согласованию с ЦУСС)=Шаблонировка во время бурения +Промывка=Промывка +Шаблонировка. Подъем КНБК=Шаблонировка во время бурения +Шаблонировка.Спуск КНБК=Шаблонировка во время бурения +Разборка КНБК с т/с=Разборка КНБК +Промывка на забое, прокачка кольмат. пачки=Помывка +ПЗР к спуску ОК-178мм.=ПЗР при спуске ОК +Спуск ОК 178 мм (до устья, не потайная) с промежуточными промывками=Спуск ОК +Цементирование ОК-178мм=Цементирование +Отворот и выброс допускной трубы, демонтаж ПВО, замыв шурфа для выброса СБТ-127мм, чистка емкостей, приготовление БР=Демонтаж ПВО +Промывка, установка смазывающей пачки=Промывка +Выброс СБТ-127мм на мостки, чистка емкостей, приготовление БР=Подъем БИ с выбросом на мостки +Подъем КНБК с выбросом БИ - 500м (выброс согласовать с куратором ЦУСС)=Подъем КНБК +Монтаж ПВО, замена трубных плашек 127мм на 102мм, замена рабочего переводника на СВП, приготовление БР=Перетяжка талевого каната +ПЗР к спуску ОК 178мм=ПЗР при спуске ОК +Спуск ОК 178мм с промывками. Вывоз БР с БДЕ=Спуск ОК +Цементирование 178мм ОК. Вывоз БР с БДЕ=Цементирование +Частичный демонтаж ПВО=Демонтаж ПВО +Выброс БИ 127 на мостки - 1600м (Оставляем БИ 127 1400 м на бурение под кондуктор). Вывоз БР, чистка емкостей=Подъем БИ с выбросом на мостки +Частичный монтаж ПВО=Монтаж ПВО +Опрессовка (200 атм) глухих плашек ППГ, БГ, БД, выкидных линий, крестовины с коренными задвижками, ЗБР. Сборка БИ-102мм - 1000м для бурения ГС свечами.=Опрессовка ПВО +Сборка КНБК на бурение секции под хвостовик 114мм=Сборка КНБК +Спуск КНБК со сборкой БИ 102 и промежуточными промывками.=Промывка - перевод скважины на новый раствор +Опрессовка трубных плашек ППГ, ПУГ. Промывка, перезапись гаммы=Опрессовка ПВО +Разбурка оснастки (ЦКОД, цем.стакан, БК)=Разбуривание тех.оснастки +Перевод на новый раствор=Промывка - перевод скважины на новый раствор +Чистка ЦСГО=Чистка ЦСГО/емкостного блока +Бурение горизонтального участка скважины (прокачка укрепляющих пачек ч/з каждые 100 м)=Бурение ротором +Подъем БИ в БК Ø178мм.=Подъем КНБК +Спуск БИ со сборкой ТБТ 88,9мм на опрессовку (20м до БК 178)=Спуск КНБК +Опрессовка БИ, установка на подсвечник ТБТ=Опрессовка БИ +Проработка в 2 этапа:1 этап - прямая принудительная проработка; 2 этап - спуск на "сухую"(имитация спуска хвостовика)=Проработка принудительная +Cборка хвостовика=Сборка хвостовика 114мм (согласно схеме) +Промывка, прокачка ВУС=Промывка +Подъем КНБК=Подъем КНБК +ПЗР к спуску хвостовика=ПЗР при спуске ОК +Сборка хвостовика 114мм (согласно схеме)=Сборка хвостовика 114мм (согласно схеме) +Спуск хвостовика 114мм на БИ. В БК 178 перевод на тех.воду (по согл.с ЦУСС)=Спуск ОК +Активация подвески (4ч). Перевод на жидкость заканчивания (2ч).=Активация подвески, опрессовка +Подъем БИ с выбросом на мостки. Оставляем ТБТ 89 (800 м) на следующую скв=Подъем БИ с выбросом на мостки +Демонтаж ПВО=Демонтаж ПВО +Монтаж, опрессовка ФА=Монтаж, опрессовка ФА +5% времени на ТО БУ=Ремонт +Монтаж ФА=Монтаж, опрессовка ФА +Подъем разъединителя с выбросом СБТ-102мм на мостки=Подъем инструмента +Активация подвески. Перевод на жидкость заканчивания. Опрессовка пакера подвески хвостовика.=Активация подвески (потайной колонны, хвостовика) +ПР к спуску хвостовика=ПЗР при спуске ОК +Подъем КНБК с частичным выбросом СБТ-102мм на приемные мостки=Подъем БИ с выбросом на мостки +Бурение горизонтального участка скважины (прокачка укрепляющих пачек ч/з каждые 100м)=Бурение ротором +Промывка перезапись ГК=Промывка +Спуск КНБК со сборкой СБТ-102мм с приемных мостков, с промежуточными промывками каждые 500м=Спуск бурильного инструмента со сборкой с мостков +Сборка КНБК для бурения горизонтального участка скважины=Сборка БИ с мостков на подсвечник +Опрессовка глухих плашек ППГ, БГ, БД, выкидных линий, крестовины с коренными задвижками, приготовление бур.раствора=Опрессовка ПВО +ВМР=ВМР +Долив затруба при подъёме=Долив затруба при подъёме +Закачка/прокачка пачки=Закачка/прокачка пачки +Комплекс ГИС на жестком кабеле=Комплекс ГИС на жестком кабеле +Комплекс ГИС на кабеле=Комплекс ГИС на кабеле +Комплекс ГИС на трубах=Комплекс ГИС на трубах +Контролируемое ГНВП=Контролируемое ГНВП +Ловильные работы=Ловильные работы +Наработка жёлоба=Наработка жёлоба +Наращивание=Наращивание +НПВ / прочее=НПВ / прочее +Обвязка устья с циркуляционной системой=Обвязка устья с циркуляционной системой +Оборудование устья=Оборудование устья +Обработка БР=Обработка БР +Обработка раствора (несоответствие параметров)=Обработка раствора (несоответствие параметров) +Ожидание=Ожидание +Определение места прихвата и ЛМ=Определение места прихвата и ЛМ +Опрессовка ОК=Опрессовка ОК +Ориентирование ТС при бурении=Ориентирование ТС при бурении +Отворот допускной трубы=Отворот допускной трубы +Перезапись гаммы-каротажа=Перезапись гаммы-каротажа +Перемонтаж ПВО=Перемонтаж ПВО +ПЗР к спуску УЭЦН=ПЗР к спуску УЭЦН +ПЗР при сборке КНБК=ПЗР при сборке КНБК +ПЗР при цементировании=ПЗР при цементировании +Поглощение=Поглощение +Подготовка ствола скважины. Перезапись ГК в интервале установки КО.=Подготовка ствола скважины. Перезапись ГК в интервале установки КО. +Подъем БИ с выбросом на мостки=Подъем БИ с выбросом на мостки +подъем ОК=подъем ОК +Подъем приборов ГИС (на трубах)=Подъем приборов ГИС (на трубах) +Полная замена талевого каната=Полная замена талевого каната +ПР перед забуркой направления=ПР перед забуркой направления +Приготовление БР=Приготовление БР +Продувка манифольда=Продувка манифольда +Промывка перед наращиванием=Промывка перед наращиванием +Проработка во время бурения=Проработка во время бурения +Проработка перед наращиванием=Проработка перед наращиванием +Работа яссом=Работа яссом +Разборка комплекса приборов ГИС=Разборка комплекса приборов ГИС +Разбуривание тех.оснастк=Разбуривание тех.оснастки +Расхаживани=Расхаживание +Ревизия КНБК/инструмента/ЗТС=Ревизия КНБК/инструмента/ЗТС +Ремонт бурового оборудования=Ремонт бурового оборудования +Сальникообразование=Сальникообразование +Сборка и спуск ТБТ=Сборка и спуск ТБТ +Сборка комплекса приборов ГИС=Сборка комплекса приборов ГИС +Сборка устройства ориентирования КО=Сборка устройства ориентирования КО +Смена рабочего переводника ВСП=Смена рабочего переводника ВСП +СПО - колокол=СПО - колокол +СПО - метчик=СПО - метчик +СПО - овершот=СПО - овершот +СПО - труболовка=СПО - труболовка +Спуск БИ со сборкой с мостков=Спуск БИ со сборкой с мостков +Спуск инструмента=Спуск инструмента +Спуск инструмента с проработкой=Спуск инструмента с проработкой +Спуск КО на транспотрной колонне=Спуск КО на транспотрной колонне +Спуск приборов ГИС (на трубах)=Спуск приборов ГИС (на трубах) +Срезка=Срезка +Тайм-дриллинг=Тайм-дриллинг +Тех.отстой=Тех.отстой +Торпедирование (встряхивание)=Торпедирование (встряхивание) +Торпедирование (отстрел)=Торпедирование (отстрел) +Удержание в клиньях=Удержание в клиньях +Установка ванн=Установка ванн +Утяжеление БР=Утяжеление БР +Учебная тревога "Выброс"=Учебная тревога "Выброс" +Фрезеровка=Фрезеровка +Шаблонировка подъем БИ, продувка=Шаблонировка подъем БИ, продувка +Шаблонировка перед наращиванием=Шаблонировка перед наращиванием +Демонтаж ПВО ( переоборудование устья скважины). Вывоз БР=Демонтаж ПВО +Сборка БИ 127/147с мостков установкой на подсвечник=Сборка БИ с мостков на подсвечник +Спуск приборов комплекса АМАК.=Спуск приборов ГИС (на трубах) +Подъем с записью=Подъем приборов ГИС (на трубах) +ОЗЦ под давлением (по согласованию с ЦУСС)=ОЗЦ +"Демонтаж ПВО ( переоборудование устья скважины). Вывоз БР=Демонтаж ПВО +Сборка CБТ-127 (0м) с мостков установкой на подсвечник (оставлено СБТ-127 (1500м) с пердыдущей скв). Заготовка БР=Сборка БИ с мостков на подсвечник +ПЗР к спуску ОК=ПЗР при спуске ОК +Выброс СБТ 127 (2100м), оставляется СБТ-127 (700 м) на след скв. ЗБР, чистка емкостей, вывоз БР.=Подъем БИ с выбросом на мостки +Монтаж ПВО повторный (смена плашек ПВО). ЗБР, чистка емкостей, вывоз БР=Монтаж ПВО +Опрессовка ПВО (200 атм), глухие=Опрессовка ПВО +Сборка ТБТ на 2 этапе (кол-во по согласованию с ЦУСС). Подъем/спуск БИ со сборкой ТБТ 102 мм. Опрессовка БИ (1.5 ч)=Сборка и спуск ТБТ \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/Sections.txt b/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/Sections.txt new file mode 100644 index 00000000..b0b2df54 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/Files/Dictionaries/Sections.txt @@ -0,0 +1,7 @@ +направ=Направление +конд=Кондуктор +техн=Техническая колонна +экспл=Эксплуатационная колонна +транс=Транспортный ствол +пилот=Пилотный ствол +хвост=Хвостовик \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportTemplate.xlsx b/AsbCloudInfrastructure/Services/WellOperationImport/Files/WellOperationImportTemplate.xlsx similarity index 100% rename from AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportTemplate.xlsx rename to AsbCloudInfrastructure/Services/WellOperationImport/Files/WellOperationImportTemplate.xlsx diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationExportService.cs b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationExportService.cs new file mode 100644 index 00000000..df49b7aa --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationExportService.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; +using AsbCloudApp.Services; +using AsbCloudApp.Services.WellOperationImport; +using AsbCloudInfrastructure.Services.WellOperationImport.Constants; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.WellOperationImport; + +public class WellOperationExportService : IWellOperationExportService +{ + private readonly IWellOperationRepository wellOperationRepository; + private readonly IWellService wellService; + private readonly IWellOperationImportTemplateService wellOperationImportTemplateService; + + public WellOperationExportService(IWellOperationRepository wellOperationRepository, + IWellService wellService, + IWellOperationImportTemplateService wellOperationImportTemplateService) + { + this.wellOperationRepository = wellOperationRepository; + this.wellService = wellService; + this.wellOperationImportTemplateService = wellOperationImportTemplateService; + } + + public async Task ExportAsync(int idWell, CancellationToken cancellationToken) + { + var operations = await wellOperationRepository.GetAsync(new WellOperationRequest() + { + IdWell = idWell + }, cancellationToken); + + var timezone = wellService.GetTimezone(idWell); + + return await MakeExcelFileStreamAsync(operations, timezone.Hours, cancellationToken); + } + + private async Task MakeExcelFileStreamAsync(IEnumerable operations, double timezoneOffset, + CancellationToken cancellationToken) + { + using Stream ecxelTemplateStream = wellOperationImportTemplateService.GetExcelTemplateStream(); + + using var workbook = new XLWorkbook(ecxelTemplateStream, XLEventTracking.Disabled); + await AddOperationsToWorkbook(workbook, operations, timezoneOffset, cancellationToken); + + var memoryStream = new MemoryStream(); + workbook.SaveAs(memoryStream, new SaveOptions { }); + memoryStream.Seek(0, SeekOrigin.Begin); + return memoryStream; + } + + private async Task AddOperationsToWorkbook(XLWorkbook workbook, IEnumerable operations, double timezoneOffset, + CancellationToken cancellationToken) + { + var planOperations = operations.Where(o => o.IdType == 0); + if (planOperations.Any()) + { + var sheetPlan = workbook.Worksheets.FirstOrDefault(ws => ws.Name == DefaultTemplateInfo.SheetNamePlan); + if (sheetPlan is not null) + await AddOperationsToSheetAsync(sheetPlan, planOperations, timezoneOffset, cancellationToken); + } + + var factOperations = operations.Where(o => o.IdType == 1); + if (factOperations.Any()) + { + var sheetFact = workbook.Worksheets.FirstOrDefault(ws => ws.Name == DefaultTemplateInfo.SheetNameFact); + if (sheetFact is not null) + await AddOperationsToSheetAsync(sheetFact, factOperations, timezoneOffset, cancellationToken); + } + } + + private async Task AddOperationsToSheetAsync(IXLWorksheet sheet, IEnumerable operations, double timezoneOffset, + CancellationToken cancellationToken) + { + var operationsToArray = operations.ToArray(); + + var sections = wellOperationRepository.GetSectionTypes(); + var categories = wellOperationRepository.GetCategories(false); + + for (int i = 0; i < operationsToArray.Length; i++) + { + var row = sheet.Row(1 + i + DefaultTemplateInfo.HeaderRowsCount); + AddOperationToRow(row, operationsToArray[i], sections, categories, timezoneOffset); + } + } + + private static void AddOperationToRow(IXLRow row, WellOperationDto operation, IEnumerable sections, + IEnumerable categories, double timezoneOffset) + { + row.Cell(DefaultTemplateInfo.ColumnSection).Value = sections.First(s => s.Id == operation.IdWellSectionType).Caption; + row.Cell(DefaultTemplateInfo.ColumnCategory).Value = categories.First(o => o.Id == operation.IdCategory).Name; + row.Cell(DefaultTemplateInfo.ColumnCategoryInfo).Value = operation.CategoryInfo; + row.Cell(DefaultTemplateInfo.ColumnDepthStart).Value = operation.DepthStart; + row.Cell(DefaultTemplateInfo.ColumnDepthEnd).Value = operation.DepthEnd; + row.Cell(DefaultTemplateInfo.ColumnDate).Value = new DateTimeOffset(operation.DateStart).ToRemoteDateTime(timezoneOffset); + row.Cell(DefaultTemplateInfo.ColumnDuration).Value = operation.DurationHours; + row.Cell(DefaultTemplateInfo.ColumnComment).Value = operation.Comment; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportService.cs b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportService.cs new file mode 100644 index 00000000..c57a3531 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportService.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.WellOperationImport; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; +using AsbCloudApp.Services.WellOperationImport; +using AsbCloudDb.Model; +using AsbCloudInfrastructure.Services.WellOperationImport.Constants; + +namespace AsbCloudInfrastructure.Services.WellOperationImport; + +public class WellOperationImportService : IWellOperationImportService +{ + private readonly IEnumerable excelParsers; + private readonly IWellOperationRepository wellOperationRepository; + + private static readonly DateTime dateLimitMin = new(2001, 1, 1, 0, 0, 0); + private static readonly DateTime dateLimitMax = new(2099, 1, 1, 0, 0, 0); + private static readonly TimeSpan drillingDurationLimitMax = TimeSpan.FromDays(366); + + public WellOperationImportService(IEnumerable excelParsers, + IWellOperationRepository wellOperationRepository) + { + this.excelParsers = excelParsers; + this.wellOperationRepository = wellOperationRepository; + } + + public async Task ImportAsync(int idWell, int idUser, int idType, Stream stream, WellOperationParserOptionsDto options, + bool deleteWellOperationsBeforeImport, CancellationToken cancellationToken) + { + var excelParser = excelParsers.FirstOrDefault(p => p.IdTemplate == options.IdTemplate && + p.IdTypes.Contains(idType)); + + if (excelParser is null) + throw new ArgumentInvalidException("Невозможно импортировать файл", nameof(options.IdTemplate)); + + if (idType != WellOperation.IdOperationTypePlan && idType != WellOperation.IdOperationTypeFact) + throw new ArgumentInvalidException("Операции не существует", nameof(idType)); + + RowDto[] rows; + var validationErrors = new List(); + + var sections = wellOperationRepository.GetSectionTypes(); + var categories = wellOperationRepository.GetCategories(false); + + switch (options.IdTemplate) + { + case 0: + options.SheetName = idType == WellOperation.IdOperationTypePlan + ? DefaultTemplateInfo.SheetNamePlan + : DefaultTemplateInfo.SheetNameFact; + rows = excelParser.Parse(stream, options).ToArray(); + break; + default: + if (string.IsNullOrWhiteSpace(options.SheetName)) + throw new FileFormatException("Не указано название листа"); + rows = excelParser.Parse(stream, options).ToArray(); + break; + } + + var operations = new List(); + + foreach (var row in rows) + { + try + { + var section = sections.FirstOrDefault(s => + string.Equals(s.Caption, row.Section, StringComparison.CurrentCultureIgnoreCase)); + + if (section is null) + throw new FileFormatException($"Лист '{options.SheetName}'. В строке '{row.Number}' не удалось определить секцию"); + + var category = categories.FirstOrDefault(c => + string.Equals(c.Name, row.Category, StringComparison.CurrentCultureIgnoreCase)); + + if (category is null) + throw new FileFormatException($"Лист '{options.SheetName}'. В строке '{row.Number}' не удалось определить операцию"); + + if (row.DepthStart is not (>= 0d and <= 20_000d)) + throw new FileFormatException($"Лист '{options.SheetName}'. Строка '{row.Number}' некорректная глубина на начало операции"); + + if (row.DepthEnd is not (>= 0d and <= 20_000d)) + throw new FileFormatException($"Лист '{options.SheetName}'. Строка '{row.Number}' некорректная глубина на конец операции"); + + if (row.Date < dateLimitMin && row.Date > dateLimitMax) + throw new FileFormatException($"Лист '{options.SheetName}'. Строка '{row.Number}' неправильно получена дата начала операции"); + + if (operations.LastOrDefault()?.DateStart > row.Date) + throw new FileFormatException($"Лист '{options.SheetName}' строка '{row.Number}' дата позднее даты предыдущей операции"); + + if (row.Duration is not (>= 0d and <= 240d)) + throw new FileFormatException($"Лист '{options.SheetName}'. Строка '{row.Number}' некорректная длительность операции"); + + operations.Add(new WellOperationDto + { + IdWell = idWell, + IdUser = idUser, + IdType = idType, + IdWellSectionType = section.Id, + IdCategory = category.Id, + CategoryInfo = row.CategoryInfo, + DepthStart = row.DepthStart, + DepthEnd = row.DepthEnd, + DateStart = row.Date, + DurationHours = row.Duration + }); + } + catch (FileFormatException ex) + { + validationErrors.Add(ex.Message); + } + } + + if (operations.Any() && operations.Min(o => o.DateStart) - operations.Max(o => o.DateStart) > drillingDurationLimitMax) + validationErrors.Add($"Лист {options.SheetName} содержит диапазон дат больше {drillingDurationLimitMax}"); + + if (validationErrors.Any()) + throw new FileFormatException(string.Join("\r\n", validationErrors)); + + if(!operations.Any()) + return; + + if (deleteWellOperationsBeforeImport) + { + var existingOperations = await wellOperationRepository.GetAsync(new WellOperationRequest + { + IdWell = idWell + }, cancellationToken); + + await wellOperationRepository.DeleteAsync(existingOperations.Select(o => o.Id), cancellationToken); + } + + await wellOperationRepository.InsertRangeAsync(operations, cancellationToken); + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportTemplateService.cs b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportTemplateService.cs new file mode 100644 index 00000000..edb71c14 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportTemplateService.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using AsbCloudApp.Services.WellOperationImport; + +namespace AsbCloudInfrastructure.Services.WellOperationImport; + +public class WellOperationImportTemplateService : IWellOperationImportTemplateService +{ + public Stream GetExcelTemplateStream() + { + var resourceName = Assembly.GetExecutingAssembly() + .GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("WellOperationImportTemplate.xlsx"))!; + + var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(resourceName)!; + + return stream; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportService.cs b/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportService.cs deleted file mode 100644 index 949aaf71..00000000 --- a/AsbCloudInfrastructure/Services/WellOperationService/WellOperationImportService.cs +++ /dev/null @@ -1,341 +0,0 @@ -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 -{ - - /* - * 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 - .Where(c => c.Id >= 5000) - .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, int idUser, bool deleteWellOperationsBeforeImport = false) - { - using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled); - var operations = ParseFileStream(stream); - foreach (var operation in operations) - { - operation.IdWell = idWell; - operation.IdUser = idUser; - } - - 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); - entity.LastUpdateDate = DateTimeOffset.UtcNow; - 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()} указана некорректная операция ({categoryName})"); - - 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; - } - } - -} diff --git a/AsbCloudWebApi/Controllers/WellOperationController.cs b/AsbCloudWebApi/Controllers/WellOperationController.cs index 335981df..82bc6bf6 100644 --- a/AsbCloudWebApi/Controllers/WellOperationController.cs +++ b/AsbCloudWebApi/Controllers/WellOperationController.cs @@ -11,6 +11,8 @@ using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading; using System.Threading.Tasks; +using AsbCloudApp.Data.WellOperationImport; +using AsbCloudApp.Services.WellOperationImport; namespace AsbCloudWebApi.Controllers { @@ -25,15 +27,22 @@ namespace AsbCloudWebApi.Controllers { private readonly IWellOperationRepository operationRepository; private readonly IWellService wellService; + private readonly IWellOperationExportService wellOperationExportService; + private readonly IWellOperationImportTemplateService wellOperationImportTemplateService; private readonly IWellOperationImportService wellOperationImportService; private readonly IUserRepository userRepository; - public WellOperationController(IWellOperationRepository operationRepository, IWellService wellService, + public WellOperationController(IWellOperationRepository operationService, + IWellService wellService, + IWellOperationImportTemplateService wellOperationImportTemplateService, + IWellOperationExportService wellOperationExportService, IWellOperationImportService wellOperationImportService, IUserRepository userRepository) { this.operationRepository = operationRepository; this.wellService = wellService; + this.wellOperationImportTemplateService = wellOperationImportTemplateService; + this.wellOperationExportService = wellOperationExportService; this.wellOperationImportService = wellOperationImportService; this.userRepository = userRepository; } @@ -278,22 +287,32 @@ namespace AsbCloudWebApi.Controllers /// - /// Импортирует операции из excel (xlsx) файла + /// Импорт плановых операций из excel (xlsx) файла /// /// id скважины + /// Тип операции + /// Начальная строка + /// Конечная строка /// Коллекция из одного файла xlsx - /// Удалить операции перед импортом = 1, если фал валидный - /// Токен отмены задачи + /// Удалить операции перед импортом = 1, если файл валидный + /// Название листа + /// Токен отмены задачи + /// Шаблон файла. 0 - стандартный, 1 - Газпромнефть Хантос /// [HttpPost("import/{options}")] [Permission] public async Task ImportAsync(int idWell, + [Required] int idType, + string? sheetName, + [Required] int idTemplate, + int? startRow, + int? endRow, [FromForm] IFormFileCollection files, int options, CancellationToken token) { - int? idCompany = User.GetCompanyId(); - int? idUser = User.GetUserId(); + var idCompany = User.GetCompanyId(); + var idUser = User.GetUserId(); if (idCompany is null || idUser is null) return Forbid(); @@ -309,16 +328,23 @@ namespace AsbCloudWebApi.Controllers return Forbid(); if (files.Count < 1) - return BadRequest("нет файла"); + 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, idUser.Value, (options & 1) > 0); + await wellOperationImportService.ImportAsync(idWell, idUser.Value, idType, stream, new WellOperationParserOptionsDto + { + SheetName = sheetName, + IdTemplate = idTemplate, + StartRow = startRow, + EndRow = endRow + }, (options & 1) > 0, token); } catch (FileFormatException ex) { @@ -349,7 +375,7 @@ namespace AsbCloudWebApi.Controllers idWell, token).ConfigureAwait(false)) return Forbid(); - var stream = wellOperationImportService.Export(idWell); + var stream = await wellOperationExportService.ExportAsync(idWell, token); var fileName = await wellService.GetWellCaptionByIdAsync(idWell, token) + "_operations.xlsx"; return File(stream, "application/octet-stream", fileName); } @@ -389,7 +415,7 @@ namespace AsbCloudWebApi.Controllers [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK, "application/octet-stream")] public IActionResult GetTemplate() { - var stream = wellOperationImportService.GetExcelTemplateStream(); + var stream = wellOperationImportTemplateService.GetExcelTemplateStream(); var fileName = "ЕЦП_шаблон_файла_операций.xlsx"; return File(stream, "application/octet-stream", fileName); }