Fix Worker 1348
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.8" />
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using Serilog;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Extensions.Http;
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.WriteTo.Console()
|
.WriteTo.Console()
|
||||||
@@ -37,7 +39,7 @@ builder.Services.AddHttpClient("ElectoralApiClient", client =>
|
|||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(baseUrl);
|
client.BaseAddress = new Uri(baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- TIMEOUT MÁS LARGO ---
|
// --- TIMEOUT MÁS LARGO ---
|
||||||
// Aumentamos el tiempo de espera a 90 segundos.
|
// Aumentamos el tiempo de espera a 90 segundos.
|
||||||
// Esto le dará a las peticiones lentas de la API tiempo suficiente para responder.
|
// Esto le dará a las peticiones lentas de la API tiempo suficiente para responder.
|
||||||
@@ -75,7 +77,9 @@ builder.Services.AddHttpClient("ElectoralApiClient", client =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return handler;
|
return handler;
|
||||||
});
|
})
|
||||||
|
|
||||||
|
.AddPolicyHandler(GetRetryPolicy());
|
||||||
|
|
||||||
builder.Services.AddSingleton<IElectoralApiService, ElectoralApiService>();
|
builder.Services.AddSingleton<IElectoralApiService, ElectoralApiService>();
|
||||||
|
|
||||||
@@ -83,7 +87,8 @@ builder.Services.AddHostedService<Worker>();
|
|||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
host.Run();
|
host.Run();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -93,4 +98,20 @@ catch (Exception ex)
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Log.CloseAndFlush();
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||||
|
{
|
||||||
|
return HttpPolicyExtensions
|
||||||
|
// Manejar peticiones que fallaron por errores de red O que devolvieron un error de servidor (como 504)
|
||||||
|
.HandleTransientHttpError()
|
||||||
|
// O que devolvieron un código 504 Gateway Timeout específicamente
|
||||||
|
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.GatewayTimeout)
|
||||||
|
// Esperar y reintentar. La espera se duplica en cada reintento.
|
||||||
|
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||||
|
onRetry: (outcome, timespan, retryAttempt, context) =>
|
||||||
|
{
|
||||||
|
// Opcional: Loguear cada reintento. Necesitarías pasar ILogger aquí.
|
||||||
|
// Log.Warning("Retrying due to {StatusCode}. Waited {seconds}s. Attempt {retryAttempt}/3", outcome.Result.StatusCode, timespan.TotalSeconds, retryAttempt);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -318,68 +318,110 @@ public class Worker : BackgroundService
|
|||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sondea la proyección de bancas para diputados y senadores.
|
||||||
|
/// Este método busca dinámicamente en la base de datos las categorías relevantes (Senadores/Diputados)
|
||||||
|
/// y los ámbitos de "Sección Electoral" (NivelId = 20), que es el nivel al que se reparten las bancas.
|
||||||
|
/// Luego, consulta la API para cada combinación y actualiza la tabla de proyecciones.
|
||||||
|
/// </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()
|
.AsNoTracking() // Optimización de rendimiento: solo vamos a leer estos datos.
|
||||||
.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.
|
||||||
if (!categoriasDeBancas.Any())
|
if (!categoriasDeBancas.Any())
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas.");
|
_logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var secciones = await dbContext.AmbitosGeograficos
|
// 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
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null)
|
.Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null)
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
if (!secciones.Any())
|
// Si no se encuentra ninguna Sección Electoral en la BD (lo cual sería raro después de una sincronización exitosa),
|
||||||
|
// registramos una advertencia y salimos.
|
||||||
|
if (!seccionesElectorales.Any())
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No se encontraron ámbitos de tipo 'Sección Electoral' en la BD para sondear bancas.");
|
_logger.LogWarning("No se encontraron ámbitos de tipo 'Sección Electoral' (NivelId 20) en la BD para sondear bancas.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones y {catCount} categorías...", secciones.Count, categoriasDeBancas.Count);
|
_logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones electorales y {catCount} categorías...", seccionesElectorales.Count, categoriasDeBancas.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;
|
||||||
foreach (var seccion in secciones)
|
|
||||||
|
// Bucle externo: recorremos cada una de las 8 Secciones Electorales.
|
||||||
|
foreach (var seccion in seccionesElectorales)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break; // Salida limpia si la aplicación se detiene.
|
||||||
|
|
||||||
|
// 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;
|
||||||
var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionId!, categoria.Id);
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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...
|
||||||
if (!hasReceivedAnyNewData)
|
if (!hasReceivedAnyNewData)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Se recibieron nuevos datos de bancas. Limpiando la tabla de proyecciones para evitar duplicados...");
|
_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);
|
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
||||||
|
// Activamos la bandera para no volver a ejecutar este borrado.
|
||||||
hasReceivedAnyNewData = true;
|
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'.
|
||||||
var nuevaProyeccion = 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);
|
await dbContext.ProyeccionesBancas.AddAsync(nuevaProyeccion, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PASO 5: Guardar los cambios en la Base de Datos
|
||||||
|
// Si la bandera 'hasReceivedAnyNewData' se activó, significa que hemos añadido nuevas proyecciones
|
||||||
|
// al DbContext y necesitamos persistirlas.
|
||||||
if (hasReceivedAnyNewData)
|
if (hasReceivedAnyNewData)
|
||||||
{
|
{
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
@@ -387,83 +429,127 @@ public class Worker : BackgroundService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos, la tabla no fue modificada.");
|
// 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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca en la API si hay nuevos telegramas totalizados que no se encuentren en la base de datos local.
|
||||||
|
/// Este método itera sobre cada Partido/Municipio (que la API identifica como "Sección" con NivelId = 30),
|
||||||
|
/// obtiene la lista de IDs de telegramas para cada uno, los compara con los IDs locales,
|
||||||
|
/// y finalmente descarga y guarda solo los que son nuevos.
|
||||||
|
/// </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 SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken)
|
private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// PASO 1: Preparar el DbContext
|
||||||
|
// Creamos un scope para obtener una instancia fresca de DbContext para esta operación.
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
var secciones = await dbContext.AmbitosGeograficos
|
// PASO 2: Obtener los Partidos/Municipios (NivelId = 30)
|
||||||
.AsNoTracking()
|
// --- CORRECCIÓN CLAVE ---
|
||||||
.Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null)
|
// La API requiere un 'seccionId' para listar telegramas, y el manual lo define como "Id del partido".
|
||||||
|
// La respuesta de getCatalogo nos confirmó que los registros de Partidos/Municipios usan NivelId = 30.
|
||||||
|
var partidos = await dbContext.AmbitosGeograficos
|
||||||
|
.AsNoTracking() // Optimización: Solo necesitamos leer estos datos.
|
||||||
|
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
if (!secciones.Any())
|
// Si la sincronización inicial no cargó ningún partido, no podemos continuar.
|
||||||
|
if (!partidos.Any())
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No hay Secciones Electorales en la BD para sondear telegramas.");
|
_logger.LogWarning("No se encontraron Partidos (NivelId 30) en la BD para sondear telegramas.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Iniciando sondeo de Telegramas nuevos...");
|
_logger.LogInformation("Iniciando sondeo de Telegramas nuevos para {count} partidos...", partidos.Count);
|
||||||
|
|
||||||
foreach (var seccion in secciones)
|
// PASO 3: Iterar sobre cada Partido para buscar telegramas.
|
||||||
|
foreach (var partido in partidos)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break; // Salida limpia si la aplicación se detiene.
|
||||||
|
|
||||||
var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, seccion.DistritoId!, seccion.SeccionId!);
|
// 3.1 - OBTENER LA LISTA COMPLETA DE IDs DE LA API PARA EL PARTIDO ACTUAL
|
||||||
|
var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!);
|
||||||
|
|
||||||
|
// Solo procesamos si la API devolvió una lista con al menos un telegrama.
|
||||||
if (listaTelegramasApi is { Count: > 0 })
|
if (listaTelegramasApi is { Count: > 0 })
|
||||||
{
|
{
|
||||||
|
// La API devuelve una lista de arrays de string (ej. [ ["id1"], ["id2"] ]).
|
||||||
|
// Usamos .Select(t => t[0]) para extraer solo el ID de cada sub-array.
|
||||||
var idsDeApi = listaTelegramasApi.Select(t => t[0]).Distinct().ToList();
|
var idsDeApi = listaTelegramasApi.Select(t => t[0]).Distinct().ToList();
|
||||||
|
|
||||||
|
// 3.2 - COMPARAR CON LA BASE DE DATOS LOCAL PARA ENCONTRAR LOS NUEVOS
|
||||||
|
// Esta es una consulta muy eficiente: le pedimos a la BD que nos devuelva solo los IDs que
|
||||||
|
// ya existen de la lista que nos acaba de dar la API.
|
||||||
var idsYaEnDb = await dbContext.Telegramas
|
var idsYaEnDb = await dbContext.Telegramas
|
||||||
.Where(t => idsDeApi.Contains(t.Id))
|
.Where(t => idsDeApi.Contains(t.Id))
|
||||||
.Select(t => t.Id)
|
.Select(t => t.Id)
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
|
// Comparamos las dos listas para obtener una tercera: la de los IDs que están en la API pero no en nuestra BD.
|
||||||
var nuevosTelegramasIds = idsDeApi.Except(idsYaEnDb).ToList();
|
var nuevosTelegramasIds = idsDeApi.Except(idsYaEnDb).ToList();
|
||||||
|
|
||||||
|
// Si no hay telegramas nuevos para este partido, simplemente continuamos con el siguiente.
|
||||||
if (!nuevosTelegramasIds.Any())
|
if (!nuevosTelegramasIds.Any())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_logger.LogInformation("Se encontraron {count} telegramas nuevos en la sección {seccionId}. Descargando...", nuevosTelegramasIds.Count, seccion.SeccionId);
|
|
||||||
|
|
||||||
|
_logger.LogInformation("Se encontraron {count} telegramas nuevos en el partido '{nombrePartido}' ({seccionId}). Descargando...", nuevosTelegramasIds.Count, partido.Nombre, partido.SeccionId);
|
||||||
|
|
||||||
|
// 3.3 - DESCARGAR Y GUARDAR CADA NUEVO TELEGRAMA
|
||||||
foreach (var mesaId in nuevosTelegramasIds)
|
foreach (var mesaId in nuevosTelegramasIds)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
// Descargamos el contenido completo del telegrama (imagen base64 y metadatos).
|
||||||
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
|
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
|
||||||
if (telegramaFile != null)
|
if (telegramaFile != null)
|
||||||
{
|
{
|
||||||
var nuevoTelegrama = new Telegrama
|
var nuevoTelegrama = new Telegrama
|
||||||
{
|
{
|
||||||
Id = telegramaFile.NombreArchivo,
|
Id = telegramaFile.NombreArchivo,
|
||||||
AmbitoGeograficoId = seccion.Id,
|
AmbitoGeograficoId = partido.Id, // Lo asociamos al ID del partido.
|
||||||
ContenidoBase64 = telegramaFile.Imagen,
|
ContenidoBase64 = telegramaFile.Imagen,
|
||||||
FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(),
|
FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(),
|
||||||
FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime()
|
FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime()
|
||||||
};
|
};
|
||||||
|
// Añadimos la nueva entidad a la sesión de EF Core para su inserción.
|
||||||
await dbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
|
await dbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guardamos los cambios en la base de datos después de procesar todos los telegramas nuevos de UN partido.
|
||||||
|
// Esto es más eficiente que guardar en cada iteración del bucle interno.
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Sondeo de Telegramas completado.");
|
_logger.LogInformation("Sondeo de Telegramas completado.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
// Capturamos cualquier error para que no detenga el worker y lo registramos para depuración.
|
||||||
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas.");
|
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene y actualiza el resumen de votos a nivel provincial.
|
||||||
|
/// Esta versión mejorada utiliza una transacción para garantizar la consistencia de los datos.
|
||||||
|
/// </summary>
|
||||||
private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken)
|
private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -475,21 +561,40 @@ public class Worker : BackgroundService
|
|||||||
if (provincia == null) return;
|
if (provincia == null) return;
|
||||||
|
|
||||||
var resumen = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!);
|
var resumen = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!);
|
||||||
if (resumen?.ValoresTotalizadosPositivos is { Count: > 0 })
|
|
||||||
|
// --- CAMBIO CLAVE: Lógica de actualización robusta ---
|
||||||
|
// Solo procedemos si la respuesta de la API es válida Y contiene datos de votos positivos.
|
||||||
|
if (resumen?.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos)
|
||||||
{
|
{
|
||||||
|
// Usamos una transacción explícita para asegurar que la operación sea atómica:
|
||||||
|
// O se completa todo (borrado e inserción), o no se hace nada.
|
||||||
|
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
|
||||||
|
|
||||||
|
// 1. Borramos los datos viejos.
|
||||||
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken);
|
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken);
|
||||||
foreach (var voto in resumen.ValoresTotalizadosPositivos)
|
|
||||||
|
// 2. Insertamos los nuevos datos.
|
||||||
|
foreach (var voto in nuevosVotos)
|
||||||
{
|
{
|
||||||
await dbContext.ResumenesVotos.AddAsync(new ResumenVoto
|
dbContext.ResumenesVotos.Add(new ResumenVoto
|
||||||
{
|
{
|
||||||
AmbitoGeograficoId = provincia.Id,
|
AmbitoGeograficoId = provincia.Id,
|
||||||
AgrupacionPoliticaId = voto.IdAgrupacion,
|
AgrupacionPoliticaId = voto.IdAgrupacion,
|
||||||
Votos = voto.Votos,
|
Votos = voto.Votos,
|
||||||
VotosPorcentaje = voto.VotosPorcentaje
|
VotosPorcentaje = voto.VotosPorcentaje
|
||||||
}, stoppingToken);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Guardamos los cambios y confirmamos la transacción.
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
_logger.LogInformation("Sondeo de Resumen Provincial completado.");
|
await transaction.CommitAsync(stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Sondeo de Resumen Provincial completado. La tabla ha sido actualizada.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Si la API no devuelve datos de votos, no hacemos NADA en la base de datos.
|
||||||
|
_logger.LogInformation("Sondeo de Resumen Provincial completado. No se recibieron datos nuevos, la tabla no fue modificada.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+a4e47b6e3d1f8b0746f4f910f56a94e17b2e030c")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -290,6 +290,10 @@
|
|||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[9.0.8, )"
|
"version": "[9.0.8, )"
|
||||||
},
|
},
|
||||||
|
"Microsoft.Extensions.Http.Polly": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[9.0.8, )"
|
||||||
|
},
|
||||||
"Serilog.Extensions.Hosting": {
|
"Serilog.Extensions.Hosting": {
|
||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[9.0.0, )"
|
"version": "[9.0.0, )"
|
||||||
|
|||||||
Reference in New Issue
Block a user