food-market/src/food-market.api/Background/HangfireJobsConfigurator.cs
nns 848e747379 fix: внутренний ШК 2x (не 02/ведущий-0) + разовый прогон «Проверки данных» при старте
Найдено Mac-Claude на стенде:
1. Генератор внутреннего EAN-13 давал «02…» (ведущий 0 → зона UPC-A, может совпасть
   с реальным товаром). InternalBarcode.PickPrefix теперь берёт 2x из restricted-
   circulation 20–29 (без ведущего нуля), не пересекаясь с весовыми префиксами орг;
   «02» — только крайний резерв. Юнит-тест обновлён.
2. «Проверка данных» на Демо показывала все 0 и lastCheckedAt=null — ночной cron ещё
   не отрабатывал после пересева. Добавлен разовый BackgroundJob.Enqueue(DataQualityJob)
   при старте: после деплоя/пересева счётчики считаются сразу по всем организациям.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:45:17 +05:00

182 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Hangfire;
namespace foodmarket.Api.Background;
/// <summary>Регистрирует recurring jobs Hangfire при старте приложения.
/// Идемпотентно: RecurringJob.AddOrUpdate перетирает существующее
/// определение, так что миграции job'ов проходят без ручной очистки таблицы
/// <c>hangfire.recurring_jobs</c>.
///
/// Cron-выражения берутся из конфига (<c>Hangfire:Cron:*</c>) с дефолтом
/// «каждый день в 03:30 UTC» для cleanup-джобов — за 30 минут до бэкапа,
/// чтобы бэкап не цеплял временные блокировки PruneStockMovements.</summary>
public class HangfireJobsConfigurator : IHostedService
{
private readonly IRecurringJobManager _jobs;
private readonly IConfiguration _cfg;
public HangfireJobsConfigurator(IRecurringJobManager jobs, IConfiguration cfg)
{
_jobs = jobs;
_cfg = cfg;
}
public Task StartAsync(CancellationToken ct)
{
// Cron в Hangfire — стандартный 5-полевой (UTC по умолчанию).
var cronStock = _cfg["Hangfire:Cron:PruneStockMovements"] ?? "30 3 * * *";
var cronAudit = _cfg["Hangfire:Cron:PruneAuditLog"] ?? "45 3 * * *";
_jobs.AddOrUpdate<HousekeepingJobs>(
recurringJobId: "prune-stock-movements",
methodCall: j => j.PruneStockMovementsAsync(CancellationToken.None),
cronExpression: cronStock,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate<HousekeepingJobs>(
recurringJobId: "prune-audit-log",
methodCall: j => j.PruneAuditLogAsync(CancellationToken.None),
cronExpression: cronAudit,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 20: per-tenant audit-log + drafts + revoked refresh-tokens.
// Default 03:00 UTC group (можно переопределить через конфиг).
var cronOrgAudit = _cfg["Hangfire:Cron:PruneOrgAuditLog"] ?? "0 3 * * *";
var cronDrafts = _cfg["Hangfire:Cron:PruneDrafts"] ?? "15 3 * * *";
var cronTokens = _cfg["Hangfire:Cron:PruneRevokedRefreshTokens"] ?? "20 3 * * *";
_jobs.AddOrUpdate<HousekeepingJobs>(
recurringJobId: "prune-org-audit-log",
methodCall: j => j.PruneOrgAuditLogAsync(CancellationToken.None),
cronExpression: cronOrgAudit,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate<HousekeepingJobs>(
recurringJobId: "prune-drafts",
methodCall: j => j.PruneDraftsAsync(CancellationToken.None),
cronExpression: cronDrafts,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate<HousekeepingJobs>(
recurringJobId: "prune-revoked-refresh-tokens",
methodCall: j => j.PruneRevokedRefreshTokensAsync(CancellationToken.None),
cronExpression: cronTokens,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 25: чистка quality-{epoch}-* test-org, созданных
// ~/quality-watchdog.sh hourly. Default 02:30 UTC, до утреннего prune-блока.
var cronQualityOrgs = _cfg["Hangfire:Cron:PruneQualityTestOrgs"] ?? "30 2 * * *";
_jobs.AddOrUpdate<HousekeepingJobs>(
recurringJobId: "prune-quality-test-orgs",
methodCall: j => j.PruneQualityTestOrgsAsync(CancellationToken.None),
cronExpression: cronQualityOrgs,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 30: широкий cleanup всех autotest-органа/юзеров (e2e, perf,
// smoke, a11y, ratetest, mt6*, cat3*, ...). Default 02:45 UTC
// (между prune-quality 02:30 и prune-stock 03:30).
var cronAutoTest = _cfg["Hangfire:Cron:PruneAutoTestEntities"] ?? "45 2 * * *";
_jobs.AddOrUpdate<HousekeepingJobs>(
recurringJobId: "prune-autotest-entities",
methodCall: j => j.PruneAutoTestEntitiesAsync(CancellationToken.None),
cronExpression: cronAutoTest,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 20: weekly VACUUM ANALYZE топ-5 таблиц + ежечасный disk-monitor.
var cronVacuum = _cfg["Hangfire:Cron:VacuumTopTables"] ?? "0 4 * * 0"; // Воскресенье 04:00 UTC
var cronDisk = _cfg["Hangfire:Cron:DiskMonitor"] ?? "0 * * * *"; // каждый час
_jobs.AddOrUpdate<DatabaseMaintenanceJobs>(
recurringJobId: "vacuum-top-tables",
methodCall: j => j.VacuumTopTablesAsync(CancellationToken.None),
cronExpression: cronVacuum,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate<DiskMonitoringJob>(
recurringJobId: "disk-monitor",
methodCall: j => j.CheckAsync(CancellationToken.None),
cronExpression: cronDisk,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 22: weekly DB schema documentation generation.
var cronSchema = _cfg["Hangfire:Cron:DbSchemaDocs"] ?? "0 5 * * 0"; // Воскресенье 05:00 UTC
_jobs.AddOrUpdate<DbSchemaDocsJob>(
recurringJobId: "db-schema-docs",
methodCall: j => j.GenerateAsync(CancellationToken.None),
cronExpression: cronSchema,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 24: weekly API endpoint reference generation.
var cronApiRef = _cfg["Hangfire:Cron:ApiReferenceDocs"] ?? "30 5 * * 0"; // Воскресенье 05:30 UTC
_jobs.AddOrUpdate<ApiReferenceDocsJob>(
recurringJobId: "api-reference-docs",
methodCall: j => j.GenerateAsync(CancellationToken.None),
cronExpression: cronApiRef,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Email-уведомления: weekly-summary в понедельник 07:00 UTC,
// low-stock каждый день в 08:00 UTC (после weekly чтобы не дублить
// если оба совпадают). Cron-ы конфигурируются — на тестовом стенде
// имеет смысл сдвинуть/отключить через "off" специальное значение
// (не реализовано, пока просто кастомные cron).
var cronWeekly = _cfg["Hangfire:Cron:WeeklySummary"] ?? "0 7 * * 1";
var cronLowStock = _cfg["Hangfire:Cron:LowStockAlert"] ?? "0 8 * * *";
_jobs.AddOrUpdate<EmailNotificationJobs>(
recurringJobId: "weekly-summary",
methodCall: j => j.SendWeeklySummariesAsync(CancellationToken.None),
cronExpression: cronWeekly,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate<EmailNotificationJobs>(
recurringJobId: "low-stock-alert",
methodCall: j => j.SendLowStockAlertsAsync(CancellationToken.None),
cronExpression: cronLowStock,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Telegram-сводка владельцу — 09:00 МСК = 06:00 UTC. По вкусу
// переопределяется через Hangfire:Cron:TelegramOwnerDailySummary.
var cronTgDaily = _cfg["Hangfire:Cron:TelegramOwnerDailySummary"] ?? "0 6 * * *";
_jobs.AddOrUpdate<OwnerDailySummaryJob>(
recurringJobId: "telegram-owner-daily-summary",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronTgDaily,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Уведомления об отрицательных остатках: агрегированная рассылка + эскалация.
// Бежит часто (каждые 5 минут), сам дебаунсит через NotifiedAt и настройку
// интервала/тихих часов организации.
var cronNegStock = _cfg["Hangfire:Cron:NegativeStockNotify"] ?? "*/5 * * * *";
_jobs.AddOrUpdate<NegativeStockNotificationJob>(
recurringJobId: "negative-stock-notify",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronNegStock,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Смены открытые >24ч — предупреждение кассиру/админам (мягкое правило).
var cronShiftWatch = _cfg["Hangfire:Cron:ShiftWatch"] ?? "7 * * * *"; // раз в час
_jobs.AddOrUpdate<ShiftWatchJob>(
recurringJobId: "shift-watch-24h",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronShiftWatch,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// «Проверка данных» — ночной полный проход (низкий приоритет, не в часы продаж).
var cronDataQuality = _cfg["Hangfire:Cron:DataQuality"] ?? "20 2 * * *"; // 02:20 UTC = 07:20 МСК/Алматы
_jobs.AddOrUpdate<DataQualityJob>(
recurringJobId: "data-quality-scan",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronDataQuality,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Разовый прогон при старте: чтобы после деплоя/пересева Демо (и любой новой
// организации) счётчики «Проверки данных» были посчитаны сразу, не дожидаясь
// ночного cron (иначе панель/бейдж показывают 0 и lastCheckedAt=null).
BackgroundJob.Enqueue<DataQualityJob>(j => j.RunAsync(CancellationToken.None));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}