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,14 +84,14 @@ 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 |         try | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Iniciando sincronización de catálogos maestros..."); |             _logger.LogInformation("Iniciando sincronización de catálogos maestros..."); | ||||||
| @@ -235,22 +236,27 @@ private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingT | |||||||
|         { |         { | ||||||
|             _logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos."); |             _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) |  | ||||||
|             { |  | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; |  | ||||||
|  |  | ||||||
|  |             // 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 => | ||||||
|  |             { | ||||||
|  |                 // 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. | ||||||
|  |                 await semaforo.WaitAsync(stoppingToken); | ||||||
|  |                 try | ||||||
|  |                 { | ||||||
|  |                     // Una vez que obtiene el permiso, ejecuta la petición a la API. | ||||||
|                     var resultados = await _apiService.GetResultadosAsync( |                     var resultados = await _apiService.GetResultadosAsync( | ||||||
|                         authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id |                         authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id | ||||||
|                     ); |                     ); | ||||||
|  |  | ||||||
|  |                     // Si la API devuelve datos válidos... | ||||||
|                     if (resultados != null) |                     if (resultados != null) | ||||||
|                     { |                     { | ||||||
|                     todosLosResultados[municipio.Id] = resultados; |                         // ...los guardamos en el diccionario concurrente. | ||||||
|  |                         resultadosPorId[municipio.Id] = resultados; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |                 finally | ||||||
|             if (todosLosResultados.Any()) |  | ||||||
|                 { |                 { | ||||||
|                 // La llamada ahora es correcta porque el método receptor espera 3 argumentos |                     // ¡CRUCIAL! Liberamos el pase del semáforo, permitiendo que la siguiente | ||||||
|                 await GuardarResultadosDeMunicipiosAsync(dbContext, todosLosResultados, stoppingToken); |                     // 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()) | ||||||
|  |             { | ||||||
|  |                 // Llamamos a nuestro método de guardado masivo y optimizado, pasándole todos los resultados | ||||||
|  |                 // 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