万字长文【Hangfire + .net 6 实现AutoTask定时任务调度平台】

前言

一、成品界面展示

二、技术栈

三、数据库设计

四、代码分享与讲解

代码结构

AutoTaskFilter(定时任务过滤器)

添加定时任务

修改定时任务

新增修改触发器

删除任务

停止任务

恢复任务 

立刻触发任务

获取日志

任务实现服务

HttpRequestJobService

AutoReportJobService

邮件服务

ExeJobService

ScriptJobService


前言

        我司是客户MES(生产制造执行系统)供应商。甲方公司有IT和运维,为了减少客制化开发成本,IT会自行开发程序、接口等,希望我司能够接入IT开发的程序,由甲方自行配置定义调度方式 频率等。

        本文分为成品界面展示、技术栈、数据库表设计、代码分享与讲解四个章节,篇幅较长会分多次更新,感兴趣的朋友可以点下收藏订阅持续阅读。

        有任何问题欢迎评论、探讨指正


一、成品界面展示

任务分组:用于收纳任务,便于查看

任务清单:查看任务状态、执行状况

创建任务,目前支持5种类型

        HttpRequest:发送http请求

        AutoReport*:自动化报表,功能非常强大,可以根据SQL循环发送邮件,邮件全字段动态,{ {}} 写SQL,支持附件,根据SQL导出(CSV,EXCEL两种格式)

        可执行程序

        dll类库

        脚本(目前支持cmd,python,js)

        日志

二、技术栈

前端:VUE3 + element ui

后端:.net 6 + furion(底层框架) + sqlsugar(国产orm) + hangfire + MailKit(邮件依赖) + MiniExcel(excel导出依赖)

数据库:Oracle 11g

三、数据库设计

正常情况来说,在注册hangfire服务启动程序后会自动创建相应的表,但特殊情况或使用量比较少的hangfire orm依赖可能在这一步会出问题,这个时候就手动建表,另外,本文的表在hangfire原生表中做了很多扩展。

hf_server:记录hangfire服务器的信息(如使用默认配置,每次启动程序就是一个新的serverid)

hf_set:这张表非常重要!红框内是在原生的基础上新增的字段。此表用于记录创建的每个任务,

如调用RecurringJob.AddOrUpdate后,如果key+value不存在就会在这张表中新增一行。

hf_hash:任务创建后会在这张表中添加参数 下一次触发时间、创建时间、Cron表达式等等

hf_job:作业表,RecurringJob(循环任务)设定后每次执行都会在这张表中增加一条记录

hf_job_parameter:作业参数表

hf_job_state:作业状态表,作业每一次执行的状态(Enqueued 入队列,Processing 执行中,Failed 失败,Successed 成功,Deleted 已删除)都会被记录此表

auto_task_group:任务分组表 本文扩展表,原生表没有。

auto_task_group_rel:任务分组&hf_set关系表,本文扩展表,原生表没有。

sys_mail_sender_cfg:发件邮箱配置表,本文扩展表,原生表没有。当任务类型为autoreport(自动化报表),且勾选邮件时,读取此配置表发送邮件,支持设置多个发件邮箱

四、代码分享与讲解

代码结构

AutoTaskService:自动任务核心服务

Dtos文件夹:存放数据传输对象

Job:作业具体实现代码


AutoTaskFilter(定时任务过滤器)

非常重要,因为UI可以设定任务是否启用,hangfire是不支持设置暂停任务或触发器的。

通过此过滤器在作业执行前后拦截,判断是否可以执行作业,不允许执行的情况下将Canceled属性设置为false

public class AutoTaskFilter : JobFilterAttribute, IServerFilter
{
    public AutoTaskFilter()
    {

    }

    /// <summary>
    /// 执行前
    /// </summary>
    /// <param name="filterContext"></param>
    public void OnPerforming(PerformingContext filterContext)
    {
        var job = filterContext.BackgroundJob.Job;
        var recurringJobId = job.Args.First().ToString();

        var logger = App.GetService<ILogger>();
        var db = App.GetService<ISqlSugarClient>();

        var hfSet = db.Queryable<HfSet>().Single(set => set.Value == recurringJobId);
        var currTime = DateTime.Now;
        if (hfSet?.StartTime > DateTime.Now)
        {
            filterContext.Canceled = true;
            logger.Information($"任务[{recurringJobId}]未达到执行开始时间[{hfSet.StartTime}],取消执行");
            return;
        }
        if (hfSet?.EndTime < DateTime.Now)
        {
            filterContext.Canceled = true;
            logger.Information($"任务[{recurringJobId}]已超过执行结束时间[{hfSet.StartTime}],取消执行");
            return;
        }

        if (hfSet.Status != EHfSetStatus.启用)
        {
            filterContext.Canceled = true;
            logger.Information($"任务[{recurringJobId}]当前状态为[{hfSet.Status}],取消执行");
            return;
        }

        logger.Information($"任务[{recurringJobId}]开始执行,传入参数:" + Environment.NewLine + job.Args[1]);

    }

    /// <summary>
    /// 执行后
    /// </summary>
    /// <param name="filterContext"></param>
    public void OnPerformed(PerformedContext filterContext)
    {
        lock (objLock)
        {
            var job = filterContext.BackgroundJob.Job;
            var recurringJobId = job.Args.First().ToString();

            var logger = App.GetService<ILogger>();

            logger.Information($"任务[{recurringJobId}]执行完成");
        }
    }
}

添加定时任务

需要注意的是,本文对hf_set做了扩展,需要另外写代码更新扩展字段以及添加分组和het_set关系

      /// <summary>
      /// 添加任务 cron不写会生成一个默认每天0点的触发器
      /// </summary>
      /// <param name="request"></param>
      /// <returns></returns>
      public async Task AddJob(AddJobRequest request)
      {
          //检查Job是否存在 名称为key
          var any = await _db.Queryable<HfSet>().AnyAsync(set => set.Value == request.JobName);
          if (any) throw Oops.Oh($"定时任务[{request.JobName}]已存在");

          var cron = string.IsNullOrEmpty(request.Cron) ? _defaultCron : request.Cron;

          IAutoTaskJob job = GetJobService(request.JobType);
          if (job is null) throw Oops.Oh($"任务类型[{request.JobType}]暂未实现");

          RecurringJob.AddOrUpdate(request.JobName, () => job.Execute(request.JobName, request.Param), cron, new RecurringJobOptions()
          {
              TimeZone = _chinaTimeZone,

          });

          var hfSet = await _db.Queryable<HfSet>().SingleAsync(set => set.Value == request.JobName);

          // 更新扩展字段 所有事务都处理完后再更新任务
          try
          {
              _db.Ado.BeginTran();

              hfSet.JobType = request.JobType;
              hfSet.Status = request.JobStatus;
              hfSet.Param = request.Param;
              hfSet.StartTime = request.StartTime;
              hfSet.EndTime = request.EndTime;

              await _db.Updateable(hfSet).ExecuteCommandAsync();

              // 添加分组 任务关系
              var rel = new AutoTaskGroupRel()
              {
                  HfSetId = hfSet.Id,
                  GroupId = request.GroupId
              };
              await _autoTaskGroupRel.AddAsync(rel);

              _db.Ado.CommitTran();
          }
          catch (Exception)
          {
              _db.Ado.RollbackTran();
              throw;
          }
      }
    public class AddJobRequest
    {
        public string JobName { get; set; }
        public EJobType JobType { get; set; }
        public EHfSetStatus JobStatus { get; set; }
        public DateTime? StartTime { get; set; }
        public DateTime? EndTime { get; set; }
        public string Cron { get; set; }
        public string Param { get; set; }
        public long GroupId { get; set; }
    }

    public enum EHfSetStatus
    {
        停止,
        启用
    }

    public enum EJobType
    {
        HttpRequest,
        AutoReport,
        Exe,
        Dll,
        Script
    }
        private IAutoTaskJob GetJobService(EJobType jobType)
        {
            IAutoTaskJob job;

            switch (jobType)
    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值