Implement Quartz.NET integration with dynamic task management

Co-authored-by: huiyadanli <15783049+huiyadanli@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-07-04 08:27:03 +00:00
parent a0dcf5f9f1
commit 827e74b916
10 changed files with 1994 additions and 2 deletions

View File

@@ -129,6 +129,18 @@ public partial class App : Application
services.AddSingleton(sp=> sp.GetRequiredService<HomePageViewModel>().Config.HardwareAccelerationConfig);
services.AddSingleton<BgiOnnxFactory>();
// Quartz.NET 服务
services.AddQuartz(q =>
{
// 使用内存存储
q.UseInMemoryStore();
// 使用默认线程池
q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 10);
});
services.AddSingleton<Service.Quartz.SchedulerManager>();
services.AddSingleton<Service.Quartz.DynamicTaskExampleService>();
services.AddHostedService<Service.Quartz.QuartzHostedService>();
// Configuration
//services.Configure<AppConfig>(context.Configuration.GetSection(nameof(AppConfig)));
}

View File

@@ -85,6 +85,8 @@
<PackageReference Include="WPF-UI.Violeta" Version="4.0.2.3" />
<PackageReference Include="gong-wpf-dragdrop" Version="3.2.1" />
<PackageReference Include="YoloSharp" Version="6.0.3" />
<PackageReference Include="Quartz" Version="3.13.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Debug'">

View File

@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Script.Group;
using BetterGenshinImpact.Service.Quartz;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Quartz;
namespace BetterGenshinImpact.Examples;
/// <summary>
/// Quartz.NET 动态任务管理示例程序
/// 演示如何在控制台应用中使用调度功能
/// </summary>
public class QuartzExampleProgram
{
public static async Task Main(string[] args)
{
Console.WriteLine("=== Quartz.NET 动态任务管理示例 ===\n");
// 创建主机和服务
var host = CreateHostBuilder(args).Build();
try
{
// 启动主机
await host.StartAsync();
// 获取服务
var schedulerManager = host.Services.GetRequiredService<SchedulerManager>();
var dynamicTaskService = host.Services.GetRequiredService<DynamicTaskExampleService>();
// 运行示例
await RunExamples(schedulerManager, dynamicTaskService);
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
finally
{
await host.StopAsync();
}
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
// 注册 Quartz.NET
services.AddQuartz(q =>
{
q.UseInMemoryStore();
q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 5);
});
// 注册自定义服务
services.AddSingleton<SchedulerManager>();
services.AddSingleton<DynamicTaskExampleService>();
services.AddHostedService<QuartzHostedService>();
// 注册模拟的脚本服务
services.AddSingleton<MockScriptService>();
});
private static async Task RunExamples(SchedulerManager schedulerManager, DynamicTaskExampleService dynamicTaskService)
{
// 创建示例脚本组
var scriptGroup1 = CreateExampleScriptGroup("每日任务组", "Daily");
var scriptGroup2 = CreateExampleScriptGroup("每周任务组", "Monday");
var scriptGroup3 = CreateExampleScriptGroup("自定义任务组", "0 0/15 * * * ? *"); // 每15分钟
Console.WriteLine("1. 添加定时任务示例");
Console.WriteLine("===================");
// 示例1添加每日任务
Console.WriteLine("添加每日任务每天8:30执行...");
bool success1 = await dynamicTaskService.CreateDailyTaskAsync(scriptGroup1, 8, 30);
Console.WriteLine($"结果: {(success1 ? "" : "")}\n");
// 示例2添加每周任务
Console.WriteLine("添加每周任务每周一9:00执行...");
bool success2 = await dynamicTaskService.CreateWeeklyTaskAsync(scriptGroup2, 1, 9, 0);
Console.WriteLine($"结果: {(success2 ? "" : "")}\n");
// 示例3添加自定义Cron表达式任务
Console.WriteLine("添加自定义任务每15分钟执行...");
bool success3 = await schedulerManager.AddScheduledTaskAsync(scriptGroup3, "0 0/15 * * * ? *");
Console.WriteLine($"结果: {(success3 ? "" : "")}\n");
// 示例4批量添加任务
Console.WriteLine("2. 批量操作示例");
Console.WriteLine("================");
var scriptGroups = new List<ScriptGroup> { scriptGroup1, scriptGroup2, scriptGroup3 };
int batchCount = await dynamicTaskService.AddMultipleScriptGroupSchedulesAsync(scriptGroups, "0 0 12 * * ? *");
Console.WriteLine($"批量添加任务结果: 成功添加 {batchCount} 个任务\n");
// 示例5查看所有任务
Console.WriteLine("3. 查看任务状态");
Console.WriteLine("===============");
var allTasks = await schedulerManager.GetAllScheduledTasksAsync();
Console.WriteLine($"当前总任务数: {allTasks.Count}");
foreach (var task in allTasks)
{
Console.WriteLine($"- 任务: {task.JobName}");
Console.WriteLine($" 脚本组: {task.ScriptGroupName}");
Console.WriteLine($" Cron表达式: {task.CronExpression}");
Console.WriteLine($" 下次执行: {task.NextFireTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""}");
Console.WriteLine();
}
// 示例6生成任务报告
Console.WriteLine("4. 任务执行报告");
Console.WriteLine("===============");
var report = await dynamicTaskService.GetScheduledTaskReportAsync();
Console.WriteLine($"总任务数: {report.TotalTasks}");
Console.WriteLine($"活跃任务数: {report.ActiveTasks}");
Console.WriteLine("按脚本组统计:");
foreach (var group in report.TasksByScriptGroup)
{
Console.WriteLine($" {group.Key}: {group.Value} 个任务");
}
Console.WriteLine("\n即将执行的任务:");
foreach (var execution in report.NextExecutions.Take(5))
{
Console.WriteLine($" {execution.ScriptGroupName} - {execution.NextFireTime:yyyy-MM-dd HH:mm:ss}");
}
Console.WriteLine();
// 示例7任务管理操作
Console.WriteLine("5. 任务管理操作");
Console.WriteLine("===============");
// 暂停任务
if (allTasks.Count > 0)
{
var firstTask = allTasks[0];
Console.WriteLine($"暂停任务: {firstTask.JobName}");
bool pauseResult = await schedulerManager.PauseScheduledTaskAsync(firstTask.JobName);
Console.WriteLine($"暂停结果: {(pauseResult ? "" : "")}");
// 等待一秒
await Task.Delay(1000);
// 恢复任务
Console.WriteLine($"恢复任务: {firstTask.JobName}");
bool resumeResult = await schedulerManager.ResumeScheduledTaskAsync(firstTask.JobName);
Console.WriteLine($"恢复结果: {(resumeResult ? "" : "")}");
}
Console.WriteLine();
// 示例8更新任务
if (allTasks.Count > 0)
{
var firstTask = allTasks[0];
var newCron = "0 0 10 * * ? *"; // 每天10点执行
Console.WriteLine($"更新任务Cron表达式: {firstTask.JobName}");
Console.WriteLine($"新的Cron表达式: {newCron}");
bool updateResult = await schedulerManager.UpdateScheduledTaskAsync(firstTask.JobName, newCron);
Console.WriteLine($"更新结果: {(updateResult ? "" : "")}");
}
Console.WriteLine();
// 示例9清理任务
Console.WriteLine("6. 清理任务");
Console.WriteLine("===========");
int cleanupCount = await dynamicTaskService.ClearAllScheduledTasksAsync();
Console.WriteLine($"清理结果: 删除了 {cleanupCount} 个任务");
}
private static ScriptGroup CreateExampleScriptGroup(string name, string schedule)
{
var scriptGroup = new ScriptGroup
{
Name = name,
Index = 1
};
// 添加示例项目
var project = new ScriptGroupProject
{
Name = $"{name}_脚本",
FolderName = "example",
Type = "Javascript",
Status = "Enabled",
Schedule = schedule,
Index = 1
};
scriptGroup.AddProject(project);
return scriptGroup;
}
}
/// <summary>
/// 模拟脚本服务,用于演示
/// </summary>
public class MockScriptService : BetterGenshinImpact.Service.Interface.IScriptService
{
private readonly ILogger<MockScriptService> _logger;
public MockScriptService(ILogger<MockScriptService> logger)
{
_logger = logger;
}
public async Task RunMulti(List<ScriptGroupProject> projects, string groupName, object? taskProgress)
{
_logger.LogInformation("模拟执行脚本组: {GroupName},包含 {ProjectCount} 个项目", groupName, projects.Count);
foreach (var project in projects)
{
_logger.LogInformation("执行项目: {ProjectName} ({ProjectType})", project.Name, project.Type);
// 模拟执行时间
await Task.Delay(100);
}
_logger.LogInformation("脚本组 {GroupName} 执行完成", groupName);
}
}

View File

@@ -0,0 +1,389 @@
using System;
using System.Collections.Generic;
using System.Globalization;
namespace BetterGenshinImpact.Service.Quartz;
/// <summary>
/// Cron 表达式帮助类
/// 提供常用的 Cron 表达式生成和解析功能
/// </summary>
public static class CronExpressionHelper
{
/// <summary>
/// 预定义的 Cron 表达式集合
/// </summary>
public static readonly Dictionary<string, string> PredefinedExpressions = new()
{
// 基本时间间隔
{ "每分钟", "0 * * * * ? *" },
{ "每5分钟", "0 0/5 * * * ? *" },
{ "每10分钟", "0 0/10 * * * ? *" },
{ "每15分钟", "0 0/15 * * * ? *" },
{ "每30分钟", "0 0/30 * * * ? *" },
{ "每小时", "0 0 * * * ? *" },
{ "每2小时", "0 0 0/2 * * ? *" },
{ "每4小时", "0 0 0/4 * * ? *" },
{ "每6小时", "0 0 0/6 * * ? *" },
{ "每12小时", "0 0 0/12 * * ? *" },
// 每日时间点
{ "每天午夜", "0 0 0 * * ? *" },
{ "每天早上6点", "0 0 6 * * ? *" },
{ "每天早上8点", "0 0 8 * * ? *" },
{ "每天上午9点", "0 0 9 * * ? *" },
{ "每天中午12点", "0 0 12 * * ? *" },
{ "每天下午6点", "0 0 18 * * ? *" },
{ "每天晚上9点", "0 0 21 * * ? *" },
{ "每天晚上11点", "0 0 23 * * ? *" },
// 工作日和周末
{ "工作日上午9点", "0 0 9 ? * MON-FRI *" },
{ "工作日下午6点", "0 0 18 ? * MON-FRI *" },
{ "周末上午10点", "0 0 10 ? * SAT,SUN *" },
// 每周特定时间
{ "每周一上午9点", "0 0 9 ? * MON *" },
{ "每周二上午9点", "0 0 9 ? * TUE *" },
{ "每周三上午9点", "0 0 9 ? * WED *" },
{ "每周四上午9点", "0 0 9 ? * THU *" },
{ "每周五上午9点", "0 0 9 ? * FRI *" },
{ "每周六上午10点", "0 0 10 ? * SAT *" },
{ "每周日上午10点", "0 0 10 ? * SUN *" },
// 每月特定时间
{ "每月1号午夜", "0 0 0 1 * ? *" },
{ "每月15号午夜", "0 0 0 15 * ? *" },
{ "每月最后一天", "0 0 0 L * ? *" },
{ "每月第一个周一", "0 0 0 ? * MON#1 *" },
{ "每月最后一个周五", "0 0 0 ? * FRIL *" },
// 季度和年度
{ "每季度第一天", "0 0 0 1 1/3 ? *" },
{ "每年1月1日", "0 0 0 1 1 ? *" },
{ "每年生日提醒", "0 0 9 1 1 ? *" } // 示例每年1月1日上午9点
};
/// <summary>
/// 创建每日执行的 Cron 表达式
/// </summary>
/// <param name="hour">小时 (0-23)</param>
/// <param name="minute">分钟 (0-59)</param>
/// <param name="second">秒 (0-59默认0)</param>
/// <returns>Cron 表达式</returns>
public static string CreateDaily(int hour, int minute, int second = 0)
{
ValidateTime(hour, minute, second);
return $"{second} {minute} {hour} * * ? *";
}
/// <summary>
/// 创建每周执行的 Cron 表达式
/// </summary>
/// <param name="dayOfWeek">星期几 (1=周一, 7=周日)</param>
/// <param name="hour">小时 (0-23)</param>
/// <param name="minute">分钟 (0-59)</param>
/// <param name="second">秒 (0-59默认0)</param>
/// <returns>Cron 表达式</returns>
public static string CreateWeekly(DayOfWeek dayOfWeek, int hour, int minute, int second = 0)
{
ValidateTime(hour, minute, second);
var dayNames = new[] { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
var dayName = dayNames[(int)dayOfWeek];
return $"{second} {minute} {hour} ? * {dayName} *";
}
/// <summary>
/// 创建每月执行的 Cron 表达式
/// </summary>
/// <param name="dayOfMonth">月中的第几天 (1-31)</param>
/// <param name="hour">小时 (0-23)</param>
/// <param name="minute">分钟 (0-59)</param>
/// <param name="second">秒 (0-59默认0)</param>
/// <returns>Cron 表达式</returns>
public static string CreateMonthly(int dayOfMonth, int hour, int minute, int second = 0)
{
if (dayOfMonth < 1 || dayOfMonth > 31)
throw new ArgumentException("月中的天数必须在1-31之间", nameof(dayOfMonth));
ValidateTime(hour, minute, second);
return $"{second} {minute} {hour} {dayOfMonth} * ? *";
}
/// <summary>
/// 创建间隔执行的 Cron 表达式
/// </summary>
/// <param name="intervalMinutes">间隔分钟数</param>
/// <param name="startMinute">开始分钟 (默认0)</param>
/// <returns>Cron 表达式</returns>
public static string CreateInterval(int intervalMinutes, int startMinute = 0)
{
if (intervalMinutes <= 0 || intervalMinutes > 59)
throw new ArgumentException("间隔分钟数必须在1-59之间", nameof(intervalMinutes));
if (startMinute < 0 || startMinute > 59)
throw new ArgumentException("开始分钟必须在0-59之间", nameof(startMinute));
return $"0 {startMinute}/{intervalMinutes} * * * ? *";
}
/// <summary>
/// 创建工作日执行的 Cron 表达式
/// </summary>
/// <param name="hour">小时 (0-23)</param>
/// <param name="minute">分钟 (0-59)</param>
/// <param name="second">秒 (0-59默认0)</param>
/// <returns>Cron 表达式</returns>
public static string CreateWorkdays(int hour, int minute, int second = 0)
{
ValidateTime(hour, minute, second);
return $"{second} {minute} {hour} ? * MON-FRI *";
}
/// <summary>
/// 创建周末执行的 Cron 表达式
/// </summary>
/// <param name="hour">小时 (0-23)</param>
/// <param name="minute">分钟 (0-59)</param>
/// <param name="second">秒 (0-59默认0)</param>
/// <returns>Cron 表达式</returns>
public static string CreateWeekends(int hour, int minute, int second = 0)
{
ValidateTime(hour, minute, second);
return $"{second} {minute} {hour} ? * SAT,SUN *";
}
/// <summary>
/// 创建多个时间点执行的 Cron 表达式
/// </summary>
/// <param name="hours">小时数组</param>
/// <param name="minute">分钟</param>
/// <param name="second">秒 (默认0)</param>
/// <returns>Cron 表达式</returns>
public static string CreateMultipleHours(int[] hours, int minute, int second = 0)
{
if (hours == null || hours.Length == 0)
throw new ArgumentException("小时数组不能为空", nameof(hours));
foreach (var hour in hours)
{
if (hour < 0 || hour > 23)
throw new ArgumentException($"小时 {hour} 必须在0-23之间", nameof(hours));
}
if (minute < 0 || minute > 59)
throw new ArgumentException("分钟必须在0-59之间", nameof(minute));
if (second < 0 || second > 59)
throw new ArgumentException("秒必须在0-59之间", nameof(second));
var hoursStr = string.Join(",", hours);
return $"{second} {minute} {hoursStr} * * ? *";
}
/// <summary>
/// 解析 Cron 表达式为人类可读的描述
/// </summary>
/// <param name="cronExpression">Cron 表达式</param>
/// <returns>描述文本</returns>
public static string ParseToDescription(string cronExpression)
{
if (string.IsNullOrWhiteSpace(cronExpression))
return "无效的Cron表达式";
// 查找预定义表达式
foreach (var predefined in PredefinedExpressions)
{
if (predefined.Value.Equals(cronExpression, StringComparison.OrdinalIgnoreCase))
{
return predefined.Key;
}
}
try
{
var parts = cronExpression.Trim().Split(' ');
if (parts.Length < 6)
return "格式不正确的Cron表达式";
var second = parts[0];
var minute = parts[1];
var hour = parts[2];
var day = parts[3];
var month = parts[4];
var dayOfWeek = parts[5];
var description = "自定义时间: ";
// 解析秒
if (second != "*" && second != "0")
{
description += $"第{second}秒 ";
}
// 解析分钟
if (minute.Contains("/"))
{
var intervalParts = minute.Split('/');
if (intervalParts.Length == 2 && intervalParts[0] == "0")
{
description += $"每{intervalParts[1]}分钟 ";
}
}
else if (minute != "*")
{
description += $"{minute}分 ";
}
// 解析小时
if (hour.Contains("/"))
{
var intervalParts = hour.Split('/');
if (intervalParts.Length == 2 && intervalParts[0] == "0")
{
description += $"每{intervalParts[1]}小时 ";
}
}
else if (hour.Contains(","))
{
description += $"在{hour.Replace(",", "")}点 ";
}
else if (hour != "*")
{
description += $"{hour}点 ";
}
// 解析星期
if (dayOfWeek != "*" && dayOfWeek != "?")
{
var dayNames = new Dictionary<string, string>
{
{ "MON", "周一" }, { "TUE", "周二" }, { "WED", "周三" }, { "THU", "周四" },
{ "FRI", "周五" }, { "SAT", "周六" }, { "SUN", "周日" },
{ "MON-FRI", "工作日" }, { "SAT,SUN", "周末" }
};
if (dayNames.TryGetValue(dayOfWeek, out var dayName))
{
description += $"({dayName}) ";
}
else
{
description += $"({dayOfWeek}) ";
}
}
// 解析月中的天
if (day != "*" && day != "?")
{
if (day == "L")
{
description += "(月末) ";
}
else
{
description += $"(每月{day}号) ";
}
}
return description.Trim();
}
catch
{
return "无法解析的Cron表达式";
}
}
/// <summary>
/// 验证 Cron 表达式是否有效
/// </summary>
/// <param name="cronExpression">Cron 表达式</param>
/// <returns>是否有效</returns>
public static bool IsValidCronExpression(string cronExpression)
{
if (string.IsNullOrWhiteSpace(cronExpression))
return false;
try
{
// 使用 Quartz.NET 的 CronExpression 进行验证
var expression = new Quartz.CronExpression(cronExpression);
return expression.IsSatisfiedBy(DateTime.Now);
}
catch
{
return false;
}
}
/// <summary>
/// 获取 Cron 表达式的下次执行时间
/// </summary>
/// <param name="cronExpression">Cron 表达式</param>
/// <param name="fromTime">起始时间(可选,默认为当前时间)</param>
/// <returns>下次执行时间如果表达式无效则返回null</returns>
public static DateTime? GetNextExecutionTime(string cronExpression, DateTime? fromTime = null)
{
try
{
var expression = new Quartz.CronExpression(cronExpression);
var baseTime = fromTime ?? DateTime.Now;
var nextTime = expression.GetNextValidTimeAfter(baseTime);
return nextTime?.DateTime;
}
catch
{
return null;
}
}
/// <summary>
/// 获取 Cron 表达式的多个执行时间
/// </summary>
/// <param name="cronExpression">Cron 表达式</param>
/// <param name="count">获取的执行时间数量</param>
/// <param name="fromTime">起始时间(可选,默认为当前时间)</param>
/// <returns>执行时间列表</returns>
public static List<DateTime> GetNextExecutionTimes(string cronExpression, int count, DateTime? fromTime = null)
{
var times = new List<DateTime>();
try
{
var expression = new Quartz.CronExpression(cronExpression);
var currentTime = fromTime ?? DateTime.Now;
for (int i = 0; i < count; i++)
{
var nextTime = expression.GetNextValidTimeAfter(currentTime);
if (nextTime.HasValue)
{
times.Add(nextTime.Value.DateTime);
currentTime = nextTime.Value.DateTime;
}
else
{
break;
}
}
}
catch
{
// 表达式无效,返回空列表
}
return times;
}
private static void ValidateTime(int hour, int minute, int second)
{
if (hour < 0 || hour > 23)
throw new ArgumentException("小时必须在0-23之间", nameof(hour));
if (minute < 0 || minute > 59)
throw new ArgumentException("分钟必须在0-59之间", nameof(minute));
if (second < 0 || second > 59)
throw new ArgumentException("秒必须在0-59之间", nameof(second));
}
}

View File

@@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Script.Group;
using Microsoft.Extensions.Logging;
namespace BetterGenshinImpact.Service.Quartz;
/// <summary>
/// 动态任务管理示例服务
/// 提供动态添加、删除、管理定时任务的示例方法
/// </summary>
public class DynamicTaskExampleService
{
private readonly SchedulerManager _schedulerManager;
private readonly ILogger<DynamicTaskExampleService> _logger;
public DynamicTaskExampleService(SchedulerManager schedulerManager, ILogger<DynamicTaskExampleService> logger)
{
_schedulerManager = schedulerManager;
_logger = logger;
}
/// <summary>
/// 示例:为脚本组添加定时任务
/// </summary>
/// <param name="scriptGroup">脚本组</param>
/// <param name="cronExpression">Cron表达式</param>
/// <returns>任务是否添加成功</returns>
public async Task<bool> AddScriptGroupScheduleAsync(ScriptGroup scriptGroup, string cronExpression)
{
try
{
// 检查脚本组是否有效
if (scriptGroup?.Projects == null || !scriptGroup.Projects.Any())
{
_logger.LogWarning("脚本组 {ScriptGroupName} 没有有效的项目", scriptGroup?.Name);
return false;
}
// 检查是否有启用的项目
var enabledProjects = scriptGroup.Projects.Where(p => p.Status == "Enabled").ToList();
if (enabledProjects.Count == 0)
{
_logger.LogWarning("脚本组 {ScriptGroupName} 没有启用的项目", scriptGroup.Name);
return false;
}
// 添加定时任务
return await _schedulerManager.AddScheduledTaskAsync(scriptGroup, cronExpression);
}
catch (Exception ex)
{
_logger.LogError(ex, "添加脚本组定时任务失败:{Message}", ex.Message);
return false;
}
}
/// <summary>
/// 示例:批量添加多个脚本组的定时任务
/// </summary>
/// <param name="scriptGroups">脚本组列表</param>
/// <param name="defaultCronExpression">默认Cron表达式</param>
/// <returns>成功添加的任务数量</returns>
public async Task<int> AddMultipleScriptGroupSchedulesAsync(List<ScriptGroup> scriptGroups, string defaultCronExpression = "0 0 0 * * ? *")
{
int successCount = 0;
foreach (var scriptGroup in scriptGroups)
{
try
{
// 使用脚本组的调度配置或默认配置
var cronExpression = !string.IsNullOrEmpty(scriptGroup.Projects?.FirstOrDefault()?.Schedule)
? SchedulerManager.ConvertScheduleToCron(scriptGroup.Projects.FirstOrDefault()!.Schedule)
: defaultCronExpression;
if (await AddScriptGroupScheduleAsync(scriptGroup, cronExpression))
{
successCount++;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "添加脚本组 {ScriptGroupName} 定时任务失败:{Message}", scriptGroup.Name, ex.Message);
}
}
_logger.LogInformation("批量添加定时任务完成,成功添加 {SuccessCount}/{TotalCount} 个任务", successCount, scriptGroups.Count);
return successCount;
}
/// <summary>
/// 示例:根据脚本组名称删除定时任务
/// </summary>
/// <param name="scriptGroupName">脚本组名称</param>
/// <returns>删除的任务数量</returns>
public async Task<int> RemoveScriptGroupSchedulesAsync(string scriptGroupName)
{
try
{
var allTasks = await _schedulerManager.GetAllScheduledTasksAsync();
var tasksToRemove = allTasks.Where(t => t.ScriptGroupName == scriptGroupName).ToList();
int removedCount = 0;
foreach (var task in tasksToRemove)
{
if (await _schedulerManager.RemoveScheduledTaskAsync(task.JobName))
{
removedCount++;
}
}
_logger.LogInformation("删除脚本组 {ScriptGroupName} 的定时任务完成,成功删除 {RemovedCount} 个任务", scriptGroupName, removedCount);
return removedCount;
}
catch (Exception ex)
{
_logger.LogError(ex, "删除脚本组 {ScriptGroupName} 定时任务失败:{Message}", scriptGroupName, ex.Message);
return 0;
}
}
/// <summary>
/// 示例:创建每日执行的定时任务
/// </summary>
/// <param name="scriptGroup">脚本组</param>
/// <param name="hour">执行小时0-23</param>
/// <param name="minute">执行分钟0-59</param>
/// <returns>任务是否创建成功</returns>
public async Task<bool> CreateDailyTaskAsync(ScriptGroup scriptGroup, int hour = 0, int minute = 0)
{
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
{
_logger.LogError("时间参数无效:小时 {Hour},分钟 {Minute}", hour, minute);
return false;
}
var cronExpression = $"0 {minute} {hour} * * ? *";
return await AddScriptGroupScheduleAsync(scriptGroup, cronExpression);
}
/// <summary>
/// 示例:创建每周执行的定时任务
/// </summary>
/// <param name="scriptGroup">脚本组</param>
/// <param name="dayOfWeek">星期几1=周一7=周日)</param>
/// <param name="hour">执行小时0-23</param>
/// <param name="minute">执行分钟0-59</param>
/// <returns>任务是否创建成功</returns>
public async Task<bool> CreateWeeklyTaskAsync(ScriptGroup scriptGroup, int dayOfWeek, int hour = 0, int minute = 0)
{
if (dayOfWeek < 1 || dayOfWeek > 7 || hour < 0 || hour > 23 || minute < 0 || minute > 59)
{
_logger.LogError("参数无效:星期 {DayOfWeek},小时 {Hour},分钟 {Minute}", dayOfWeek, hour, minute);
return false;
}
var dayNames = new[] { "", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN" };
var cronExpression = $"0 {minute} {hour} ? * {dayNames[dayOfWeek]} *";
return await AddScriptGroupScheduleAsync(scriptGroup, cronExpression);
}
/// <summary>
/// 示例:创建间隔执行的定时任务
/// </summary>
/// <param name="scriptGroup">脚本组</param>
/// <param name="intervalMinutes">间隔分钟数</param>
/// <returns>任务是否创建成功</returns>
public async Task<bool> CreateIntervalTaskAsync(ScriptGroup scriptGroup, int intervalMinutes)
{
if (intervalMinutes <= 0)
{
_logger.LogError("间隔时间无效:{IntervalMinutes} 分钟", intervalMinutes);
return false;
}
var cronExpression = $"0 0/{intervalMinutes} * * * ? *";
return await AddScriptGroupScheduleAsync(scriptGroup, cronExpression);
}
/// <summary>
/// 示例:获取定时任务报告
/// </summary>
/// <returns>定时任务报告</returns>
public async Task<ScheduledTaskReport> GetScheduledTaskReportAsync()
{
try
{
var allTasks = await _schedulerManager.GetAllScheduledTasksAsync();
return new ScheduledTaskReport
{
TotalTasks = allTasks.Count,
ActiveTasks = allTasks.Count(t => t.NextFireTime.HasValue),
TasksByScriptGroup = allTasks.GroupBy(t => t.ScriptGroupName)
.ToDictionary(g => g.Key, g => g.Count()),
NextExecutions = allTasks.Where(t => t.NextFireTime.HasValue)
.OrderBy(t => t.NextFireTime)
.Take(10)
.Select(t => new NextExecution
{
JobName = t.JobName,
ScriptGroupName = t.ScriptGroupName,
NextFireTime = t.NextFireTime!.Value,
CronExpression = t.CronExpression
})
.ToList()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "获取定时任务报告失败:{Message}", ex.Message);
return new ScheduledTaskReport();
}
}
/// <summary>
/// 示例:清理所有定时任务
/// </summary>
/// <returns>清理的任务数量</returns>
public async Task<int> ClearAllScheduledTasksAsync()
{
try
{
var allTasks = await _schedulerManager.GetAllScheduledTasksAsync();
int clearedCount = 0;
foreach (var task in allTasks)
{
if (await _schedulerManager.RemoveScheduledTaskAsync(task.JobName))
{
clearedCount++;
}
}
_logger.LogInformation("清理定时任务完成,成功清理 {ClearedCount} 个任务", clearedCount);
return clearedCount;
}
catch (Exception ex)
{
_logger.LogError(ex, "清理定时任务失败:{Message}", ex.Message);
return 0;
}
}
}
/// <summary>
/// 定时任务报告
/// </summary>
public class ScheduledTaskReport
{
public int TotalTasks { get; set; }
public int ActiveTasks { get; set; }
public Dictionary<string, int> TasksByScriptGroup { get; set; } = new();
public List<NextExecution> NextExecutions { get; set; } = new();
}
/// <summary>
/// 下次执行信息
/// </summary>
public class NextExecution
{
public string JobName { get; set; } = "";
public string ScriptGroupName { get; set; } = "";
public DateTime NextFireTime { get; set; }
public string CronExpression { get; set; } = "";
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Quartz;
namespace BetterGenshinImpact.Service.Quartz;
/// <summary>
/// Quartz.NET 调度服务 - 管理定时任务的生命周期
/// </summary>
public class QuartzHostedService : IHostedService
{
private readonly IScheduler _scheduler;
private readonly ILogger<QuartzHostedService> _logger;
public QuartzHostedService(IScheduler scheduler, ILogger<QuartzHostedService> logger)
{
_scheduler = scheduler;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("启动 Quartz.NET 调度服务");
await _scheduler.Start(cancellationToken);
_logger.LogInformation("Quartz.NET 调度服务启动成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "启动 Quartz.NET 调度服务失败:{Message}", ex.Message);
throw;
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("停止 Quartz.NET 调度服务");
await _scheduler.Shutdown(cancellationToken);
_logger.LogInformation("Quartz.NET 调度服务停止成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "停止 Quartz.NET 调度服务失败:{Message}", ex.Message);
}
}
}

View File

@@ -0,0 +1,214 @@
# Quartz.NET 集成使用指南
## 概述
本项目已集成 Quartz.NET 调度框架,支持动态添加、删除和管理定时任务。现有的手动执行功能保持不变,新增的调度功能为脚本组提供自动化执行能力。
## 主要特性
1. **非破坏性集成**:现有功能完全保留
2. **动态任务管理**:运行时添加/删除定时任务
3. **Cron表达式支持**:完整的时间调度配置
4. **可视化管理**通过UI界面管理定时任务
5. **任务监控**:查看任务状态和执行报告
## 核心组件
### 1. ScriptExecutionJob
负责实际执行脚本组的任务类,实现了 `IJob` 接口。
### 2. SchedulerManager
提供完整的调度管理功能:
- 动态添加任务
- 删除任务
- 更新任务
- 暂停/恢复任务
- 查询任务状态
### 3. DynamicTaskExampleService
演示如何使用调度功能的示例服务,包含各种实用方法。
### 4. QuartzHostedService
管理 Quartz.NET 调度器的生命周期。
## 使用示例
### 基本用法
```csharp
// 获取服务实例
var schedulerManager = App.GetService<SchedulerManager>();
var dynamicTaskService = App.GetService<DynamicTaskExampleService>();
// 添加每日执行的定时任务
var scriptGroup = GetYourScriptGroup();
bool success = await dynamicTaskService.CreateDailyTaskAsync(scriptGroup, 8, 30); // 每天8:30执行
// 添加每周执行的定时任务
bool success = await dynamicTaskService.CreateWeeklyTaskAsync(scriptGroup, 1, 9, 0); // 每周一9:00执行
// 添加间隔执行的定时任务
bool success = await dynamicTaskService.CreateIntervalTaskAsync(scriptGroup, 30); // 每30分钟执行
```
### 自定义 Cron 表达式
```csharp
// 直接使用 Cron 表达式
var cronExpression = "0 0 8,12,18 * * ? *"; // 每天8点、12点、18点执行
bool success = await schedulerManager.AddScheduledTaskAsync(scriptGroup, cronExpression);
// 使用预定义的调度配置转换
var schedule = "Daily"; // 或其他预定义值
var cronExpression = SchedulerManager.ConvertScheduleToCron(schedule);
bool success = await schedulerManager.AddScheduledTaskAsync(scriptGroup, cronExpression);
```
### 任务管理
```csharp
// 查看所有定时任务
var tasks = await schedulerManager.GetAllScheduledTasksAsync();
// 删除特定任务
bool success = await schedulerManager.RemoveScheduledTaskAsync("TaskName");
// 暂停任务
bool success = await schedulerManager.PauseScheduledTaskAsync("TaskName");
// 恢复任务
bool success = await schedulerManager.ResumeScheduledTaskAsync("TaskName");
// 更新任务的Cron表达式
bool success = await schedulerManager.UpdateScheduledTaskAsync("TaskName", "0 0 10 * * ? *");
```
### 批量操作
```csharp
// 批量添加多个脚本组的定时任务
var scriptGroups = GetAllScriptGroups();
int successCount = await dynamicTaskService.AddMultipleScriptGroupSchedulesAsync(scriptGroups);
// 删除指定脚本组的所有任务
int removedCount = await dynamicTaskService.RemoveScriptGroupSchedulesAsync("ScriptGroupName");
// 清理所有定时任务
int clearedCount = await dynamicTaskService.ClearAllScheduledTasksAsync();
```
### 任务报告
```csharp
// 获取定时任务报告
var report = await dynamicTaskService.GetScheduledTaskReportAsync();
Console.WriteLine($"总任务数: {report.TotalTasks}");
Console.WriteLine($"活跃任务数: {report.ActiveTasks}");
// 查看即将执行的任务
foreach (var execution in report.NextExecutions)
{
Console.WriteLine($"{execution.ScriptGroupName} 将在 {execution.NextFireTime} 执行");
}
```
## UI 集成
`ScriptControlViewModel` 中新增了以下命令,可以在界面中使用:
1. **OnAddScheduledTaskAsync** - 添加定时任务
2. **OnViewScheduledTasksAsync** - 查看所有定时任务
3. **OnRemoveScheduledTasksAsync** - 删除定时任务
4. **OnViewScheduledTaskReportAsync** - 查看任务报告
5. **OnBatchAddScheduledTasksAsync** - 批量添加任务
## Cron 表达式说明
| 表达式 | 含义 |
|--------|------|
| `0 0 0 * * ? *` | 每天午夜执行 |
| `0 0 8 * * ? *` | 每天上午8点执行 |
| `0 30 9 ? * MON-FRI *` | 工作日上午9:30执行 |
| `0 0 0 ? * MON *` | 每周一午夜执行 |
| `0 0 0 1 * ? *` | 每月1号午夜执行 |
| `0 0/30 * * * ? *` | 每30分钟执行 |
| `0 0 8,12,18 * * ? *` | 每天8点、12点、18点执行 |
## 预定义调度配置转换
现有的调度配置会自动转换为相应的 Cron 表达式:
- `Daily``0 0 0 * * ? *` (每天午夜)
- `EveryTwoDays``0 0 0 1/2 * ? *` (每两天)
- `Monday``0 0 0 ? * MON *` (每周一)
- `Tuesday``0 0 0 ? * TUE *` (每周二)
- 其他自定义值被视为 Cron 表达式直接使用
## 注意事项
1. **任务执行环境**:定时任务在后台线程中执行,请确保脚本兼容性
2. **错误处理**:任务执行失败不会影响其他任务的调度
3. **性能考虑**:避免添加过于频繁的定时任务
4. **数据持久化**:当前使用内存存储,应用重启后需要重新添加任务
5. **并发控制**默认最大并发数为10可在配置中调整
## 扩展开发
### 自定义任务类型
```csharp
// 创建自定义任务
public class CustomScriptJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// 自定义任务逻辑
}
}
// 注册自定义任务
services.AddQuartz(q =>
{
q.UseInMemoryStore();
// 添加自定义任务
q.AddJob<CustomScriptJob>(opts => opts.WithIdentity("CustomJob"));
});
```
### 数据持久化
如需任务数据持久化,可配置数据库存储:
```csharp
services.AddQuartz(q =>
{
// 使用数据库存储替代内存存储
q.UsePersistentStore(s =>
{
s.UseProperties = true;
s.UseSqlServer("ConnectionString");
s.UseJsonSerializer();
});
});
```
## 故障排除
### 常见问题
1. **服务未初始化**:确保在 `App.xaml.cs` 中正确注册了 Quartz.NET 服务
2. **任务不执行**:检查 Cron 表达式格式是否正确
3. **脚本组数据错误**:确保脚本组包含有效的项目且状态为启用
4. **并发冲突**:避免同时运行多个相同的脚本组任务
### 日志监控
所有任务执行都会记录详细日志,可通过日志文件查看:
- 任务开始执行
- 任务执行完成
- 任务执行异常
- 调度器状态变化
## 总结
Quartz.NET 集成为 Better Genshin Impact 提供了强大的定时任务功能,通过简洁的 API 和友好的 UI 界面,用户可以轻松实现脚本组的自动化调度执行。该集成保持了原有功能的完整性,同时扩展了应用的自动化能力。

View File

@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Script.Group;
using Microsoft.Extensions.Logging;
using Quartz;
namespace BetterGenshinImpact.Service.Quartz;
/// <summary>
/// 调度管理器 - 管理动态添加和删除定时任务
/// </summary>
public class SchedulerManager
{
private readonly IScheduler _scheduler;
private readonly ILogger<SchedulerManager> _logger;
public SchedulerManager(IScheduler scheduler, ILogger<SchedulerManager> logger)
{
_scheduler = scheduler;
_logger = logger;
}
/// <summary>
/// 动态添加定时任务
/// </summary>
/// <param name="scriptGroup">脚本组</param>
/// <param name="cronExpression">Cron表达式</param>
/// <param name="jobName">任务名称(可选)</param>
/// <returns>任务是否添加成功</returns>
public async Task<bool> AddScheduledTaskAsync(ScriptGroup scriptGroup, string cronExpression, string? jobName = null)
{
try
{
jobName ??= $"ScriptGroup_{scriptGroup.Name}_{DateTime.Now.Ticks}";
var jobKey = new JobKey(jobName, "ScriptGroup");
var triggerKey = new TriggerKey($"{jobName}_trigger", "ScriptGroup");
// 检查任务是否已存在
if (await _scheduler.CheckExists(jobKey))
{
_logger.LogWarning("任务 {JobName} 已存在", jobName);
return false;
}
// 创建任务
var job = JobBuilder.Create<ScriptExecutionJob>()
.WithIdentity(jobKey)
.UsingJobData("ScriptGroupName", scriptGroup.Name)
.UsingJobData("ScriptGroupData", scriptGroup.ToJson())
.WithDescription($"脚本组 {scriptGroup.Name} 的定时任务")
.Build();
// 创建触发器
var trigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.WithCronSchedule(cronExpression)
.WithDescription($"脚本组 {scriptGroup.Name} 的定时触发器")
.Build();
// 添加任务到调度器
await _scheduler.ScheduleJob(job, trigger);
_logger.LogInformation("成功添加定时任务:{JobName}Cron表达式{CronExpression}", jobName, cronExpression);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "添加定时任务失败:{Message}", ex.Message);
return false;
}
}
/// <summary>
/// 删除定时任务
/// </summary>
/// <param name="jobName">任务名称</param>
/// <returns>任务是否删除成功</returns>
public async Task<bool> RemoveScheduledTaskAsync(string jobName)
{
try
{
var jobKey = new JobKey(jobName, "ScriptGroup");
if (!await _scheduler.CheckExists(jobKey))
{
_logger.LogWarning("任务 {JobName} 不存在", jobName);
return false;
}
await _scheduler.DeleteJob(jobKey);
_logger.LogInformation("成功删除定时任务:{JobName}", jobName);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "删除定时任务失败:{Message}", ex.Message);
return false;
}
}
/// <summary>
/// 获取所有定时任务
/// </summary>
/// <returns>任务列表</returns>
public async Task<List<ScheduledTaskInfo>> GetAllScheduledTasksAsync()
{
try
{
var jobKeys = await _scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals("ScriptGroup"));
var tasks = new List<ScheduledTaskInfo>();
foreach (var jobKey in jobKeys)
{
var jobDetail = await _scheduler.GetJobDetail(jobKey);
var triggers = await _scheduler.GetTriggersOfJob(jobKey);
foreach (var trigger in triggers)
{
var nextFireTime = trigger.GetNextFireTimeUtc();
var previousFireTime = trigger.GetPreviousFireTimeUtc();
tasks.Add(new ScheduledTaskInfo
{
JobName = jobKey.Name,
ScriptGroupName = jobDetail?.JobDataMap.GetString("ScriptGroupName") ?? "",
CronExpression = trigger is ICronTrigger cronTrigger ? cronTrigger.CronExpressionString : "",
NextFireTime = nextFireTime?.DateTime,
PreviousFireTime = previousFireTime?.DateTime,
Description = jobDetail?.Description ?? ""
});
}
}
return tasks;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取定时任务列表失败:{Message}", ex.Message);
return new List<ScheduledTaskInfo>();
}
}
/// <summary>
/// 更新定时任务的Cron表达式
/// </summary>
/// <param name="jobName">任务名称</param>
/// <param name="newCronExpression">新的Cron表达式</param>
/// <returns>任务是否更新成功</returns>
public async Task<bool> UpdateScheduledTaskAsync(string jobName, string newCronExpression)
{
try
{
var jobKey = new JobKey(jobName, "ScriptGroup");
var triggerKey = new TriggerKey($"{jobName}_trigger", "ScriptGroup");
if (!await _scheduler.CheckExists(jobKey))
{
_logger.LogWarning("任务 {JobName} 不存在", jobName);
return false;
}
// 创建新的触发器
var newTrigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.WithCronSchedule(newCronExpression)
.WithDescription($"更新的定时触发器")
.Build();
// 重新调度任务
await _scheduler.RescheduleJob(triggerKey, newTrigger);
_logger.LogInformation("成功更新定时任务:{JobName}新的Cron表达式{CronExpression}", jobName, newCronExpression);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "更新定时任务失败:{Message}", ex.Message);
return false;
}
}
/// <summary>
/// 暂停定时任务
/// </summary>
/// <param name="jobName">任务名称</param>
/// <returns>任务是否暂停成功</returns>
public async Task<bool> PauseScheduledTaskAsync(string jobName)
{
try
{
var jobKey = new JobKey(jobName, "ScriptGroup");
if (!await _scheduler.CheckExists(jobKey))
{
_logger.LogWarning("任务 {JobName} 不存在", jobName);
return false;
}
await _scheduler.PauseJob(jobKey);
_logger.LogInformation("成功暂停定时任务:{JobName}", jobName);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "暂停定时任务失败:{Message}", ex.Message);
return false;
}
}
/// <summary>
/// 恢复定时任务
/// </summary>
/// <param name="jobName">任务名称</param>
/// <returns>任务是否恢复成功</returns>
public async Task<bool> ResumeScheduledTaskAsync(string jobName)
{
try
{
var jobKey = new JobKey(jobName, "ScriptGroup");
if (!await _scheduler.CheckExists(jobKey))
{
_logger.LogWarning("任务 {JobName} 不存在", jobName);
return false;
}
await _scheduler.ResumeJob(jobKey);
_logger.LogInformation("成功恢复定时任务:{JobName}", jobName);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "恢复定时任务失败:{Message}", ex.Message);
return false;
}
}
/// <summary>
/// 根据脚本组配置转换为Cron表达式
/// </summary>
/// <param name="schedule">调度配置</param>
/// <returns>Cron表达式</returns>
public static string ConvertScheduleToCron(string schedule)
{
return schedule switch
{
"Daily" => "0 0 0 * * ? *", // 每天午夜执行
"EveryTwoDays" => "0 0 0 1/2 * ? *", // 每两天执行
"Monday" => "0 0 0 ? * MON *", // 每周一执行
"Tuesday" => "0 0 0 ? * TUE *", // 每周二执行
"Wednesday" => "0 0 0 ? * WED *", // 每周三执行
"Thursday" => "0 0 0 ? * THU *", // 每周四执行
"Friday" => "0 0 0 ? * FRI *", // 每周五执行
"Saturday" => "0 0 0 ? * SAT *", // 每周六执行
"Sunday" => "0 0 0 ? * SUN *", // 每周日执行
_ => schedule // 假设是自定义Cron表达式
};
}
}
/// <summary>
/// 定时任务信息
/// </summary>
public class ScheduledTaskInfo
{
public string JobName { get; set; } = "";
public string ScriptGroupName { get; set; } = "";
public string CronExpression { get; set; } = "";
public DateTime? NextFireTime { get; set; }
public DateTime? PreviousFireTime { get; set; }
public string Description { get; set; } = "";
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Script.Group;
using BetterGenshinImpact.Service.Interface;
using Microsoft.Extensions.Logging;
using Quartz;
namespace BetterGenshinImpact.Service.Quartz;
/// <summary>
/// 脚本执行任务 - 用于 Quartz.NET 调度执行
/// </summary>
public class ScriptExecutionJob : IJob
{
private readonly ILogger<ScriptExecutionJob> _logger;
private readonly IScriptService _scriptService;
public ScriptExecutionJob(ILogger<ScriptExecutionJob> logger, IScriptService scriptService)
{
_logger = logger;
_scriptService = scriptService;
}
public async Task Execute(IJobExecutionContext context)
{
try
{
var jobDataMap = context.JobDetail.JobDataMap;
var scriptGroupName = jobDataMap.GetString("ScriptGroupName");
var scriptGroupData = jobDataMap.GetString("ScriptGroupData");
if (string.IsNullOrEmpty(scriptGroupName) || string.IsNullOrEmpty(scriptGroupData))
{
_logger.LogError("任务执行失败:缺少必要的参数 ScriptGroupName 或 ScriptGroupData");
return;
}
_logger.LogInformation("开始执行定时任务:{ScriptGroupName}", scriptGroupName);
// 反序列化脚本组数据
var scriptGroup = ScriptGroup.FromJson(scriptGroupData);
if (scriptGroup == null)
{
_logger.LogError("任务执行失败:无法反序列化脚本组数据");
return;
}
// 获取有效的项目列表
var enabledProjects = scriptGroup.Projects.Where(p => p.Status == "Enabled").ToList();
if (enabledProjects.Count == 0)
{
_logger.LogWarning("脚本组 {ScriptGroupName} 没有启用的项目", scriptGroupName);
return;
}
// 执行脚本组
await _scriptService.RunMulti(enabledProjects, scriptGroupName, null);
_logger.LogInformation("定时任务执行完成:{ScriptGroupName}", scriptGroupName);
}
catch (Exception ex)
{
_logger.LogError(ex, "定时任务执行异常:{Message}", ex.Message);
}
}
}

View File

@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Core.Script.Group;
@@ -51,6 +52,10 @@ public partial class ScriptControlViewModel : ViewModel
private readonly IScriptService _scriptService;
private readonly Service.Quartz.SchedulerManager? _schedulerManager;
private readonly Service.Quartz.DynamicTaskExampleService? _dynamicTaskService;
/// <summary>
/// 配置组配置
/// </summary>
@@ -76,6 +81,8 @@ public partial class ScriptControlViewModel : ViewModel
{
_snackbarService = snackbarService;
_scriptService = scriptService;
_schedulerManager = App.GetService<Service.Quartz.SchedulerManager>();
_dynamicTaskService = App.GetService<Service.Quartz.DynamicTaskExampleService>();
ScriptGroups.CollectionChanged += ScriptGroupsCollectionChanged;
}
@@ -1712,7 +1719,489 @@ public partial class ScriptControlViewModel : ViewModel
{
RunnerContext.Instance.Reset();
}
}
#region Quartz.NET
/// <summary>
/// 为当前选中的脚本组添加定时任务
/// </summary>
[RelayCommand]
public async Task OnAddScheduledTaskAsync()
{
if (SelectedScriptGroup == null)
{
_snackbarService.Show(
"未选择配置组",
"请先选择一个配置组",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
return;
}
if (_dynamicTaskService == null)
{
_snackbarService.Show(
"服务未初始化",
"Quartz.NET 服务未正确初始化",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(2)
);
return;
}
try
{
// 创建输入对话框
var stackPanel = new StackPanel();
// Cron表达式输入
var cronLabel = new TextBlock { Text = "Cron表达式:", Margin = new Thickness(0, 0, 0, 5) };
var cronTextBox = new TextBox
{
Text = "0 0 0 * * ? *", // 默认每天午夜执行
Margin = new Thickness(0, 0, 0, 10)
};
// 预设选项
var presetLabel = new TextBlock { Text = "或选择预设:", Margin = new Thickness(0, 0, 0, 5) };
var presetComboBox = new ComboBox
{
ItemsSource = new[]
{
new { Text = "每天执行", Value = "0 0 0 * * ? *" },
new { Text = "每小时执行", Value = "0 0 * * * ? *" },
new { Text = "每30分钟执行", Value = "0 0/30 * * * ? *" },
new { Text = "每周一执行", Value = "0 0 0 ? * MON *" },
new { Text = "每月1号执行", Value = "0 0 0 1 * ? *" },
new { Text = "工作日执行", Value = "0 0 0 ? * MON-FRI *" }
},
DisplayMemberPath = "Text",
SelectedValuePath = "Value",
Margin = new Thickness(0, 0, 0, 10)
};
presetComboBox.SelectionChanged += (s, e) =>
{
if (presetComboBox.SelectedValue != null)
{
cronTextBox.Text = presetComboBox.SelectedValue.ToString();
}
};
stackPanel.Children.Add(cronLabel);
stackPanel.Children.Add(cronTextBox);
stackPanel.Children.Add(presetLabel);
stackPanel.Children.Add(presetComboBox);
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "添加定时任务",
Content = stackPanel,
CloseButtonText = "取消",
PrimaryButtonText = "添加",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
var result = await uiMessageBox.ShowDialogAsync();
if (result == MessageBoxResult.Primary)
{
var cronExpression = cronTextBox.Text?.Trim();
if (string.IsNullOrEmpty(cronExpression))
{
_snackbarService.Show(
"参数错误",
"请输入有效的Cron表达式",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
return;
}
bool success = await _dynamicTaskService.AddScriptGroupScheduleAsync(SelectedScriptGroup, cronExpression);
if (success)
{
_snackbarService.Show(
"任务添加成功",
$"已为配置组 {SelectedScriptGroup.Name} 添加定时任务",
ControlAppearance.Success,
null,
TimeSpan.FromSeconds(3)
);
}
else
{
_snackbarService.Show(
"任务添加失败",
"请检查Cron表达式是否正确",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "添加定时任务失败");
_snackbarService.Show(
"添加定时任务失败",
ex.Message,
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
/// <summary>
/// 查看所有定时任务
/// </summary>
[RelayCommand]
public async Task OnViewScheduledTasksAsync()
{
if (_schedulerManager == null)
{
_snackbarService.Show(
"服务未初始化",
"Quartz.NET 服务未正确初始化",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(2)
);
return;
}
try
{
var tasks = await _schedulerManager.GetAllScheduledTasksAsync();
if (tasks.Count == 0)
{
_snackbarService.Show(
"没有定时任务",
"当前没有配置任何定时任务",
ControlAppearance.Info,
null,
TimeSpan.FromSeconds(2)
);
return;
}
// 创建任务列表显示
var stackPanel = new StackPanel();
foreach (var task in tasks.OrderBy(t => t.ScriptGroupName))
{
var taskPanel = new StackPanel
{
Margin = new Thickness(0, 0, 0, 10),
Background = new SolidColorBrush(Colors.LightGray) { Opacity = 0.1 }
};
taskPanel.Children.Add(new TextBlock
{
Text = $"任务名称: {task.JobName}",
FontWeight = FontWeights.Bold,
Margin = new Thickness(5, 5, 5, 0)
});
taskPanel.Children.Add(new TextBlock
{
Text = $"脚本组: {task.ScriptGroupName}",
Margin = new Thickness(5, 0, 5, 0)
});
taskPanel.Children.Add(new TextBlock
{
Text = $"Cron表达式: {task.CronExpression}",
Margin = new Thickness(5, 0, 5, 0)
});
if (task.NextFireTime.HasValue)
{
taskPanel.Children.Add(new TextBlock
{
Text = $"下次执行: {task.NextFireTime:yyyy-MM-dd HH:mm:ss}",
Margin = new Thickness(5, 0, 5, 5)
});
}
stackPanel.Children.Add(taskPanel);
}
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"定时任务列表 ({tasks.Count} 个任务)",
Content = new ScrollViewer
{
Content = stackPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Height = 400,
Width = 600
},
CloseButtonText = "关闭",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
await uiMessageBox.ShowDialogAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "查看定时任务失败");
_snackbarService.Show(
"查看定时任务失败",
ex.Message,
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
/// <summary>
/// 删除定时任务
/// </summary>
[RelayCommand]
public async Task OnRemoveScheduledTasksAsync()
{
if (SelectedScriptGroup == null)
{
_snackbarService.Show(
"未选择配置组",
"请先选择一个配置组",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
return;
}
if (_dynamicTaskService == null)
{
_snackbarService.Show(
"服务未初始化",
"Quartz.NET 服务未正确初始化",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(2)
);
return;
}
try
{
var result = MessageBox.Show(
$"确定要删除配置组 {SelectedScriptGroup.Name} 的所有定时任务吗?",
"删除定时任务",
MessageBoxButton.YesNo,
MessageBoxImage.Question
);
if (result == System.Windows.MessageBoxResult.Yes)
{
int removedCount = await _dynamicTaskService.RemoveScriptGroupSchedulesAsync(SelectedScriptGroup.Name);
_snackbarService.Show(
"删除完成",
$"成功删除 {removedCount} 个定时任务",
ControlAppearance.Success,
null,
TimeSpan.FromSeconds(3)
);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "删除定时任务失败");
_snackbarService.Show(
"删除定时任务失败",
ex.Message,
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
/// <summary>
/// 查看定时任务报告
/// </summary>
[RelayCommand]
public async Task OnViewScheduledTaskReportAsync()
{
if (_dynamicTaskService == null)
{
_snackbarService.Show(
"服务未初始化",
"Quartz.NET 服务未正确初始化",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(2)
);
return;
}
try
{
var report = await _dynamicTaskService.GetScheduledTaskReportAsync();
var stackPanel = new StackPanel();
// 总览信息
stackPanel.Children.Add(new TextBlock
{
Text = "定时任务总览",
FontSize = 16,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 10)
});
stackPanel.Children.Add(new TextBlock
{
Text = $"总任务数: {report.TotalTasks}",
Margin = new Thickness(0, 0, 0, 5)
});
stackPanel.Children.Add(new TextBlock
{
Text = $"活跃任务数: {report.ActiveTasks}",
Margin = new Thickness(0, 0, 0, 10)
});
// 按脚本组分组
if (report.TasksByScriptGroup.Any())
{
stackPanel.Children.Add(new TextBlock
{
Text = "按脚本组分组:",
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 5)
});
foreach (var group in report.TasksByScriptGroup)
{
stackPanel.Children.Add(new TextBlock
{
Text = $" {group.Key}: {group.Value} 个任务",
Margin = new Thickness(10, 0, 0, 2)
});
}
stackPanel.Children.Add(new TextBlock { Text = "", Margin = new Thickness(0, 0, 0, 5) });
}
// 即将执行的任务
if (report.NextExecutions.Any())
{
stackPanel.Children.Add(new TextBlock
{
Text = "即将执行的任务:",
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 5)
});
foreach (var execution in report.NextExecutions)
{
stackPanel.Children.Add(new TextBlock
{
Text = $" {execution.ScriptGroupName} - {execution.NextFireTime:yyyy-MM-dd HH:mm:ss}",
Margin = new Thickness(10, 0, 0, 2)
});
}
}
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "定时任务报告",
Content = new ScrollViewer
{
Content = stackPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Height = 400,
Width = 500
},
CloseButtonText = "关闭",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
await uiMessageBox.ShowDialogAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "查看定时任务报告失败");
_snackbarService.Show(
"查看定时任务报告失败",
ex.Message,
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
/// <summary>
/// 批量添加定时任务示例
/// </summary>
[RelayCommand]
public async Task OnBatchAddScheduledTasksAsync()
{
if (_dynamicTaskService == null)
{
_snackbarService.Show(
"服务未初始化",
"Quartz.NET 服务未正确初始化",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(2)
);
return;
}
try
{
var result = MessageBox.Show(
"这将为所有启用的脚本组添加定时任务(每天午夜执行)。确定继续吗?",
"批量添加定时任务",
MessageBoxButton.YesNo,
MessageBoxImage.Question
);
if (result == System.Windows.MessageBoxResult.Yes)
{
var enabledScriptGroups = ScriptGroups.Where(sg =>
sg.Projects.Any(p => p.Status == "Enabled")).ToList();
int successCount = await _dynamicTaskService.AddMultipleScriptGroupSchedulesAsync(enabledScriptGroups);
_snackbarService.Show(
"批量添加完成",
$"成功添加 {successCount}/{enabledScriptGroups.Count} 个定时任务",
ControlAppearance.Success,
null,
TimeSpan.FromSeconds(3)
);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "批量添加定时任务失败");
_snackbarService.Show(
"批量添加定时任务失败",
ex.Message,
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
#endregion
}