You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Razor Pages中数据库对象后台定时任务的实现方案咨询

问题描述

我当前在Razor Page的OnGet方法中调用一个任务,该任务可实现基本功能,但希望改为自主后台运行。

该任务会拉取所有Job及其关联Invoice的列表并执行多项服务检查:核心功能为检查逾期Invoice,若存在逾期则更新Job状态为逾期、添加滞纳金并向客户发送邮件;同时还会对Job对象进行错误检查与清理,比如修正不符合预期的状态码。

目前通过依赖注入(DI)将数据类通过接口注入页面,用户访问企业账户时便会执行所有操作。此方案虽可行,但存在弊端:无需每次页面加载都执行,否则会浪费系统资源(尤其数据量较大时),还会影响页面响应速度;该任务实际每日执行一次即可。此外,若用户不频繁登录账户,通知无法及时发送(费用日期计算与函数调用时间无关,此点无影响)。

因此咨询两个问题:

  1. 如何将该任务改为每日执行一次的定时任务?
  2. 这是否为当前场景的最优解决方案?

以下为当前使用的方法代码:

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(快速上手,自带管理面板)

  1. 安装NuGet包:Hangfire.AspNetCore和对应数据库存储包(比如Hangfire.SqlServer)
  2. 在Program.cs中配置服务:
builder.Services.AddHangfire(config => 
    config.UseSqlServerStorage(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddHangfireServer();
  1. 注册定时任务,在Program.cs中间件配置后添加:
var app = builder.Build();
// 可选:挂载Hangfire后台面板,用于查看任务执行状态
app.UseHangfireDashboard("/hangfire");

// 注册每日凌晨1点执行的任务
RecurringJob.AddOrUpdate<IInvoiceService>(
    service => service.CheckAllMastersForPastDueAsync(),
    Cron.Daily(1, 0));
  1. 重构代码:将CheckIfPastDueAsync移到独立的服务类(比如InvoiceService)中,并新增遍历所有Master的方法CheckAllMastersForPastDueAsync,方便Hangfire通过DI调用。

方式二:使用内置IHostedService(轻量无依赖)

  1. 创建后台服务类:
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();
    }
}
  1. 在Program.cs中注册服务:
builder.Services.AddHostedService<InvoiceCheckerHostedService>();

2. 是否为最优解决方案?

是的,每日定时任务完全匹配你的场景:

  • 彻底避免页面加载时执行任务的资源浪费,大幅提升页面响应速度
  • 无需依赖用户登录,任务按时自动执行,保证逾期通知和滞纳金计算的及时性
  • 任务逻辑与页面解耦,代码结构更清晰,后续维护更方便

额外优化建议

  • 批量处理数据库操作:用EF Core的批量更新替代逐个UpdateAsync,减少数据库交互次数
  • 添加日志记录:记录任务执行时间、处理数量、异常信息,方便排查问题
  • 分布式锁:多实例部署时,添加数据库锁或分布式锁,避免任务重复执行

内容的提问来源于stack exchange,提问作者David Bahler

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 14:55:03