公司場景 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();
延伸閱讀
- 概觀 — 核心概念、Cron 速查表、與其他排程方案比較
- QuestPDF 文件產出 — 搭配定時報表產出
- Serilog 結構化日誌 — Job 執行記錄