Найдено 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>
182 lines
10 KiB
C#
182 lines
10 KiB
C#
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;
|
||
}
|