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)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); 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 groupedQuery = reservaRows
@ -166,7 +172,13 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); 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) groupedQuery = query.GroupBy(x => x.Cliente)
@ -622,6 +634,122 @@ namespace line_gestao_api.Controllers
return Ok(ToDetailDto(x, vigencia)); 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 // ✅ 5. CREATE
// ========================================================== // ==========================================================
@ -1738,7 +1866,7 @@ namespace line_gestao_api.Controllers
await _db.MuregLines.ExecuteDeleteAsync(); await _db.MuregLines.ExecuteDeleteAsync();
await _db.MobileLines.ExecuteDeleteAsync(); await _db.MobileLines.ExecuteDeleteAsync();
var buffer = new List<MobileLine>(600); var buffer = new List<MobileLine>(1000);
var imported = 0; var imported = 0;
var maxItemFromGeral = 0; var maxItemFromGeral = 0;
@ -1805,11 +1933,12 @@ namespace line_gestao_api.Controllers
imported++; imported++;
if (item > maxItemFromGeral) maxItemFromGeral = item; if (item > maxItemFromGeral) maxItemFromGeral = item;
if (buffer.Count >= 500) if (buffer.Count >= 1000)
{ {
await _db.MobileLines.AddRangeAsync(buffer); await _db.MobileLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
buffer.Clear(); buffer.Clear();
ClearImportTracking();
} }
} }
@ -1817,6 +1946,8 @@ namespace line_gestao_api.Controllers
{ {
await _db.MobileLines.AddRangeAsync(buffer); await _db.MobileLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
buffer.Clear();
ClearImportTracking();
} }
auditSession = _spreadsheetImportAuditService.StartRun( auditSession = _spreadsheetImportAuditService.StartRun(
@ -1828,40 +1959,48 @@ namespace line_gestao_api.Controllers
// ✅ IMPORTA MUREG (ALTERADO: NÃO ESTOURA ERRO SE LINHANOVA JÁ EXISTIR) // ✅ IMPORTA MUREG (ALTERADO: NÃO ESTOURA ERRO SE LINHANOVA JÁ EXISTIR)
// ========================= // =========================
await ImportMuregFromWorkbook(wb); await ImportMuregFromWorkbook(wb);
ClearImportTracking();
// ========================= // =========================
// ✅ IMPORTA FATURAMENTO PF/PJ // ✅ IMPORTA FATURAMENTO PF/PJ
// ========================= // =========================
await ImportBillingFromWorkbook(wb); await ImportBillingFromWorkbook(wb);
ClearImportTracking();
// ========================= // =========================
// ✅ IMPORTA DADOS DOS USUÁRIOS (UserDatas) // ✅ IMPORTA DADOS DOS USUÁRIOS (UserDatas)
// ========================= // =========================
var userDataImported = await ImportUserDatasFromWorkbook(wb); var userDataImported = await ImportUserDatasFromWorkbook(wb);
ClearImportTracking();
if (userDataImported) if (userDataImported)
{ {
await RepairReservaClientAssignmentsAsync(); await RepairReservaClientAssignmentsAsync();
ClearImportTracking();
} }
// ========================= // =========================
// ✅ IMPORTA VIGÊNCIA // ✅ IMPORTA VIGÊNCIA
// ========================= // =========================
await ImportVigenciaFromWorkbook(wb); await ImportVigenciaFromWorkbook(wb);
ClearImportTracking();
// ========================= // =========================
// ✅ IMPORTA TROCA DE NÚMERO // ✅ IMPORTA TROCA DE NÚMERO
// ========================= // =========================
await ImportTrocaNumeroFromWorkbook(wb); await ImportTrocaNumeroFromWorkbook(wb);
ClearImportTracking();
// ========================= // =========================
// ✅ IMPORTA CHIPS VIRGENS // ✅ IMPORTA CHIPS VIRGENS
// ========================= // =========================
await ImportChipsVirgensFromWorkbook(wb); await ImportChipsVirgensFromWorkbook(wb);
ClearImportTracking();
// ========================= // =========================
// ✅ IMPORTA CONTROLE DE RECEBIDOS // ✅ IMPORTA CONTROLE DE RECEBIDOS
// ========================= // =========================
await ImportControleRecebidosFromWorkbook(wb); await ImportControleRecebidosFromWorkbook(wb);
ClearImportTracking();
// ========================= // =========================
// ✅ IMPORTA RESUMO // ✅ IMPORTA RESUMO
@ -1872,11 +2011,13 @@ namespace line_gestao_api.Controllers
} }
await ImportResumoFromWorkbook(wb, auditSession); await ImportResumoFromWorkbook(wb, auditSession);
ClearImportTracking();
// ========================= // =========================
// ✅ IMPORTA PARCELAMENTOS // ✅ IMPORTA PARCELAMENTOS
// ========================= // =========================
var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true); var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true);
ClearImportTracking();
if (auditSession != null) if (auditSession != null)
{ {
await _spreadsheetImportAuditService.SaveRunAsync(auditSession); await _spreadsheetImportAuditService.SaveRunAsync(auditSession);
@ -2122,17 +2263,15 @@ namespace line_gestao_api.Controllers
// limpa MUREG antes (idempotente) // limpa MUREG antes (idempotente)
await _db.MuregLines.ExecuteDeleteAsync(); await _db.MuregLines.ExecuteDeleteAsync();
// ✅ dicionários para resolver MobileLineId por Linha/Chip // Carrega uma vez para evitar consultas por linha durante a importacao da MUREG.
var mobilePairs = await _db.MobileLines var mobileCache = await _db.MobileLines
.AsNoTracking() .ToDictionaryAsync(x => x.Id);
.Select(x => new { x.Id, x.Item, x.Linha, x.Chip })
.ToListAsync();
var mobileByLinha = new Dictionary<string, Guid>(StringComparer.Ordinal); var mobileByLinha = new Dictionary<string, Guid>(StringComparer.Ordinal);
var mobileByChip = new Dictionary<string, Guid>(StringComparer.Ordinal); var mobileByChip = new Dictionary<string, Guid>(StringComparer.Ordinal);
var mobileByItem = new Dictionary<int, Guid>(); 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)) if (m.Item > 0 && !mobileByItem.ContainsKey(m.Item))
mobileByItem[m.Item] = m.Id; 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 buffer = new List<MuregLine>(1000);
var mobileCache = new Dictionary<Guid, MobileLine>();
var buffer = new List<MuregLine>(600);
var lastRow = wsM.LastRowUsed()?.RowNumber() ?? startRow; var lastRow = wsM.LastRowUsed()?.RowNumber() ?? startRow;
for (int r = startRow; r <= lastRow; r++) for (int r = startRow; r <= lastRow; r++)
@ -2204,12 +2340,7 @@ namespace line_gestao_api.Controllers
string? linhaAntigaSnapshot = linhaAntiga; string? linhaAntigaSnapshot = linhaAntiga;
if (string.IsNullOrWhiteSpace(linhaAntigaSnapshot)) if (string.IsNullOrWhiteSpace(linhaAntigaSnapshot))
{ {
if (!mobileCache.TryGetValue(mobileLineId, out var mobTmp)) mobileCache.TryGetValue(mobileLineId, out var mobTmp);
{
mobTmp = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == mobileLineId);
if (mobTmp != null) mobileCache[mobileLineId] = mobTmp;
}
linhaAntigaSnapshot = mobTmp?.Linha; linhaAntigaSnapshot = mobTmp?.Linha;
} }
@ -2241,12 +2372,7 @@ namespace line_gestao_api.Controllers
} }
else else
{ {
// carrega entity tracked (cache) e atualiza mobileCache.TryGetValue(mobileLineId, out var mobile);
if (!mobileCache.TryGetValue(mobileLineId, out var mobile))
{
mobile = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == mobileLineId);
if (mobile != null) mobileCache[mobileLineId] = mobile;
}
if (mobile != null) 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.MuregLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
@ -2289,6 +2415,7 @@ namespace line_gestao_api.Controllers
{ {
await _db.MuregLines.AddRangeAsync(buffer); await _db.MuregLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
buffer.Clear();
} }
} }
@ -5244,9 +5371,16 @@ namespace line_gestao_api.Controllers
DtEfetivacaoServico = vigencia?.DtEfetivacaoServico, DtEfetivacaoServico = vigencia?.DtEfetivacaoServico,
DtTerminoFidelizacao = vigencia?.DtTerminoFidelizacao, DtTerminoFidelizacao = vigencia?.DtTerminoFidelizacao,
VencConta = x.VencConta, 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) private static void ApplyReservaRule(MobileLine x)
{ {
if (IsReservaValue(x.Cliente)) x.Cliente = "RESERVA"; if (IsReservaValue(x.Cliente)) x.Cliente = "RESERVA";

View File

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