using Dapper;
|
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.Extensions.DependencyInjection;
|
using Newtonsoft.Json;
|
using Quartz;
|
using Quartz.Impl;
|
using Quartz.Impl.Matchers;
|
using Quartz.Impl.Triggers;
|
using Quartz.Spi;
|
using System;
|
using System.Collections.Generic;
|
using System.Linq;
|
using System.Threading.Tasks;
|
using VueWebCoreApi.Quartz;
|
using VueWebCoreApi.Tools;
|
|
namespace VueWebCoreApi.Extensions
|
{
|
/// <summary>
|
/// Quartz.NET 扩展方法类
|
/// 封装Quartz任务的初始化、增删改查、启停、立即执行等核心操作
|
/// 解决了Scheduler启动、内存与数据库状态同步、Cron表达式兼容、Key匹配等核心问题
|
/// </summary>
|
public static class QuartzNETExtension
|
{
|
/// <summary>
|
/// 全局返回信息对象(统一接口返回格式)
|
/// 包含code(状态码)、count(数量)、message(提示信息)、data(数据)
|
/// </summary>
|
public static ToMessage mes = new ToMessage();
|
/// <summary>
|
/// 内存级任务列表缓存
|
/// 用于减少数据库查询,同时保证内存与数据库状态一致
|
/// </summary>
|
private static List<TaskOptions> _taskList = new List<TaskOptions>();
|
|
/// <summary>
|
/// 扩展IApplicationBuilder,初始化Quartz任务调度器并加载所有任务
|
/// 【核心修复】解决Quartz默认不启动Scheduler的问题
|
/// </summary>
|
/// <param name="app">ASP.NET Core应用构建器</param>
|
/// <param name="env">Web主机环境</param>
|
/// <returns>应用构建器(链式调用)</returns>
|
public static IApplicationBuilder UseQuartz(this IApplicationBuilder app, IWebHostEnvironment env)
|
{
|
// 从DI容器获取依赖服务
|
var services = app.ApplicationServices;
|
// Quartz调度器工厂
|
var schedulerFactory = services.GetService<ISchedulerFactory>();
|
// 自定义Quartz仓储(数据库操作)
|
var quartzRepo = services.GetService<QuartzRepository>();
|
|
// 1. 核心修复:Quartz默认不会自动启动Scheduler,必须显式启动
|
var scheduler = schedulerFactory.GetScheduler().GetAwaiter().GetResult();
|
if (!scheduler.IsStarted)
|
{
|
scheduler.Start().GetAwaiter().GetResult();
|
}
|
// 2. 从数据库加载所有配置的任务
|
var taskList = quartzRepo.GetAllTasksAsync().GetAwaiter().GetResult();
|
if (taskList.Count == 0)
|
{
|
//无任务时记录启动日志
|
quartzRepo.WriteStartLogAsync($"{DateTime.Now:yyyy-MM-dd HH:mm:ss},没有默认配置任务").GetAwaiter().GetResult();
|
return app;
|
}
|
// 3. 遍历任务并初始化(记录失败数和异常信息)
|
int errorCount = 0;
|
string errorMsg = string.Empty;
|
// 初始化内存任务列表
|
_taskList = taskList;
|
|
foreach (var task in taskList)
|
{
|
try
|
{
|
//var result = task.AddJob(schedulerFactory, true, services.GetService<IJobFactory>()).GetAwaiter().GetResult();
|
// 调用TaskOptions的AddJob方法添加任务到调度器
|
// 传入仓储用于数据库操作,传入JobFactory用于自定义Job实例化
|
var result = task.AddJob(schedulerFactory, true, services.GetService<IJobFactory>(), quartzRepo).GetAwaiter().GetResult();
|
}
|
catch (Exception ex)
|
{
|
// 记录单个任务初始化失败信息
|
errorCount++;
|
errorMsg += $"作业:{task.TaskName},异常:{ex.Message};";
|
}
|
}
|
// 4. 记录整体初始化结果日志
|
var content = $"成功:{taskList.Count - errorCount}个,失败{errorCount}个,异常:{errorMsg}";
|
quartzRepo.WriteStartLogAsync($"{DateTime.Now:yyyy-MM-dd HH:mm:ss},{content}").GetAwaiter().GetResult();
|
|
return app;
|
}
|
|
/// <summary>
|
/// 获取调度器中所有任务,并同步内存/数据库状态(核心修复:解决内存列表与数据库不一致问题)
|
/// </summary>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>最新的任务列表</returns>
|
public static async Task<List<TaskOptions>> GetJobs(this ISchedulerFactory schedulerFactory, QuartzRepository quartzRepo)
|
{
|
var list = new List<TaskOptions>();
|
try
|
{
|
var scheduler = await schedulerFactory.GetScheduler();
|
// 确保Scheduler处于启动状态(防御性编程)
|
if (!scheduler.IsStarted) await scheduler.Start();
|
// 1. 获取所有任务分组(Quartz任务按分组管理)
|
var groups = await scheduler.GetJobGroupNames();
|
|
foreach (var groupName in groups)
|
{
|
// 2. 获取分组下所有JobKey(任务唯一标识:名称+分组)
|
var jobKeys = await scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(groupName));
|
foreach (var jobKey in jobKeys)
|
{
|
// 3. 核心修复:优先从数据库获取最新任务(避免内存数据过期)
|
var dbTask = await quartzRepo.TaskExists(jobKey.Name, jobKey.Group);
|
// 数据库无数据时,降级从内存列表获取
|
var task = dbTask.FirstOrDefault() ?? _taskList.FirstOrDefault(x => x.GroupName == jobKey.Group && x.TaskName == jobKey.Name);
|
// 过滤无效任务
|
if (task == null) continue;
|
// 4. 获取任务关联的触发器(Quartz任务通过触发器触发执行)
|
var triggers = await scheduler.GetTriggersOfJob(jobKey);
|
foreach (var trigger in triggers)
|
{
|
// 5. 同步触发器实际状态到任务对象,并更新到数据库
|
task.Status = (int)await scheduler.GetTriggerState(trigger.Key);
|
await quartzRepo.UpdateTaskAsync(task); // 状态同步到库
|
// 6. 同步任务最后执行时间
|
if (trigger.GetPreviousFireTimeUtc() != null)
|
{
|
// 优先从触发器获取最后执行时间(UTC转本地时间)
|
task.LastRunTime = trigger.GetPreviousFireTimeUtc()?.LocalDateTime;
|
await quartzRepo.UpdateTaskLastRunTimeAsync(task.TaskName, task.GroupName, task.LastRunTime.Value);
|
}
|
else
|
{
|
// 触发器无记录时,从执行日志中获取最后执行时间
|
var logs = await quartzRepo.GetJobRunLogAsync(task.TaskName, task.GroupName, 1, 1);
|
if (logs.Count > 0 && DateTime.TryParse(logs[0].BeginDate, out var lastRunTime))
|
{
|
task.LastRunTime = lastRunTime;
|
await quartzRepo.UpdateTaskLastRunTimeAsync(task.TaskName, task.GroupName, lastRunTime);
|
}
|
}
|
}
|
list.Add(task);
|
}
|
}
|
// 7. 同步内存列表为最新数据(保证后续操作使用最新状态)
|
_taskList = list;
|
}
|
catch (Exception ex)
|
{
|
mes.code = "300";
|
mes.count = 0;
|
mes.message = ex.Message + ex.StackTrace;
|
mes.data = null;
|
await quartzRepo.WriteStartLogAsync($"获取作业异常:{ex.Message}{ex.StackTrace}");
|
return new List<TaskOptions>();
|
}
|
return list;
|
}
|
|
/// <summary>
|
/// 新增Quartz任务(初始化/手动添加通用)
|
/// </summary>
|
/// <param name="task">任务配置项</param>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="init">是否为初始化阶段(初始化时不重复入库)</param>
|
/// <param name="jobFactory">Job工厂(自定义Job实例化逻辑)</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>统一返回结果(ToMessage)</returns>
|
public static async Task<object> AddJob(this TaskOptions task, ISchedulerFactory schedulerFactory, bool init = false, IJobFactory jobFactory = null, QuartzRepository quartzRepo = null)
|
{
|
try
|
{
|
// 1. 修复前端传入的Cron表达式兼容性问题
|
task.Interval = FixCronExpression(task.Interval);
|
// 2. 验证Cron表达式有效性
|
var (isValid, msg) = task.Interval.IsValidExpression();
|
if (!isValid)
|
return new ToMessage { code = "300", count = 0, message = msg, data = null };
|
// 3. 非初始化阶段:校验任务是否已存在(避免重复添加)
|
if (!init && await quartzRepo.TaskExistsAsync(task.TaskName, task.GroupName))
|
{
|
return new ToMessage { code = "300", count = 0, message = $"作业:{task.TaskName},分组:{task.GroupName}已经存在", data = null };
|
}
|
// 4. 非初始化阶段:新增任务到内存+数据库+记录操作日志
|
if (!init)
|
{
|
_taskList.Add(task);// 内存添加
|
await quartzRepo.CreateTaskAsync(task);// 数据库添加
|
await quartzRepo.AddJobActionLogAsync(JobAction.新增.ToString(), task.TaskName, task.GroupName, "新增任务成功");// 操作日志
|
}
|
// 5. 构建Quartz Job(任务执行体)
|
var job = JobBuilder.Create<HttpResultfulJob>()// HttpResultfulJob:自定义的HTTP请求型Job
|
.WithIdentity(task.TaskName, task.GroupName)// 设置Job唯一标识(名称+分组)
|
.Build();
|
|
// 6. 构建Cron触发器(按Cron表达式触发)
|
var trigger = TriggerBuilder.Create()
|
.WithIdentity(task.TaskName, task.GroupName)// 触发器唯一标识(与Job保持一致)
|
.StartNow()// 立即启动
|
.WithDescription(task.Describe)// 任务描述
|
.WithCronSchedule(task.Interval)// 绑定Cron表达式
|
.Build();
|
|
// 7. 获取调度器并确保启动
|
var scheduler = await schedulerFactory.GetScheduler();
|
if (!scheduler.IsStarted) await scheduler.Start(); // 确保启动
|
// 8. 设置自定义JobFactory(如需控制Job的依赖注入/实例化)
|
if (jobFactory != null)
|
{
|
scheduler.JobFactory = jobFactory;
|
}
|
|
// 9. 将Job和触发器绑定到调度器
|
await scheduler.ScheduleJob(job, trigger);
|
|
// 10. 根据任务状态设置触发器启停
|
if (task.Status == (int)TriggerState.Normal)
|
{
|
await scheduler.ResumeTrigger(trigger.Key);// 显式恢复(确保触发器正常运行)
|
}
|
else
|
{
|
await scheduler.PauseJob(job.Key);// 暂停任务
|
await quartzRepo.WriteStartLogAsync($"作业:{task.TaskName},分组:{task.GroupName},新建时未启动原因,状态为:{task.Status}");
|
}
|
mes.code = "200";
|
mes.count = 0;
|
mes.message = "执行成功!";
|
mes.data = null;
|
}
|
catch (Exception ex)
|
{
|
return new ToMessage { code = "300", count = 0, message = ex.Message, data = null };
|
}
|
return mes;
|
}
|
|
/// <summary>
|
/// 移除(删除)任务
|
/// </summary>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="task">任务配置</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>操作结果</returns>
|
public static Task<object> Remove(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
|
{
|
return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.删除, task, quartzRepo);
|
}
|
|
/// <summary>
|
/// 修改任务配置
|
/// </summary>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="task">新的任务配置</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>操作结果</returns>
|
public static Task<object> Update(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
|
{
|
return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.修改, task, quartzRepo);
|
}
|
|
/// <summary>
|
/// 暂停任务
|
/// </summary>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="task">任务配置</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>操作结果</returns>
|
public static Task<object> Pause(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
|
{
|
return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.暂停, task, quartzRepo);
|
}
|
|
/// <summary>
|
/// 启动(恢复)任务
|
/// </summary>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="task">任务配置</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>操作结果</returns>
|
public static Task<object> Start(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
|
{
|
return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.开启, task, quartzRepo);
|
}
|
|
/// <summary>
|
/// 立即执行任务(无视Cron表达式)
|
/// </summary>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="task">任务配置</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>操作结果</returns>
|
public static Task<object> Run(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
|
{
|
return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.立即执行, task, quartzRepo);
|
}
|
|
/// <summary>
|
/// 任务操作核心逻辑(核心修复:Key匹配+状态同步+Scheduler启动校验)
|
/// 封装删除/修改/暂停/开启/立即执行的通用逻辑,避免代码冗余
|
/// </summary>
|
/// <param name="schedulerFactory">调度器工厂</param>
|
/// <param name="taskName">任务名称</param>
|
/// <param name="groupName">分组名称</param>
|
/// <param name="action">操作类型(删除/修改/暂停/开启/立即执行)</param>
|
/// <param name="task">任务配置</param>
|
/// <param name="quartzRepo">Quartz仓储</param>
|
/// <returns>操作结果</returns>
|
private static async Task<object> TriggerAction(this ISchedulerFactory schedulerFactory, string taskName, string groupName, JobAction action, TaskOptions task, QuartzRepository quartzRepo)
|
{
|
try
|
{
|
// 1. 预处理Cron表达式(修复前端传入的表达式)
|
if (task != null) task.Interval = FixCronExpression(task.Interval);
|
|
// 2. 获取调度器并确保启动(核心修复1:全局Scheduler启动校验)
|
var scheduler = await schedulerFactory.GetScheduler();
|
// 核心修复1:确保Scheduler全局启动
|
if (!scheduler.IsStarted) await scheduler.Start();
|
|
// 3. 核心修复2:正确构建JobKey(任务名+分组),解决Key匹配失败问题
|
var jobKey = new JobKey(taskName, groupName);
|
if (!await scheduler.CheckExists(jobKey))
|
{
|
return new ToMessage { code = "300", count = 0, message = $"未找到任务[{taskName}-{groupName}]", data = null };
|
}
|
|
// 4. 获取任务关联的触发器(无触发器则任务无法执行)
|
var triggers = await scheduler.GetTriggersOfJob(jobKey);
|
if (triggers == null || !triggers.Any())
|
{
|
return new ToMessage { code = "300", count = 0, message = $"任务[{taskName}-{groupName}]无触发器", data = null };
|
}
|
// 取第一个触发器(单任务默认绑定一个触发器)
|
var trigger = triggers.First();
|
|
object result = null;
|
// 5. 根据操作类型执行不同逻辑
|
switch (action)
|
{
|
case JobAction.删除:
|
// 暂停触发器 -> 解绑触发器 -> 删除Job -> 同步内存+数据库 -> 记录日志
|
await scheduler.PauseTrigger(trigger.Key);
|
await scheduler.UnscheduleJob(trigger.Key);
|
await scheduler.DeleteJob(jobKey);
|
_taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
|
await quartzRepo.DeleteTaskAsync(taskName, groupName);
|
await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "删除任务成功");
|
break;
|
|
case JobAction.修改:
|
// 修改逻辑:先删除旧任务 -> 重新添加新配置 -> 记录日志
|
await scheduler.PauseTrigger(trigger.Key);
|
await scheduler.UnscheduleJob(trigger.Key);
|
await scheduler.DeleteJob(jobKey);
|
_taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
|
await quartzRepo.DeleteTaskAsync(taskName, groupName);
|
result = await task.AddJob(schedulerFactory, false, null, quartzRepo);// 重新添加任务
|
await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, JsonConvert.SerializeObject(task));
|
break;
|
|
case JobAction.暂停:
|
// 暂停触发器 -> 更新任务状态 -> 同步内存+数据库 -> 记录日志
|
await scheduler.PauseTrigger(trigger.Key);
|
task.Status = (int)TriggerState.Paused;
|
await quartzRepo.UpdateTaskAsync(task);
|
_taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
|
_taskList.Add(task); // 同步内存状态
|
await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "暂停任务成功");
|
break;
|
|
case JobAction.开启:
|
// 恢复触发器 -> 更新任务状态 -> 同步内存+数据库 -> 记录日志
|
await scheduler.ResumeTrigger(trigger.Key);
|
task.Status = (int)TriggerState.Normal;
|
await quartzRepo.UpdateTaskAsync(task);
|
_taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
|
_taskList.Add(task); // 同步内存状态
|
await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "开启任务成功");
|
break;
|
|
case JobAction.立即执行:
|
// 立即执行前确保任务处于启动状态 -> 触发Job立即执行 -> 记录日志
|
if (task.Status != (int)TriggerState.Normal)
|
{
|
task.Status = (int)TriggerState.Normal;
|
await quartzRepo.UpdateTaskAsync(task);
|
_taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
|
_taskList.Add(task);
|
await scheduler.ResumeTrigger(trigger.Key);
|
}
|
await scheduler.TriggerJob(jobKey); // 立即触发任务执行
|
await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "立即执行任务成功");
|
break;
|
}
|
mes.code = "200";
|
mes.count = 0;
|
mes.message = $"作业{action.ToString()}成功";
|
mes.data = null;
|
return result ?? mes;
|
}
|
catch (Exception ex)
|
{
|
await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, $"操作失败:{ex.Message}");
|
return new ToMessage { code = "300", count = 0, message = ex.Message, data = null };
|
}
|
}
|
|
/// <summary>
|
/// 修复前端传入的Cron表达式缺陷
|
/// 兼容ASP.NET Core 3.1中Cron表达式的Day/Week字段写法问题
|
/// </summary>
|
/// <param name="cron">原始Cron表达式</param>
|
/// <returns>修复后的Cron表达式</returns>
|
private static string FixCronExpression(string cron)
|
{
|
if (string.IsNullOrEmpty(cron))
|
return cron;
|
|
// 拆分Cron表达式(标准Cron:秒 分 时 日 月 周 年(可选))
|
var str = cron.Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
if (str.Length < 6)
|
return cron;
|
|
// 兼容 ASP.NET Core 3.1 的写法(替换 or 语法)
|
// 修复场景:Day字段为1L/2L...7L 且 周字段为? 时,交换Day和周字段
|
// 原因:前端可能混淆了Day/周字段的写法,Quartz对L(最后一天)的解析有特定规则
|
var dayValue = str[3];// 第4个字段:Day
|
if ((dayValue == "1L" || dayValue == "2L" || dayValue == "3L" ||
|
dayValue == "4L" || dayValue == "5L" || dayValue == "6L" || dayValue == "7L") && str[5] == "?")
|
{
|
str[5] = str[3]; // 周字段 = 原Day字段
|
str[3] = "?"; // Day字段 = ?
|
return string.Join(" ", str);
|
}
|
|
return cron;
|
}
|
|
/// <summary>
|
/// 验证Cron表达式是否有效
|
/// </summary>
|
/// <param name="cronExpression">Cron表达式</param>
|
/// <returns>是否有效 + 错误信息</returns>
|
public static (bool, string) IsValidExpression(this string cronExpression)
|
{
|
try
|
{
|
// 通过Quartz内置的CronTriggerImpl验证表达式
|
var trigger = new CronTriggerImpl();
|
trigger.CronExpressionString = cronExpression;
|
// 计算第一个触发时间:null表示表达式无效
|
var date = trigger.ComputeFirstFireTimeUtc(null);
|
return (date != null, date == null ? $"表达式{cronExpression}无效!" : string.Empty);
|
}
|
catch (Exception e)
|
{
|
return (false, $"表达式{cronExpression}无效!{e.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 从Job执行上下文获取任务配置
|
/// 用于Job执行时获取最新的任务参数
|
/// </summary>
|
/// <param name="context">Job执行上下文</param>
|
/// <returns>任务配置项</returns>
|
public static TaskOptions GetTaskOptions(this IJobExecutionContext context)
|
{
|
var jobKey = context.JobDetail.Key;
|
// 从内存列表中获取匹配的任务(Job执行时优先用内存,减少数据库查询)
|
return _taskList.FirstOrDefault(x => x.TaskName == jobKey.Name && x.GroupName == jobKey.Group);
|
}
|
}
|
}
|