Fix Cambios de optimizaciones
This commit is contained in:
@@ -72,11 +72,17 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResultadosDto>() : null;
|
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResultadosDto>() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId, int categoriaId)
|
public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId)
|
||||||
{
|
{
|
||||||
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
// Usamos la categoriaId recibida en lugar de una fija
|
var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}";
|
||||||
var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}";
|
|
||||||
|
// Añadimos el seccionProvincialId a la URL SÓLO si tiene un valor.
|
||||||
|
if (!string.IsNullOrEmpty(seccionProvincialId))
|
||||||
|
{
|
||||||
|
requestUri += $"&seccionProvincialId={seccionProvincialId}";
|
||||||
|
}
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||||
request.Headers.Add("Authorization", $"Bearer {authToken}");
|
request.Headers.Add("Authorization", $"Bearer {authToken}");
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public interface IElectoralApiService
|
|||||||
Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId);
|
Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId);
|
||||||
Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId);
|
Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId);
|
||||||
Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId);
|
Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId);
|
||||||
Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId, int categoriaId);
|
Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId);
|
||||||
Task<List<string[]>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null);
|
Task<List<string[]>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null);
|
||||||
Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId);
|
Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId);
|
||||||
Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId);
|
Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId);
|
||||||
|
|||||||
@@ -83,137 +83,159 @@ public class Worker : BackgroundService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones)
|
/// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones)
|
||||||
/// desde la API a la base de datos local.
|
/// desde la API a la base de datos local. Se ejecuta una sola vez al iniciar el worker.
|
||||||
/// </summary>
|
/// Utiliza una estrategia de guardado en lotes para manejar grandes volúmenes de datos
|
||||||
private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken)
|
/// sin sobrecargar la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
|
||||||
|
private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
|
||||||
|
|
||||||
|
// PASO 1: Obtener el token de autenticación. Sin él, no podemos hacer nada.
|
||||||
|
var authToken = await _apiService.GetAuthTokenAsync();
|
||||||
|
if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
|
_logger.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela.");
|
||||||
var authToken = await _apiService.GetAuthTokenAsync();
|
return;
|
||||||
if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested)
|
}
|
||||||
|
|
||||||
|
// Creamos un scope de servicios para obtener una instancia fresca de DbContext.
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
|
// PASO 2: Sincronizar las categorías electorales.
|
||||||
|
// Es un catálogo pequeño y es la base para las siguientes consultas.
|
||||||
|
var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
|
||||||
|
if (categoriasApi is null || !categoriasApi.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("La API no devolvió datos para el catálogo de Categorías. La sincronización no puede continuar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList();
|
||||||
|
_logger.LogInformation("Se procesarán {count} categorías electorales.", distinctCategorias.Count);
|
||||||
|
|
||||||
|
var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken);
|
||||||
|
foreach (var categoriaDto in distinctCategorias)
|
||||||
|
{
|
||||||
|
if (!categoriasEnDb.ContainsKey(categoriaDto.CategoriaId))
|
||||||
{
|
{
|
||||||
_logger.LogError("No se pudo obtener token para la sincronización de catálogos.");
|
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Guardamos las categorías primero para asegurar su existencia.
|
||||||
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
// PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente.
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
// Esto evita hacer miles de consultas a la BD dentro de un bucle.
|
||||||
|
|
||||||
// --- 1. SINCRONIZAR CATEGORÍAS ELECTORALES ---
|
// Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos.
|
||||||
var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
|
var ambitosEnDb = new Dictionary<string, AmbitoGeografico>();
|
||||||
if (categoriasApi is null || !categoriasApi.Any())
|
var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken);
|
||||||
|
foreach (var ambito in todosLosAmbitos)
|
||||||
|
{
|
||||||
|
string clave = $"{ambito.NivelId}|{ambito.DistritoId}|{ambito.SeccionProvincialId}|{ambito.SeccionId}|{ambito.MunicipioId}|{ambito.CircuitoId}|{ambito.EstablecimientoId}|{ambito.MesaId}";
|
||||||
|
if (!ambitosEnDb.ContainsKey(clave))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se recibieron datos del catálogo de Categorías.");
|
ambitosEnDb.Add(clave, ambito);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList();
|
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
||||||
_logger.LogInformation("Se procesarán {count} categorías electorales.", distinctCategorias.Count);
|
|
||||||
|
// Variable para llevar la cuenta del total de registros insertados.
|
||||||
|
int totalCambiosGuardados = 0;
|
||||||
|
|
||||||
var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken);
|
// PASO 4: Iterar sobre cada categoría para sincronizar sus ámbitos y agrupaciones.
|
||||||
foreach (var categoriaDto in distinctCategorias)
|
foreach (var categoria in distinctCategorias)
|
||||||
|
{
|
||||||
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
_logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId);
|
||||||
|
|
||||||
|
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
|
||||||
|
if (catalogoDto != null)
|
||||||
{
|
{
|
||||||
if (!categoriasEnDb.ContainsKey(categoriaDto.CategoriaId))
|
// 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext
|
||||||
|
foreach (var ambitoDto in catalogoDto.Ambitos)
|
||||||
{
|
{
|
||||||
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
|
string claveUnica = $"{ambitoDto.NivelId}|{ambitoDto.CodigoAmbitos.DistritoId}|{ambitoDto.CodigoAmbitos.SeccionProvincialId}|{ambitoDto.CodigoAmbitos.SeccionId}|{ambitoDto.CodigoAmbitos.MunicipioId}|{ambitoDto.CodigoAmbitos.CircuitoId}|{ambitoDto.CodigoAmbitos.EstablecimientoId}|{ambitoDto.CodigoAmbitos.MesaId}";
|
||||||
}
|
|
||||||
}
|
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
|
||||||
|
|
||||||
// --- 2. SINCRONIZAR ÁMBITOS Y AGRUPACIONES POR CADA CATEGORÍA ---
|
if (!ambitosEnDb.ContainsKey(claveUnica))
|
||||||
|
|
||||||
var ambitosEnDb = new Dictionary<string, AmbitoGeografico>();
|
|
||||||
var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken);
|
|
||||||
|
|
||||||
foreach (var ambito in todosLosAmbitos)
|
|
||||||
{
|
|
||||||
// Creamos una clave única que SIEMPRE funciona, incluso con nulos.
|
|
||||||
string clave = $"{ambito.NivelId}|{ambito.DistritoId}|{ambito.SeccionProvincialId}|{ambito.SeccionId}|{ambito.MunicipioId}|{ambito.CircuitoId}|{ambito.EstablecimientoId}|{ambito.MesaId}";
|
|
||||||
if (!ambitosEnDb.ContainsKey(clave))
|
|
||||||
{
|
|
||||||
ambitosEnDb.Add(clave, ambito);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
|
||||||
|
|
||||||
foreach (var categoria in distinctCategorias)
|
|
||||||
{
|
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
|
||||||
_logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId);
|
|
||||||
|
|
||||||
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
|
|
||||||
if (catalogoDto != null)
|
|
||||||
{
|
|
||||||
foreach (var ambitoDto in catalogoDto.Ambitos)
|
|
||||||
{
|
{
|
||||||
// Volvemos a generar la misma clave única para la comparación
|
var nuevoAmbito = new AmbitoGeografico
|
||||||
string claveUnica = $"{ambitoDto.NivelId}|{ambitoDto.CodigoAmbitos.DistritoId}|{ambitoDto.CodigoAmbitos.SeccionProvincialId}|{ambitoDto.CodigoAmbitos.SeccionId}|{ambitoDto.CodigoAmbitos.MunicipioId}|{ambitoDto.CodigoAmbitos.CircuitoId}|{ambitoDto.CodigoAmbitos.EstablecimientoId}|{ambitoDto.CodigoAmbitos.MesaId}";
|
|
||||||
|
|
||||||
if (!ambitosEnDb.ContainsKey(claveUnica))
|
|
||||||
{
|
{
|
||||||
var nuevoAmbito = new AmbitoGeografico
|
Nombre = ambitoDto.Nombre,
|
||||||
{
|
NivelId = ambitoDto.NivelId,
|
||||||
Nombre = ambitoDto.Nombre,
|
DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
|
||||||
NivelId = ambitoDto.NivelId,
|
SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId,
|
||||||
DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
|
SeccionId = ambitoDto.CodigoAmbitos.SeccionId,
|
||||||
SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId,
|
MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId,
|
||||||
SeccionId = ambitoDto.CodigoAmbitos.SeccionId,
|
CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
|
||||||
MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId,
|
EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
|
||||||
CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
|
MesaId = ambitoDto.CodigoAmbitos.MesaId,
|
||||||
EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
|
};
|
||||||
MesaId = ambitoDto.CodigoAmbitos.MesaId,
|
dbContext.AmbitosGeograficos.Add(nuevoAmbito);
|
||||||
};
|
ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria
|
||||||
dbContext.AmbitosGeograficos.Add(nuevoAmbito);
|
|
||||||
ambitosEnDb.Add(claveUnica, nuevoAmbito);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lógica para sincronizar AGRUPACIONES POLÍTICAS
|
// 4.2 - Procesar y añadir AGRUPACIONES nuevas al DbContext
|
||||||
var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10);
|
var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10);
|
||||||
if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId))
|
if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId))
|
||||||
|
{
|
||||||
|
// Usamos un try-catch porque no todas las categorías tienen agrupaciones a nivel provincial.
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
|
||||||
|
if (agrupacionesApi != null && agrupacionesApi.Any())
|
||||||
{
|
{
|
||||||
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
|
foreach (var agrupacionDto in agrupacionesApi)
|
||||||
if (agrupacionesApi != null && agrupacionesApi.Any())
|
|
||||||
{
|
{
|
||||||
foreach (var agrupacionDto in agrupacionesApi)
|
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
||||||
{
|
{
|
||||||
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
var nuevaAgrupacion = new AgrupacionPolitica
|
||||||
{
|
{
|
||||||
var nuevaAgrupacion = new AgrupacionPolitica
|
Id = agrupacionDto.IdAgrupacion,
|
||||||
{
|
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
||||||
Id = agrupacionDto.IdAgrupacion,
|
Nombre = agrupacionDto.NombreAgrupacion
|
||||||
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
};
|
||||||
Nombre = agrupacionDto.NombreAgrupacion
|
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
|
||||||
};
|
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
|
||||||
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
|
|
||||||
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
_logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}). Esto puede ser normal si la categoría no aplica a nivel provincial.", categoria.Nombre, categoria.CategoriaId);
|
{
|
||||||
}
|
_logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}).", categoria.Nombre, categoria.CategoriaId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. GUARDADO FINAL ---
|
// Después de procesar todos los ámbitos y agrupaciones de UNA categoría, guardamos los cambios.
|
||||||
int cambiosGuardados = await dbContext.SaveChangesAsync(stoppingToken);
|
// Esto divide la inserción masiva de ~50,000 registros en 3 transacciones más pequeñas,
|
||||||
_logger.LogInformation("{count} nuevos registros de catálogo han sido guardados en la base de datos.", cambiosGuardados);
|
// evitando timeouts y fallos en la base de datos.
|
||||||
_logger.LogInformation("Sincronización de catálogos maestros finalizada.");
|
if (dbContext.ChangeTracker.HasChanges())
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
{
|
totalCambiosGuardados += cambiosEnLote;
|
||||||
_logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos.");
|
_logger.LogInformation("Guardados {count} registros de catálogo para la categoría '{catNombre}'.", cambiosEnLote, categoria.Nombre);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ya no hay un SaveChangesAsync() gigante aquí.
|
||||||
|
_logger.LogInformation("{count} nuevos registros de catálogo han sido guardados en total.", totalCambiosGuardados);
|
||||||
|
_logger.LogInformation("Sincronización de catálogos maestros finalizada.");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// El resto de los métodos (SondearResultadosMunicipalesAsync, GuardarResultadosDeAmbitoAsync, etc.)
|
// El resto de los métodos (SondearResultadosMunicipalesAsync, GuardarResultadosDeAmbitoAsync, etc.)
|
||||||
// se mantienen como en la versión anterior que te proporcioné. Los incluyo aquí para
|
// se mantienen como en la versión anterior que te proporcioné. Los incluyo aquí para
|
||||||
@@ -225,57 +247,50 @@ public class Worker : BackgroundService
|
|||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
// Cambiamos la búsqueda a NivelId = 30, que según la API
|
|
||||||
// son los registros de "Sección" (Partidos/Municipios).
|
|
||||||
var municipiosASondear = await dbContext.AmbitosGeograficos
|
var municipiosASondear = await dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.NivelId == 30 && a.MunicipioId != null && a.DistritoId != null && a.SeccionId != null)
|
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
|
||||||
.Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId })
|
// El MunicipioId es opcional en la BD, lo quitamos del Where para asegurar que traiga todos los partidos
|
||||||
.ToListAsync(stoppingToken);
|
.Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId })
|
||||||
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
if (!municipiosASondear.Any())
|
if (!municipiosASondear.Any())
|
||||||
{
|
{
|
||||||
// Este log ahora mostrará 'NivelId 30' si falla.
|
|
||||||
_logger.LogWarning("No se encontraron Partidos (NivelId 30) en la BD para sondear resultados.");
|
_logger.LogWarning("No se encontraron Partidos (NivelId 30) en la BD para sondear resultados.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Iniciando sondeo de resultados para {count} municipios (Partidos)...", municipiosASondear.Count);
|
_logger.LogInformation("Iniciando sondeo de resultados para {count} municipios (Partidos)...", municipiosASondear.Count);
|
||||||
|
|
||||||
|
var categoriaConcejales = await dbContext.CategoriasElectorales
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken);
|
||||||
|
|
||||||
|
if (categoriaConcejales == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No se encontró la categoría 'CONCEJALES'. Omitiendo sondeo de resultados municipales.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var todosLosResultados = new Dictionary<int, Elecciones.Core.DTOs.ResultadosDto>();
|
||||||
foreach (var municipio in municipiosASondear)
|
foreach (var municipio in municipiosASondear)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
var categoriaConcejales = await dbContext.CategoriasElectorales
|
var resultados = await _apiService.GetResultadosAsync(
|
||||||
.AsNoTracking()
|
authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id
|
||||||
.FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken);
|
);
|
||||||
|
|
||||||
if (categoriaConcejales != null)
|
if (resultados != null)
|
||||||
{
|
{
|
||||||
var resultados = await _apiService.GetResultadosAsync(
|
todosLosResultados[municipio.Id] = resultados;
|
||||||
authToken,
|
|
||||||
municipio.DistritoId!,
|
|
||||||
municipio.SeccionId!,
|
|
||||||
null,
|
|
||||||
categoriaConcejales.Id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resultados != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await GuardarResultadosDeAmbitoAsync(dbContext, municipio.Id, resultados, stoppingToken);
|
|
||||||
// Ahora 'municipio.Nombre' existe y el log funcionará
|
|
||||||
_logger.LogInformation("Resultados para el municipio '{nombre}' (ID: {id}) guardados/actualizados.", municipio.Nombre, municipio.MunicipioId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Y aquí también funcionará, dándonos un error mucho más útil
|
|
||||||
_logger.LogError(ex, "FALLO CRÍTICO al guardar resultados para el municipio '{nombre}' (ID: {id}).", municipio.Nombre, municipio.MunicipioId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (todosLosResultados.Any())
|
||||||
|
{
|
||||||
|
// La llamada ahora es correcta porque el método receptor espera 3 argumentos
|
||||||
|
await GuardarResultadosDeMunicipiosAsync(dbContext, todosLosResultados, stoppingToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -283,173 +298,201 @@ public class Worker : BackgroundService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GuardarResultadosDeAmbitoAsync(EleccionesDbContext dbContext, int ambitoId, Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken)
|
/// Realiza una operación "Upsert" (Update o Insert) de forma masiva y optimizada.
|
||||||
|
/// Este método es llamado por SondearResultadosMunicipalesAsync.
|
||||||
|
/// </summary>
|
||||||
|
private async Task GuardarResultadosDeMunicipiosAsync(
|
||||||
|
EleccionesDbContext dbContext,
|
||||||
|
Dictionary<int, Elecciones.Core.DTOs.ResultadosDto> todosLosResultados,
|
||||||
|
CancellationToken stoppingToken) // <-- PARÁMETRO AÑADIDO
|
||||||
{
|
{
|
||||||
var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId }, cancellationToken: stoppingToken);
|
// Obtenemos los IDs de todos los ámbitos que vamos a actualizar.
|
||||||
if (estadoRecuento == null)
|
var ambitoIds = todosLosResultados.Keys;
|
||||||
|
|
||||||
|
// --- OPTIMIZACIÓN 1: Cargar todos los datos existentes en memoria UNA SOLA VEZ ---
|
||||||
|
var estadosRecuentoExistentes = await dbContext.EstadosRecuentos
|
||||||
|
.Where(e => ambitoIds.Contains(e.AmbitoGeograficoId))
|
||||||
|
.ToDictionaryAsync(e => e.AmbitoGeograficoId, stoppingToken);
|
||||||
|
|
||||||
|
var resultadosVotosExistentes = await dbContext.ResultadosVotos
|
||||||
|
.Where(rv => ambitoIds.Contains(rv.AmbitoGeograficoId))
|
||||||
|
.GroupBy(rv => rv.AmbitoGeograficoId)
|
||||||
|
.ToDictionaryAsync(g => g.Key, g => g.ToDictionary(item => item.AgrupacionPoliticaId), stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Procesando en memoria los resultados de {count} municipios.", todosLosResultados.Count);
|
||||||
|
|
||||||
|
// --- OPTIMIZACIÓN 2: Procesar todo en memoria ---
|
||||||
|
foreach (var kvp in todosLosResultados)
|
||||||
{
|
{
|
||||||
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId };
|
var ambitoId = kvp.Key;
|
||||||
dbContext.EstadosRecuentos.Add(estadoRecuento);
|
var resultadosDto = kvp.Value;
|
||||||
}
|
|
||||||
|
|
||||||
estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime();
|
// Lógica Upsert para EstadoRecuento
|
||||||
estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas;
|
if (!estadosRecuentoExistentes.TryGetValue(ambitoId, out var estadoRecuento))
|
||||||
estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas;
|
|
||||||
estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje;
|
|
||||||
estadoRecuento.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores;
|
|
||||||
estadoRecuento.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes;
|
|
||||||
estadoRecuento.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje;
|
|
||||||
|
|
||||||
if (resultadosDto.ValoresTotalizadosOtros != null)
|
|
||||||
{
|
|
||||||
estadoRecuento.VotosEnBlanco = resultadosDto.ValoresTotalizadosOtros.VotosEnBlanco;
|
|
||||||
estadoRecuento.VotosEnBlancoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosEnBlancoPorcentaje;
|
|
||||||
estadoRecuento.VotosNulos = resultadosDto.ValoresTotalizadosOtros.VotosNulos;
|
|
||||||
estadoRecuento.VotosNulosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosNulosPorcentaje;
|
|
||||||
estadoRecuento.VotosRecurridos = resultadosDto.ValoresTotalizadosOtros.VotosRecurridos;
|
|
||||||
estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos)
|
|
||||||
{
|
|
||||||
var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync(
|
|
||||||
rv => rv.AmbitoGeograficoId == ambitoId && rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion,
|
|
||||||
stoppingToken
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resultadoVoto == null)
|
|
||||||
{
|
{
|
||||||
resultadoVoto = new ResultadoVoto
|
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId };
|
||||||
{
|
dbContext.EstadosRecuentos.Add(estadoRecuento);
|
||||||
AmbitoGeograficoId = ambitoId,
|
}
|
||||||
AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion
|
|
||||||
};
|
// Mapeo completo de propiedades para EstadoRecuento
|
||||||
dbContext.ResultadosVotos.Add(resultadoVoto);
|
estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime();
|
||||||
|
estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas;
|
||||||
|
estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas;
|
||||||
|
estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje;
|
||||||
|
estadoRecuento.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores;
|
||||||
|
estadoRecuento.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes;
|
||||||
|
estadoRecuento.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje;
|
||||||
|
|
||||||
|
if (resultadosDto.ValoresTotalizadosOtros != null)
|
||||||
|
{
|
||||||
|
estadoRecuento.VotosEnBlanco = resultadosDto.ValoresTotalizadosOtros.VotosEnBlanco;
|
||||||
|
estadoRecuento.VotosEnBlancoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosEnBlancoPorcentaje;
|
||||||
|
estadoRecuento.VotosNulos = resultadosDto.ValoresTotalizadosOtros.VotosNulos;
|
||||||
|
estadoRecuento.VotosNulosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosNulosPorcentaje;
|
||||||
|
estadoRecuento.VotosRecurridos = resultadosDto.ValoresTotalizadosOtros.VotosRecurridos;
|
||||||
|
estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lógica Upsert para ResultadosVotos
|
||||||
|
var votosDeAmbitoExistentes = resultadosVotosExistentes.GetValueOrDefault(ambitoId);
|
||||||
|
foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos)
|
||||||
|
{
|
||||||
|
ResultadoVoto? resultadoVoto = null;
|
||||||
|
if (votosDeAmbitoExistentes != null)
|
||||||
|
{
|
||||||
|
votosDeAmbitoExistentes.TryGetValue(votoPositivoDto.IdAgrupacion, out resultadoVoto);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultadoVoto == null)
|
||||||
|
{
|
||||||
|
resultadoVoto = new ResultadoVoto
|
||||||
|
{
|
||||||
|
AmbitoGeograficoId = ambitoId,
|
||||||
|
AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion
|
||||||
|
};
|
||||||
|
dbContext.ResultadosVotos.Add(resultadoVoto);
|
||||||
|
}
|
||||||
|
resultadoVoto.CantidadVotos = votoPositivoDto.Votos;
|
||||||
|
resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje;
|
||||||
}
|
}
|
||||||
resultadoVoto.CantidadVotos = votoPositivoDto.Votos;
|
|
||||||
resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- OPTIMIZACIÓN 3: Guardar todos los cambios en UNA SOLA TRANSACCIÓN ---
|
||||||
|
_logger.LogInformation("Guardando todos los cambios de resultados municipales en la base de datos...");
|
||||||
|
// Ahora 'stoppingToken' es reconocido aquí
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
_logger.LogInformation("Guardado completado.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sondea la proyección de bancas para diputados y senadores.
|
/// Sondea la proyección de bancas. Este método ahora es más completo:
|
||||||
/// Este método busca dinámicamente en la base de datos las categorías relevantes (Senadores/Diputados)
|
/// 1. Consulta el reparto de bancas a nivel PROVINCIAL para cada categoría.
|
||||||
/// y los ámbitos de "Sección Electoral" (NivelId = 20), que es el nivel al que se reparten las bancas.
|
/// 2. Consulta el reparto de bancas desglosado por SECCIÓN ELECTORAL para cada categoría.
|
||||||
/// Luego, consulta la API para cada combinación y actualiza la tabla de proyecciones.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authToken">El token de autenticación válido para la sesión.</param>
|
|
||||||
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
|
|
||||||
private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken)
|
private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// PASO 1: Preparar el entorno
|
|
||||||
// Creamos un scope de DbContext para esta operación específica, una buena práctica en servicios de larga duración.
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
// PASO 2: Obtener las categorías que reparten bancas (Senadores y Diputados)
|
|
||||||
// Hacemos una consulta a nuestra tabla local de categorías para que el código sea dinámico
|
|
||||||
// y no dependa de IDs fijos (hardcodeados).
|
|
||||||
var categoriasDeBancas = await dbContext.CategoriasElectorales
|
var categoriasDeBancas = await dbContext.CategoriasElectorales
|
||||||
.AsNoTracking() // Optimización de rendimiento: solo vamos a leer estos datos.
|
.AsNoTracking()
|
||||||
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
|
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
// Si por alguna razón estas categorías no están en la BD, no podemos continuar.
|
var provincia = await dbContext.AmbitosGeograficos
|
||||||
if (!categoriasDeBancas.Any())
|
.AsNoTracking()
|
||||||
{
|
.FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
|
||||||
_logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PASO 3: Obtener las "Secciones Electorales" (NivelId 20)
|
|
||||||
// Esta es la corrección clave. Basado en la respuesta real de la API, las Secciones Electorales
|
|
||||||
// (Primera, Segunda, Tercera, etc.) usan NivelId = 20.
|
|
||||||
var seccionesElectorales = await dbContext.AmbitosGeograficos
|
var seccionesElectorales = await dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null)
|
.Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null)
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
// Si no se encuentra ninguna Sección Electoral en la BD (lo cual sería raro después de una sincronización exitosa),
|
if (!categoriasDeBancas.Any() || provincia == null)
|
||||||
// registramos una advertencia y salimos.
|
|
||||||
if (!seccionesElectorales.Any())
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se encontraron ámbitos de tipo 'Sección Electoral' (NivelId 20) en la BD para sondear bancas.");
|
_logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones electorales y {catCount} categorías...", seccionesElectorales.Count, categoriasDeBancas.Count);
|
_logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial y para {count} Secciones Electorales...", seccionesElectorales.Count);
|
||||||
|
|
||||||
// PASO 4: Iterar, consultar la API y preparar los datos para guardar
|
|
||||||
|
|
||||||
// Esta bandera es crucial para la estrategia de "borrar y reemplazar".
|
|
||||||
// Nos asegura que la tabla de proyecciones se vacía UNA SOLA VEZ, justo antes de insertar
|
|
||||||
// el primer lote de datos nuevos, evitando datos inconsistentes.
|
|
||||||
bool hasReceivedAnyNewData = false;
|
bool hasReceivedAnyNewData = false;
|
||||||
|
var nuevasProyecciones = new List<ProyeccionBanca>();
|
||||||
|
|
||||||
// Bucle externo: recorremos cada una de las 8 Secciones Electorales.
|
// --- NUEVA LÓGICA: Bucle para el nivel Provincial ---
|
||||||
|
foreach (var categoria in categoriasDeBancas)
|
||||||
|
{
|
||||||
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
// Llamamos a la API sin 'seccionProvincialId' para obtener el total provincial.
|
||||||
|
var repartoBancas = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id);
|
||||||
|
|
||||||
|
if (repartoBancas?.RepartoBancas is { Count: > 0 })
|
||||||
|
{
|
||||||
|
hasReceivedAnyNewData = true;
|
||||||
|
foreach (var banca in repartoBancas.RepartoBancas)
|
||||||
|
{
|
||||||
|
// Guardamos la proyección asociándola al ID del ámbito de la provincia.
|
||||||
|
nuevasProyecciones.Add(new ProyeccionBanca
|
||||||
|
{
|
||||||
|
AmbitoGeograficoId = provincia.Id,
|
||||||
|
AgrupacionPoliticaId = banca.IdAgrupacion,
|
||||||
|
NroBancas = banca.NroBancas
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LÓGICA EXISTENTE: Bucle para el nivel de Sección Electoral ---
|
||||||
foreach (var seccion in seccionesElectorales)
|
foreach (var seccion in seccionesElectorales)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break; // Salida limpia si la aplicación se detiene.
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
// Bucle interno: para cada sección, consultamos las bancas de Senadores y Diputados.
|
|
||||||
foreach (var categoria in categoriasDeBancas)
|
foreach (var categoria in categoriasDeBancas)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
// Llamamos a la API. El endpoint 'getBancas' requiere 'distritoId' y 'seccionProvincialId',
|
|
||||||
// que son precisamente los datos que tenemos en nuestros ámbitos de NivelId = 20.
|
|
||||||
var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id);
|
var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id);
|
||||||
|
|
||||||
// Verificamos que la respuesta de la API no sea nula y que contenga al menos una banca repartida.
|
|
||||||
if (repartoBancas?.RepartoBancas is { Count: > 0 })
|
if (repartoBancas?.RepartoBancas is { Count: > 0 })
|
||||||
{
|
{
|
||||||
// Si esta es la PRIMERA VEZ en todo el sondeo que recibimos datos válidos...
|
hasReceivedAnyNewData = true;
|
||||||
if (!hasReceivedAnyNewData)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Se recibieron nuevos datos de bancas. Limpiando la tabla de proyecciones para la actualización...");
|
|
||||||
// ...ejecutamos un comando SQL para borrar todos los datos viejos de la tabla.
|
|
||||||
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
|
||||||
// Activamos la bandera para no volver a ejecutar este borrado.
|
|
||||||
hasReceivedAnyNewData = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Procesamos cada banca obtenida en la respuesta de la API.
|
|
||||||
foreach (var banca in repartoBancas.RepartoBancas)
|
foreach (var banca in repartoBancas.RepartoBancas)
|
||||||
{
|
{
|
||||||
// Creamos una nueva entidad 'ProyeccionBanca'.
|
nuevasProyecciones.Add(new ProyeccionBanca
|
||||||
var nuevaProyeccion = new ProyeccionBanca
|
|
||||||
{
|
{
|
||||||
AmbitoGeograficoId = seccion.Id,
|
AmbitoGeograficoId = seccion.Id,
|
||||||
AgrupacionPoliticaId = banca.IdAgrupacion,
|
AgrupacionPoliticaId = banca.IdAgrupacion,
|
||||||
NroBancas = banca.NroBancas
|
NroBancas = banca.NroBancas
|
||||||
};
|
});
|
||||||
// Y la añadimos al ChangeTracker de EF para que la inserte.
|
|
||||||
await dbContext.ProyeccionesBancas.AddAsync(nuevaProyeccion, stoppingToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASO 5: Guardar los cambios en la Base de Datos
|
// --- LÓGICA DE GUARDADO CENTRALIZADA ---
|
||||||
// Si la bandera 'hasReceivedAnyNewData' se activó, significa que hemos añadido nuevas proyecciones
|
|
||||||
// al DbContext y necesitamos persistirlas.
|
|
||||||
if (hasReceivedAnyNewData)
|
if (hasReceivedAnyNewData)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Se recibieron {count} nuevos datos de bancas. Actualizando la tabla de proyecciones...", nuevasProyecciones.Count);
|
||||||
|
|
||||||
|
// Usamos una transacción para asegurar la consistencia.
|
||||||
|
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
|
||||||
|
|
||||||
|
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
||||||
|
await dbContext.ProyeccionesBancas.AddRangeAsync(nuevasProyecciones, stoppingToken);
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
_logger.LogInformation("Sondeo de Bancas completado. La tabla de proyecciones ha sido actualizada con nuevos datos.");
|
await transaction.CommitAsync(stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Sondeo de Bancas completado. La tabla de proyecciones ha sido actualizada.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Si no se recibieron datos nuevos, no hacemos nada en la BD.
|
|
||||||
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada.");
|
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Capturamos cualquier error inesperado para que no detenga el worker por completo.
|
|
||||||
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas.");
|
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user