diff --git a/AsbCloudApp/Services/WellOperationImport/IWellOperationImportService.cs b/AsbCloudApp/Services/WellOperationImport/IWellOperationImportService.cs index 54e813d8..f65d8888 100644 --- a/AsbCloudApp/Services/WellOperationImport/IWellOperationImportService.cs +++ b/AsbCloudApp/Services/WellOperationImport/IWellOperationImportService.cs @@ -1,5 +1,5 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Collections.Generic; +using AsbCloudApp.Data; using AsbCloudApp.Data.WellOperationImport; namespace AsbCloudApp.Services.WellOperationImport; @@ -16,7 +16,5 @@ public interface IWellOperationImportService /// /// /// - /// - /// - Task ImportAsync(int idWell, int idUser, int idType, SheetDto sheet, bool deleteBeforeImport, CancellationToken cancellationToken); + IEnumerable Import(int idWell, int idUser, int idType, SheetDto sheet); } \ No newline at end of file diff --git a/AsbCloudDb/Setup db replication.md b/AsbCloudDb/Setup db replication.md index 91c64792..4c5fe184 100644 --- a/AsbCloudDb/Setup db replication.md +++ b/AsbCloudDb/Setup db replication.md @@ -113,17 +113,12 @@ sudo -u postgres psql SELECT * FROM pg_stat_replication; ``` - 7. Для включения синхронного режима необходимо выполнить следующую команду - ``` - ALTER SYSTEM SET synchronous_standby_names TO '*'; - ``` + 7. Сделать рестарт primary-сервера. - 8. Сделать рестарт primary-сервера. - - 9. Внести запись в любую таблицу базы данных primary-сервера - 10. Убедиться, что соответствующая запись появилась в таблице базы данных standby-сервера - 11. Попытаться внести запись в таблицу базы данных standby-сервера. - 12. Убедиться, что операция завершилась с ошибкой + 8. Внести запись в любую таблицу базы данных primary-сервера + 9. Убедиться, что соответствующая запись появилась в таблице базы данных standby-сервера + 10. Попытаться внести запись в таблицу базы данных standby-сервера. + 11. Убедиться, что операция завершилась с ошибкой > cannot execute OPERATION in a read-only transaction diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/Files/WellOperationImportTemplate.xlsx b/AsbCloudInfrastructure/Services/WellOperationImport/Files/WellOperationImportTemplate.xlsx index 48e19c6c..1e89ad31 100644 Binary files a/AsbCloudInfrastructure/Services/WellOperationImport/Files/WellOperationImportTemplate.xlsx and b/AsbCloudInfrastructure/Services/WellOperationImport/Files/WellOperationImportTemplate.xlsx differ diff --git a/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportService.cs b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportService.cs index 9da293c6..bf551edc 100644 --- a/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportService.cs +++ b/AsbCloudInfrastructure/Services/WellOperationImport/WellOperationImportService.cs @@ -2,12 +2,9 @@ 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.Repositories; -using AsbCloudApp.Requests; using AsbCloudApp.Services.WellOperationImport; namespace AsbCloudInfrastructure.Services.WellOperationImport; @@ -25,14 +22,14 @@ public class WellOperationImportService : IWellOperationImportService this.wellOperationRepository = wellOperationRepository; } - public async Task ImportAsync(int idWell, int idUser, int idType, SheetDto sheet, bool deleteBeforeImport, CancellationToken cancellationToken) + public IEnumerable Import(int idWell, int idUser, int idType, SheetDto sheet) { var validationErrors = new List(); var sections = wellOperationRepository.GetSectionTypes(); var categories = wellOperationRepository.GetCategories(false); - var operations = new List(); + var wellOperations = new List(); foreach (var row in sheet.Rows) { @@ -62,14 +59,14 @@ public class WellOperationImportService : IWellOperationImportService throw new FileFormatException( $"Лист '{sheet.Name}'. Строка '{row.Number}' неправильно получена дата начала операции"); - if (operations.LastOrDefault()?.DateStart > row.Date) + if (wellOperations.LastOrDefault()?.DateStart > row.Date) throw new FileFormatException( $"Лист '{sheet.Name}' строка '{row.Number}' дата позднее даты предыдущей операции"); if (row.Duration is not (>= 0d and <= 240d)) throw new FileFormatException($"Лист '{sheet.Name}'. Строка '{row.Number}' некорректная длительность операции"); - operations.Add(new WellOperationDto + wellOperations.Add(new WellOperationDto { IdWell = idWell, IdUser = idUser, @@ -89,26 +86,12 @@ public class WellOperationImportService : IWellOperationImportService } } - if (operations.Any() && operations.Min(o => o.DateStart) - operations.Max(o => o.DateStart) > drillingDurationLimitMax) + if (wellOperations.Any() && wellOperations.Min(o => o.DateStart) - wellOperations.Max(o => o.DateStart) > drillingDurationLimitMax) validationErrors.Add($"Лист {sheet.Name} содержит диапазон дат больше {drillingDurationLimitMax}"); if (validationErrors.Any()) throw new FileFormatException(string.Join("\r\n", validationErrors)); - - if (!operations.Any()) - return; - - if (deleteBeforeImport) - { - var existingOperations = await wellOperationRepository.GetAsync(new WellOperationRequest - { - IdWell = idWell, - OperationType = idType - }, cancellationToken); - - await wellOperationRepository.DeleteAsync(existingOperations.Select(o => o.Id), cancellationToken); - } - - await wellOperationRepository.InsertRangeAsync(operations, cancellationToken); + + return wellOperations; } } \ No newline at end of file diff --git a/AsbCloudWebApi.Tests/UnitTests/Services/DailyReportServiceTest.cs b/AsbCloudWebApi.Tests/UnitTests/Services/DailyReportServiceTest.cs index 8202a95f..95008c7a 100644 --- a/AsbCloudWebApi.Tests/UnitTests/Services/DailyReportServiceTest.cs +++ b/AsbCloudWebApi.Tests/UnitTests/Services/DailyReportServiceTest.cs @@ -254,7 +254,7 @@ public class DailyReportServiceTest wellServiceMock.GetOrDefaultAsync(Arg.Any(), Arg.Any()) .ReturnsForAnyArgs(fakeWell); - trajectoryFactNnbRepositoryMock.GetAsync(Arg.Any(), Arg.Any()) + trajectoryFactNnbRepositoryMock.GetByRequestAsync(Arg.Any(), Arg.Any()) .ReturnsForAnyArgs(new[] { fakeLastFactTrajectory }); wellOperationRepositoryMock.GetAsync(Arg.Any(), Arg.Any()) diff --git a/AsbCloudWebApi/Controllers/MockController.cs b/AsbCloudWebApi/Controllers/MockController.cs index 0c95725e..4cec7ce2 100644 --- a/AsbCloudWebApi/Controllers/MockController.cs +++ b/AsbCloudWebApi/Controllers/MockController.cs @@ -1,72 +1,119 @@ -using Microsoft.AspNetCore.Mvc; +using AsbCloudApp.Exceptions; +using AsbCloudWebApi.SignalR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading; +using System.Threading.Tasks; -namespace AsbCloudWebApi.Controllers +namespace AsbCloudWebApi.Controllers; + +/// +/// Имитирует разные типы ответа сервера +/// +[Route("api/[controller]")] +[ApiController] +public class MockController : ControllerBase { - /// - /// Имитирует разные типы ответа сервера - /// - [Route("api/[controller]")] - [ApiController] - public class MockController : ControllerBase + private readonly IServiceProvider provider; + + public MockController(IServiceProvider provider) { - /// - /// имитирует http-400 - /// - [HttpGet("400")] - [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] - public IActionResult Get400([FromQuery, Required]IDictionary args) + this.provider = provider; + } + + /// + /// имитирует http-400 + /// + [HttpGet("400")] + [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] + public IActionResult Get400([FromQuery, Required]IDictionary args) + { + var errors = new Dictionary(); + + foreach (var arg in args) { - var errors = new Dictionary(); - - foreach (var arg in args) - { - var countOfErrors = ((arg.Key + arg.Value).Length % 3) + 1; - var errorsText = Enumerable.Range(0, countOfErrors) - .Select(i => $"{arg.Value} не соответствует критериям проверки № {i}"); + var countOfErrors = ((arg.Key + arg.Value).Length % 3) + 1; + var errorsText = Enumerable.Range(0, countOfErrors) + .Select(i => $"{arg.Value} не соответствует критериям проверки № {i}"); - errors.Add(arg.Key, errorsText.ToArray()); - } - - if (errors.Any()) - { - var problem = new ValidationProblemDetails(errors); - return BadRequest(problem); - } - else - { - var problem = new ValidationProblemDetails { Detail = "at least one argument must be provided" }; - return BadRequest(problem); - } + errors.Add(arg.Key, errorsText.ToArray()); } - /// - /// имитирует http-403 - /// - [HttpGet("403")] - public IActionResult Get403() + if (errors.Any()) { - return Forbid(); + var problem = new ValidationProblemDetails(errors); + return BadRequest(problem); } - - /// - /// имитирует http-401 - /// - [HttpGet("401")] - public IActionResult Get401() + else { - return Unauthorized(); - } - - /// - /// имитирует http-500 - /// - [HttpGet("500")] - public IActionResult Get500() - { - throw new System.Exception("Это тестовое исключение"); + var problem = new ValidationProblemDetails { Detail = "at least one argument must be provided" }; + return BadRequest(problem); } } + + /// + /// имитирует http-403 + /// + [HttpGet("403")] + public IActionResult Get403() + { + return Forbid(); + } + + /// + /// имитирует http-401 + /// + [HttpGet("401")] + public IActionResult Get401() + { + return Unauthorized(); + } + + /// + /// имитирует http-500 + /// + [HttpGet("500")] + public IActionResult Get500() + { + throw new System.Exception("Это тестовое исключение"); + } + + /// + /// имитация отправки SignalR данных + /// + /// + /// + /// + /// Поддерживаемые hubЫ: wellInfo, notifications, telemetry, reports + /// + /// Название вызываемого на клиенте метода. Прим.:"ReceiveDataSaub". Список методов см. в swagger definition signalr + /// Группа пользователей. Прим.: "well_1". Если не задана - все пользователи. Шаблон формирования групп см. описание методов в swagger definition signalr + /// передаваемая нагрузка. (json) + /// + /// + [HttpPost("signalr/hubs/{hubName}/{methodName}/{groupName}")] + [Authorize] + public async Task PostAsync(string hubName, string methodName, string? groupName, object body, CancellationToken token) + { + IHubClients clients = hubName.ToLower() switch { + "wellinfo" => provider.GetRequiredService>().Clients, + "notifications" => provider.GetRequiredService>().Clients, + "telemetry" => provider.GetRequiredService>().Clients, + "reports" => provider.GetRequiredService>().Clients, + _ => throw new ArgumentInvalidException(nameof(hubName), "hubName does not listed"), + }; + + IClientProxy selectedClients = string.IsNullOrEmpty(groupName) + ? clients.All + : clients.Group(groupName); + + await selectedClients.SendAsync(methodName, body, token); + return Ok(); + } } diff --git a/AsbCloudWebApi/Controllers/WellOperationController.cs b/AsbCloudWebApi/Controllers/WellOperationController.cs index 54270f49..45db3f0e 100644 --- a/AsbCloudWebApi/Controllers/WellOperationController.cs +++ b/AsbCloudWebApi/Controllers/WellOperationController.cs @@ -9,8 +9,10 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using AsbCloudApp.Data.WellOperationImport; using AsbCloudApp.Services.WellOperationImport; using AsbCloudApp.Data.WellOperationImport.Options; using AsbCloudApp.Exceptions; @@ -204,37 +206,87 @@ namespace AsbCloudWebApi.Controllers return Ok(result); } + /// + /// Добавляет новую операцию на скважину + /// + /// Id скважины + /// Тип добавляемой операции + /// Добавляемая операция + /// + /// Количество добавленных в БД записей + [HttpPost("{idType:int}")] + [Permission] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + public async Task InsertAsync( + [Range(1, int.MaxValue, ErrorMessage = "Id скважины не может быть меньше 1")] int idWell, + [Range(0, 1, ErrorMessage = "Тип операции недопустим. Допустимые: 0, 1")] int idType, + WellOperationDto wellOperation, + CancellationToken cancellationToken) + { + if (!await CanUserAccessToWellAsync(idWell, cancellationToken)) + return Forbid(); + + if (!await CanUserEditWellOperationsAsync(idWell, cancellationToken)) + return Forbid(); + + wellOperation.IdWell = idWell; + wellOperation.LastUpdateDate = DateTimeOffset.UtcNow; + wellOperation.IdUser = User.GetUserId(); + wellOperation.IdType = idType; + + var result = await operationRepository.InsertRangeAsync(new[] { wellOperation }, cancellationToken); + + return Ok(result); + } + /// /// Добавляет новые операции на скважине /// - /// id скважины - /// Данные о добавляемых операциях - /// Токен отмены задачи - /// Количество добавленных в БД строк - [HttpPost] + /// Id скважины + /// Добавляемые операции + /// Тип добавляемых операций + /// Удалить операции перед сохранением + /// + /// Количество добавленных в БД записей + [HttpPost("{idType:int}/{deleteBeforeInsert:bool}")] [Permission] - [ProducesResponseType(typeof(IEnumerable), (int)System.Net.HttpStatusCode.OK)] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] public async Task InsertRangeAsync( - [Range(1, int.MaxValue, ErrorMessage = "Id скважины не может быть меньше 1")] int idWell, - [FromBody] IEnumerable values, - CancellationToken token) + [Range(1, int.MaxValue, ErrorMessage = "Id скважины не может быть меньше 1")] int idWell, + [Range(0, 1, ErrorMessage = "Тип операции недопустим. Допустимые: 0, 1")] int idType, + bool deleteBeforeInsert, + [FromBody] IEnumerable wellOperations, + CancellationToken cancellationToken) { - if (!await CanUserAccessToWellAsync(idWell, token)) - return Forbid(); - - if (!await CanUserEditWellOperationsAsync(idWell, token)) + if (!await CanUserAccessToWellAsync(idWell, cancellationToken)) return Forbid(); - foreach (var value in values) + if (!await CanUserEditWellOperationsAsync(idWell, cancellationToken)) + return Forbid(); + + if (deleteBeforeInsert && wellOperations.Any()) { - value.IdWell = idWell; - value.LastUpdateDate = DateTimeOffset.UtcNow; - value.IdUser = User.GetUserId(); + var existingOperations = await operationRepository.GetAsync(new WellOperationRequest + { + IdWell = idWell, + OperationType = idType + }, cancellationToken); + + await operationRepository.DeleteAsync(existingOperations.Select(o => o.Id), cancellationToken); + } + + foreach (var wellOperation in wellOperations) + { + wellOperation.IdWell = idWell; + wellOperation.LastUpdateDate = DateTimeOffset.UtcNow; + wellOperation.IdUser = User.GetUserId(); + wellOperation.IdType = idType; } - var result = await operationRepository.InsertRangeAsync(values, token) - .ConfigureAwait(false); - + var result = await operationRepository.InsertRangeAsync(wellOperations, cancellationToken); + return Ok(result); } @@ -299,46 +351,19 @@ namespace AsbCloudWebApi.Controllers /// id скважины /// Параметры для парсинга файла /// Коллекция из одного файла xlsx - /// Удалить операции перед импортом = 1, если файл валидный - /// + /// /// - [HttpPost("import/default/{deleteBeforeImport}")] + [HttpPost("import/default")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [Permission] - public async Task ImportDefaultExcelFileAsync(int idWell, + public Task ImportDefaultExcelFileAsync(int idWell, [FromQuery] WellOperationImportDefaultOptionsDto options, [FromForm] IFormFileCollection files, - [Range(0, 1, ErrorMessage = "Недопустимое значение. Допустимые: 0, 1")] int deleteBeforeImport, - CancellationToken token) - { - var idUser = User.GetUserId(); - - if (!idUser.HasValue) - throw new ForbidException("Неизвестный пользователь"); - - await AssertUserHasAccessToImportWellOperationsAsync(idWell, token); - - if (files.Count < 1) - return this.ValidationBadRequest(nameof(files), "Нет файла"); - - var file = files[0]; - if (Path.GetExtension(file.FileName).ToLower() != ".xlsx") - return this.ValidationBadRequest(nameof(files), "Требуется xlsx файл."); - - using Stream stream = file.OpenReadStream(); - - try - { - var sheet = wellOperationDefaultExcelParser.Parse(stream, options); - - await wellOperationImportService.ImportAsync(idWell, idUser.Value, options.IdType, sheet, (deleteBeforeImport & 1) > 0, token); - } - catch (FileFormatException ex) - { - return this.ValidationBadRequest(nameof(files), ex.Message); - } - - return Ok(); - } + CancellationToken cancellationToken) => ImportExcelFileAsync(idWell, files, options, + (stream, _) => wellOperationDefaultExcelParser.Parse(stream, options), + cancellationToken); /// /// Импорт операций из excel (xlsx) файла. ГПНХ (Хантос) @@ -346,46 +371,19 @@ namespace AsbCloudWebApi.Controllers /// id скважины /// Параметры для парсинга файла /// Коллекция из одного файла xlsx - /// Удалить операции перед импортом = 1, если файл валидный - /// + /// /// - [HttpPost("import/gazpromKhantos/{deleteBeforeImport}")] + [HttpPost("import/gazpromKhantos")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [Permission] - public async Task ImportGazpromKhantosExcelFileAsync(int idWell, - [FromQuery] WellOperationImportGazpromKhantosOptionsDto options, - [FromForm] IFormFileCollection files, - [Range(0, 1, ErrorMessage = "Недопустимое значение. Допустимые: 0, 1")] int deleteBeforeImport, - CancellationToken token) - { - var idUser = User.GetUserId(); - - if (!idUser.HasValue) - throw new ForbidException("Неизвестный пользователь"); - - await AssertUserHasAccessToImportWellOperationsAsync(idWell, token); - - if (files.Count < 1) - return this.ValidationBadRequest(nameof(files), "Нет файла"); - - var file = files[0]; - if (Path.GetExtension(file.FileName).ToLower() != ".xlsx") - return this.ValidationBadRequest(nameof(files), "Требуется xlsx файл."); - - using Stream stream = file.OpenReadStream(); - - try - { - var sheet = wellOperationGazpromKhantosExcelParser.Parse(stream, options); - - await wellOperationImportService.ImportAsync(idWell, idUser.Value, options.IdType, sheet, (deleteBeforeImport & 1) > 0, token); - } - catch (FileFormatException ex) - { - return this.ValidationBadRequest(nameof(files), ex.Message); - } - - return Ok(); - } + public Task ImportGazpromKhantosExcelFileAsync(int idWell, + [FromQuery] WellOperationImportGazpromKhantosOptionsDto options, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken) => ImportExcelFileAsync(idWell, files, options, + (stream, _) => wellOperationGazpromKhantosExcelParser.Parse(stream, options), + cancellationToken); /// /// Создает excel файл с операциями по скважине @@ -453,7 +451,11 @@ namespace AsbCloudWebApi.Controllers return File(stream, "application/octet-stream", fileName); } - private async Task AssertUserHasAccessToImportWellOperationsAsync(int idWell, CancellationToken token) + private async Task ImportExcelFileAsync(int idWell, [FromForm] IFormFileCollection files, + TOptions options, + Func parseMethod, + CancellationToken cancellationToken) + where TOptions : IWellOperationImportOptions { var idCompany = User.GetCompanyId(); var idUser = User.GetUserId(); @@ -461,16 +463,54 @@ namespace AsbCloudWebApi.Controllers if (!idCompany.HasValue || !idUser.HasValue) throw new ForbidException("Неизвестный пользователь"); - if (!await CanUserAccessToWellAsync(idWell, token)) + if (!await CanUserAccessToWellAsync(idWell, cancellationToken)) throw new ForbidException("Нет доступа к скважине"); - if (!await CanUserEditWellOperationsAsync(idWell, token)) + if (!await CanUserEditWellOperationsAsync(idWell, cancellationToken)) throw new ForbidException("Недостаточно прав для редактирования ГГД на завершенной скважине"); - if (!await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, idWell, token)) + if (!await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, idWell, cancellationToken)) throw new ForbidException("Скважина недоступна для компании"); - } + if (files.Count < 1) + return this.ValidationBadRequest(nameof(files), "Нет файла"); + + var file = files[0]; + if (Path.GetExtension(file.FileName).ToLower() != ".xlsx") + return this.ValidationBadRequest(nameof(files), "Требуется xlsx файл."); + + using Stream stream = file.OpenReadStream(); + + try + { + var sheet = parseMethod(stream, options); + + var wellOperations = wellOperationImportService.Import(idWell, idUser.Value, options.IdType, sheet) + .OrderBy(w => w.DateStart); + + var dateStart = wellOperations.Min(w => w.DateStart); + + foreach (var wellOperation in wellOperations) + wellOperation.Day = (wellOperation.DateStart - dateStart).TotalDays; + + if (!wellOperations.Any()) + return NoContent(); + + return Ok(wellOperations); + } + catch (FileFormatException ex) + { + return this.ValidationBadRequest(nameof(files), ex.Message); + } + } + + private async Task CanUserAccessToWellAsync(int idWell, CancellationToken token) + { + int? idCompany = User.GetCompanyId(); + return idCompany is not null && await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, + idWell, token).ConfigureAwait(false); + } + private async Task CanUserEditWellOperationsAsync(int idWell, CancellationToken token) { var idUser = User.GetUserId(); @@ -485,13 +525,5 @@ namespace AsbCloudWebApi.Controllers return well.IdState != 2 || userRepository.HasPermission(idUser.Value, "WellOperation.editCompletedWell"); } - - private async Task CanUserAccessToWellAsync(int idWell, CancellationToken token) - { - int? idCompany = User.GetCompanyId(); - return idCompany is not null && await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, - idWell, token).ConfigureAwait(false); - } } - -} +} \ No newline at end of file