mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-23 09:55:48 +08:00
集成 Quartz.NET
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -16,5 +16,5 @@ public abstract class GearBaseTrigger
|
||||
/// <summary>
|
||||
/// 执行任务
|
||||
/// </summary>
|
||||
public abstract Task Run();
|
||||
public abstract Task Trigger();
|
||||
}
|
||||
@@ -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方法
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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, "中断其他任务时发生错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
BetterGenshinImpact/Service/QuartzSchedulerService.cs
Normal file
57
BetterGenshinImpact/Service/QuartzSchedulerService.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user