using foodmarket.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace foodmarket.Api.Background; /// Раз в сутки переписывает ReferencePrice = Cost для товаров, /// у которых LastSupplyAt старше 30 дней и Cost > 0. Цель: устаревшая /// «эталонная» цена не остаётся годами — её сравнивают с актуальной /// себестоимостью. Если пользователь редактировал ReferencePrice вручную /// (через PUT /api/catalog/products/...), ReferencePriceUpdatedAt уходит /// в now, и таймер начинает 30 дней заново. public class ReferencePriceRefreshJob : BackgroundService { private readonly IServiceProvider _services; private readonly ILogger _log; // Запускаем каждые 24 часа. Промах между сутками не критичен — цена // не падает, а лишь догоняет текущую Cost. private static readonly TimeSpan Period = TimeSpan.FromHours(24); public ReferencePriceRefreshJob(IServiceProvider services, ILogger log) { _services = services; _log = log; } protected override async Task ExecuteAsync(CancellationToken ct) { // Ждём 5 минут после старта чтобы не упасть на ещё не применённой миграции. try { await Task.Delay(TimeSpan.FromMinutes(5), ct); } catch (TaskCanceledException) { return; } using var timer = new PeriodicTimer(Period); do { try { await RunOnceAsync(ct); } catch (Exception ex) { _log.LogError(ex, "ReferencePriceRefreshJob: iteration failed"); } } while (await timer.WaitForNextTickAsync(ct)); } public async Task RunOnceAsync(CancellationToken ct) { using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var threshold = DateTime.UtcNow.AddDays(-30); // IgnoreQueryFilters — фоновый job работает над всеми organizations. var stale = await db.Products .IgnoreQueryFilters() .Where(p => p.LastSupplyAt != null && p.LastSupplyAt < threshold && p.Cost > 0m) .ToListAsync(ct); if (stale.Count == 0) return; var now = DateTime.UtcNow; foreach (var p in stale) { p.ReferencePrice = p.Cost; p.ReferencePriceUpdatedAt = now; } await db.SaveChangesAsync(ct); _log.LogInformation("ReferencePriceRefreshJob: refreshed {Count} stale products", stale.Count); } }