集成 Quartz.NET

This commit is contained in:
辉鸭蛋
2025-09-21 02:48:50 +08:00
parent bf6673d582
commit f1cf9f5d13
10 changed files with 237 additions and 98 deletions

View File

@@ -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<INavigationWindow, MainWindow, MainWindowViewModel>();
services.AddSingleton<NotifyIconViewModel>();
// Quartz.NET 调度器配置
services.AddQuartz(q =>
{
q.UseSimpleTypeLoader();
q.UseInMemoryStore();
q.UseDefaultThreadPool(tp =>
{
tp.MaxConcurrency = 1;
});
});
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
services.AddSingleton<QuartzSchedulerService>();
services.AddHostedService(sp => sp.GetRequiredService<QuartzSchedulerService>());
// Views
services.AddView<HomePage, HomePageViewModel>();
@@ -142,6 +158,8 @@ public partial class App : Application
services.AddSingleton<OcrFactory>();
services.AddSingleton<GearTaskStorageService>();
// Configuration
//services.Configure<AppConfig>(context.Configuration.GetSection(nameof(AppConfig)));
}

View File

@@ -69,6 +69,8 @@
<PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.Windows" Version="4.11.0.20250507" />
<PackageReference Include="Quartz" Version="3.15.0" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.122" />
<PackageReference Include="Microsoft.ClearScript.V8" Version="7.4.5" />
<PackageReference Include="Microsoft.ClearScript.V8.Native.win-x64" Version="7.4.5" />

View File

@@ -16,5 +16,5 @@ public abstract class GearBaseTrigger
/// <summary>
/// 执行任务
/// </summary>
public abstract Task Run();
public abstract Task Trigger();
}

View File

@@ -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方法

View File

@@ -0,0 +1,27 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace BetterGenshinImpact.Model.Gear.Triggers;
/// <summary>
/// 基于 Quartz.NET 的定时触发器
/// </summary>
public class QuartzCronGearTrigger : GearBaseTrigger
{
private readonly ILogger<QuartzCronGearTrigger> _logger = App.GetLogger<QuartzCronGearTrigger>();
/// <summary>
/// 是否在执行时中断其他同类型定时任务
/// </summary>
public bool ShouldInterruptOthers { get; set; } = true;
/// <summary>
/// 使用 Cron 表达式(如果设置,将覆盖 IntervalMs 设置)
/// </summary>
public string? CronExpression { get; set; }
public override async Task Trigger()
{
}
}

View File

@@ -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;
/// <summary>
/// Quartz.NET 任务定义
/// </summary>
[DisallowConcurrentExecution]
[PersistJobDataAfterExecution]
public class QuartzGearTaskJob : IJob
{
public static readonly JobKey Key = new("gear-task-job", "default-group");
private readonly ILogger<QuartzGearTaskJob> _logger = App.GetLogger<QuartzGearTaskJob>();
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<List<GearTaskRefence>>(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;
}
}
/// <summary>
/// 中断其他同类型的定时任务
/// </summary>
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, "中断其他任务时发生错误");
}
}
}

View File

@@ -11,7 +11,7 @@ namespace BetterGenshinImpact.Model.Gear.Triggers;
/// </summary>
public class SequentialGearTrigger : GearBaseTrigger
{
public override async Task Run()
public override async Task Trigger()
{
List<BaseGearTask> list = GearTaskRefenceList.Select(gearTask => gearTask.ToGearTask()).ToList();
foreach (var gearTask in list)

View File

@@ -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;
/// <summary>
/// 定时触发器
/// </summary>
public class TimedGearTrigger : GearBaseTrigger
{
/// <summary>
/// 触发间隔(毫秒)
/// </summary>
public int IntervalMs { get; set; } = 5000;
/// <summary>
/// 是否重复执行
/// </summary>
public bool IsRepeating { get; set; } = true;
/// <summary>
/// 延迟启动时间(毫秒)
/// </summary>
public int DelayMs { get; set; } = 0;
/// <summary>
/// 最大执行次数0表示无限制
/// </summary>
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);
}
/// <summary>
/// 停止触发器
/// </summary>
public void Stop()
{
_cancellationTokenSource?.Cancel();
}
private async Task ExecuteTasks()
{
List<BaseGearTask> 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);
}
}
}

View File

@@ -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;
/// <summary>
/// Quartz.NET 调度器服务
/// </summary>
public class QuartzSchedulerService : IHostedService
{
private readonly ILogger<QuartzSchedulerService> _logger;
private static ISchedulerFactory _schedulerFactory;
public QuartzSchedulerService(ILogger<QuartzSchedulerService> logger, ISchedulerFactory schedulerFactory)
{
_logger = logger;
_schedulerFactory = schedulerFactory;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
List<QuartzCronGearTrigger> allData = new List<QuartzCronGearTrigger>();
Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>> jobsDictionary = new();
foreach (var data in allData)
{
if (string.IsNullOrEmpty(data.CronExpression))
{
continue;
}
var triggerSet = new HashSet<ITrigger>();
IJobDetail job = JobBuilder.Create<QuartzGearTaskJob>()
.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)
{
}
}

View File

@@ -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
{