Fix Catálogo Maestro de Agrupaciones Políticas
- Se remueve la iteración sobre distritos. Se consulta solo por categorías electorales.
This commit is contained in:
@@ -71,25 +71,21 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
return null;*/
|
return null;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId)
|
public async Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string? distritoId, int categoriaId)
|
||||||
{
|
{
|
||||||
// "Pedir una ficha". Este método ahora devuelve un "lease" (permiso).
|
|
||||||
// Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo.
|
|
||||||
/*
|
|
||||||
using RateLimitLease lease = await _rateLimiter.AcquireAsync(1);
|
|
||||||
|
|
||||||
// Si se nos concede el permiso para proceder...
|
|
||||||
if (lease.IsAcquired)
|
|
||||||
{*/
|
|
||||||
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var requestUri = $"/api/catalogo/getAgrupaciones?distritoId={distritoId}&categoriaId={categoriaId}";
|
|
||||||
|
var requestUri = $"/api/catalogo/getAgrupaciones?categoriaId={categoriaId}";
|
||||||
|
if (!string.IsNullOrEmpty(distritoId))
|
||||||
|
{
|
||||||
|
requestUri += $"&distritoId={distritoId}";
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<AgrupacionDto>>() : null;
|
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<AgrupacionDto>>() : null;
|
||||||
/* }
|
|
||||||
// Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null.
|
|
||||||
return null;*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string? seccionId, string? municipioId, int categoriaId)
|
public async Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string? seccionId, string? municipioId, int categoriaId)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public interface IElectoralApiService
|
|||||||
Task<TokenResponse?> GetAuthTokenAsync();
|
Task<TokenResponse?> GetAuthTokenAsync();
|
||||||
// Métodos para catálogos
|
// Métodos para catálogos
|
||||||
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? 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);
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
using var innerScope = _serviceProvider.CreateScope();
|
using var innerScope = _serviceProvider.CreateScope();
|
||||||
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
// --- LLAMADA CORRECTA ---
|
|
||||||
await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken);
|
await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -440,32 +439,24 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
|
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
|
||||||
|
|
||||||
// --- CORRECCIÓN: Usar el _tokenService inyectado ---
|
|
||||||
var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
|
var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
|
||||||
|
if (string.IsNullOrEmpty(authToken))
|
||||||
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.LogError("No se pudo obtener token para la sincronización de catálogos.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creamos un scope de servicios para obtener una instancia fresca de DbContext.
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
// PASO 2: Sincronizar las categorías electorales.
|
// 1. SINCRONIZAR CATEGORÍAS
|
||||||
// Es un catálogo pequeño y es la base para las siguientes consultas.
|
|
||||||
var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
|
var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
|
||||||
if (categoriasApi is null || !categoriasApi.Any())
|
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.");
|
_logger.LogWarning("La API no devolvió datos para el catálogo de Categorías.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList();
|
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);
|
var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken);
|
||||||
foreach (var categoriaDto in distinctCategorias)
|
foreach (var categoriaDto in distinctCategorias)
|
||||||
{
|
{
|
||||||
@@ -474,62 +465,65 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
|
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Guardamos las categorías primero para asegurar su existencia.
|
|
||||||
await dbContext.SaveChangesAsync(stoppingToken);
|
await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
_logger.LogInformation("Catálogo de Categorías Electorales sincronizado.");
|
||||||
|
|
||||||
// PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente.
|
// 2. SINCRONIZAR AGRUPACIONES POLÍTICAS
|
||||||
// Esto evita hacer miles de consultas a la BD dentro de un bucle.
|
_logger.LogInformation("Iniciando sincronización de Agrupaciones Políticas...");
|
||||||
|
|
||||||
// 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))
|
|
||||||
{
|
|
||||||
ambitosEnDb.Add(clave, ambito);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
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)
|
foreach (var categoria in distinctCategorias)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
_logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId);
|
|
||||||
|
|
||||||
|
// Se pasa 'null' como distritoId para obtener todas las agrupaciones de la categoría.
|
||||||
|
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, null, categoria.CategoriaId);
|
||||||
|
|
||||||
|
if (agrupacionesApi != null)
|
||||||
|
{
|
||||||
|
foreach (var agrupacionDto in agrupacionesApi)
|
||||||
|
{
|
||||||
|
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
||||||
|
{
|
||||||
|
var nuevaAgrupacion = new AgrupacionPolitica
|
||||||
|
{
|
||||||
|
Id = agrupacionDto.IdAgrupacion,
|
||||||
|
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
||||||
|
Nombre = agrupacionDto.NombreAgrupacion
|
||||||
|
};
|
||||||
|
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
|
||||||
|
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para evitar duplicados en el mismo ciclo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int agrupacionesGuardadas = await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
|
_logger.LogInformation("Catálogo de Agrupaciones Políticas sincronizado. Se guardaron {count} nuevos registros.", agrupacionesGuardadas);
|
||||||
|
|
||||||
|
// 3. SINCRONIZAR ÁMBITOS GEOGRÁFICOS
|
||||||
|
_logger.LogInformation("Iniciando sincronización de Ámbitos Geográficos...");
|
||||||
|
var ambitosEnDbKeys = new HashSet<string>(
|
||||||
|
await dbContext.AmbitosGeograficos.Select(a => $"{a.NivelId}|{a.DistritoId}|{a.SeccionProvincialId}|{a.SeccionId}|{a.MunicipioId}|{a.CircuitoId}|{a.EstablecimientoId}|{a.MesaId}").ToListAsync(stoppingToken)
|
||||||
|
);
|
||||||
|
int totalNuevosAmbitos = 0;
|
||||||
|
|
||||||
|
foreach (var categoria in distinctCategorias)
|
||||||
|
{
|
||||||
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
|
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
|
||||||
if (catalogoDto != null)
|
if (catalogoDto != null)
|
||||||
{
|
{
|
||||||
// 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext
|
|
||||||
foreach (var ambitoDto in catalogoDto.Ambitos)
|
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}";
|
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 (ambitosEnDbKeys.Add(claveUnica)) // HashSet.Add devuelve true si el elemento no existía
|
||||||
if (!ambitosEnDb.ContainsKey(claveUnica))
|
|
||||||
{
|
{
|
||||||
string nombreCorregido = ambitoDto.Nombre;
|
string nombreCorregido = ambitoDto.Nombre;
|
||||||
|
if (ambitoDto.CodigoAmbitos.DistritoId == "01" && ambitoDto.NivelId == 30 && int.TryParse(ambitoDto.Nombre, out int numeroComuna))
|
||||||
// VERIFICAMOS SI ES UNA COMUNA DE CABA
|
|
||||||
// Condición: El DistritoId es "01" (CABA) Y el NivelId corresponde a Departamento/Comuna (30)
|
|
||||||
// Y el nombre es simplemente un número.
|
|
||||||
if (ambitoDto.CodigoAmbitos.DistritoId == "01" &&
|
|
||||||
ambitoDto.NivelId == 30 &&
|
|
||||||
int.TryParse(ambitoDto.Nombre, out int numeroComuna))
|
|
||||||
{
|
{
|
||||||
// Si cumple las condiciones, le damos el formato correcto.
|
|
||||||
nombreCorregido = $"COMUNA {numeroComuna}";
|
nombreCorregido = $"COMUNA {numeroComuna}";
|
||||||
_logger.LogInformation("Nombre de comuna de CABA corregido: de '{Original}' a '{Corregido}'", ambitoDto.Nombre, nombreCorregido);
|
|
||||||
}
|
}
|
||||||
|
dbContext.AmbitosGeograficos.Add(new AmbitoGeografico
|
||||||
var nuevoAmbito = new AmbitoGeografico
|
|
||||||
{
|
{
|
||||||
// Usamos el nombre corregido en lugar del original.
|
|
||||||
Nombre = nombreCorregido,
|
Nombre = nombreCorregido,
|
||||||
NivelId = ambitoDto.NivelId,
|
NivelId = ambitoDto.NivelId,
|
||||||
DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
|
DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
|
||||||
@@ -539,58 +533,18 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
|
CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
|
||||||
EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
|
EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
|
||||||
MesaId = ambitoDto.CodigoAmbitos.MesaId,
|
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)
|
|
||||||
{
|
|
||||||
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
|
||||||
{
|
|
||||||
var nuevaAgrupacion = new AgrupacionPolitica
|
|
||||||
{
|
|
||||||
Id = agrupacionDto.IdAgrupacion,
|
|
||||||
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
|
||||||
Nombre = agrupacionDto.NombreAgrupacion
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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())
|
if (dbContext.ChangeTracker.HasChanges())
|
||||||
{
|
{
|
||||||
int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken);
|
int ambitosGuardados = await dbContext.SaveChangesAsync(stoppingToken);
|
||||||
totalCambiosGuardados += cambiosEnLote;
|
totalNuevosAmbitos += ambitosGuardados;
|
||||||
_logger.LogInformation("Guardados {count} registros de catálogo para la categoría '{catNombre}'.", cambiosEnLote, categoria.Nombre);
|
_logger.LogInformation("Guardados {count} nuevos ámbitos para la categoría '{catNombre}'.", ambitosGuardados, categoria.Nombre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_logger.LogInformation("Catálogo de Ámbitos Geográficos sincronizado. Se guardaron {count} nuevos registros en total.", totalNuevosAmbitos);
|
||||||
// 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.");
|
_logger.LogInformation("Sincronización de catálogos maestros finalizada.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user