diff --git a/AsbCloudApp/Repositories/IWellOperationRepository.cs b/AsbCloudApp/Repositories/IWellOperationRepository.cs index d37a1e90..12246578 100644 --- a/AsbCloudApp/Repositories/IWellOperationRepository.cs +++ b/AsbCloudApp/Repositories/IWellOperationRepository.cs @@ -1,9 +1,9 @@ using AsbCloudApp.Data; +using AsbCloudApp.Data.WellOperation; +using AsbCloudApp.Requests; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using AsbCloudApp.Data.WellOperation; -using AsbCloudApp.Requests; namespace AsbCloudApp.Repositories { @@ -17,8 +17,8 @@ namespace AsbCloudApp.Repositories /// /// IEnumerable GetSectionTypes(); - - /// + + /// /// Получить страницу списка операций /// /// @@ -26,7 +26,7 @@ namespace AsbCloudApp.Repositories /// Task> GetAsync(WellOperationRequest request, CancellationToken token); - /// + /// /// Получить страницу списка операций /// /// @@ -34,7 +34,7 @@ namespace AsbCloudApp.Repositories /// Task> GetPageAsync(WellOperationRequest request, CancellationToken token); - /// + /// /// Получить статистику операции по скважине с группировкой по категориям /// /// @@ -42,14 +42,14 @@ namespace AsbCloudApp.Repositories /// Task> GetGroupOperationsStatAsync(WellOperationRequest request, CancellationToken token); - /// - /// Добавить несколько операций - /// - /// - /// - /// - /// - Task InsertRangeAsync(IEnumerable dtos, bool deleteBeforeInsert, CancellationToken token); + /// + /// Добавить несколько операций + /// + /// + /// + /// + /// + Task InsertRangeAsync(IEnumerable dtos, bool deleteBeforeInsert, CancellationToken token); /// /// Обновить существующую операцию @@ -75,13 +75,20 @@ namespace AsbCloudApp.Repositories /// Task> GetSectionsAsync(IEnumerable idsWells, CancellationToken token); - /// - /// Получить диапазон дат выполнения операций - /// - /// - /// - /// - /// - Task GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken); - } + /// + /// Получить диапазон дат выполнения операций + /// + /// + /// + /// + /// + Task GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken); + + /// + /// Возвращает первую и последнюю фактическую операцию + /// + /// + /// + (WellOperationDto First, WellOperationDto Last)? GetFirstAndLastFact(int idWell); + } } \ No newline at end of file diff --git a/AsbCloudInfrastructure/Repository/DepositRepository.cs b/AsbCloudInfrastructure/Repository/DepositRepository.cs index 804d130e..497ef3b6 100644 --- a/AsbCloudInfrastructure/Repository/DepositRepository.cs +++ b/AsbCloudInfrastructure/Repository/DepositRepository.cs @@ -16,12 +16,12 @@ namespace AsbCloudInfrastructure.Repository public class DepositRepository : IDepositRepository { private readonly IAsbCloudDbContext db; - private readonly IWellService wellService; + private readonly ITelemetryService telemetryService; - public DepositRepository(IAsbCloudDbContext db, IWellService wellService) + public DepositRepository(IAsbCloudDbContext db, ITelemetryService telemetryService) { this.db = db; - this.wellService = wellService; + this.telemetryService = telemetryService; } /// @@ -112,8 +112,12 @@ namespace AsbCloudInfrastructure.Repository { var dto = well.Adapt(); dto.WellType = well.WellType.Caption; - dto.LastTelemetryDate = wellService.GetLastTelemetryDate(well.Id) - .ToOffset(TimeSpan.FromHours(well.Timezone.Hours)); + + dto.LastTelemetryDate = DateTimeOffset.MinValue; + + if (well.IdTelemetry != null) + dto.LastTelemetryDate = telemetryService.GetDatesRange(well.IdTelemetry.Value).To; + dto.Cluster = gCluster.Key.Caption; dto.Deposit = gDeposit.Key.Caption; return dto; diff --git a/AsbCloudInfrastructure/Repository/WellOperationRepository.cs b/AsbCloudInfrastructure/Repository/WellOperationRepository.cs index 920702b1..1ab3de01 100644 --- a/AsbCloudInfrastructure/Repository/WellOperationRepository.cs +++ b/AsbCloudInfrastructure/Repository/WellOperationRepository.cs @@ -20,6 +20,7 @@ namespace AsbCloudInfrastructure.Repository; public class WellOperationRepository : CrudRepositoryBase, IWellOperationRepository { + private const string cacheKeyWellOperations = "FirstAndLastFactWellsOperations"; private readonly IMemoryCache memoryCache; private readonly IWellOperationCategoryRepository wellOperationCategoryRepository; private readonly IWellService wellService; @@ -54,15 +55,15 @@ public class WellOperationRepository : CrudRepositoryBase> GetPageAsync(WellOperationRequest request, CancellationToken token) { - var skip = request.Skip ?? 0; - var take = request.Take ?? 32; + request.Skip = request.Skip ?? 0; + request.Take = request.Take ?? 32; var (items, count) = await GetWithDaysAndNpvAsync(request, token); var paginationContainer = new PaginationContainer { - Skip = skip, - Take = take, + Skip = request.Skip!.Value, + Take = request.Take!.Value, Count = count, Items = items }; @@ -167,11 +168,17 @@ public class WellOperationRepository : CrudRepositoryBase UpdateRangeAsync(IEnumerable dtos, CancellationToken token) + public override async Task UpdateRangeAsync(IEnumerable dtos, CancellationToken token) { EnsureValidWellOperations(dtos); - return base.UpdateRangeAsync(dtos, token); + var result = await base.UpdateRangeAsync(dtos, token); + + if (result > 0) + memoryCache.Remove(cacheKeyWellOperations); + + return result; + } private static void EnsureValidWellOperations(IEnumerable dtos) @@ -194,9 +201,6 @@ public class WellOperationRepository : CrudRepositoryBase items, int count)> GetWithDaysAndNpvAsync(WellOperationRequest request, CancellationToken token) { - var skip = request.Skip ?? 0; - var take = request.Take ?? 32; - var entities = await GetByIdsWells(request.IdsWell, token); var groupedByWellAndType = entities .GroupBy(e => new { e.IdWell, e.IdType }); @@ -214,11 +218,14 @@ public class WellOperationRepository : CrudRepositoryBase filteredWellOperations = FilterByRequest(wellOperationsWithType.AsQueryable(), request); - var filteredWellOperationsPart = filteredWellOperations - .Skip(skip) - .Take(take); + count += filteredWellOperations.Count(); - var dtos = filteredWellOperationsPart + if (request.Skip != null) + filteredWellOperations = filteredWellOperations.Skip((int)request.Skip); + if (request.Take != null) + filteredWellOperations = filteredWellOperations.Take((int)request.Take); + + var dtos = filteredWellOperations .Select(entity => { var dto = Convert(entity); @@ -230,7 +237,6 @@ public class WellOperationRepository : CrudRepositoryBase + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); + var query = dbContext.Set() + .Where(o => o.IdType == WellOperation.IdOperationTypeFact) + .GroupBy(o => o.IdWell) + .Select(group => new + { + IdWell = group.Key, + FirstFact = group.OrderBy(o => o.DateStart).First(), + LastFact = group.OrderBy(o => o.DateStart).Last(), + }); + + var entities = query.ToArray(); + + var dictionary = entities.ToDictionary(s => s.IdWell, s => (Convert(s.FirstFact), Convert(s.LastFact))); + entry.Value = dictionary; + + return dictionary; + + })!; + + var firstAndLast = cachedDictionary.GetValueOrDefault(idWell); + return firstAndLast; + + } + + public override async Task DeleteAsync(int id, CancellationToken token) + { + var result = await base.DeleteAsync(id, token); + if (result > 0) + memoryCache.Remove(cacheKeyWellOperations); + + return result; + } + + public override async Task DeleteRangeAsync(IEnumerable ids, CancellationToken token) + { + var result = await base.DeleteRangeAsync(ids, token); + if (result > 0) + memoryCache.Remove(cacheKeyWellOperations); + + return result; + } + protected override WellOperation Convert(WellOperationDto src) { var entity = src.Adapt(); diff --git a/AsbCloudInfrastructure/Services/SAUB/MessageService.cs b/AsbCloudInfrastructure/Services/SAUB/MessageService.cs index 17d92d8d..ea6cc79e 100644 --- a/AsbCloudInfrastructure/Services/SAUB/MessageService.cs +++ b/AsbCloudInfrastructure/Services/SAUB/MessageService.cs @@ -142,14 +142,13 @@ namespace AsbCloudInfrastructure.Services.SAUB return Task.CompletedTask; var telemetry = telemetryService.GetOrCreateTelemetryByUid(uid); - var timezone = telemetryService.GetTimezone(telemetry.Id); - + foreach (var dto in dtos) { var entity = dto.Adapt(); entity.Id = 0; entity.IdTelemetry = telemetry.Id; - entity.DateTime = dto.Date.ToOffset(TimeSpan.FromHours(timezone.Hours)); + entity.DateTime = dto.Date.ToUniversalTime(); db.TelemetryMessages.Add(entity); } diff --git a/AsbCloudInfrastructure/Services/WellInfoService.cs b/AsbCloudInfrastructure/Services/WellInfoService.cs index 23054a2d..5d5f3bf3 100644 --- a/AsbCloudInfrastructure/Services/WellInfoService.cs +++ b/AsbCloudInfrastructure/Services/WellInfoService.cs @@ -37,7 +37,8 @@ public class WellInfoService var telemetryDataSaubCache = services.GetRequiredService>(); var messageHub = services.GetRequiredService>(); - var wells = await wellService.GetAllAsync(token); + var entries = await wellService.GetAllAsync(token); + var wells = entries.ToList(); var activeWells = wells.Where(well => well.IdState == 1); var wellsIds = activeWells.Select(w => w.Id); diff --git a/AsbCloudInfrastructure/Services/WellOperations/WellOperationExport.cs b/AsbCloudInfrastructure/Services/WellOperations/WellOperationExport.cs index 430bf92f..66e52fd8 100644 --- a/AsbCloudInfrastructure/Services/WellOperations/WellOperationExport.cs +++ b/AsbCloudInfrastructure/Services/WellOperations/WellOperationExport.cs @@ -42,7 +42,7 @@ public class WellOperationExport : ExcelExportService well.Id == idWell); if (wellInfo is null) return well.Adapt(); - + wellInfo.IdState = well.IdState; return wellInfo; } @@ -153,7 +153,7 @@ namespace AsbCloudInfrastructure.Services { if (IsTelemetryAssignedToDifferentWell(dto)) throw new ArgumentInvalidException(nameof(dto), "Телеметрия уже была привязана к другой скважине."); - + if (dto.Id != 0 && (await GetCacheAsync(token)).Any(w => w.Id == dto.Id)) throw new ArgumentInvalidException(nameof(dto), $"Нельзя повторно добавить скважину с id: {dto.Id}"); @@ -177,12 +177,12 @@ namespace AsbCloudInfrastructure.Services throw new NotImplementedException(); } - public override async Task UpdateAsync(WellDto dto, + public override async Task UpdateAsync(WellDto dto, CancellationToken token) { if (IsTelemetryAssignedToDifferentWell(dto)) throw new ArgumentInvalidException(nameof(dto), "Телеметрия уже была привязана к другой скважине."); - + var oldRelations = (await GetCacheRelationCompanyWellAsync(token)) .Where(r => r.IdWell == dto.Id).ToArray(); @@ -192,16 +192,16 @@ namespace AsbCloudInfrastructure.Services dbContext.RelationCompaniesWells .RemoveRange(dbContext.RelationCompaniesWells .Where(r => r.IdWell == dto.Id)); - + DropCacheRelationCompanyWell(); var newRelations = dto.Companies .Select(c => new RelationCompanyWell { - IdWell = dto.Id, + IdWell = dto.Id, IdCompany = c.Id }); - + dbContext.RelationCompaniesWells.AddRange(newRelations); } @@ -215,7 +215,7 @@ namespace AsbCloudInfrastructure.Services public async Task GetWellCaptionByIdAsync(int idWell, CancellationToken token) { - var entity = await GetOrDefaultAsync(idWell, token).ConfigureAwait(false); + var entity = await GetOrDefaultAsync(idWell, token).ConfigureAwait(false); return entity!.Caption; } @@ -272,10 +272,9 @@ namespace AsbCloudInfrastructure.Services if (entity.Timezone is null) dto.Timezone = GetTimezone(entity.Id); - - dto.StartDate = dbContext.WellOperations.Where(e => e.IdType == WellOperation.IdOperationTypeFact) - .AsNoTracking() - .MinOrDefault(e => e.DateStart)?.ToRemoteDateTime(dto.Timezone.Hours); + + dto.StartDate = wellOperationRepository + .GetFirstAndLastFact(entity.Id)?.First?.DateStart; dto.WellType = entity.WellType.Caption; dto.Cluster = entity.Cluster.Caption; dto.Deposit = entity.Cluster.Deposit.Caption; diff --git a/AsbCloudWebApi.IntegrationTests/Clients/ITelemetryControllerClient.cs b/AsbCloudWebApi.IntegrationTests/Clients/ITelemetryControllerClient.cs new file mode 100644 index 00000000..94f4293e --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/Clients/ITelemetryControllerClient.cs @@ -0,0 +1,23 @@ +using AsbCloudApp.Data.SAUB; +using Refit; + +namespace AsbCloudWebApi.IntegrationTests.Clients; +public interface ITelemetryControllerClient +{ + private const string BaseRoute = "/api/telemetry"; + + [Get($"{BaseRoute}/Active")] + Task GetTelemetriesInfoByLastData(CancellationToken token); + + [Post($"{BaseRoute}/{{uid}}/info")] + Task PostInfoAsync(string uid, [Body] TelemetryInfoDto info, CancellationToken token); + + [Post($"{BaseRoute}/{{uid}}/message")] + Task PostMessagesAsync(string uid, [Body] IEnumerable dtos, CancellationToken token); + + [Post($"{BaseRoute}/{{uid}}/event")] + Task PostEventsAsync(string uid, [Body] IEnumerable dtos, CancellationToken token); + + [Post($"{BaseRoute}/{{uid}}/user")] + Task PostUsersAsync(string uid, [Body] IEnumerable dtos, CancellationToken token); +} diff --git a/AsbCloudWebApi.IntegrationTests/Controllers/TelemetryControllerTest.cs b/AsbCloudWebApi.IntegrationTests/Controllers/TelemetryControllerTest.cs new file mode 100644 index 00000000..e8abd06f --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/Controllers/TelemetryControllerTest.cs @@ -0,0 +1,109 @@ +using AsbCloudApp.Data.SAUB; +using AsbCloudDb.Model; +using AsbCloudWebApi.IntegrationTests.Clients; +using Xunit; + +namespace AsbCloudWebApi.IntegrationTests.Controllers; +public class TelemetryControllerTest : BaseIntegrationTest +{ + private ITelemetryControllerClient client; + static readonly string uid = DateTime.UtcNow.ToString("yyyyMMdd_HHmmssfff"); + + private static readonly SimpleTimezone timezone = new() {TimezoneId = "a", Hours = 5 }; + private static readonly Telemetry telemetry = new Telemetry() {Id = 1, RemoteUid = uid, TimeZone = timezone, Info = new() }; + private readonly IEnumerable events = [new() { Id = 1, EventType = 1, IdCategory = 1, IdSound = 1, Message = "there is no spoon {tag1}", Tag = "tag1" }]; + private readonly IEnumerable users = [new TelemetryUserDto() { Id = 1, Level = 0, Name = "Neo", Patronymic = "Kianovich", Surname = "Theone" }]; + private readonly IEnumerable messages = [new TelemetryMessageDto() { Id = 100, IdEvent = 1, WellDepth = 5, Date = DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(5)), Arg0 = "3.14", IdTelemetryUser = 1 }]; + private readonly IEnumerable telemetryDataSaubEntities = [new TelemetryDataSaub() + { + IdTelemetry = telemetry.Id, + DateTime = DateTimeOffset.UtcNow, + AxialLoad = 2, + WellDepth = 5, + BitDepth = 5, + BlockPosition = 5, + BlockSpeed = 5, + }]; + + public TelemetryControllerTest(WebAppFactoryFixture factory) + : base(factory) + { + client = factory.GetAuthorizedHttpClient(string.Empty); + } + + [Fact] + public async Task GetTelemetriesInfoByLastData() + { + // Arrange + dbContext.CleanupDbSet(); + dbContext.Set().Add(telemetry); + dbContext.Set().AddRange(telemetryDataSaubEntities); + dbContext.SaveChanges(); + + // Act + var response = await client.GetTelemetriesInfoByLastData(CancellationToken.None); + + // Assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task PostUsersAsync() + { + // arrange + dbContext.CleanupDbSet(); + + // act + var response = await client.PostUsersAsync(uid, users, CancellationToken.None); + + // Assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + var telemetriesCount = dbContext.Set().Count(); + var telemetryUserCount = dbContext.Set().Count(); + + Assert.Equal(1, telemetriesCount); + Assert.Equal(1, telemetryUserCount); + } + + [Fact] + public async Task PostEventsAsync() + { + // arrange + dbContext.CleanupDbSet(); + + // act + var response = await client.PostEventsAsync(uid, events, CancellationToken.None); + + // Assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + var telemetriesCount = dbContext.Set().Count(); + var telemetryEventCount = dbContext.Set().Count(); + + Assert.Equal(1, telemetriesCount); + Assert.Equal(1, telemetryEventCount); + } + + [Fact] + public async Task PostMessagesAsync() + { + // arrange + dbContext.CleanupDbSet(); + + // act + _ = await client.PostEventsAsync(uid, events, CancellationToken.None); + _ = await client.PostUsersAsync(uid, users, CancellationToken.None); + var response = await client.PostMessagesAsync(uid, messages, CancellationToken.None); + + // Assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + var telemetriesCount = dbContext.Set().Count(); + var telemetryEventCount = dbContext.Set().Count(); + var telemetryUserCount = dbContext.Set().Count(); + var telemetryMessageCount = dbContext.Set().Count(); + + Assert.Equal(1, telemetriesCount); + Assert.Equal(1, telemetryEventCount); + Assert.Equal(1, telemetryUserCount); + Assert.Equal(1, telemetryMessageCount); + } +} diff --git a/AsbCloudWebApi.Tests/Background/WorkTest.cs b/AsbCloudWebApi.Tests/Background/WorkTest.cs index 97a0c6c2..75628370 100644 --- a/AsbCloudWebApi.Tests/Background/WorkTest.cs +++ b/AsbCloudWebApi.Tests/Background/WorkTest.cs @@ -21,7 +21,7 @@ public class WorkTest ((ISupportRequiredService)serviceProviderMock).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactoryMock); } - [Fact] + [Fact, MethodImpl(MethodImplOptions.NoOptimization)] public async Task Start_ShouldReturn_Success() { //arrange @@ -50,7 +50,7 @@ public class WorkTest Assert.InRange(lastState.ExecutionTime, TimeSpan.Zero, executionTime); } - [Fact] + [Fact, MethodImpl(MethodImplOptions.NoOptimization)] public async Task ExecutionWork_Invokes_Callback() { //arrange