實現Quartz.NET的HTTP作業排程

我只吃饭不洗碗發表於2024-07-03

Quartz.NET作為一個開源的作業排程庫,廣泛應用於.NET應用程式中,以實現複雜的定時任務,本次記錄利用Quartz.NET實現HTTP作業排程,透過自定義HTTP作業,實現對外部API的定時呼叫和如何管理這些作業,包括建立、修改、暫停、恢復和刪除作業。

1.首先定義了一個HttpJob類,該類實現了IJob介面,用於執行HTTP請求。利用了RestRequest來構建請求,並透過靜態字典Delegates儲存每個作業的配置資訊,如URL、請求方法和請求頭等

public class HttpJob : IJob
{
    public static readonly Dictionary<string, HttpJobInfo> Delegates = new();

    public async Task Execute(IJobExecutionContext context)
    {
        var delegateKey = context.JobDetail.JobDataMap.GetString("delegateKey");
        if (delegateKey != null && Delegates.TryGetValue(delegateKey, out var func))
        {
            var requestBody = new RestRequest();
            if (func.Headers != null)
            {
                foreach (var header in func.Headers)
                {
                    requestBody.AddHeader(header.Key, header.Value);
                }
            }

            var content = HttpHelper.HttpRequest(func.Url, func.Request, requestBody);
            JobLogHelper.AddJobLog(new JobLog() { JobName = context.JobDetail.Key.Name, GroupName = context.JobDetail.Key.Group, RunTime = DateTime.Now, RunResult = content });
            UpdateLastExecutionTime(context.JobDetail.Key.Name, context.JobDetail.Key.Group, DateTime.Now);
        }
        await Task.CompletedTask;
    }
}

2.作業資訊的持久化:為了持久化作業資訊,定義了JobInfo類來儲存作業的基本資訊,如名稱、組名、Cron表示式等,並將這些資訊儲存在本地的JSON檔案中。

public class JobInfo
{
    public required string JobName { get; set; }
    public required string GroupName { get; set; }
    public required string CronExpression { get; set; }
    public DateTime LastExecutionTime { get; set; }
    public JobStatus Status { get; set; }
    public required HttpJobInfo HttpJob { get; set; }
}

3.實現了QuartzHelper類,用於管理作業的生命週期。這包括載入作業資訊、建立作業、排程作業、暫停/恢復作業以及刪除作業等功能。

實現Quartz.NET的HTTP作業排程
 public class QuartzHelper
 {
     private IScheduler scheduler;
     private List<JobInfo> jobInfos;

     private string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "jobs.json");

     /// <summary>
     /// 建構函式,初始化定時工作管理員
     /// </summary>
     public QuartzHelper()
     {
         ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
         scheduler = schedulerFactory.GetScheduler().Result;
         scheduler.Start().Wait();
         LoadJobInfosApi().Wait();

     }
     /// <summary>
     /// 儲存作業資訊到本地 JSON 檔案
     /// </summary>
     private void SaveJobInfos()
     {
         string json = JsonConvert.SerializeObject(jobInfos);
         File.WriteAllText(filePath, json);
     }

     /// <summary>
     /// 載入本地 JSON 檔案中的作業資訊
     /// </summary>
     private async Task LoadJobInfosApi()
     {
         if (File.Exists(filePath))
         {
             string json = File.ReadAllText(filePath);
             jobInfos = JsonConvert.DeserializeObject<List<JobInfo>>(json) ?? new List<JobInfo>();
             foreach (var jobInfo in jobInfos)
             {
                
                 // 建立委託的唯一鍵
                 var delegateKey = Guid.NewGuid().ToString();
                 // 將委託儲存在靜態字典中
                 HttpJob.Delegates[delegateKey] = jobInfo.HttpJob;

                 // 建立並排程作業
                 IJobDetail job = JobBuilder.Create<HttpJob>()
                     .WithIdentity(jobInfo.JobName, jobInfo.GroupName).UsingJobData("delegateKey", delegateKey) // 將委託的鍵新增到JobDataMap
                     .Build();

                 ITrigger trigger = TriggerBuilder.Create()
                     .WithIdentity(jobInfo.JobName, jobInfo.GroupName)
                     .WithCronSchedule(jobInfo.CronExpression)
                     //.StartNow()
                     .Build();

                 await scheduler.ScheduleJob(job, trigger);

                 // 根據任務狀態恢復或暫停任務
                 if (jobInfo.Status == JobStatus.正常執行)
                 {
                     await ResumeJob(jobInfo.JobName, jobInfo.GroupName);
                 }
                 else
                 {
                     await PauseJob(jobInfo.JobName, jobInfo.GroupName);
                 }
             }
         }
         else
         {
             jobInfos = new List<JobInfo>();
         }
     }



     #region 執行普通任務時使用,傳委託時可以參考此方法
     ///// <summary>
     ///// 新建任務並立即執行
     ///// </summary>
     //[Obsolete("執行普通任務時使用,可以傳委託使用")]
     //public async Task AddJob(string jobName, string groupName, string cronExpression, Func<bool> func, string description = "")
     //{
     //    if (jobInfos.Any(c => c.JobName == jobName && c.GroupName == groupName))
     //    {
     //        return;
     //    }

     //    // 建立委託的唯一鍵
     //    var delegateKey = Guid.NewGuid().ToString();
     //    // 將委託儲存在靜態字典中
     //   // MyJobClass.Delegates[delegateKey] = func;

     //    // 建立作業資訊並儲存到列表  需要將func 加入到jobInfo 中做作業持久化!!!!
     //    var jobInfo = new JobInfo { JobName = jobName, GroupName = groupName, CronExpression = cronExpression, Status = JobStatus.正常執行, Description = description, JobCreateTime = DateTime.Now };
     //    jobInfos.Add(jobInfo);
     //    SaveJobInfos();

     //    // 建立Quartz作業和觸發器
     //    IJobDetail job = JobBuilder.Create<MyJobClass>()
     //        .WithIdentity(jobName, groupName)
     //        .UsingJobData("delegateKey", delegateKey) // 將委託的鍵新增到JobDataMap
     //        .Build();

     //    ITrigger trigger = TriggerBuilder.Create()
     //        .WithIdentity(jobName + "Trigger", groupName)
     //        .StartNow()
     //        .WithCronSchedule(cronExpression).WithDescription(description)
     //        .Build();

     //    await scheduler.ScheduleJob(job, trigger);

     //}

     #endregion

     /// <summary>
     /// 新建任務並立即執行
     /// </summary>       
   
     public async Task AddJobApi(string jobName, string groupName, string cronExpression, HttpJobInfo httpJobInfo, string description = "")
     {
         if (jobInfos.Any(c => c.JobName == jobName && c.GroupName == groupName))
         {
             return;
         }

         // 建立委託的唯一鍵
         var delegateKey = Guid.NewGuid().ToString();
         // 將委託儲存在靜態字典中
         HttpJob.Delegates[delegateKey] = httpJobInfo;

         // 建立作業資訊並儲存到列表  需要將func 加入到jobInfo 中做作業持久化!!!!
         var jobInfo = new JobInfo { JobName = jobName, GroupName = groupName, CronExpression = cronExpression, HttpJob = httpJobInfo, Status = JobStatus.正常執行, Description = description, JobCreateTime = DateTime.Now };
         jobInfos.Add(jobInfo);
         SaveJobInfos();

         // 建立Quartz作業和觸發器
         IJobDetail job = JobBuilder.Create<HttpJob>()
             .WithIdentity(jobName, groupName)
             .UsingJobData("delegateKey", delegateKey) // 將委託的鍵新增到JobDataMap
             .Build();

         ITrigger trigger = TriggerBuilder.Create()
             .WithIdentity(jobName + "Trigger", groupName)
             .StartNow()
             .WithCronSchedule(cronExpression).WithDescription(description)
             .Build();

         await scheduler.ScheduleJob(job, trigger);

     }


     /// <summary>
     /// 暫停任務
     /// </summary>
     public async Task PauseJob(string jobName, string groupName)
     {
         await scheduler.PauseJob(new JobKey(jobName, groupName));
         var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName == groupName);
         if (job != null)
         {
             job.Status = JobStatus.暫停;
             SaveJobInfos();
         }
     }

     /// <summary>
     /// 開啟任務
     /// </summary>
     public async Task ResumeJob(string jobName, string groupName)
     {
         await scheduler.ResumeJob(new JobKey(jobName, groupName));
         var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName == groupName);
         if (job != null)
         {
             job.Status = JobStatus.正常執行;
             SaveJobInfos();
         }
     }

     /// <summary>
     /// 立即執行任務
     /// </summary>
     public async Task TriggerJob(string jobName, string groupName)
     {
         await scheduler.TriggerJob(new JobKey(jobName, groupName));
         var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName == groupName);
         if (job != null)
         {
             job.LastExecutionTime = DateTime.Now;
             SaveJobInfos();
         }
     }


     /// <summary>
     /// 修改任務
     /// </summary>
     public async Task ModifyJob(string jobName, string groupName, string cronExpression, HttpJobInfo httpJobInfo, string description = "")
     {
         await DeleteJob(jobName, groupName);
         await AddJobApi(jobName, groupName, cronExpression, httpJobInfo, description);
     }
     /// <summary>
     /// 刪除任務
     /// </summary>
     public async Task DeleteJob(string jobName, string groupName)
     {
         await scheduler.DeleteJob(new JobKey(jobName, groupName));
         jobInfos.RemoveAll(j => j.JobName == jobName && j.GroupName == groupName);
         SaveJobInfos();
     }

     /// <summary>
     /// 獲取當前所有任務列表
     /// </summary>
     public List<JobInfo> GetAllJobs()
     {
         if (File.Exists(filePath))
         {
             string json = File.ReadAllText(filePath);
             jobInfos = JsonConvert.DeserializeObject<List<JobInfo>>(json) ?? new List<JobInfo>();
             return jobInfos;
         }
         else
             return null;
         
     }


 }
QuartzHelper

4.為了跟蹤作業的執行情況,設計了JobLog類和JobLogHelper類,用於記錄和查詢作業執行日誌。

實現Quartz.NET的HTTP作業排程
public class JobLogHelper
{
    private static string _filePath;

    /// <summary>
    /// 根據作業名稱和組名稱獲取當日的作業執行日誌
    /// </summary>
    /// <param name="jobName"></param>
    /// <param name="groupName"></param>
    /// <returns></returns>
    public static List<JobLog> GetJobLog(string jobName, string groupName)
    {
        _filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"jobsLog-{DateTime.Now:yyyyMMdd}.json");

        // 檢查檔案是否存在
        if (!File.Exists(_filePath))
        {
            return new List<JobLog>();
        }
        var jsonText = $"[{File.ReadAllText(_filePath)}]";
        var list = JsonConvert.DeserializeObject<List<JobLog>>(jsonText);
        if (list != null)
        {
            var result = list.Where(c => c.JobName == jobName && groupName == c.GroupName).OrderByDescending(c => c.RunTime).ToList();
            return result;
        }

        return null;
    }
    /// <summary>
    ///獲取所有的 作業執行日誌  //可以從這裡擴充其他查詢條件
    /// </summary>
    /// <returns></returns>
    public static List<JobLog> GetAllLogs()
    {
        List<JobLog> jobLogs = new List<JobLog>();
        var logFilePaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "jobsLog-*.json");
        logFilePaths.ToList().ForEach(c =>
        {
            var jsonText = $"[{File.ReadAllText(_filePath)}]";
            var list = JsonConvert.DeserializeObject<List<JobLog>>(jsonText);
            if (list != null) jobLogs.AddRange(list);
        });
        return jobLogs;
    }
    /// <summary>
    /// 新增作業執行日誌
    /// </summary>
    /// <param name="jobLog"></param>
    public static void AddJobLog(JobLog jobLog)
    {
        _filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"jobsLog-{DateTime.Now:yyyyMMdd}.json");
        string json = JsonConvert.SerializeObject(jobLog) + ",\n";
        File.AppendAllText(_filePath, json);
    }
}
作業執行日誌

5.最後,透過ASP.NET Core的Controller提供了一系列Web API介面,以便於透過HTTP請求管理作業。這些介面包括獲取作業列表、新增作業、修改作業、刪除作業、暫停作業、恢復作業和立即執行作業等。

實現Quartz.NET的HTTP作業排程
 [Route("api/[controller]")]
 [ApiController]
 public class QuartzController : ControllerBase
 {
     private readonly QuartzHelper _quartzHelper;
     public QuartzController(QuartzHelper quartzHelper)
     {
         _quartzHelper = quartzHelper;
     }

     [HttpGet]
     [Route("job/GetJobs")]
     public object GetJobs()
     {
         return Ok(new {code=200,data = _quartzHelper.GetAllJobs() });
     }

     [HttpGet]
     [Route("job/GetJobLog")]
     public object GetJobLog(string jobName, string groupName)
     {
         return Ok(new { code = 200, data = JobLogHelper.GetJobLog(jobName, groupName) });         
     }
     [HttpGet]
     [Route("job/GetJobLogs")]
     public object GetJobLogs()
     {
         return Ok(new { code = 200, data = JobLogHelper.GetAllLogs() });
     }


     [HttpPost]
     [Route("job/AddJob")]
     public async Task<object> Add(JobInfo jobInfo)
     {
         try
         {
             await _quartzHelper.AddJobApi(jobInfo.JobName, jobInfo.GroupName, jobInfo.CronExpression, jobInfo.HttpJob, jobInfo.Description);
             return Ok(new { code = 200, msg = "建立成功!" });
         }
         catch (Exception ex)
         {
             return Ok(new { code = 500, msg = ex.Message });
         }
     }

     [HttpPost]
     [Route("job/ModifyJob")]
     public async Task<object> Edit(JobInfo jobInfo)
     {
         try
         {
             await _quartzHelper.ModifyJob(jobInfo.JobName, jobInfo.GroupName, jobInfo.CronExpression, jobInfo.HttpJob, jobInfo.Description);
             return Ok(new { code = 200, msg = "修改成功!" });
         }
         catch (Exception ex)
         {
             return Ok(new { code = 500, msg = ex.Message });
         }
     }

     [HttpGet]
     [Route("job/DeleteJob")]
     public async Task<object> Delete(string jobName, string groupName)
     {
         try
         {
             await _quartzHelper.DeleteJob(jobName, groupName);
             return Ok(new { code = 200, msg = "刪除成功!" });
         }
         catch (Exception ex)
         {
             return Ok(new { code = 500, msg = ex.Message });
         }
     }

     [HttpGet]
     [Route("job/PauseJob")]
     public async Task<object> PauseJob(string jobName, string groupName)
     {
         try
         {
             await _quartzHelper.PauseJob(jobName, groupName);
             return Ok(new { code = 200, msg = "暫停成功!" });
         }
         catch (Exception ex)
         {
             return Ok(new { code = 500, msg = ex.Message });
         }
     }

     [HttpGet]
     [Route("job/ResumeJob")]
     public async Task<object> ResumeJob(string jobName, string groupName)
     {
         try
         {
             await _quartzHelper.ResumeJob(jobName, groupName);
             return Ok(new { code = 200, msg = "開啟任務成功!" });
         }
         catch (Exception ex)
         {
             return Ok(new { code = 500, msg = ex.Message });
         }
     }
     [HttpGet]
     [Route("job/TriggerJob")]
     public async Task<object> TriggerJob(string jobName, string groupName)
     {
         try
         {
             await _quartzHelper.TriggerJob(jobName, groupName);
             return Ok(new { code = 200, msg = "立即執行任務命令已執行!" });
         }
         catch (Exception ex)
         {
             return Ok(new { code = 500, msg = ex.Message });
         }
     }
 }
Web API介面

原始碼地址:https://github.com/yycb1994/Quartz.Net

相關文章