Merge branch 'dev' into fix/29192645-drill-test-dtos

This commit is contained in:
ngfrolov 2024-02-19 13:15:57 +05:00
commit 0c5133ee84
Signed by untrusted user who does not match committer: ng.frolov
GPG Key ID: E99907A0357B29A7
5 changed files with 217 additions and 184 deletions

View File

@ -119,18 +119,21 @@ namespace AsbCloudApp.Repositories
Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken);
/// <summary>
/// Валидация данных
/// Удаление полных дубликатов операций по всем скважинам
/// </summary>
/// <param name="wellOperations"></param>
/// <param name="onProgressCallback"></param>
/// <param name="token"></param>
/// <returns></returns>
IEnumerable<ValidationResult> Validate(IEnumerable<WellOperationDto> wellOperations);
Task<int> RemoveDuplicates(Action<string, double?> onProgressCallback, CancellationToken token);
/// <summary>
/// Валидация данных (проверка с базой)
/// Усечение пересекающейся последующей операции по дате и глубине забоя
/// </summary>
/// <param name="wellOperations"></param>
/// <param name="cancellationToken"></param>
/// <param name="geDate">Фильтр по дате. Если хоть одна операция попадет в в фильтр, то будет обработана вся скважина, а не только эта операция</param>
/// <param name="leDate">Фильтр по дате. Если хоть одна операция попадет в в фильтр, то будет обработана вся скважина, а не только эта операция</param>
/// <param name="onProgressCallback"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ValidationResult>> ValidateWithDbAsync(IEnumerable<WellOperationDto> wellOperations, CancellationToken cancellationToken);
Task<int> TrimOverlapping(DateTimeOffset? geDate, DateTimeOffset leDate, Action<string, double?> onProgressCallback, CancellationToken token);
}
}

View File

@ -67,6 +67,23 @@ namespace AsbCloudDb.Model
[JsonIgnore]
[ForeignKey(nameof(IdPlan))]
public virtual WellOperation? OperationPlan { get; set; } = null!;
public bool IsSame(WellOperation other)
{
var isSame = IdWell == other.IdWell &&
IdWellSectionType == other.IdWellSectionType &&
IdCategory == other.IdCategory &&
IdType == other.IdType &&
IdPlan == other.IdPlan &&
DepthStart == other.DepthStart &&
DepthEnd == other.DepthEnd &&
DateStart == other.DateStart &&
DurationHours == other.DurationHours &&
CategoryInfo == other.CategoryInfo &&
Comment == other.Comment;
return isSame;
}
}
}

View File

@ -10,13 +10,13 @@ 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;
/// <summary>
/// репозиторий операций по скважине
/// </summary>
@ -321,59 +321,6 @@ public class WellOperationRepository : IWellOperationRepository
return dtos;
}
public async Task<IEnumerable<ValidationResult>> ValidateWithDbAsync(IEnumerable<WellOperationDto> wellOperationDtos, CancellationToken token)
{
var firstOperation = wellOperationDtos
.FirstOrDefault();
if (firstOperation is null)
return Enumerable.Empty<ValidationResult>();
var request = new WellOperationRequest()
{
IdWell = firstOperation.IdWell,
OperationType = firstOperation.IdType,
};
var entities = await BuildQuery(request)
.AsNoTracking()
.ToArrayAsync(token);
var wellOperationsUnion = entities.Union(wellOperationDtos).OrderBy(o => o.DateStart);
var results = Validate(wellOperationsUnion);
return results;
}
public IEnumerable<ValidationResult> Validate(IEnumerable<WellOperationDto> wellOperationDtos)
{
var enumerator = wellOperationDtos.OrderBy(o => o.DateStart)
.GetEnumerator();
if (!enumerator.MoveNext())
yield break;
var previous = enumerator.Current;
while(enumerator.MoveNext())
{
var current = enumerator.Current;
var previousDateStart = previous.DateStart.ToUniversalTime();
var currentDateStart = current.DateStart.ToUniversalTime();
var previousDateEnd = previous.DateStart.AddHours(previous.DurationHours).ToUniversalTime();
if (previousDateStart.AddDays(Gap) < currentDateStart)
{
yield return new ValidationResult(
"Разница дат между операциями не должна превышать 90 дней",
new[] { nameof(wellOperationDtos) });
}
previous = current;
}
}
/// <inheritdoc/>
public async Task<int> InsertRangeAsync(
IEnumerable<WellOperationDto> wellOperationDtos,
@ -530,4 +477,161 @@ public class WellOperationRepository : IWellOperationRepository
return dtos;
}
public async Task<int> RemoveDuplicates(Action<string, double?> onProgressCallback, CancellationToken token)
{
IQueryable<WellOperation> dbset = db.Set<WellOperation>();
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<WellOperation>();
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<int> TrimOverlapping(DateTimeOffset? geDate, DateTimeOffset leDate, Action<string, double?> onProgressCallback, CancellationToken token)
{
var leDateUtc = leDate.ToUniversalTime();
IQueryable<WellOperation> query = db.Set<WellOperation>();
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<WellOperation>();
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);
}
}

View File

@ -36,56 +36,6 @@ public class WellOperationControllerTest : BaseIntegrationTest
}
};
private readonly WellOperationDto[] dtosWithError = new WellOperationDto[]
{
new()
{
Id = 3,
IdWell = idWell,
IdType = 1,
DateStart = DateTimeOffset.Now,
CategoryInfo = "1",
CategoryName = "1",
Comment = "1",
Day = 1,
DepthEnd = 20,
DepthStart = 10,
DurationHours = 1,
IdCategory = 5000,
IdParentCategory = null,
IdPlan = null,
IdUser = 1,
IdWellSectionType = 1,
LastUpdateDate = DateTimeOffset.Now,
NptHours = 1,
WellSectionTypeName = null,
UserName = null
},
new()
{
Id = 4,
IdWell = idWell,
IdType = 1,
DateStart = DateTimeOffset.Now.AddDays(1000),
CategoryInfo = "1",
CategoryName = "1",
Comment = "1",
Day = 1,
DepthEnd = 20,
DepthStart = 10,
DurationHours = 1,
IdCategory = 5000,
IdParentCategory = null,
IdPlan = null,
IdUser = 1,
IdWellSectionType = 1,
LastUpdateDate = DateTimeOffset.Now,
NptHours = 1,
WellSectionTypeName = null,
UserName = null
}
};
private IWellOperationClient client;
public WellOperationControllerTest(WebAppFactoryFixture factory)
@ -109,21 +59,6 @@ public class WellOperationControllerTest : BaseIntegrationTest
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Неуспешное добавление операций (без предварительной очистки данных)
/// </summary>
/// <returns></returns>
[Fact]
public async Task InsertRange_returns_error()
{
//act
var response = await client.InsertRangeAsync(idWell, 1, false, dtosWithError);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
/// <summary>
/// Успешное добавление операций (с предварительной очисткой данных)
/// </summary>
@ -138,20 +73,6 @@ public class WellOperationControllerTest : BaseIntegrationTest
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Неуспешное добавление операций (с предварительной очисткой данных)
/// </summary>
/// <returns></returns>
[Fact]
public async Task InsertRangeWithDeleteBefore_returns_error()
{
//act
var response = await client.InsertRangeAsync(idWell, 1, true, dtosWithError);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
/// <summary>
/// Успешное обновление операции
/// </summary>
@ -166,21 +87,6 @@ public class WellOperationControllerTest : BaseIntegrationTest
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Неуспешное обновление операции
/// </summary>
/// <returns></returns>
[Fact]
public async Task UpdateAsync_returns_error()
{
//act
var dto = dtosWithError.LastOrDefault()!;
var response = await client.UpdateAsync(idWell, 1, dto, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
/// <summary>
/// Получение плановых операций

View File

@ -236,10 +236,6 @@ namespace AsbCloudWebApi.Controllers
wellOperation.IdUser = User.GetUserId();
wellOperation.IdType = idType;
var validationResult = await operationRepository.ValidateWithDbAsync(new[] { wellOperation }, cancellationToken);
if (validationResult.Any())
return this.ValidationBadRequest(validationResult);
var result = await operationRepository.InsertRangeAsync(new[] { wellOperation }, cancellationToken);
return Ok(result);
@ -290,32 +286,11 @@ namespace AsbCloudWebApi.Controllers
wellOperation.IdType = idType;
}
var validationResult = await Validate(wellOperations, deleteBeforeInsert, cancellationToken);
if (validationResult.Any())
return this.ValidationBadRequest(validationResult);
var result = await operationRepository.InsertRangeAsync(wellOperations, cancellationToken);
return Ok(result);
}
/// <summary>
/// Валидация данных перед вставкой / обновлением / импортом
/// </summary>
/// <param name="wellOperations"></param>
/// <param name="deleteBeforeInsert"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<IEnumerable<ValidationResult>> Validate(IEnumerable<WellOperationDto> wellOperations, bool deleteBeforeInsert, CancellationToken cancellationToken)
{
if (deleteBeforeInsert)
return operationRepository.Validate(wellOperations);
else
return await operationRepository.ValidateWithDbAsync(wellOperations, cancellationToken);
}
/// <summary>
/// Обновляет выбранную операцию на скважине
/// </summary>
@ -341,10 +316,6 @@ namespace AsbCloudWebApi.Controllers
value.LastUpdateDate = DateTimeOffset.UtcNow;
value.IdUser = User.GetUserId();
var validationResult = await operationRepository.ValidateWithDbAsync(new[] { value }, token);
if (validationResult.Any())
return this.ValidationBadRequest(validationResult);
var result = await operationRepository.UpdateAsync(value, token)
.ConfigureAwait(false);
return Ok(result);
@ -505,6 +476,38 @@ namespace AsbCloudWebApi.Controllers
return File(stream, "application/octet-stream", fileName);
}
/// <summary>
/// Удаляет полые дубликаты операций
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("/api/well/wellOperations/RemoveDuplicates")]
[Permission]
[Obsolete]
[ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> RemoveDuplicates(CancellationToken token)
{
var result = await operationRepository.RemoveDuplicates((_, _) => { }, token);
return Ok(result);
}
/// <summary>
/// Удаляет полностью пересекающиеся операции или "подрезает" более поздние их по глубине и дате.
/// </summary>
/// <param name="geDate"></param>
/// <param name="leDate"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("/api/well/wellOperations/TrimOverlapping")]
[Permission]
[Obsolete]
[ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> TrimOverlapping(DateTimeOffset? geDate, DateTimeOffset leDate, CancellationToken token)
{
var result = await operationRepository.TrimOverlapping(geDate, leDate, (_, _) => { }, token);
return Ok(result);
}
/// <summary>
/// Возвращает шаблон файла импорта
/// </summary>