Versión 1.0: Aplicación funcionalmente completa con todas las características principales implementadas.

This commit is contained in:
2025-10-29 11:36:20 -03:00
parent 5b3dede4d5
commit 3fbb254ac3
19 changed files with 587 additions and 250 deletions

View File

@@ -0,0 +1,40 @@
// backend/src/Titulares.Api/Data/ConfiguracionRepositorio.cs
using Dapper;
using Microsoft.Data.SqlClient;
using Titulares.Api.Models;
namespace Titulares.Api.Data;
public class ConfiguracionRepositorio
{
private readonly string _connectionString;
public ConfiguracionRepositorio(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection")!;
}
public async Task<ConfiguracionApp> ObtenerAsync()
{
using var connection = new SqlConnection(_connectionString);
// Siempre obtenemos la fila con Id = 1
return await connection.QuerySingleAsync<ConfiguracionApp>("SELECT * FROM Configuracion WHERE Id = 1");
}
public async Task<bool> ActualizarAsync(ConfiguracionApp config)
{
using var connection = new SqlConnection(_connectionString);
var sql = @"
UPDATE Configuracion
SET
IntervaloMinutos = @IntervaloMinutos,
CantidadTitularesAScrapear = @CantidadTitularesAScrapear,
RutaCsv = @RutaCsv,
ViñetaPorDefecto = @ViñetaPorDefecto
WHERE Id = 1;
";
var affectedRows = await connection.ExecuteAsync(sql, config);
return affectedRows > 0;
}
}

View File

@@ -101,50 +101,90 @@ public class TitularRepositorio
try
{
// --- PARTE 1: AÑADIR NUEVOS TITULARES ---
var urlsEnDb = (await connection.QueryAsync<string>(
"SELECT UrlFuente FROM Titulares WHERE UrlFuente IS NOT NULL", transaction: transaction))
.ToHashSet();
var articulosNuevos = articulosScrapeados
.Where(a => !urlsEnDb.Contains(a.UrlFuente))
// 1. Obtenemos TODOS los titulares actuales en memoria, ordenados correctamente.
var titularesActuales = (await connection.QueryAsync<Titular>(
"SELECT * FROM Titulares ORDER BY OrdenVisual ASC", transaction: transaction))
.ToList();
if (articulosNuevos.Any())
{
var cantidadNuevos = articulosNuevos.Count;
await connection.ExecuteAsync(
"UPDATE Titulares SET OrdenVisual = OrdenVisual + @cantidadNuevos",
new { cantidadNuevos },
transaction: transaction);
var urlsEnDb = titularesActuales
.Where(t => t.UrlFuente != null)
.Select(t => t.UrlFuente!)
.ToHashSet();
for (int i = 0; i < cantidadNuevos; i++)
// 2. Identificamos los nuevos artículos y los titulares de scraping existentes.
var articulosNuevos = articulosScrapeados.Where(a => !urlsEnDb.Contains(a.UrlFuente)).ToList();
var titularesScrapingExistentes = titularesActuales.Where(t => !t.EsEntradaManual).ToList();
// 3. Creamos una nueva lista combinada en C#
var listaCombinada = new List<Titular>();
// Añadimos los nuevos artículos scrapeados al principio de la lista de scraping
foreach (var articuloNuevo in articulosNuevos)
{
listaCombinada.Add(new Titular
{
var articulo = articulosNuevos[i];
var sqlInsert = @"
INSERT INTO Titulares (Texto, UrlFuente, OrdenVisual, Tipo, Fuente)
VALUES (@Texto, @UrlFuente, @OrdenVisual, 'Scraped', 'eldia.com');
";
await connection.ExecuteAsync(sqlInsert, new { articulo.Texto, articulo.UrlFuente, OrdenVisual = i }, transaction: transaction);
Texto = articuloNuevo.Texto,
UrlFuente = articuloNuevo.UrlFuente,
EsEntradaManual = false,
Tipo = "Scraped",
Fuente = "eldia.com"
});
}
// Añadimos los viejos artículos de scraping después de los nuevos
listaCombinada.AddRange(titularesScrapingExistentes);
// 4. Tomamos solo la cantidad de titulares de scraping que necesitamos.
var scrapingFinal = listaCombinada.Take(cantidadALimitar).ToList();
// 5. Re-insertamos los titulares manuales en la lista final, preservando su orden relativo.
var listaFinal = new List<Titular>();
var manuales = titularesActuales.Where(t => t.EsEntradaManual).OrderBy(t => t.OrdenVisual).ToList();
var scraping = scrapingFinal;
// Reconstruimos la lista completa, titular por titular, para preservar el orden
int punteroManual = 0;
int punteroScraping = 0;
for (int i = 0; i < titularesActuales.Count; i++)
{
var titularOriginal = titularesActuales[i];
if (titularOriginal.EsEntradaManual)
{
if (punteroManual < manuales.Count)
{
listaFinal.Add(manuales[punteroManual]);
punteroManual++;
}
}
else
{
if (punteroScraping < scraping.Count)
{
listaFinal.Add(scraping[punteroScraping]);
punteroScraping++;
}
}
}
// --- PARTE 2: PURGAR LOS SOBRANTES ---
var sqlDelete = @"
DELETE FROM Titulares
WHERE Id IN (
-- Seleccionamos los IDs de los titulares que queremos borrar.
-- Son aquellos que no son manuales, ordenados por antigüedad (los más nuevos primero),
-- y nos saltamos los primeros 'N' que queremos conservar.
SELECT Id
FROM Titulares
WHERE EsEntradaManual = 0
ORDER BY OrdenVisual ASC
OFFSET @Limite ROWS
);
";
// Usamos cantidadALimitar como el límite de cuántos conservar.
await connection.ExecuteAsync(sqlDelete, new { Limite = cantidadALimitar }, transaction: transaction);
// Si sobraron manuales o de scraping (por cambio de cantidad), los añadimos al final.
while (punteroScraping < scraping.Count) { listaFinal.Add(scraping[punteroScraping++]); }
while (punteroManual < manuales.Count) { listaFinal.Add(manuales[punteroManual++]); }
// 6. Borramos TODOS los titulares actuales de la DB.
await connection.ExecuteAsync("DELETE FROM Titulares", transaction: transaction);
// 7. Re-insertamos la lista final y perfectamente ordenada, asignando el nuevo OrdenVisual.
for (int i = 0; i < listaFinal.Count; i++)
{
var titular = listaFinal[i];
titular.OrdenVisual = i; // Asignamos el orden correcto y contiguo.
var sqlInsert = @"
INSERT INTO Titulares (Texto, UrlFuente, ModificadoPorUsuario, EsEntradaManual, OrdenVisual, Tipo, Fuente, Viñeta)
VALUES (@Texto, @UrlFuente, @ModificadoPorUsuario, @EsEntradaManual, @OrdenVisual, @Tipo, @Fuente, @Viñeta);
";
await connection.ExecuteAsync(sqlInsert, titular, transaction: transaction);
}
transaction.Commit();
}