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 { /// /// Quartz.NET 扩展方法类 /// 封装Quartz任务的初始化、增删改查、启停、立即执行等核心操作 /// 解决了Scheduler启动、内存与数据库状态同步、Cron表达式兼容、Key匹配等核心问题 /// public static class QuartzNETExtension { /// /// 全局返回信息对象(统一接口返回格式) /// 包含code(状态码)、count(数量)、message(提示信息)、data(数据) /// public static ToMessage mes = new ToMessage(); /// /// 内存级任务列表缓存 /// 用于减少数据库查询,同时保证内存与数据库状态一致 /// private static List _taskList = new List(); /// /// 扩展IApplicationBuilder,初始化Quartz任务调度器并加载所有任务 /// 【核心修复】解决Quartz默认不启动Scheduler的问题 /// /// ASP.NET Core应用构建器 /// Web主机环境 /// 应用构建器(链式调用) public static IApplicationBuilder UseQuartz(this IApplicationBuilder app, IWebHostEnvironment env) { // 从DI容器获取依赖服务 var services = app.ApplicationServices; // Quartz调度器工厂 var schedulerFactory = services.GetService(); // 自定义Quartz仓储(数据库操作) var quartzRepo = services.GetService(); // 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()).GetAwaiter().GetResult(); // 调用TaskOptions的AddJob方法添加任务到调度器 // 传入仓储用于数据库操作,传入JobFactory用于自定义Job实例化 var result = task.AddJob(schedulerFactory, true, services.GetService(), 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; } /// /// 获取调度器中所有任务,并同步内存/数据库状态(核心修复:解决内存列表与数据库不一致问题) /// /// 调度器工厂 /// Quartz仓储 /// 最新的任务列表 public static async Task> GetJobs(this ISchedulerFactory schedulerFactory, QuartzRepository quartzRepo) { var list = new List(); 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.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(); } return list; } /// /// 新增Quartz任务(初始化/手动添加通用) /// /// 任务配置项 /// 调度器工厂 /// 是否为初始化阶段(初始化时不重复入库) /// Job工厂(自定义Job实例化逻辑) /// Quartz仓储 /// 统一返回结果(ToMessage) public static async Task 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:自定义的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; } /// /// 移除(删除)任务 /// /// 调度器工厂 /// 任务配置 /// Quartz仓储 /// 操作结果 public static Task Remove(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo) { return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.删除, task, quartzRepo); } /// /// 修改任务配置 /// /// 调度器工厂 /// 新的任务配置 /// Quartz仓储 /// 操作结果 public static Task Update(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo) { return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.修改, task, quartzRepo); } /// /// 暂停任务 /// /// 调度器工厂 /// 任务配置 /// Quartz仓储 /// 操作结果 public static Task Pause(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo) { return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.暂停, task, quartzRepo); } /// /// 启动(恢复)任务 /// /// 调度器工厂 /// 任务配置 /// Quartz仓储 /// 操作结果 public static Task Start(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo) { return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.开启, task, quartzRepo); } /// /// 立即执行任务(无视Cron表达式) /// /// 调度器工厂 /// 任务配置 /// Quartz仓储 /// 操作结果 public static Task Run(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo) { return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.立即执行, task, quartzRepo); } /// /// 任务操作核心逻辑(核心修复:Key匹配+状态同步+Scheduler启动校验) /// 封装删除/修改/暂停/开启/立即执行的通用逻辑,避免代码冗余 /// /// 调度器工厂 /// 任务名称 /// 分组名称 /// 操作类型(删除/修改/暂停/开启/立即执行) /// 任务配置 /// Quartz仓储 /// 操作结果 private static async Task 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 }; } } /// /// 修复前端传入的Cron表达式缺陷 /// 兼容ASP.NET Core 3.1中Cron表达式的Day/Week字段写法问题 /// /// 原始Cron表达式 /// 修复后的Cron表达式 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; } /// /// 验证Cron表达式是否有效 /// /// Cron表达式 /// 是否有效 + 错误信息 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}"); } } /// /// 从Job执行上下文获取任务配置 /// 用于Job执行时获取最新的任务参数 /// /// Job执行上下文 /// 任务配置项 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); } } }