using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MotoresArgentinosV2.Core.Entities; using MotoresArgentinosV2.Infrastructure.Data; namespace MotoresArgentinosV2.Infrastructure.Services; /// /// Servicio de fondo que genera sitemap.xml dinámicamente. /// Ejecuta al iniciar la app y luego cada 6 horas. /// Incluye rutas estáticas del frontend + vehículos activos. /// public class SitemapGeneratorService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private static readonly TimeSpan Interval = TimeSpan.FromHours(6); private const string BaseUrl = "https://motoresargentinos.com"; // Rutas estáticas públicas del frontend con sus prioridades private static readonly (string Path, string Priority, string ChangeFreq)[] StaticRoutes = [ ("/", "1.0", "daily"), ("/explorar", "0.8", "daily"), ("/publicar", "0.6", "monthly"), ("/vender", "0.6", "monthly"), ("/condiciones","0.3", "yearly"), ]; public SitemapGeneratorService(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("SitemapGeneratorService iniciado."); while (!stoppingToken.IsCancellationRequested) { try { await GenerateSitemapAsync(); } catch (OperationCanceledException) { break; } catch (Exception ex) { _logger.LogError(ex, "Error generando sitemap."); } await Task.Delay(Interval, stoppingToken); } } private async Task GenerateSitemapAsync() { using var scope = _serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var config = scope.ServiceProvider.GetRequiredService(); // Ruta de salida: configurable para Docker (volumen compartido) o dev local var outputPath = config["AppSettings:SitemapOutputPath"] ?? Path.Combine("wwwroot", "sitemap.xml"); // Obtener todos los avisos activos var activeAds = await context.Ads .AsNoTracking() .Where(a => a.StatusID == (int)AdStatusEnum.Active) .Select(a => new { a.AdID, a.PublishedAt }) .ToListAsync(); var now = DateTime.UtcNow.ToString("yyyy-MM-dd"); var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); // 1. Rutas estáticas foreach (var (path, priority, changeFreq) in StaticRoutes) { sb.AppendLine(" "); sb.AppendLine($" {BaseUrl}{path}"); sb.AppendLine($" {now}"); sb.AppendLine($" {changeFreq}"); sb.AppendLine($" {priority}"); sb.AppendLine(" "); } // 2. Rutas dinámicas (vehículos activos) foreach (var ad in activeAds) { var lastmod = ad.PublishedAt?.ToString("yyyy-MM-dd") ?? now; sb.AppendLine(" "); sb.AppendLine($" {BaseUrl}/vehiculo/{ad.AdID}"); sb.AppendLine($" {lastmod}"); sb.AppendLine(" weekly"); sb.AppendLine(" 0.7"); sb.AppendLine(" "); } sb.AppendLine(""); // Escritura atómica: escribir en .tmp, luego mover var dir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } var tempPath = outputPath + ".tmp"; await File.WriteAllTextAsync(tempPath, sb.ToString(), Encoding.UTF8); File.Move(tempPath, outputPath, overwrite: true); var totalUrls = StaticRoutes.Length + activeAds.Count; _logger.LogInformation("Sitemap generado con {TotalUrls} URLs ({StaticCount} estáticas + {DynamicCount} vehículos). Archivo: {Path}", totalUrls, StaticRoutes.Length, activeAds.Count, outputPath); } }