From f1cf9f5d133a27a02fca9bab6938367cdf94bc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=89=E9=B8=AD=E8=9B=8B?= Date: Sun, 21 Sep 2025 02:48:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90=20Quartz.NET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BetterGenshinImpact/App.xaml.cs | 18 +++ .../BetterGenshinImpact.csproj | 2 + .../Model/Gear/Triggers/GearBaseTrigger.cs | 2 +- .../Model/Gear/Triggers/HotkeyGearTrigger.cs | 2 +- .../Gear/Triggers/QuartzCronGearTrigger.cs | 27 ++++ .../Triggers/QuartzJob/QuartzGearTaskJob.cs | 125 ++++++++++++++++++ .../Gear/Triggers/SequentialGearTrigger.cs | 2 +- .../Model/Gear/Triggers/TimedGearTrigger.cs | 90 ------------- .../Service/QuartzSchedulerService.cs | 57 ++++++++ .../Pages/Component/GearTriggerViewModel.cs | 10 +- 10 files changed, 237 insertions(+), 98 deletions(-) create mode 100644 BetterGenshinImpact/Model/Gear/Triggers/QuartzCronGearTrigger.cs create mode 100644 BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs delete mode 100644 BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs create mode 100644 BetterGenshinImpact/Service/QuartzSchedulerService.cs diff --git a/BetterGenshinImpact/App.xaml.cs b/BetterGenshinImpact/App.xaml.cs index a266c522..a6beabbb 100644 --- a/BetterGenshinImpact/App.xaml.cs +++ b/BetterGenshinImpact/App.xaml.cs @@ -31,6 +31,8 @@ using Serilog.Sinks.RichTextBox.Abstraction; using Wpf.Ui; using Wpf.Ui.DependencyInjection; using Wpf.Ui.Violeta.Controls; +using Quartz; +using BetterGenshinImpact.Model.Gear.Triggers; namespace BetterGenshinImpact; @@ -96,6 +98,20 @@ public partial class App : Application // Main window with navigation services.AddView(); services.AddSingleton(); + + // Quartz.NET 调度器配置 + services.AddQuartz(q => + { + q.UseSimpleTypeLoader(); + q.UseInMemoryStore(); + q.UseDefaultThreadPool(tp => + { + tp.MaxConcurrency = 1; + }); + }); + services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + services.AddSingleton(); + services.AddHostedService(sp => sp.GetRequiredService()); // Views services.AddView(); @@ -142,6 +158,8 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); + + // Configuration //services.Configure(context.Configuration.GetSection(nameof(AppConfig))); } diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index 785ed73f..d5c1f245 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -69,6 +69,8 @@ + + diff --git a/BetterGenshinImpact/Model/Gear/Triggers/GearBaseTrigger.cs b/BetterGenshinImpact/Model/Gear/Triggers/GearBaseTrigger.cs index 8a2ced64..896fb47a 100644 --- a/BetterGenshinImpact/Model/Gear/Triggers/GearBaseTrigger.cs +++ b/BetterGenshinImpact/Model/Gear/Triggers/GearBaseTrigger.cs @@ -16,5 +16,5 @@ public abstract class GearBaseTrigger /// /// 执行任务 /// - public abstract Task Run(); + public abstract Task Trigger(); } \ No newline at end of file diff --git a/BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs b/BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs index d614baca..473f2b4d 100644 --- a/BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs +++ b/BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs @@ -31,7 +31,7 @@ public class HotkeyGearTrigger : GearBaseTrigger private DateTime _lastExecutionTime = DateTime.MinValue; private bool _isExecuting = false; - public override async Task Run() + public override async Task Trigger() { // 热键触发器通常不直接调用Run方法 // 而是通过热键事件触发ExecuteTasks方法 diff --git a/BetterGenshinImpact/Model/Gear/Triggers/QuartzCronGearTrigger.cs b/BetterGenshinImpact/Model/Gear/Triggers/QuartzCronGearTrigger.cs new file mode 100644 index 00000000..a8dbe7b4 --- /dev/null +++ b/BetterGenshinImpact/Model/Gear/Triggers/QuartzCronGearTrigger.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace BetterGenshinImpact.Model.Gear.Triggers; + +/// +/// 基于 Quartz.NET 的定时触发器 +/// +public class QuartzCronGearTrigger : GearBaseTrigger +{ + private readonly ILogger _logger = App.GetLogger(); + + /// + /// 是否在执行时中断其他同类型定时任务 + /// + public bool ShouldInterruptOthers { get; set; } = true; + + /// + /// 使用 Cron 表达式(如果设置,将覆盖 IntervalMs 设置) + /// + public string? CronExpression { get; set; } + + public override async Task Trigger() + { + + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs b/BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs new file mode 100644 index 00000000..c8898b13 --- /dev/null +++ b/BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BetterGenshinImpact.Model.Gear.Tasks; +using Microsoft.Extensions.Logging; +using Quartz; + +namespace BetterGenshinImpact.Model.Gear.Triggers.QuartzJob; + +/// +/// Quartz.NET 任务定义 +/// +[DisallowConcurrentExecution] +[PersistJobDataAfterExecution] +public class QuartzGearTaskJob : IJob +{ + public static readonly JobKey Key = new("gear-task-job", "default-group"); + + private readonly ILogger _logger = App.GetLogger(); + + public async Task Execute(IJobExecutionContext context) + { + try + { + var jobDataMap = context.JobDetail.JobDataMap; + var taskListJson = jobDataMap.GetString("GearTaskReferenceList"); + var triggerName = jobDataMap.GetString("TriggerName") ?? "Unknown"; + var triggerId = jobDataMap.GetString("TriggerId") ?? Guid.NewGuid().ToString(); + + if (string.IsNullOrEmpty(taskListJson)) + { + _logger.LogWarning("触发器 {TriggerName} 没有配置任何任务", triggerName); + return; + } + + var gearTaskReferenceList = System.Text.Json.JsonSerializer.Deserialize>(taskListJson); + if (gearTaskReferenceList == null || !gearTaskReferenceList.Any()) + { + _logger.LogWarning("触发器 {TriggerName} 任务列表反序列化失败或为空", triggerName); + return; + } + + _logger.LogInformation("开始执行触发器 {TriggerName} 的任务,任务数量: {TaskCount}", triggerName, gearTaskReferenceList.Count); + + // 检查是否需要中断其他同类型任务 + var shouldInterrupt = jobDataMap.GetBooleanValue("ShouldInterruptOthers"); + if (shouldInterrupt) + { + await InterruptOtherJobs(context, triggerId); + } + + // 执行任务列表 + var tasks = gearTaskReferenceList.Where(x => x.Enabled).Select(x => x.ToGearTask()).ToList(); + foreach (var task in tasks) + { + if (context.CancellationToken.IsCancellationRequested) + { + _logger.LogInformation("任务执行被取消"); + break; + } + + try + { + await task.Run(context.CancellationToken); + _logger.LogDebug("任务 {TaskName} 执行完成", task.Name); + } + catch (OperationCanceledException) + { + _logger.LogInformation("任务 {TaskName} 被取消", task.Name); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "任务 {TaskName} 执行失败", task.Name); + // 继续执行下一个任务,不中断整个流程 + } + } + + _logger.LogInformation("触发器 {TriggerName} 的任务执行完成", triggerName); + } + catch (Exception ex) + { + _logger.LogError(ex, "执行定时任务时发生错误"); + throw; + } + } + + /// + /// 中断其他同类型的定时任务 + /// + private async Task InterruptOtherJobs(IJobExecutionContext context, string currentTriggerId) + { + try + { + var scheduler = context.Scheduler; + var currentJobKey = context.JobDetail.Key; + + // 获取所有正在执行的任务 + var executingJobs = await scheduler.GetCurrentlyExecutingJobs(); + + foreach (var executingJob in executingJobs) + { + // 跳过当前任务 + if (executingJob.JobDetail.Key.Equals(currentJobKey)) + continue; + + // 检查是否是同类型的 GearTaskJob + if (executingJob.JobDetail.JobType == typeof(QuartzGearTaskJob)) + { + var executingTriggerId = executingJob.JobDetail.JobDataMap.GetString("TriggerId"); + if (executingTriggerId != currentTriggerId) + { + _logger.LogInformation("中断正在执行的任务: {JobKey}", executingJob.JobDetail.Key); + await scheduler.Interrupt(executingJob.JobDetail.Key); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "中断其他任务时发生错误"); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Model/Gear/Triggers/SequentialGearTrigger.cs b/BetterGenshinImpact/Model/Gear/Triggers/SequentialGearTrigger.cs index f2234e22..82347f24 100644 --- a/BetterGenshinImpact/Model/Gear/Triggers/SequentialGearTrigger.cs +++ b/BetterGenshinImpact/Model/Gear/Triggers/SequentialGearTrigger.cs @@ -11,7 +11,7 @@ namespace BetterGenshinImpact.Model.Gear.Triggers; /// public class SequentialGearTrigger : GearBaseTrigger { - public override async Task Run() + public override async Task Trigger() { List list = GearTaskRefenceList.Select(gearTask => gearTask.ToGearTask()).ToList(); foreach (var gearTask in list) diff --git a/BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs b/BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs deleted file mode 100644 index 4da3776b..00000000 --- a/BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Linq; -using BetterGenshinImpact.Model.Gear.Tasks; - -namespace BetterGenshinImpact.Model.Gear.Triggers; - -/// -/// 定时触发器 -/// -public class TimedGearTrigger : GearBaseTrigger -{ - /// - /// 触发间隔(毫秒) - /// - public int IntervalMs { get; set; } = 5000; - - /// - /// 是否重复执行 - /// - public bool IsRepeating { get; set; } = true; - - /// - /// 延迟启动时间(毫秒) - /// - public int DelayMs { get; set; } = 0; - - /// - /// 最大执行次数(0表示无限制) - /// - public int MaxExecutions { get; set; } = 0; - - private int _executionCount = 0; - private CancellationTokenSource? _cancellationTokenSource; - - public override async Task Run() - { - _cancellationTokenSource = new CancellationTokenSource(); - _executionCount = 0; - - // 延迟启动 - if (DelayMs > 0) - { - await Task.Delay(DelayMs, _cancellationTokenSource.Token); - } - - do - { - if (_cancellationTokenSource.Token.IsCancellationRequested) - break; - - // 执行任务 - await ExecuteTasks(); - _executionCount++; - - // 检查是否达到最大执行次数 - if (MaxExecutions > 0 && _executionCount >= MaxExecutions) - break; - - // 如果不重复执行,则退出 - if (!IsRepeating) - break; - - // 等待下次执行 - await Task.Delay(IntervalMs, _cancellationTokenSource.Token); - } - while (IsRepeating && !_cancellationTokenSource.Token.IsCancellationRequested); - } - - /// - /// 停止触发器 - /// - public void Stop() - { - _cancellationTokenSource?.Cancel(); - } - - private async Task ExecuteTasks() - { - List list = GearTaskRefenceList.Select(gearTask => gearTask.ToGearTask()).ToList(); - foreach (var gearTask in list) - { - if (_cancellationTokenSource?.Token.IsCancellationRequested == true) - break; - await gearTask.Run(_cancellationTokenSource?.Token ?? CancellationToken.None); - } - } -} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/QuartzSchedulerService.cs b/BetterGenshinImpact/Service/QuartzSchedulerService.cs new file mode 100644 index 00000000..b73ced5a --- /dev/null +++ b/BetterGenshinImpact/Service/QuartzSchedulerService.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using BetterGenshinImpact.Model.Gear.Triggers; +using BetterGenshinImpact.Model.Gear.Triggers.QuartzJob; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Quartz; + +namespace BetterGenshinImpact.Service; + +/// +/// Quartz.NET 调度器服务 +/// +public class QuartzSchedulerService : IHostedService +{ + private readonly ILogger _logger; + private static ISchedulerFactory _schedulerFactory; + + public QuartzSchedulerService(ILogger logger, ISchedulerFactory schedulerFactory) + { + _logger = logger; + _schedulerFactory = schedulerFactory; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + List allData = new List(); + + Dictionary> jobsDictionary = new(); + foreach (var data in allData) + { + if (string.IsNullOrEmpty(data.CronExpression)) + { + continue; + } + + var triggerSet = new HashSet(); + IJobDetail job = JobBuilder.Create() + .UsingJobData("jobData", data.ToString()) + .Build(); + ITrigger trigger = TriggerBuilder.Create() + .WithCronSchedule(data.CronExpression) + .ForJob(job) + .Build(); + triggerSet.Add(trigger); + jobsDictionary.Add(job, triggerSet); + } + + var scheduler = await _schedulerFactory.GetScheduler(cancellationToken); + await scheduler.ScheduleJobs(jobsDictionary, replace: true, cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs index aede491c..c409dd98 100644 --- a/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs @@ -131,12 +131,12 @@ public partial class GearTriggerViewModel : ObservableObject GearBaseTrigger trigger = TriggerType switch { TriggerType.Sequential => new SequentialGearTrigger(), - TriggerType.Timed => new TimedGearTrigger + TriggerType.Timed => new QuartzCronGearTrigger { - IntervalMs = IntervalMs, - IsRepeating = IsRepeating, - DelayMs = DelayMs, - MaxExecutions = MaxExecutions + // IntervalMs = IntervalMs, + // IsRepeating = IsRepeating, + // DelayMs = DelayMs, + // MaxExecutions = MaxExecutions }, TriggerType.Hotkey => new HotkeyGearTrigger {