メインコンテンツまでスキップ

公司場景 Pattern

Pattern 1: 定期資料備份

場景:每天凌晨自動備份 SQLite 資料庫和生產記錄,保留最近 30 天的備份。

public class DatabaseBackupJob : IJob
{
private readonly ILogger<DatabaseBackupJob> _logger;
private readonly string _dbPath;
private readonly string _backupDir;

public DatabaseBackupJob(
ILogger<DatabaseBackupJob> logger,
IConfiguration configuration)
{
_logger = logger;
_dbPath = configuration["Database:Path"]!;
_backupDir = configuration["Backup:Directory"]!;
}

public async Task Execute(IJobExecutionContext context)
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var backupPath = Path.Combine(_backupDir, $"equipment_{timestamp}.db");

_logger.LogInformation(
"Starting database backup: {Source} → {Destination}",
_dbPath, backupPath);

// SQLite 的 online backup
using var source = new SqliteConnection($"Data Source={_dbPath}");
using var destination = new SqliteConnection($"Data Source={backupPath}");
await source.OpenAsync();
await destination.OpenAsync();
source.BackupDatabase(destination);

_logger.LogInformation("Database backup completed: {BackupPath}", backupPath);

// 清理過期備份
await CleanupOldBackupsAsync();
}

private Task CleanupOldBackupsAsync()
{
var cutoff = DateTime.Now.AddDays(-30);
var oldFiles = Directory.GetFiles(_backupDir, "equipment_*.db")
.Where(f => File.GetCreationTime(f) < cutoff)
.ToList();

foreach (var file in oldFiles)
{
File.Delete(file);
_logger.LogDebug("Deleted old backup: {File}", file);
}

_logger.LogInformation("Cleanup completed, removed {Count} old backups", oldFiles.Count);
return Task.CompletedTask;
}
}

排程設定

q.AddJob<DatabaseBackupJob>(opts => opts
.WithIdentity("DatabaseBackup", "Maintenance"));

q.AddTrigger(opts => opts
.ForJob("DatabaseBackup", "Maintenance")
.WithCronSchedule("0 0 2 * * ?") // 每天凌晨 2:00
.WithDescription("每日資料庫備份"));

Pattern 2: 定時報表產出

場景:每週一早上 8 點自動產出上週生產報表,搭配 QuestPDF 產出 PDF 或 ClosedXML 產出 Excel。

public class WeeklyReportJob : IJob
{
private readonly ILogger<WeeklyReportJob> _logger;
private readonly IProductionDataService _dataService;
private readonly IReportGenerator _reportGenerator;

public WeeklyReportJob(
ILogger<WeeklyReportJob> logger,
IProductionDataService dataService,
IReportGenerator reportGenerator)
{
_logger = logger;
_dataService = dataService;
_reportGenerator = reportGenerator;
}

public async Task Execute(IJobExecutionContext context)
{
var endDate = DateTime.Today;
var startDate = endDate.AddDays(-7);

_logger.LogInformation(
"Generating weekly report: {StartDate} ~ {EndDate}",
startDate.ToString("yyyy-MM-dd"),
endDate.ToString("yyyy-MM-dd"));

// 查詢上週生產資料
var data = await _dataService.GetProductionSummaryAsync(startDate, endDate);

// 產出 PDF 報表
var pdfPath = Path.Combine("reports",
$"WeeklyReport_{startDate:yyyyMMdd}_{endDate:yyyyMMdd}.pdf");
await _reportGenerator.GeneratePdfAsync(data, pdfPath);

// 產出 Excel 明細
var excelPath = Path.Combine("reports",
$"WeeklyDetail_{startDate:yyyyMMdd}_{endDate:yyyyMMdd}.xlsx");
await _reportGenerator.GenerateExcelAsync(data, excelPath);

_logger.LogInformation(
"Weekly report generated: {PdfPath}, {ExcelPath}",
pdfPath, excelPath);
}
}

排程設定

q.AddJob<WeeklyReportJob>(opts => opts
.WithIdentity("WeeklyReport", "Reports"));

q.AddTrigger(opts => opts
.ForJob("WeeklyReport", "Reports")
.WithCronSchedule("0 0 8 ? * MON") // 每週一早上 8:00
.WithDescription("每週一產出上週生產報表"));

Pattern 3: 設備校驗提醒

場景:設備校驗到期前 7 天自動發出提醒,避免超過校驗期限。

public class CalibrationReminderJob : IJob
{
private readonly ILogger<CalibrationReminderJob> _logger;
private readonly IDeviceRepository _deviceRepository;
private readonly INotificationService _notificationService;

public CalibrationReminderJob(
ILogger<CalibrationReminderJob> logger,
IDeviceRepository deviceRepository,
INotificationService notificationService)
{
_logger = logger;
_deviceRepository = deviceRepository;
_notificationService = notificationService;
}

public async Task Execute(IJobExecutionContext context)
{
var warningThreshold = DateTime.Today.AddDays(7);

var expiringDevices = await _deviceRepository
.GetDevicesExpiringBeforeAsync(warningThreshold);

foreach (var device in expiringDevices)
{
var daysUntilExpiry = (device.CalibrationExpiry - DateTime.Today).Days;

_logger.LogWarning(
"Calibration expiring: {DeviceId} expires in {Days} days ({ExpiryDate})",
device.Id, daysUntilExpiry, device.CalibrationExpiry.ToString("yyyy-MM-dd"));

await _notificationService.SendCalibrationReminderAsync(
device.Id,
device.CalibrationExpiry,
device.ResponsibleEngineer);
}

_logger.LogInformation(
"Calibration check completed: {Count} devices expiring within 7 days",
expiringDevices.Count);
}
}

排程設定

q.AddJob<CalibrationReminderJob>(opts => opts
.WithIdentity("CalibrationReminder", "Maintenance"));

q.AddTrigger(opts => opts
.ForJob("CalibrationReminder", "Maintenance")
.WithCronSchedule("0 0 9 * * MON-FRI") // 每個工作日早上 9:00
.WithDescription("每日校驗到期檢查"));

Pattern 4: 資料清理排程

場景:每月清理 90 天前的歷史資料,避免資料庫無限膨脹。

public class DataCleanupJob : IJob
{
private readonly ILogger<DataCleanupJob> _logger;
private readonly IDbContext _dbContext;

public DataCleanupJob(
ILogger<DataCleanupJob> logger,
IDbContext dbContext)
{
_logger = logger;
_dbContext = dbContext;
}

public async Task Execute(IJobExecutionContext context)
{
var cutoff = DateTime.UtcNow.AddDays(-90);

_logger.LogInformation("Starting data cleanup: removing records before {Cutoff}", cutoff);

// 分批刪除,避免長時間鎖表
var totalDeleted = 0;
const int batchSize = 1000;

while (true)
{
var deleted = await _dbContext.Database.ExecuteSqlRawAsync(
"DELETE FROM SensorReadings WHERE Timestamp < {0} LIMIT {1}",
cutoff, batchSize);

totalDeleted += deleted;

if (deleted < batchSize)
break;

_logger.LogDebug("Deleted {Count} records, continuing...", deleted);
await Task.Delay(100); // 給其他操作喘息空間
}

_logger.LogInformation(
"Data cleanup completed: {TotalDeleted} records removed",
totalDeleted);
}
}

排程設定

q.AddJob<DataCleanupJob>(opts => opts
.WithIdentity("DataCleanup", "Maintenance"));

q.AddTrigger(opts => opts
.ForJob("DataCleanup", "Maintenance")
.WithCronSchedule("0 0 3 1 * ?") // 每月 1 日凌晨 3:00
.WithDescription("每月歷史資料清理"));

Pattern 5: 排程持久化

預設情況下 Quartz.NET 用記憶體儲存排程,應用程式重啟後排程狀態會遺失。透過 AdoJobStore 可以將排程持久化到資料庫。

使用 SQLite 持久化

services.AddQuartz(q =>
{
q.UsePersistentStore(store =>
{
store.UseProperties = true;
store.RetryInterval = TimeSpan.FromSeconds(15);

// SQLite 設定
store.UseMicrosoftSQLite(options =>
{
options.ConnectionString = "Data Source=quartz.db";
options.TablePrefix = "QRTZ_";
});

store.UseJsonSerializer();
});

// 註冊 Job 和 Trigger(同前)
q.AddJob<DatabaseBackupJob>(opts => opts
.WithIdentity("DatabaseBackup", "Maintenance")
.StoreDurably());

q.AddTrigger(opts => opts
.ForJob("DatabaseBackup", "Maintenance")
.WithCronSchedule("0 0 2 * * ?"));
});
何時需要持久化
  • WPF 桌面應用:通常不需要。每次啟動重新註冊排程即可,簡單可靠
  • 伺服器端服務:需要。排程不應因為重啟而遺失
  • 叢集環境:必須。多台機器共用排程資料,避免重複執行

Misfire 處理策略

Misfire 是指排程的觸發時間已過,但 Job 沒有執行(例如應用程式當時沒在運行)。Quartz.NET 提供多種策略:

策略行為適合場景
FireAndProceed立刻補執行一次,然後按照正常排程繼續備份、報表(補一次就好)
DoNothing跳過錯過的,按正常排程繼續監控類(過去的不需要補)
IgnoreMisfirePolicy把所有錯過的都補執行資料匯入(每筆都要處理)
// 備份 Job:錯過就補執行一次
q.AddTrigger(opts => opts
.ForJob("DatabaseBackup", "Maintenance")
.WithCronSchedule("0 0 2 * * ?", x => x
.WithMisfireHandlingInstructionFireAndProceed())
.WithDescription("每日備份(錯過自動補執行)"));

// 校驗提醒:錯過就跳過(今天會再檢查)
q.AddTrigger(opts => opts
.ForJob("CalibrationReminder", "Maintenance")
.WithCronSchedule("0 0 9 * * MON-FRI", x => x
.WithMisfireHandlingInstructionDoNothing())
.WithDescription("校驗提醒(錯過跳過)"));

DI 整合完整範例

// Program.cs
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddQuartz(q =>
{
// 使用記憶體儲存(桌面應用足夠)
q.UseInMemoryStore();

// 排程 1: 每日備份
q.AddJob<DatabaseBackupJob>(opts => opts
.WithIdentity("DatabaseBackup", "Maintenance"));
q.AddTrigger(opts => opts
.ForJob("DatabaseBackup", "Maintenance")
.WithCronSchedule("0 0 2 * * ?"));

// 排程 2: 每週報表
q.AddJob<WeeklyReportJob>(opts => opts
.WithIdentity("WeeklyReport", "Reports"));
q.AddTrigger(opts => opts
.ForJob("WeeklyReport", "Reports")
.WithCronSchedule("0 0 8 ? * MON"));

// 排程 3: 每日校驗檢查
q.AddJob<CalibrationReminderJob>(opts => opts
.WithIdentity("CalibrationReminder", "Maintenance"));
q.AddTrigger(opts => opts
.ForJob("CalibrationReminder", "Maintenance")
.WithCronSchedule("0 0 9 * * MON-FRI"));

// 排程 4: 每月清理
q.AddJob<DataCleanupJob>(opts => opts
.WithIdentity("DataCleanup", "Maintenance"));
q.AddTrigger(opts => opts
.ForJob("DataCleanup", "Maintenance")
.WithCronSchedule("0 0 3 1 * ?"));
});

// 啟動排程器為 HostedService
builder.Services.AddQuartzHostedService(opts =>
{
opts.WaitForJobsToComplete = true;
});

var host = builder.Build();
await host.RunAsync();

延伸閱讀