前言
我司是客户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)