using AsbCloudApp.Data; using AsbCloudApp.Repositories; using AsbCloudApp.Requests; using AsbCloudApp.Services; using AsbCloudDb; using AsbCloudDb.Model; using Mapster; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Repository; /// /// репозиторий операций по скважине /// public class WellOperationRepository : IWellOperationRepository { private const string KeyCacheSections = "OperationsBySectionSummarties"; private readonly IAsbCloudDbContext db; private readonly IMemoryCache memoryCache; private readonly IWellService wellService; private readonly IWellOperationCategoryRepository wellOperationCategoryRepository; public WellOperationRepository(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService, IWellOperationCategoryRepository wellOperationCategoryRepository) { this.db = db; this.memoryCache = memoryCache; this.wellService = wellService; this.wellOperationCategoryRepository = wellOperationCategoryRepository; } public IEnumerable GetSectionTypes() => memoryCache .GetOrCreateBasic(db.Set()) .OrderBy(s => s.Order) .Select(s => s.Adapt()); public async Task GetOperationsPlanAsync(int idWell, DateTime? currentDate, CancellationToken token) { var timezone = wellService.GetTimezone(idWell); var request = new WellOperationRequest() { IdWell = idWell, OperationType = WellOperation.IdOperationTypePlan, }; var dtos = await BuildQuery(request) .AsNoTracking() .ToArrayAsync(token); var dateLastAssosiatedPlanOperation = await GetDateLastAssosiatedPlanOperationAsync(idWell, currentDate, timezone.Hours, token); var result = new WellOperationPlanDto() { WellOperationsPlan = dtos.Select(Convert), DateLastAssosiatedPlanOperation = dateLastAssosiatedPlanOperation }; return result; } private async Task GetDateLastAssosiatedPlanOperationAsync( int idWell, DateTime? lessThenDate, double timeZoneHours, CancellationToken token) { if (lessThenDate is null) return null; var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours); var timeZoneOffset = TimeSpan.FromHours(timeZoneHours); var lastFactOperation = await db.WellOperations .Where(o => o.IdWell == idWell) .Where(o => o.IdType == WellOperation.IdOperationTypeFact) .Where(o => o.IdPlan != null) .Where(o => o.DateStart < currentDateOffset) .Include(x => x.OperationPlan) .OrderByDescending(x => x.DateStart) .FirstOrDefaultAsync(token) .ConfigureAwait(false); if (lastFactOperation is not null) return DateTime.SpecifyKind(lastFactOperation.OperationPlan!.DateStart.UtcDateTime + timeZoneOffset, DateTimeKind.Unspecified); return null; } /// public async Task> GetSectionsAsync(IEnumerable idsWells, CancellationToken token) { var cache = await memoryCache.GetOrCreateAsync(KeyCacheSections, async (entry) => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); var query = db.Set() .GroupBy(operation => new { operation.IdWell, operation.IdType, operation.IdWellSectionType, operation.WellSectionType.Caption, }) .Select(group => new { group.Key.IdWell, group.Key.IdType, group.Key.IdWellSectionType, group.Key.Caption, First = group .OrderBy(operation => operation.DateStart) .Select(operation => new { operation.DateStart, operation.DepthStart, }) .First(), Last = group .OrderByDescending(operation => operation.DateStart) .Select(operation => new { operation.DateStart, operation.DurationHours, operation.DepthEnd, }) .First(), }); var dbData = await query.ToArrayAsync(token); var sections = dbData.Select( item => new SectionByOperationsDto { IdWell = item.IdWell, IdType = item.IdType, IdWellSectionType = item.IdWellSectionType, Caption = item.Caption, DateStart = item.First.DateStart, DepthStart = item.First.DepthStart, DateEnd = item.Last.DateStart.AddHours(item.Last.DurationHours), DepthEnd = item.Last.DepthEnd, }) .ToArray() .AsEnumerable(); entry.Value = sections; return sections; }); var sections = cache.Where(s => idsWells.Contains(s.IdWell)); return sections; } public async Task GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken) { var timezone = wellService.GetTimezone(idWell); var query = db.WellOperations.Where(o => o.IdWell == idWell && o.IdType == idType); if (!await query.AnyAsync(cancellationToken)) return null; var minDate = await query.MinAsync(o => o.DateStart, cancellationToken); var maxDate = await query.MaxAsync(o => o.DateStart, cancellationToken); return new DatesRangeDto { From = minDate.ToRemoteDateTime(timezone.Hours), To = maxDate.ToRemoteDateTime(timezone.Hours) }; } /// public DateTimeOffset? FirstOperationDate(int idWell) { var sections = GetSectionsAsync(new[] { idWell }, CancellationToken.None).Result; var first = sections.FirstOrDefault(section => section.IdType == WellOperation.IdOperationTypeFact) ?? sections.FirstOrDefault(section => section.IdType == WellOperation.IdOperationTypePlan); return first?.DateStart; } /// public async Task> GetAsync( WellOperationRequest request, CancellationToken token) { var query = BuildQuery(request) .AsNoTracking(); var dtos = await query.ToArrayAsync(token); return dtos.Select(Convert); } public async Task> GetAsync( WellsOperationRequest request, CancellationToken token) { var query = db.WellOperations .Where(o => request.IdsWell.Contains(o.IdWell)) .Where(o => request.IdType == o.IdType) .Where(o => request.SectionTypeIds != null ? request.SectionTypeIds.Contains(o.IdWellSectionType) : true) .Where(o => request.OperationCategoryIds != null ? request.OperationCategoryIds.Contains(o.IdCategory) : true) .Select(o => new WellOperationDataDto(){ DepthStart = o.DepthStart, DurationHours = o.DurationHours, IdCategory = o.IdCategory, IdWell = o.IdWell, IdWellSectionType = o.IdWellSectionType, OperationCategoryName = o.OperationCategory.Name, WellSectionTypeCaption = o.WellSectionType.Caption, }) .AsNoTracking(); var dtos = await query.ToArrayAsync(token); return dtos; } /// public async Task> GetPageAsync( WellOperationRequest request, CancellationToken token) { var query = BuildQuery(request); var result = new PaginationContainer { Skip = request.Skip ?? 0, Take = request.Take ?? 32, Count = await query.CountAsync(token), }; var dtos = await query.ToArrayAsync(token); result.Items = dtos.Select(Convert); return result; } /// public async Task GetOrDefaultAsync(int id, CancellationToken token) { var entity = await db.WellOperations .Include(s => s.WellSectionType) .Include(s => s.OperationCategory) .FirstOrDefaultAsync(e => e.Id == id, token) .ConfigureAwait(false); if (entity is null) return null; var timezone = wellService.GetTimezone(entity.IdWell); var dto = entity.Adapt(); dto.WellSectionTypeName = entity.WellSectionType.Caption; dto.DateStart = entity.DateStart.ToRemoteDateTime(timezone.Hours); dto.CategoryName = entity.OperationCategory.Name; return dto; } /// public async Task> GetGroupOperationsStatAsync( WellOperationRequest request, CancellationToken token) { // TODO: Rename controller method request.OperationType = WellOperation.IdOperationTypeFact; var query = BuildQuery(request); var entities = await query .Select(o => new { o.IdCategory, DurationMinutes = o.DurationHours * 60, DurationDepth = o.DepthEnd - o.DepthStart }) .ToListAsync(token); var parentRelationDictionary = wellOperationCategoryRepository.Get(true) .ToDictionary(c => c.Id, c => new { c.Name, c.IdParent }); var dtos = entities .GroupBy(o => o.IdCategory) .Select(g => new WellGroupOpertionDto { IdCategory = g.Key, Category = parentRelationDictionary[g.Key].Name, Count = g.Count(), MinutesAverage = g.Average(o => o.DurationMinutes), MinutesMin = g.Min(o => o.DurationMinutes), MinutesMax = g.Max(o => o.DurationMinutes), TotalMinutes = g.Sum(o => o.DurationMinutes), DeltaDepth = g.Sum(o => o.DurationDepth), IdParent = parentRelationDictionary[g.Key].IdParent }); while (dtos.All(x => x.IdParent != null)) { dtos = dtos .GroupBy(o => o.IdParent!) .Select(g => { var idCategory = g.Key ?? int.MinValue; var category = parentRelationDictionary.GetValueOrDefault(idCategory); var newDto = new WellGroupOpertionDto { IdCategory = idCategory, Category = category?.Name ?? "unknown", Count = g.Sum(o => o.Count), DeltaDepth = g.Sum(o => o.DeltaDepth), TotalMinutes = g.Sum(o => o.TotalMinutes), Items = g.ToList(), IdParent = category?.IdParent, }; return newDto; }); } return dtos; } /// public async Task InsertRangeAsync( IEnumerable wellOperationDtos, CancellationToken token) { var firstOperation = wellOperationDtos .FirstOrDefault(); if (firstOperation is null) return 0; var idWell = firstOperation.IdWell; var timezone = wellService.GetTimezone(idWell); foreach (var dto in wellOperationDtos) { var entity = dto.Adapt(); entity.Id = default; entity.DateStart = dto.DateStart.DateTime.ToUtcDateTimeOffset(timezone.Hours); entity.IdWell = idWell; entity.LastUpdateDate = DateTimeOffset.UtcNow; db.WellOperations.Add(entity); } var result = await db.SaveChangesAsync(token); if (result > 0) memoryCache.Remove(KeyCacheSections); return result; } /// public async Task UpdateAsync( WellOperationDto dto, CancellationToken token) { var timezone = wellService.GetTimezone(dto.IdWell); var entity = dto.Adapt(); entity.DateStart = dto.DateStart.DateTime.ToUtcDateTimeOffset(timezone.Hours); entity.LastUpdateDate = DateTimeOffset.UtcNow; db.WellOperations.Update(entity); var result = await db.SaveChangesAsync(token); if (result > 0) memoryCache.Remove(KeyCacheSections); return result; } /// public async Task DeleteAsync(IEnumerable ids, CancellationToken token) { var query = db.WellOperations.Where(e => ids.Contains(e.Id)); db.WellOperations.RemoveRange(query); var result = await db.SaveChangesAsync(token); if (result > 0) memoryCache.Remove(KeyCacheSections); return result; } /// /// В результате попрежнему требуется конвертировать дату /// /// /// /// private IQueryable BuildQuery(WellOperationRequest request) { var timezone = wellService.GetTimezone(request.IdWell); var timeZoneOffset = timezone.Hours; var query = db.WellOperations .Include(s => s.WellSectionType) .Include(s => s.OperationCategory) .Where(o => o.IdWell == request.IdWell); if (request.OperationType.HasValue) query = query.Where(e => e.IdType == request.OperationType.Value); if (request.SectionTypeIds?.Any() == true) query = query.Where(e => request.SectionTypeIds.Contains(e.IdWellSectionType)); if (request.OperationCategoryIds?.Any() == true) query = query.Where(e => request.OperationCategoryIds.Contains(e.IdCategory)); if (request.GeDepth.HasValue) query = query.Where(e => e.DepthEnd >= request.GeDepth.Value); if (request.LeDepth.HasValue) query = query.Where(e => e.DepthEnd <= request.LeDepth.Value); if (request.GeDate.HasValue) { var geDateOffset = request.GeDate.Value.ToUtcDateTimeOffset(timeZoneOffset); query = query.Where(e => e.DateStart >= geDateOffset); } if (request.LtDate.HasValue) { var ltDateOffset = request.LtDate.Value.ToUtcDateTimeOffset(timeZoneOffset); query = query.Where(e => e.DateStart < ltDateOffset); } var currentWellOperations = db.WellOperations .Where(subOp => subOp.IdWell == request.IdWell); var wellOperationsWithCategoryNpt = currentWellOperations .Where(subOp => subOp.IdType == 1) .Where(subOp => WellOperationCategory.NonProductiveTimeSubIds.Contains(subOp.IdCategory)); var dtos = query.Select(o => new WellOperationDto { Id = o.Id, IdPlan = o.IdPlan, IdType = o.IdType, IdWell = o.IdWell, IdWellSectionType = o.IdWellSectionType, IdCategory = o.IdCategory, IdParentCategory = o.OperationCategory.IdParent, CategoryName = o.OperationCategory.Name, WellSectionTypeName = o.WellSectionType.Caption, DateStart = o.DateStart, DepthStart = o.DepthStart, DepthEnd = o.DepthEnd, DurationHours = o.DurationHours, CategoryInfo = o.CategoryInfo, Comment = o.Comment, NptHours = wellOperationsWithCategoryNpt .Where(subOp => subOp.DateStart <= o.DateStart) .Select(subOp => subOp.DurationHours) .Sum(), Day = (o.DateStart - currentWellOperations .Where(subOp => subOp.IdType == o.IdType) .Where(subOp => subOp.DateStart <= o.DateStart) .Min(subOp => subOp.DateStart)) .TotalDays, IdUser = o.IdUser, LastUpdateDate = o.LastUpdateDate, }); if (request.SortFields?.Any() == true) { dtos = dtos.SortBy(request.SortFields); } dtos = dtos .OrderBy(e => e.DateStart) .ThenBy(e => e.DepthEnd) .ThenBy(e => e.Id); if (request.Skip.HasValue) dtos = dtos.Skip(request.Skip.Value); if (request.Take.HasValue) dtos = dtos.Take(request.Take.Value); return dtos.AsNoTracking(); } private WellOperationDto Convert(WellOperationDto dto) { var timezone = wellService.GetTimezone(dto.IdWell); var timezoneOffset = TimeSpan.FromHours(timezone.Hours); var dtoWithRemoteDateTime = dto.Adapt(); dtoWithRemoteDateTime.DateStart = dto.DateStart.ToOffset(TimeSpan.FromHours(timezoneOffset.Hours)); dtoWithRemoteDateTime.LastUpdateDate = dto.LastUpdateDate?.ToOffset(TimeSpan.FromHours(timezoneOffset.Hours)); return dtoWithRemoteDateTime; } public async Task RemoveDuplicates(Action onProgressCallback, CancellationToken token) { IQueryable dbset = db.Set(); var query = dbset .GroupBy(o => new { o.IdWell, o.IdType }) .Select(g => new { g.Key.IdWell, g.Key.IdType }); var groups = await query .ToArrayAsync(token); var count = groups.Count(); var i = 0; var totalRemoved = 0; var total = 0; foreach (var group in groups) { var result = await RemoveDuplicatesInGroup(group.IdWell, group.IdType, token); totalRemoved += result.removed; total += result.total; var percent = i++ / count; var message = $"RemoveDuplicates [{i} of {count}] wellId: {group.IdWell}, opType: {group.IdType}, affected: {result.removed} of {result.total}"; onProgressCallback?.Invoke(message, percent); Trace.TraceInformation(message); } var messageDone = $"RemoveDuplicates done [{i} of {count}] totalAffected: {totalRemoved} of {total}"; Trace.TraceInformation(messageDone); onProgressCallback?.Invoke(messageDone, 1); return totalRemoved; } private async Task<(int removed, int total)> RemoveDuplicatesInGroup(int idWell, int idType, CancellationToken token) { var dbset = db.Set(); var entities = await dbset .Where(o => o.IdWell == idWell && o.IdType == idType) .OrderBy(o => o.DateStart) .ToListAsync(token); using var entitiesEnumerator = entities.GetEnumerator(); if (!entitiesEnumerator.MoveNext()) return (0, 0); var preEntity = entitiesEnumerator.Current; while (entitiesEnumerator.MoveNext()) { var entity = entitiesEnumerator.Current; if (preEntity.IsSame(entity)) dbset.Remove(entity); else preEntity = entity; } var removed = await db.SaveChangesAsync(token); return (removed, entities.Count); } public async Task TrimOverlapping(DateTimeOffset? geDate, DateTimeOffset leDate, Action onProgressCallback, CancellationToken token) { var leDateUtc = leDate.ToUniversalTime(); IQueryable query = db.Set(); if (geDate.HasValue) { var geDateUtc = geDate.Value.ToUniversalTime(); query = query.Where(e => e.DateStart >= geDateUtc); } var groups = await query .GroupBy(o => new { o.IdWell, o.IdType }) .Select(g => new{ MaxDate = g.Max(o => o.DateStart), g.Key.IdWell, g.Key.IdType, }) .Where(g => g.MaxDate <= leDateUtc) .ToArrayAsync(token); var count = groups.Count(); var i = 0; (int takeover, int trimmed,int total) totalResult = (0, 0, 0); foreach (var group in groups) { var result = await TrimOverlapping(group.IdWell, group.IdType, token); totalResult.takeover += result.takeover; totalResult.trimmed += result.trimmed; totalResult.total += result.total; var percent = i++ / count; var message = $"TrimOverlapping [{i} of {count}] wellId: {group.IdWell}, opType: {group.IdType}, takeover:{result.takeover}, trimmed:{result.trimmed}, of {result.total}"; onProgressCallback?.Invoke(message, percent); Trace.TraceInformation(message); } var messageDone = $"TrimOverlapping done [{i} of {count}] total takeover:{totalResult.takeover}, total trimmed:{totalResult.trimmed} of {totalResult.total}"; Trace.TraceInformation(messageDone); onProgressCallback?.Invoke(messageDone, 1); return totalResult.takeover + totalResult.trimmed; } private async Task<(int takeover, int trimmed, int total)> TrimOverlapping(int idWell, int idType, CancellationToken token) { var dbset = db.Set(); var query = dbset .Where(o => o.IdWell == idWell) .Where(o => o.IdType == idType) .OrderBy(o => o.DateStart) .ThenBy(o => o.DepthStart); var entities = await query .ToListAsync(token); using var entitiesEnumerator = entities.GetEnumerator(); if (!entitiesEnumerator.MoveNext()) return (0, 0, 0); int takeover = 0; int trimmed = 0; var preEntity = entitiesEnumerator.Current; while (entitiesEnumerator.MoveNext()) { var entity = entitiesEnumerator.Current; var preDepth = preEntity.DepthEnd; if (preEntity.DepthEnd >= entity.DepthEnd) { dbset.Remove(entity); takeover++; continue; } if (preEntity.DepthEnd > entity.DepthStart) { entity.DepthStart = preEntity.DepthEnd; trimmed++; } var preDate = preEntity.DateStart.AddHours(preEntity.DurationHours); if (preDate >= entity.DateStart.AddHours(entity.DurationHours)) { dbset.Remove(entity); takeover++; continue; } if (preDate > entity.DateStart) { var entityDateEnd = entity.DateStart.AddHours(entity.DurationHours); entity.DateStart = preDate; entity.DurationHours = (entityDateEnd - entity.DateStart).TotalHours; trimmed++; } preEntity = entity; } var affected = await db.SaveChangesAsync(token); return (takeover, trimmed, entities.Count); } }