Razor Pages中数据库对象后台定时任务的实现方案咨询
问题描述
我当前在Razor Page的OnGet方法中调用一个任务,该任务可实现基本功能,但希望改为自主后台运行。
该任务会拉取所有Job及其关联Invoice的列表并执行多项服务检查:核心功能为检查逾期Invoice,若存在逾期则更新Job状态为逾期、添加滞纳金并向客户发送邮件;同时还会对Job对象进行错误检查与清理,比如修正不符合预期的状态码。
目前通过依赖注入(DI)将数据类通过接口注入页面,用户访问企业账户时便会执行所有操作。此方案虽可行,但存在弊端:无需每次页面加载都执行,否则会浪费系统资源(尤其数据量较大时),还会影响页面响应速度;该任务实际每日执行一次即可。此外,若用户不频繁登录账户,通知无法及时发送(费用日期计算与函数调用时间无关,此点无影响)。
因此咨询两个问题:
- 如何将该任务改为每日执行一次的定时任务?
- 这是否为当前场景的最优解决方案?
以下为当前使用的方法代码:
public async Task CheckIfPastDueAsync(Master master) { List<Job> jobs = new List<Job>(); //需要处理企业没有客户的情况,若无客户则直接返回,无需执行后续操作。 if (master.Customers == null) { return; } foreach (var customer in master.Customers) { if (customer.Jobs == null) { continue; } jobs.AddRange(customer.Jobs); } //若无待检查的Job,同样无需执行后续操作。 if (jobs.Count == 0) { return; } foreach (var job in jobs) { //跟踪Job对象是否需要更新,若无变更则无需调用UpdateAsync方法。 int changes = 0; //忽略尚未关联Invoice的Job if (job.Invoices == null) { continue; } //跳过已全额付款的Job if (job.Status == 6) { continue; } if (job.Status is 1 or 2 or 3) { //至此说明Job已关联Invoice,但状态未正确反映,需修正。 job.Status = 4; changes++; } foreach (var invoice in job.Invoices) { //检查是否存在应标记为已付款但未正确标记的Job if (invoice.Paid == invoice.Total) { job.Status = 6; changes++; //无需再处理该Invoice,直接进入下一个(按设计每个Job仅关联一个Invoice,因此也会进入下一个Job) continue; } //检查是否存在错误标记为逾期的Job(例如数据库操作失误导致),若存在则修正。 if (invoice.DueDate > DateTime.Now && invoice.Job.Status == 5) { invoice.Job.Status = 4; } //针对未标记为逾期的Job执行检查,若长时间未检查可批量添加对应日期的滞纳金。 if (DateTime.Now >= invoice.DueDate && job.Status != 6 && job.Status != 5) { job.Status = 5; changes++; var late = DateTime.Now - invoice.DueDate; //计算Invoice逾期时长,并根据企业政策规定的周期计算需添加的滞纳金数量。 var penalties = Convert.ToInt32(late.TotalDays / master.PayPeriod); int newpenalties = penalties; int existing = 0; //此场景下该条件理论上不会触发,仅作为异常处理保留。 if (invoice.Fees != null) { existing = invoice.Fees.Count; newpenalties -= existing; } //此场景下该条件理论上必然成立,仅作为异常处理保留。 //理论上,进入此分支时至少存在一笔待添加的滞纳金。 if (newpenalties > 0) { for (int i = 0; i < newpenalties; i++) { var fee = new Fee() { Description = "Late Fee", Date = invoice.DueDate.AddDays((i+1) * master.PayPeriod + existing * master.PayPeriod), //当前为滞纳金比例占位符,后续将改为关联Master对象的企业政策配置变量。 Amount = Math.Round(invoice.Total * 0.025m, 2), InvoiceId = invoice.Id }; await _feeService.CreateAsync(fee); invoice.Total += fee.Amount; await _invoiceService.UpdateAsync(invoice); } await EmailAsync(invoice); } //无需执行后续检查,直接进入下一个Invoice。 continue; } //针对已标记为逾期的Job检查是否需添加额外滞纳金,此场景下无需更新Job对象等操作。 if (job.Status == 5) { var late = DateTime.Now - invoice.DueDate; var penalties = Convert.ToInt32(late.TotalDays / master.PayPeriod); int newpenalties = penalties; int existing = 0; //此场景下Invoice应至少已存在一笔滞纳金,该条件理论上必然成立。 if (invoice.Fees != null) { //existing变量用于日期计算,避免invoice.Fees为null时抛出异常。 existing = invoice.Fees.Count; newpenalties -= existing; } //此场景下可能通过初始检查但无新滞纳金需添加。 if (newpenalties > 0) { for (int i = 0; i < newpenalties; i++) { var fee = new Fee() { Description = "Late Fee", Date = invoice.DueDate.AddDays((i+1) * master.PayPeriod + existing * master.PayPeriod), Amount = Math.Round(invoice.Total * 0.025m, 2), InvoiceId = invoice.Id }; await _feeService.CreateAsync(fee); invoice.Total += fee.Amount; await _invoiceService.UpdateAsync(invoice); } await EmailAsync(invoice); } } } if (changes > 0) { await _jobService.UpdateAsync(job); } } }
解决方案
1. 实现每日定时任务的两种常用方式
方式一:使用Hangfire(快速上手,自带管理面板)
- 安装NuGet包:
Hangfire.AspNetCore和对应数据库存储包(比如Hangfire.SqlServer) - 在
Program.cs中配置服务:
builder.Services.AddHangfire(config => config.UseSqlServerStorage(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddHangfireServer();
- 注册定时任务,在
Program.cs中间件配置后添加:
var app = builder.Build(); // 可选:挂载Hangfire后台面板,用于查看任务执行状态 app.UseHangfireDashboard("/hangfire"); // 注册每日凌晨1点执行的任务 RecurringJob.AddOrUpdate<IInvoiceService>( service => service.CheckAllMastersForPastDueAsync(), Cron.Daily(1, 0));
- 重构代码:将
CheckIfPastDueAsync移到独立的服务类(比如InvoiceService)中,并新增遍历所有Master的方法CheckAllMastersForPastDueAsync,方便Hangfire通过DI调用。
方式二:使用内置IHostedService(轻量无依赖)
- 创建后台服务类:
public class InvoiceCheckerHostedService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private Timer _timer; public InvoiceCheckerHostedService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } protected override Task ExecuteAsync(CancellationToken stoppingToken) { // 设置首次执行时间为次日凌晨1点,之后每日执行一次 var now = DateTime.Now; var firstRun = new DateTime(now.Year, now.Month, now.Day, 1, 0, 0).AddDays(1); var delay = firstRun - now; if (delay < TimeSpan.Zero) delay = delay.AddDays(1); _timer = new Timer(RunCheckTask, null, delay, TimeSpan.FromDays(1)); return Task.CompletedTask; } private async void RunCheckTask(object state) { using var scope = _scopeFactory.CreateScope(); var invoiceService = scope.ServiceProvider.GetRequiredService<IInvoiceService>(); var masterService = scope.ServiceProvider.GetRequiredService<IMasterService>(); var masters = await masterService.GetAllAsync(); foreach (var master in masters) { await invoiceService.CheckIfPastDueAsync(master); } } public override void Dispose() { _timer?.Dispose(); base.Dispose(); } }
- 在
Program.cs中注册服务:
builder.Services.AddHostedService<InvoiceCheckerHostedService>();
2. 是否为最优解决方案?
是的,每日定时任务完全匹配你的场景:
- 彻底避免页面加载时执行任务的资源浪费,大幅提升页面响应速度
- 无需依赖用户登录,任务按时自动执行,保证逾期通知和滞纳金计算的及时性
- 任务逻辑与页面解耦,代码结构更清晰,后续维护更方便
额外优化建议
- 批量处理数据库操作:用EF Core的批量更新替代逐个
UpdateAsync,减少数据库交互次数 - 添加日志记录:记录任务执行时间、处理数量、异常信息,方便排查问题
- 分布式锁:多实例部署时,添加数据库锁或分布式锁,避免任务重复执行
内容的提问来源于stack exchange,提问作者David Bahler
相关产品推荐
相关产品推荐

