perf: otimiza importacao e exportacao de linhas

This commit is contained in:
Eduardo Lopes 2026-03-12 23:55:58 -03:00
parent b25cdaa507
commit b5b36276cb
2 changed files with 169 additions and 28 deletions

View File

@ -144,7 +144,13 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.Trim();
reservaRows = reservaRows.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%"));
reservaRows = reservaRows.Where(x =>
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Chip ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Status ?? "", $"%{s}%"));
}
groupedQuery = reservaRows
@ -166,7 +172,13 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.Trim();
query = query.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%"));
query = query.Where(x =>
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Chip ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Status ?? "", $"%{s}%"));
}
groupedQuery = query.GroupBy(x => x.Cliente)
@ -622,6 +634,122 @@ namespace line_gestao_api.Controllers
return Ok(ToDetailDto(x, vigencia));
}
[HttpPost("export-details")]
public async Task<ActionResult<List<MobileLineDetailDto>>> GetExportDetails([FromBody] MobileLineExportDetailsRequestDto? req)
{
var orderedIds = (req?.Ids ?? new List<Guid>())
.Where(id => id != Guid.Empty)
.Distinct()
.ToList();
if (orderedIds.Count == 0)
{
return Ok(new List<MobileLineDetailDto>());
}
var lines = await _db.MobileLines
.AsNoTracking()
.Include(x => x.Setor)
.Include(x => x.Aparelho)
.Where(x => orderedIds.Contains(x.Id))
.ToListAsync();
if (lines.Count == 0)
{
return Ok(new List<MobileLineDetailDto>());
}
var vigenciaByLinha = new Dictionary<string, VigenciaLine>(StringComparer.Ordinal);
var vigenciaByItem = new Dictionary<int, VigenciaLine>();
var lineKeys = lines
.Select(x => NullIfEmptyDigits(x.Linha))
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.Ordinal)
.ToList();
var itemKeys = lines
.Where(x => x.Item > 0)
.Select(x => x.Item)
.Distinct()
.ToList();
if (lineKeys.Count > 0 || itemKeys.Count > 0)
{
IQueryable<VigenciaLine> vigenciaQuery = _db.VigenciaLines.AsNoTracking();
if (lineKeys.Count > 0 && itemKeys.Count > 0)
{
vigenciaQuery = vigenciaQuery.Where(v => lineKeys.Contains(v.Linha!) || itemKeys.Contains(v.Item));
}
else if (lineKeys.Count > 0)
{
vigenciaQuery = vigenciaQuery.Where(v => lineKeys.Contains(v.Linha!));
}
else
{
vigenciaQuery = vigenciaQuery.Where(v => itemKeys.Contains(v.Item));
}
var vigencias = await vigenciaQuery.ToListAsync();
foreach (var vigencia in vigencias
.Where(v => !string.IsNullOrWhiteSpace(v.Linha))
.GroupBy(v => NullIfEmptyDigits(v.Linha)!, StringComparer.Ordinal)
.Select(g => g
.OrderByDescending(x => x.UpdatedAt)
.ThenByDescending(x => x.CreatedAt)
.First()))
{
var key = NullIfEmptyDigits(vigencia.Linha);
if (!string.IsNullOrWhiteSpace(key))
{
vigenciaByLinha[key] = vigencia;
}
}
foreach (var vigencia in vigencias
.Where(v => string.IsNullOrWhiteSpace(v.Linha))
.GroupBy(v => v.Item)
.Select(g => g
.OrderByDescending(x => x.UpdatedAt)
.ThenByDescending(x => x.CreatedAt)
.First()))
{
if (vigencia.Item > 0)
{
vigenciaByItem[vigencia.Item] = vigencia;
}
}
}
var linesById = lines.ToDictionary(x => x.Id);
var result = new List<MobileLineDetailDto>(orderedIds.Count);
foreach (var id in orderedIds)
{
if (!linesById.TryGetValue(id, out var line))
{
continue;
}
VigenciaLine? vigencia = null;
var lineKey = NullIfEmptyDigits(line.Linha);
if (!string.IsNullOrWhiteSpace(lineKey))
{
vigenciaByLinha.TryGetValue(lineKey, out vigencia);
}
else if (line.Item > 0)
{
vigenciaByItem.TryGetValue(line.Item, out vigencia);
}
result.Add(ToDetailDto(line, vigencia));
}
return Ok(result);
}
// ==========================================================
// ✅ 5. CREATE
// ==========================================================
@ -1738,7 +1866,7 @@ namespace line_gestao_api.Controllers
await _db.MuregLines.ExecuteDeleteAsync();
await _db.MobileLines.ExecuteDeleteAsync();
var buffer = new List<MobileLine>(600);
var buffer = new List<MobileLine>(1000);
var imported = 0;
var maxItemFromGeral = 0;
@ -1805,11 +1933,12 @@ namespace line_gestao_api.Controllers
imported++;
if (item > maxItemFromGeral) maxItemFromGeral = item;
if (buffer.Count >= 500)
if (buffer.Count >= 1000)
{
await _db.MobileLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync();
buffer.Clear();
ClearImportTracking();
}
}
@ -1817,6 +1946,8 @@ namespace line_gestao_api.Controllers
{
await _db.MobileLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync();
buffer.Clear();
ClearImportTracking();
}
auditSession = _spreadsheetImportAuditService.StartRun(
@ -1828,40 +1959,48 @@ namespace line_gestao_api.Controllers
// ✅ IMPORTA MUREG (ALTERADO: NÃO ESTOURA ERRO SE LINHANOVA JÁ EXISTIR)
// =========================
await ImportMuregFromWorkbook(wb);
ClearImportTracking();
// =========================
// ✅ IMPORTA FATURAMENTO PF/PJ
// =========================
await ImportBillingFromWorkbook(wb);
ClearImportTracking();
// =========================
// ✅ IMPORTA DADOS DOS USUÁRIOS (UserDatas)
// =========================
var userDataImported = await ImportUserDatasFromWorkbook(wb);
ClearImportTracking();
if (userDataImported)
{
await RepairReservaClientAssignmentsAsync();
ClearImportTracking();
}
// =========================
// ✅ IMPORTA VIGÊNCIA
// =========================
await ImportVigenciaFromWorkbook(wb);
ClearImportTracking();
// =========================
// ✅ IMPORTA TROCA DE NÚMERO
// =========================
await ImportTrocaNumeroFromWorkbook(wb);
ClearImportTracking();
// =========================
// ✅ IMPORTA CHIPS VIRGENS
// =========================
await ImportChipsVirgensFromWorkbook(wb);
ClearImportTracking();
// =========================
// ✅ IMPORTA CONTROLE DE RECEBIDOS
// =========================
await ImportControleRecebidosFromWorkbook(wb);
ClearImportTracking();
// =========================
// ✅ IMPORTA RESUMO
@ -1872,11 +2011,13 @@ namespace line_gestao_api.Controllers
}
await ImportResumoFromWorkbook(wb, auditSession);
ClearImportTracking();
// =========================
// ✅ IMPORTA PARCELAMENTOS
// =========================
var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true);
ClearImportTracking();
if (auditSession != null)
{
await _spreadsheetImportAuditService.SaveRunAsync(auditSession);
@ -2122,17 +2263,15 @@ namespace line_gestao_api.Controllers
// limpa MUREG antes (idempotente)
await _db.MuregLines.ExecuteDeleteAsync();
// ✅ dicionários para resolver MobileLineId por Linha/Chip
var mobilePairs = await _db.MobileLines
.AsNoTracking()
.Select(x => new { x.Id, x.Item, x.Linha, x.Chip })
.ToListAsync();
// Carrega uma vez para evitar consultas por linha durante a importacao da MUREG.
var mobileCache = await _db.MobileLines
.ToDictionaryAsync(x => x.Id);
var mobileByLinha = new Dictionary<string, Guid>(StringComparer.Ordinal);
var mobileByChip = new Dictionary<string, Guid>(StringComparer.Ordinal);
var mobileByItem = new Dictionary<int, Guid>();
foreach (var m in mobilePairs)
foreach (var m in mobileCache.Values)
{
if (m.Item > 0 && !mobileByItem.ContainsKey(m.Item))
mobileByItem[m.Item] = m.Id;
@ -2152,10 +2291,7 @@ namespace line_gestao_api.Controllers
}
}
// ✅ cache de entidades tracked para atualizar a GERAL sem consultar toda hora
var mobileCache = new Dictionary<Guid, MobileLine>();
var buffer = new List<MuregLine>(600);
var buffer = new List<MuregLine>(1000);
var lastRow = wsM.LastRowUsed()?.RowNumber() ?? startRow;
for (int r = startRow; r <= lastRow; r++)
@ -2204,12 +2340,7 @@ namespace line_gestao_api.Controllers
string? linhaAntigaSnapshot = linhaAntiga;
if (string.IsNullOrWhiteSpace(linhaAntigaSnapshot))
{
if (!mobileCache.TryGetValue(mobileLineId, out var mobTmp))
{
mobTmp = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == mobileLineId);
if (mobTmp != null) mobileCache[mobileLineId] = mobTmp;
}
mobileCache.TryGetValue(mobileLineId, out var mobTmp);
linhaAntigaSnapshot = mobTmp?.Linha;
}
@ -2241,12 +2372,7 @@ namespace line_gestao_api.Controllers
}
else
{
// carrega entity tracked (cache) e atualiza
if (!mobileCache.TryGetValue(mobileLineId, out var mobile))
{
mobile = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == mobileLineId);
if (mobile != null) mobileCache[mobileLineId] = mobile;
}
mobileCache.TryGetValue(mobileLineId, out var mobile);
if (mobile != null)
{
@ -2277,7 +2403,7 @@ namespace line_gestao_api.Controllers
}
}
if (buffer.Count >= 500)
if (buffer.Count >= 1000)
{
await _db.MuregLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync();
@ -2289,6 +2415,7 @@ namespace line_gestao_api.Controllers
{
await _db.MuregLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync();
buffer.Clear();
}
}
@ -5244,9 +5371,16 @@ namespace line_gestao_api.Controllers
DtEfetivacaoServico = vigencia?.DtEfetivacaoServico,
DtTerminoFidelizacao = vigencia?.DtTerminoFidelizacao,
VencConta = x.VencConta,
TipoDeChip = x.TipoDeChip
TipoDeChip = x.TipoDeChip,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
};
private void ClearImportTracking()
{
_db.ChangeTracker.Clear();
}
private static void ApplyReservaRule(MobileLine x)
{
if (IsReservaValue(x.Cliente)) x.Cliente = "RESERVA";

View File

@ -84,6 +84,8 @@
public DateTime? DtTerminoFidelizacao { get; set; }
public string? VencConta { get; set; }
public string? TipoDeChip { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
// ✅ UPDATE REQUEST (SEM Id)
@ -153,4 +155,9 @@
public string? Skil { get; set; }
}
public class MobileLineExportDetailsRequestDto
{
public List<Guid> Ids { get; set; } = new();
}
}