diff --git a/BetterGenshinImpact/App.xaml.cs b/BetterGenshinImpact/App.xaml.cs index abf005e1..7b38332f 100644 --- a/BetterGenshinImpact/App.xaml.cs +++ b/BetterGenshinImpact/App.xaml.cs @@ -129,6 +129,18 @@ public partial class App : Application services.AddSingleton(sp=> sp.GetRequiredService().Config.HardwareAccelerationConfig); services.AddSingleton(); + // Quartz.NET 服务 + services.AddQuartz(q => + { + // 使用内存存储 + q.UseInMemoryStore(); + // 使用默认线程池 + q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 10); + }); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + // Configuration //services.Configure(context.Configuration.GetSection(nameof(AppConfig))); } diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index 5f41163e..d1f8c910 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -85,6 +85,8 @@ + + diff --git a/BetterGenshinImpact/Examples/QuartzExampleProgram.cs b/BetterGenshinImpact/Examples/QuartzExampleProgram.cs new file mode 100644 index 00000000..e368ff4d --- /dev/null +++ b/BetterGenshinImpact/Examples/QuartzExampleProgram.cs @@ -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; + +/// +/// Quartz.NET 动态任务管理示例程序 +/// 演示如何在控制台应用中使用调度功能 +/// +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(); + var dynamicTaskService = host.Services.GetRequiredService(); + + // 运行示例 + 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(); + services.AddSingleton(); + services.AddHostedService(); + + // 注册模拟的脚本服务 + services.AddSingleton(); + }); + + 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 { 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; + } +} + +/// +/// 模拟脚本服务,用于演示 +/// +public class MockScriptService : BetterGenshinImpact.Service.Interface.IScriptService +{ + private readonly ILogger _logger; + + public MockScriptService(ILogger logger) + { + _logger = logger; + } + + public async Task RunMulti(List 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); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/Quartz/CronExpressionHelper.cs b/BetterGenshinImpact/Service/Quartz/CronExpressionHelper.cs new file mode 100644 index 00000000..4ca88f6d --- /dev/null +++ b/BetterGenshinImpact/Service/Quartz/CronExpressionHelper.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace BetterGenshinImpact.Service.Quartz; + +/// +/// Cron 表达式帮助类 +/// 提供常用的 Cron 表达式生成和解析功能 +/// +public static class CronExpressionHelper +{ + /// + /// 预定义的 Cron 表达式集合 + /// + public static readonly Dictionary 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点 + }; + + /// + /// 创建每日执行的 Cron 表达式 + /// + /// 小时 (0-23) + /// 分钟 (0-59) + /// 秒 (0-59,默认0) + /// Cron 表达式 + public static string CreateDaily(int hour, int minute, int second = 0) + { + ValidateTime(hour, minute, second); + return $"{second} {minute} {hour} * * ? *"; + } + + /// + /// 创建每周执行的 Cron 表达式 + /// + /// 星期几 (1=周一, 7=周日) + /// 小时 (0-23) + /// 分钟 (0-59) + /// 秒 (0-59,默认0) + /// Cron 表达式 + 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} *"; + } + + /// + /// 创建每月执行的 Cron 表达式 + /// + /// 月中的第几天 (1-31) + /// 小时 (0-23) + /// 分钟 (0-59) + /// 秒 (0-59,默认0) + /// Cron 表达式 + 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} * ? *"; + } + + /// + /// 创建间隔执行的 Cron 表达式 + /// + /// 间隔分钟数 + /// 开始分钟 (默认0) + /// Cron 表达式 + 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} * * * ? *"; + } + + /// + /// 创建工作日执行的 Cron 表达式 + /// + /// 小时 (0-23) + /// 分钟 (0-59) + /// 秒 (0-59,默认0) + /// Cron 表达式 + public static string CreateWorkdays(int hour, int minute, int second = 0) + { + ValidateTime(hour, minute, second); + return $"{second} {minute} {hour} ? * MON-FRI *"; + } + + /// + /// 创建周末执行的 Cron 表达式 + /// + /// 小时 (0-23) + /// 分钟 (0-59) + /// 秒 (0-59,默认0) + /// Cron 表达式 + public static string CreateWeekends(int hour, int minute, int second = 0) + { + ValidateTime(hour, minute, second); + return $"{second} {minute} {hour} ? * SAT,SUN *"; + } + + /// + /// 创建多个时间点执行的 Cron 表达式 + /// + /// 小时数组 + /// 分钟 + /// 秒 (默认0) + /// Cron 表达式 + 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} * * ? *"; + } + + /// + /// 解析 Cron 表达式为人类可读的描述 + /// + /// Cron 表达式 + /// 描述文本 + 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 + { + { "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表达式"; + } + } + + /// + /// 验证 Cron 表达式是否有效 + /// + /// Cron 表达式 + /// 是否有效 + 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; + } + } + + /// + /// 获取 Cron 表达式的下次执行时间 + /// + /// Cron 表达式 + /// 起始时间(可选,默认为当前时间) + /// 下次执行时间,如果表达式无效则返回null + 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; + } + } + + /// + /// 获取 Cron 表达式的多个执行时间 + /// + /// Cron 表达式 + /// 获取的执行时间数量 + /// 起始时间(可选,默认为当前时间) + /// 执行时间列表 + public static List GetNextExecutionTimes(string cronExpression, int count, DateTime? fromTime = null) + { + var times = new List(); + + 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)); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/Quartz/DynamicTaskExampleService.cs b/BetterGenshinImpact/Service/Quartz/DynamicTaskExampleService.cs new file mode 100644 index 00000000..b711e1a3 --- /dev/null +++ b/BetterGenshinImpact/Service/Quartz/DynamicTaskExampleService.cs @@ -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; + +/// +/// 动态任务管理示例服务 +/// 提供动态添加、删除、管理定时任务的示例方法 +/// +public class DynamicTaskExampleService +{ + private readonly SchedulerManager _schedulerManager; + private readonly ILogger _logger; + + public DynamicTaskExampleService(SchedulerManager schedulerManager, ILogger logger) + { + _schedulerManager = schedulerManager; + _logger = logger; + } + + /// + /// 示例:为脚本组添加定时任务 + /// + /// 脚本组 + /// Cron表达式 + /// 任务是否添加成功 + public async Task 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; + } + } + + /// + /// 示例:批量添加多个脚本组的定时任务 + /// + /// 脚本组列表 + /// 默认Cron表达式 + /// 成功添加的任务数量 + public async Task AddMultipleScriptGroupSchedulesAsync(List 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; + } + + /// + /// 示例:根据脚本组名称删除定时任务 + /// + /// 脚本组名称 + /// 删除的任务数量 + public async Task 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; + } + } + + /// + /// 示例:创建每日执行的定时任务 + /// + /// 脚本组 + /// 执行小时(0-23) + /// 执行分钟(0-59) + /// 任务是否创建成功 + public async Task 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); + } + + /// + /// 示例:创建每周执行的定时任务 + /// + /// 脚本组 + /// 星期几(1=周一,7=周日) + /// 执行小时(0-23) + /// 执行分钟(0-59) + /// 任务是否创建成功 + public async Task 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); + } + + /// + /// 示例:创建间隔执行的定时任务 + /// + /// 脚本组 + /// 间隔分钟数 + /// 任务是否创建成功 + public async Task CreateIntervalTaskAsync(ScriptGroup scriptGroup, int intervalMinutes) + { + if (intervalMinutes <= 0) + { + _logger.LogError("间隔时间无效:{IntervalMinutes} 分钟", intervalMinutes); + return false; + } + + var cronExpression = $"0 0/{intervalMinutes} * * * ? *"; + return await AddScriptGroupScheduleAsync(scriptGroup, cronExpression); + } + + /// + /// 示例:获取定时任务报告 + /// + /// 定时任务报告 + public async Task 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(); + } + } + + /// + /// 示例:清理所有定时任务 + /// + /// 清理的任务数量 + public async Task 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; + } + } +} + +/// +/// 定时任务报告 +/// +public class ScheduledTaskReport +{ + public int TotalTasks { get; set; } + public int ActiveTasks { get; set; } + public Dictionary TasksByScriptGroup { get; set; } = new(); + public List NextExecutions { get; set; } = new(); +} + +/// +/// 下次执行信息 +/// +public class NextExecution +{ + public string JobName { get; set; } = ""; + public string ScriptGroupName { get; set; } = ""; + public DateTime NextFireTime { get; set; } + public string CronExpression { get; set; } = ""; +} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/Quartz/QuartzHostedService.cs b/BetterGenshinImpact/Service/Quartz/QuartzHostedService.cs new file mode 100644 index 00000000..89a92cf7 --- /dev/null +++ b/BetterGenshinImpact/Service/Quartz/QuartzHostedService.cs @@ -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; + +/// +/// Quartz.NET 调度服务 - 管理定时任务的生命周期 +/// +public class QuartzHostedService : IHostedService +{ + private readonly IScheduler _scheduler; + private readonly ILogger _logger; + + public QuartzHostedService(IScheduler scheduler, ILogger 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); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/Quartz/README.md b/BetterGenshinImpact/Service/Quartz/README.md new file mode 100644 index 00000000..c040a99a --- /dev/null +++ b/BetterGenshinImpact/Service/Quartz/README.md @@ -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(); +var dynamicTaskService = App.GetService(); + +// 添加每日执行的定时任务 +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(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 界面,用户可以轻松实现脚本组的自动化调度执行。该集成保持了原有功能的完整性,同时扩展了应用的自动化能力。 \ No newline at end of file diff --git a/BetterGenshinImpact/Service/Quartz/SchedulerManager.cs b/BetterGenshinImpact/Service/Quartz/SchedulerManager.cs new file mode 100644 index 00000000..8411cfd4 --- /dev/null +++ b/BetterGenshinImpact/Service/Quartz/SchedulerManager.cs @@ -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; + +/// +/// 调度管理器 - 管理动态添加和删除定时任务 +/// +public class SchedulerManager +{ + private readonly IScheduler _scheduler; + private readonly ILogger _logger; + + public SchedulerManager(IScheduler scheduler, ILogger logger) + { + _scheduler = scheduler; + _logger = logger; + } + + /// + /// 动态添加定时任务 + /// + /// 脚本组 + /// Cron表达式 + /// 任务名称(可选) + /// 任务是否添加成功 + public async Task 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() + .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; + } + } + + /// + /// 删除定时任务 + /// + /// 任务名称 + /// 任务是否删除成功 + public async Task 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; + } + } + + /// + /// 获取所有定时任务 + /// + /// 任务列表 + public async Task> GetAllScheduledTasksAsync() + { + try + { + var jobKeys = await _scheduler.GetJobKeys(GroupMatcher.GroupEquals("ScriptGroup")); + var tasks = new List(); + + 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(); + } + } + + /// + /// 更新定时任务的Cron表达式 + /// + /// 任务名称 + /// 新的Cron表达式 + /// 任务是否更新成功 + public async Task 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; + } + } + + /// + /// 暂停定时任务 + /// + /// 任务名称 + /// 任务是否暂停成功 + public async Task 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; + } + } + + /// + /// 恢复定时任务 + /// + /// 任务名称 + /// 任务是否恢复成功 + public async Task 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; + } + } + + /// + /// 根据脚本组配置转换为Cron表达式 + /// + /// 调度配置 + /// Cron表达式 + 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表达式 + }; + } +} + +/// +/// 定时任务信息 +/// +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; } = ""; +} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/Quartz/ScriptExecutionJob.cs b/BetterGenshinImpact/Service/Quartz/ScriptExecutionJob.cs new file mode 100644 index 00000000..f0f1c70b --- /dev/null +++ b/BetterGenshinImpact/Service/Quartz/ScriptExecutionJob.cs @@ -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; + +/// +/// 脚本执行任务 - 用于 Quartz.NET 调度执行 +/// +public class ScriptExecutionJob : IJob +{ + private readonly ILogger _logger; + private readonly IScriptService _scriptService; + + public ScriptExecutionJob(ILogger 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); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs index e965c9f7..4b9a3271 100644 --- a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs @@ -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; + /// /// 配置组配置 /// @@ -76,6 +81,8 @@ public partial class ScriptControlViewModel : ViewModel { _snackbarService = snackbarService; _scriptService = scriptService; + _schedulerManager = App.GetService(); + _dynamicTaskService = App.GetService(); ScriptGroups.CollectionChanged += ScriptGroupsCollectionChanged; } @@ -1712,7 +1719,489 @@ public partial class ScriptControlViewModel : ViewModel { RunnerContext.Instance.Reset(); } - - } + + #region Quartz.NET 动态任务管理示例 + + /// + /// 为当前选中的脚本组添加定时任务 + /// + [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) + ); + } + } + + /// + /// 查看所有定时任务 + /// + [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) + ); + } + } + + /// + /// 删除定时任务 + /// + [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) + ); + } + } + + /// + /// 查看定时任务报告 + /// + [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) + ); + } + } + + /// + /// 批量添加定时任务示例 + /// + [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 } \ No newline at end of file