Feat/Fix: Paralelismo y Coreccion de lista.
This commit is contained in:
@@ -89,12 +89,11 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<RepartoBancasDto>() : null;
|
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<RepartoBancasDto>() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<string[]>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null)
|
public async Task<List<string>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null)
|
||||||
{
|
{
|
||||||
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}";
|
var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}";
|
||||||
|
|
||||||
// Añadimos el parámetro categoriaId a la URL SÓLO si se proporciona un valor.
|
|
||||||
if (categoriaId.HasValue)
|
if (categoriaId.HasValue)
|
||||||
{
|
{
|
||||||
requestUri += $"&categoriaId={categoriaId.Value}";
|
requestUri += $"&categoriaId={categoriaId.Value}";
|
||||||
@@ -103,8 +102,9 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
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);
|
||||||
// Si la respuesta es 400, devolvemos null para que el worker sepa que falló.
|
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<string[]>>() : null;
|
// Ahora deserializamos al tipo correcto: List<string>
|
||||||
|
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<string>>() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId)
|
public async Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public interface IElectoralApiService
|
|||||||
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? seccionProvincialId, 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);
|
||||||
Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId);
|
Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Elecciones.Database;
|
|||||||
using Elecciones.Database.Entities;
|
using Elecciones.Database.Entities;
|
||||||
using Elecciones.Infrastructure.Services;
|
using Elecciones.Infrastructure.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -56,18 +57,18 @@ public class Worker : BackgroundService
|
|||||||
_logger.LogInformation("--- Iniciando sondeo de Resultados Municipales ---");
|
_logger.LogInformation("--- Iniciando sondeo de Resultados Municipales ---");
|
||||||
await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
|
await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Proyección de Bancas ---");
|
|
||||||
await SondearProyeccionBancasAsync(authToken, stoppingToken);
|
|
||||||
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas ---");
|
|
||||||
await SondearNuevosTelegramasAsync(authToken, stoppingToken);
|
|
||||||
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Resumen Provincial ---");
|
_logger.LogInformation("--- Iniciando sondeo de Resumen Provincial ---");
|
||||||
await SondearResumenProvincialAsync(authToken, stoppingToken);
|
await SondearResumenProvincialAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Estado de Recuento General ---");
|
_logger.LogInformation("--- Iniciando sondeo de Estado de Recuento General ---");
|
||||||
await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
|
await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("--- Iniciando sondeo de Proyección de Bancas ---");
|
||||||
|
await SondearProyeccionBancasAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas ---");
|
||||||
|
await SondearNuevosTelegramasAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Ciclo de sondeo completado. Esperando 5 minutos para el siguiente...");
|
_logger.LogInformation("Ciclo de sondeo completado. Esperando 5 minutos para el siguiente...");
|
||||||
@@ -83,174 +84,179 @@ 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. Se ejecuta una sola vez al iniciar el worker.
|
/// desde la API a la base de datos local. Se ejecuta una sola vez al iniciar el worker.
|
||||||
/// Utiliza una estrategia de guardado en lotes para manejar grandes volúmenes de datos
|
/// Utiliza una estrategia de guardado en lotes para manejar grandes volúmenes de datos
|
||||||
/// sin sobrecargar la base de datos.
|
/// sin sobrecargar la base de datos.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
|
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
|
||||||
private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken)
|
private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
|
try
|
||||||
|
|
||||||
// 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.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela.");
|
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creamos un scope de servicios para obtener una instancia fresca de DbContext.
|
// PASO 1: Obtener el token de autenticación. Sin él, no podemos hacer nada.
|
||||||
using var scope = _serviceProvider.CreateScope();
|
var authToken = await _apiService.GetAuthTokenAsync();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested)
|
||||||
|
|
||||||
// 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))
|
|
||||||
{
|
{
|
||||||
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
|
_logger.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Guardamos las categorías primero para asegurar su existencia.
|
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
|
||||||
|
|
||||||
// PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente.
|
// Creamos un scope de servicios para obtener una instancia fresca de DbContext.
|
||||||
// Esto evita hacer miles de consultas a la BD dentro de un bucle.
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
// Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos.
|
// PASO 2: Sincronizar las categorías electorales.
|
||||||
var ambitosEnDb = new Dictionary<string, AmbitoGeografico>();
|
// Es un catálogo pequeño y es la base para las siguientes consultas.
|
||||||
var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken);
|
var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
|
||||||
foreach (var ambito in todosLosAmbitos)
|
if (categoriasApi is null || !categoriasApi.Any())
|
||||||
{
|
|
||||||
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);
|
_logger.LogWarning("La API no devolvió datos para el catálogo de Categorías. La sincronización no puede continuar.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
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);
|
||||||
|
|
||||||
// Variable para llevar la cuenta del total de registros insertados.
|
var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken);
|
||||||
int totalCambiosGuardados = 0;
|
foreach (var categoriaDto in distinctCategorias)
|
||||||
|
|
||||||
// PASO 4: Iterar sobre cada categoría para sincronizar sus ámbitos y agrupaciones.
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
// 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext
|
if (!categoriasEnDb.ContainsKey(categoriaDto.CategoriaId))
|
||||||
foreach (var ambitoDto in catalogoDto.Ambitos)
|
|
||||||
{
|
{
|
||||||
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}";
|
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
|
||||||
|
|
||||||
if (!ambitosEnDb.ContainsKey(claveUnica))
|
|
||||||
{
|
|
||||||
var nuevoAmbito = new AmbitoGeografico
|
|
||||||
{
|
|
||||||
Nombre = ambitoDto.Nombre,
|
|
||||||
NivelId = ambitoDto.NivelId,
|
|
||||||
DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
|
|
||||||
SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId,
|
|
||||||
SeccionId = ambitoDto.CodigoAmbitos.SeccionId,
|
|
||||||
MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId,
|
|
||||||
CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
|
|
||||||
EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
|
|
||||||
MesaId = ambitoDto.CodigoAmbitos.MesaId,
|
|
||||||
};
|
|
||||||
dbContext.AmbitosGeograficos.Add(nuevoAmbito);
|
|
||||||
ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Guardamos las categorías primero para asegurar su existencia.
|
||||||
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
|
||||||
// 4.2 - Procesar y añadir AGRUPACIONES nuevas al DbContext
|
// PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente.
|
||||||
var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10);
|
// Esto evita hacer miles de consultas a la BD dentro de un bucle.
|
||||||
if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId))
|
|
||||||
|
// Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos.
|
||||||
|
var ambitosEnDb = new Dictionary<string, AmbitoGeografico>();
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
// Usamos un try-catch porque no todas las categorías tienen agrupaciones a nivel provincial.
|
ambitosEnDb.Add(clave, ambito);
|
||||||
try
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
||||||
|
|
||||||
|
// Variable para llevar la cuenta del total de registros insertados.
|
||||||
|
int totalCambiosGuardados = 0;
|
||||||
|
|
||||||
|
// PASO 4: Iterar sobre cada categoría para sincronizar sus ámbitos y agrupaciones.
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext
|
||||||
|
foreach (var ambitoDto in catalogoDto.Ambitos)
|
||||||
{
|
{
|
||||||
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
|
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 (agrupacionesApi != null && agrupacionesApi.Any())
|
|
||||||
|
if (!ambitosEnDb.ContainsKey(claveUnica))
|
||||||
{
|
{
|
||||||
foreach (var agrupacionDto in agrupacionesApi)
|
var nuevoAmbito = new AmbitoGeografico
|
||||||
{
|
{
|
||||||
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
Nombre = ambitoDto.Nombre,
|
||||||
|
NivelId = ambitoDto.NivelId,
|
||||||
|
DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
|
||||||
|
SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId,
|
||||||
|
SeccionId = ambitoDto.CodigoAmbitos.SeccionId,
|
||||||
|
MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId,
|
||||||
|
CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
|
||||||
|
EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
|
||||||
|
MesaId = ambitoDto.CodigoAmbitos.MesaId,
|
||||||
|
};
|
||||||
|
dbContext.AmbitosGeograficos.Add(nuevoAmbito);
|
||||||
|
ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.2 - Procesar y añadir AGRUPACIONES nuevas al DbContext
|
||||||
|
var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10);
|
||||||
|
if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId))
|
||||||
|
{
|
||||||
|
// Usamos un try-catch porque no todas las categorías tienen agrupaciones a nivel provincial.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
|
||||||
|
if (agrupacionesApi != null && agrupacionesApi.Any())
|
||||||
|
{
|
||||||
|
foreach (var agrupacionDto in agrupacionesApi)
|
||||||
{
|
{
|
||||||
var nuevaAgrupacion = new AgrupacionPolitica
|
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
||||||
{
|
{
|
||||||
Id = agrupacionDto.IdAgrupacion,
|
var nuevaAgrupacion = new AgrupacionPolitica
|
||||||
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
{
|
||||||
Nombre = agrupacionDto.NombreAgrupacion
|
Id = agrupacionDto.IdAgrupacion,
|
||||||
};
|
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
||||||
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
|
Nombre = agrupacionDto.NombreAgrupacion
|
||||||
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
|
};
|
||||||
|
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
|
||||||
|
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}).", categoria.Nombre, categoria.CategoriaId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}).", categoria.Nombre, categoria.CategoriaId);
|
// Después de procesar todos los ámbitos y agrupaciones de UNA categoría, guardamos los cambios.
|
||||||
}
|
// Esto divide la inserción masiva de ~50,000 registros en 3 transacciones más pequeñas,
|
||||||
|
// evitando timeouts y fallos en la base de datos.
|
||||||
|
if (dbContext.ChangeTracker.HasChanges())
|
||||||
|
{
|
||||||
|
int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
totalCambiosGuardados += cambiosEnLote;
|
||||||
|
_logger.LogInformation("Guardados {count} registros de catálogo para la categoría '{catNombre}'.", cambiosEnLote, categoria.Nombre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Después de procesar todos los ámbitos y agrupaciones de UNA categoría, guardamos los cambios.
|
// Ya no hay un SaveChangesAsync() gigante aquí.
|
||||||
// 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 total.", totalCambiosGuardados);
|
||||||
// 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.)
|
/// <summary>
|
||||||
// se mantienen como en la versión anterior que te proporcioné. Los incluyo aquí para
|
/// Sondea los resultados electorales para todos los municipios/partidos de forma optimizada.
|
||||||
// que tengas el archivo completo y sin errores.
|
/// Utiliza paralelismo controlado para ejecutar múltiples peticiones a la API simultáneamente
|
||||||
|
/// sin sobrecargar la red, y luego guarda todos los resultados en la base de datos de forma masiva.
|
||||||
|
/// </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 SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken)
|
private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// PASO 1: Preparar el DbContext y los datos necesarios.
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
|
// Obtenemos de nuestra BD local la lista de todos los partidos (NivelId=30) que necesitamos consultar.
|
||||||
var municipiosASondear = await dbContext.AmbitosGeograficos
|
var municipiosASondear = await dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
|
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
|
||||||
// El MunicipioId es opcional en la BD, lo quitamos del Where para asegurar que traiga todos los partidos
|
|
||||||
.Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId })
|
.Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId })
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
@@ -259,8 +265,8 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT
|
|||||||
_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);
|
|
||||||
|
|
||||||
|
// Obtenemos la categoría "CONCEJALES", ya que los resultados municipales aplican a esta.
|
||||||
var categoriaConcejales = await dbContext.CategoriasElectorales
|
var categoriaConcejales = await dbContext.CategoriasElectorales
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken);
|
.FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken);
|
||||||
@@ -271,29 +277,64 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var todosLosResultados = new Dictionary<int, Elecciones.Core.DTOs.ResultadosDto>();
|
// PASO 2: Ejecutar las consultas a la API con paralelismo controlado.
|
||||||
foreach (var municipio in municipiosASondear)
|
|
||||||
|
// Definimos cuántas peticiones queremos que se ejecuten simultáneamente.
|
||||||
|
// Un valor entre 8 y 16 es generalmente seguro y ofrece una gran mejora de velocidad.
|
||||||
|
const int GRADO_DE_PARALELISMO = 10;
|
||||||
|
// Creamos un semáforo que actuará como un "control de acceso" con 10 pases libres.
|
||||||
|
var semaforo = new SemaphoreSlim(GRADO_DE_PARALELISMO);
|
||||||
|
|
||||||
|
// Usamos un ConcurrentDictionary para almacenar los resultados. A diferencia de un Dictionary normal,
|
||||||
|
// este permite que múltiples tareas escriban en él al mismo tiempo sin conflictos.
|
||||||
|
var resultadosPorId = new ConcurrentDictionary<int, Elecciones.Core.DTOs.ResultadosDto>();
|
||||||
|
|
||||||
|
_logger.LogInformation("Iniciando sondeo de resultados para {count} municipios con un paralelismo de {degree}...", municipiosASondear.Count, GRADO_DE_PARALELISMO);
|
||||||
|
|
||||||
|
// Creamos una lista de tareas (Tasks), una por cada municipio a consultar.
|
||||||
|
// El método .Select() no ejecuta las tareas todavía, solo las prepara.
|
||||||
|
var tareas = municipiosASondear.Select(async municipio =>
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
// Cada tarea debe "pedir permiso" al semáforo antes de ejecutarse.
|
||||||
|
// Si ya hay 10 tareas en ejecución, esta línea esperará hasta que una termine.
|
||||||
var resultados = await _apiService.GetResultadosAsync(
|
await semaforo.WaitAsync(stoppingToken);
|
||||||
authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id
|
try
|
||||||
);
|
|
||||||
|
|
||||||
if (resultados != null)
|
|
||||||
{
|
{
|
||||||
todosLosResultados[municipio.Id] = resultados;
|
// Una vez que obtiene el permiso, ejecuta la petición a la API.
|
||||||
}
|
var resultados = await _apiService.GetResultadosAsync(
|
||||||
}
|
authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id
|
||||||
|
);
|
||||||
|
|
||||||
if (todosLosResultados.Any())
|
// Si la API devuelve datos válidos...
|
||||||
|
if (resultados != null)
|
||||||
|
{
|
||||||
|
// ...los guardamos en el diccionario concurrente.
|
||||||
|
resultadosPorId[municipio.Id] = resultados;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// ¡CRUCIAL! Liberamos el pase del semáforo, permitiendo que la siguiente
|
||||||
|
// tarea en espera pueda comenzar su ejecución.
|
||||||
|
semaforo.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ahora sí, ejecutamos todas las tareas preparadas en paralelo y esperamos a que todas terminen.
|
||||||
|
await Task.WhenAll(tareas);
|
||||||
|
|
||||||
|
// PASO 3: Guardar los resultados en la base de datos.
|
||||||
|
// Solo procedemos si recolectamos al menos un resultado válido.
|
||||||
|
if (resultadosPorId.Any())
|
||||||
{
|
{
|
||||||
// La llamada ahora es correcta porque el método receptor espera 3 argumentos
|
// Llamamos a nuestro método de guardado masivo y optimizado, pasándole todos los resultados
|
||||||
await GuardarResultadosDeMunicipiosAsync(dbContext, todosLosResultados, stoppingToken);
|
// recolectados para que los inserte en una única y eficiente transacción.
|
||||||
|
await GuardarResultadosDeMunicipiosAsync(dbContext, resultadosPorId.ToDictionary(kv => kv.Key, kv => kv.Value), stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
// Capturamos cualquier error inesperado en el proceso para que el worker no se detenga.
|
||||||
_logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales.");
|
_logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,10 +539,10 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Busca en la API si hay nuevos telegramas totalizados que no se encuentren en la base de datos local.
|
/// Busca y descarga nuevos telegramas de forma masiva y concurrente.
|
||||||
/// Este método itera sobre cada Partido/Municipio (que la API identifica como "Sección" con NivelId = 30),
|
/// Este método crea una lista de todas las combinaciones de Partido/Categoría,
|
||||||
/// obtiene la lista de IDs de telegramas para cada uno, los compara con los IDs locales,
|
/// las consulta a la API con un grado de paralelismo controlado, y cada tarea concurrente
|
||||||
/// y finalmente descarga y guarda solo los que son nuevos.
|
/// maneja su propia lógica de descarga y guardado en la base de datos.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authToken">El token de autenticación válido para la sesión.</param>
|
/// <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>
|
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
|
||||||
@@ -509,16 +550,17 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var scope = _serviceProvider.CreateScope();
|
// PASO 1: Obtener los datos base para las consultas.
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
// Usamos un DbContext inicial solo para leer los catálogos.
|
||||||
|
using var initialScope = _serviceProvider.CreateScope();
|
||||||
|
var initialDbContext = initialScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
// Obtenemos todos los partidos (NivelId=30) y todas las categorías para iterar
|
var partidos = await initialDbContext.AmbitosGeograficos
|
||||||
var partidos = await dbContext.AmbitosGeograficos
|
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
|
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
var categorias = await dbContext.CategoriasElectorales
|
var categorias = await initialDbContext.CategoriasElectorales
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(stoppingToken);
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
@@ -528,26 +570,29 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Iniciando sondeo de Telegramas nuevos para {partidosCount} partidos y {categoriasCount} categorías...", partidos.Count, categorias.Count);
|
// Creamos una lista de todas las consultas que necesitamos hacer (135 partidos * 3 categorías = 405 consultas).
|
||||||
|
var combinaciones = partidos.SelectMany(partido => categorias, (partido, categoria) => new { partido, categoria });
|
||||||
|
|
||||||
foreach (var partido in partidos)
|
const int GRADO_DE_PARALELISMO = 10;
|
||||||
|
var semaforo = new SemaphoreSlim(GRADO_DE_PARALELISMO);
|
||||||
|
|
||||||
|
_logger.LogInformation("Iniciando sondeo de Telegramas para {count} combinaciones... con paralelismo de {degree}", combinaciones.Count(), GRADO_DE_PARALELISMO);
|
||||||
|
|
||||||
|
var tareas = combinaciones.Select(async item =>
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
await semaforo.WaitAsync(stoppingToken);
|
||||||
|
try
|
||||||
foreach (var categoria in categorias)
|
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
var idsDeApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, item.partido.DistritoId!, item.partido.SeccionId!, item.categoria.Id);
|
||||||
|
|
||||||
// Llamamos al servicio pasando la categoriaId.
|
if (idsDeApi is { Count: > 0 })
|
||||||
// Si mañana la API ya no lo necesita, simplemente lo ignorará.
|
|
||||||
// Si lo vuelve a necesitar en el futuro, nuestro código ya está preparado.
|
|
||||||
var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id);
|
|
||||||
|
|
||||||
// El resto de la lógica es la misma: si la respuesta es válida y contiene datos...
|
|
||||||
if (listaTelegramasApi is { Count: > 0 })
|
|
||||||
{
|
{
|
||||||
var idsDeApi = listaTelegramasApi.Select(t => t[0]).Distinct().ToList();
|
using var innerScope = _serviceProvider.CreateScope();
|
||||||
var idsYaEnDb = await dbContext.Telegramas
|
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
|
// --- CORRECCIÓN CLAVE ---
|
||||||
|
// 'idsDeApi' ya es una List<string>, no necesitamos hacer .Select(t => t[0])
|
||||||
|
var idsYaEnDb = await innerDbContext.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);
|
||||||
@@ -556,11 +601,12 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT
|
|||||||
|
|
||||||
if (!nuevosTelegramasIds.Any())
|
if (!nuevosTelegramasIds.Any())
|
||||||
{
|
{
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Se encontraron {count} telegramas nuevos en el partido '{nombre}' para la categoría '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre);
|
_logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, item.partido.Nombre, item.categoria.Nombre);
|
||||||
|
|
||||||
|
// Iteramos y descargamos cada nuevo telegrama.
|
||||||
foreach (var mesaId in nuevosTelegramasIds)
|
foreach (var mesaId in nuevosTelegramasIds)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
@@ -570,18 +616,28 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT
|
|||||||
var nuevoTelegrama = new Telegrama
|
var nuevoTelegrama = new Telegrama
|
||||||
{
|
{
|
||||||
Id = telegramaFile.NombreArchivo,
|
Id = telegramaFile.NombreArchivo,
|
||||||
AmbitoGeograficoId = partido.Id,
|
AmbitoGeograficoId = item.partido.Id,
|
||||||
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()
|
||||||
};
|
};
|
||||||
await dbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
|
await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
// Guardamos los cambios de ESTA tarea específica en la BD.
|
||||||
|
await innerDbContext.SaveChangesAsync(stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
finally
|
||||||
|
{
|
||||||
|
// Liberamos el semáforo para que otra tarea pueda comenzar.
|
||||||
|
semaforo.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ejecutamos todas las tareas en paralelo y esperamos a que finalicen.
|
||||||
|
await Task.WhenAll(tareas);
|
||||||
|
|
||||||
_logger.LogInformation("Sondeo de Telegramas completado.");
|
_logger.LogInformation("Sondeo de Telegramas completado.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user