Wms/Utility/Entity/JobBriefInfoEntity.cs
New file @@ -0,0 +1,89 @@ using Quartz; using System; using System.Collections.Generic; using System.Text; namespace Utility.Entity { public class JobBriefInfoEntity { /// <summary> /// 任务组名 /// </summary> public string GroupName { get; set; } /// <summary> /// 任务信息 /// </summary> public List<JobBriefInfo> JobInfoList { get; set; } = new List<JobBriefInfo>(); } public class JobBriefInfo { /// <summary> /// 任务名称 /// </summary> public string Name { get; set; } /// <summary> /// 下次执行时间 /// </summary> public DateTime? NextFireTime { get; set; } /// <summary> /// 上次执行时间 /// </summary> public DateTime? PreviousFireTime { get; set; } /// <summary> /// 上次执行的异常信息 /// </summary> public string LastErrMsg { get; set; } /// <summary> /// 任务状态 /// </summary> public TriggerState TriggerState { get; set; } /// <summary> /// 显示状态 /// </summary> public string DisplayState { get { var state = string.Empty; switch (TriggerState) { case TriggerState.Normal: state = "正常"; break; case TriggerState.Paused: state = "暂停"; break; case TriggerState.Complete: state = "完成"; break; case TriggerState.Error: state = "异常"; break; case TriggerState.Blocked: state = "阻塞"; break; case TriggerState.None: state = "不存在"; break; default: state = "未知"; break; } return state; } } /// <summary> /// 已经执行次数 /// </summary> public long RunNumber { get; set; } } } Wms/Utility/Entity/JobInfoEntity.cs
New file @@ -0,0 +1,115 @@ using Quartz; using System; using System.Collections.Generic; using System.Text; namespace Utility.Entity { public class JobInfoEntity { /// <summary> /// 任务组名 /// </summary> public string GroupName { get; set; } /// <summary> /// 任务信息 /// </summary> public List<JobInfo> JobInfoList { get; set; } = new List<JobInfo>(); } public class JobInfo { /// <summary> /// 任务名称 /// </summary> public string Name { get; set; } /// <summary> /// 下次执行时间 /// </summary> public DateTime? NextFireTime { get; set; } /// <summary> /// 上次执行时间 /// </summary> public DateTime? PreviousFireTime { get; set; } /// <summary> /// 开始时间 /// </summary> public DateTime BeginTime { get; set; } /// <summary> /// 结束时间 /// </summary> public DateTime? EndTime { get; set; } /// <summary> /// 上次执行的异常信息 /// </summary> public string LastErrMsg { get; set; } /// <summary> /// 任务状态 /// </summary> public TriggerState TriggerState { get; set; } /// <summary> /// 描述 /// </summary> public string Description { get; set; } /// <summary> /// 显示状态 /// </summary> public string DisplayState { get { var state = string.Empty; switch (TriggerState) { case TriggerState.Normal: state = "正常"; break; case TriggerState.Paused: state = "暂停"; break; case TriggerState.Complete: state = "完成"; break; case TriggerState.Error: state = "异常"; break; case TriggerState.Blocked: state = "阻塞"; break; case TriggerState.None: state = "不存在"; break; default: state = "未知"; break; } return state; } } /// <summary> /// 时间间隔 /// </summary> public string Interval { get; set; } /// <summary> /// 触发地址 /// </summary> public string TriggerAddress { get; set; } public string RequestType { get; set; } /// <summary> /// 已经执行的次数 /// </summary> public long RunNumber { get; set; } public long JobType { get; set; } } } Wms/Utility/Entity/LogModel.cs
New file @@ -0,0 +1,50 @@ using System; using System.Collections.Generic; using System.Text; namespace Utility.Entity { public abstract class LogModel { /// <summary> /// 开始执行时间 /// </summary> public string BeginTime { get; set; } /// <summary> /// 结束时间 /// </summary> public string EndTime { get; set; } /// <summary> /// 耗时(秒) /// </summary> public string ExecuteTime { get; set; } /// <summary> /// 任务名称 /// </summary> public string JobName { get; set; } /// <summary> /// 结果 /// </summary> public string Result { get; set; } /// <summary> /// 异常消息 /// </summary> public string ErrorMsg { get; set; } } public class LogUrlModel : LogModel { /// <summary> /// 请求地址 /// </summary> public string Url { get; set; } /// <summary> /// 请求类型 /// </summary> public string RequestType { get; set; } /// <summary> /// 请求参数 /// </summary> public string Parameters { get; set; } } } Wms/Utility/Entity/ScheduleEntity.cs
New file @@ -0,0 +1,78 @@ using System; using System.Collections.Generic; using System.Text; namespace Utility { public class ScheduleEntity { /// <summary> /// 任务名称 /// </summary> public string JobName { get; set; } /// <summary> /// 任务分组 /// </summary> public string JobGroup { get; set; } /// <summary> /// 任务类型 /// </summary> public JobTypeEnum JobType { get; set; } = JobTypeEnum.Url; /// <summary> /// 开始时间 /// </summary> public DateTimeOffset BeginTime { get; set; } = DateTime.Now; /// <summary> /// 结束时间 /// </summary> public DateTimeOffset? EndTime { get; set; } /// <summary> /// Cron表达式 /// </summary> public string Cron { get; set; } /// <summary> /// 执行次数(默认无限循环) /// </summary> public int? RunTimes { get; set; } /// <summary> /// 执行间隔时间,单位毫秒(如果有Cron,则IntervalSecond失效) /// </summary> public int? IntervalMilliseconds { get; set; } /// <summary> /// 触发器类型 /// </summary> public TriggerTypeEnum TriggerType { get; set; } /// <summary> /// 描述 /// </summary> public string Description { get; set; } #region Url /// <summary> /// 请求url /// </summary> public string RequestUrl { get; set; } /// <summary> /// 请求参数(Post,Put请求用) /// </summary> public string RequestParameters { get; set; } /// <summary> /// Headers(可以包含如:Authorization授权认证) /// 格式:{"Authorization":"userpassword.."} /// </summary> public string Headers { get; set; } /// <summary> /// 请求类型 /// </summary> public RequestTypeEnum RequestType { get; set; } = RequestTypeEnum.Post; #endregion } public class ModifyJobInput { public ScheduleEntity NewScheduleEntity { get; set; } public ScheduleEntity OldScheduleEntity { get; set; } } } Wms/Utility/Enum/JobTypeEnum.cs
New file @@ -0,0 +1,23 @@ using System; using System.Collections.Generic; using System.Text; namespace Utility { public enum JobTypeEnum { None = 0, /// <summary> /// 网络请求 /// </summary> Url = 1, /// <summary> /// 内置 /// </summary> BuiltIn = 2, /// <summary> /// 热加载 /// </summary> Hotreload = 3, } } Wms/Utility/Enum/RequestTypeEnum.cs
New file @@ -0,0 +1,15 @@ using System; using System.Collections.Generic; using System.Text; namespace Utility { public enum RequestTypeEnum { None = 0, Get = 1, Post = 2, Put = 4, Delete = 8 } } Wms/Utility/Enum/TriggerTypeEnum.cs
New file @@ -0,0 +1,13 @@ using System; using System.Collections.Generic; using System.Text; namespace Utility { public enum TriggerTypeEnum { None = 0, Cron = 1, Simple = 2, } } Wms/Utility/Job/Constant.cs
New file @@ -0,0 +1,61 @@ using System; using System.Collections.Generic; using System.Text; namespace Utility.Job { public class Constant { /// <summary> /// 请求url RequestUrl /// </summary> public const string REQUESTURL = "RequestUrl"; /// <summary> /// 请求参数 RequestParameters /// </summary> public const string REQUESTPARAMETERS = "RequestParameters"; /// <summary> /// Headers(可以包含:Authorization授权认证) /// </summary> public const string HEADERS = "Headers"; /// <summary> /// 是否发送邮件 /// </summary> public const string MAILMESSAGE = "MailMessage"; /// <summary> /// 请求类型 RequestType /// </summary> public const string REQUESTTYPE = "RequestType"; /// <summary> /// 日志 LogList /// </summary> public const string LOGLIST = "LogList"; /// <summary> /// 异常 Exception /// </summary> public const string EXCEPTION = "Exception"; /// <summary> /// 执行次数 /// </summary> public const string RUNNUMBER = "RunNumber"; public const string MailTitle = "MailTitle"; public const string MailContent = "MailContent"; public const string MailTo = "MailTo"; public const string JobTypeEnum = "JobTypeEnum"; public const string EndAt = "EndAt"; public const string ClientID = "ClientID"; public const string Host = "Host"; public const string Password = "Password"; public const string Port = "Port"; public const string UserName = "UserName"; public static string Topic = "Topic"; public static string Payload = "Payload"; public static string BuiltIn = "BuiltIn"; } } Wms/Utility/Job/HostedService.cs
New file @@ -0,0 +1,30 @@ using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using System.Threading; namespace Utility.Job { public class HostedService : IHostedService { private SchedulerCenter schedulerCenter; public HostedService(SchedulerCenter schedulerCenter) { this.schedulerCenter = schedulerCenter; } public async Task StartAsync(CancellationToken cancellationToken) { //开启调度器 await schedulerCenter.StartScheduleAsync(); } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } } Wms/Utility/Job/HttpHelper.cs
New file @@ -0,0 +1,181 @@ using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Net.Http.Headers; namespace Utility.Job { /// <summary> /// 请求帮助类 /// </summary> public class HttpHelper { public static readonly HttpHelper Instance; static HttpHelper() { Instance = new HttpHelper(); } /// <summary> /// 不同url分配不同HttpClient /// </summary> public static ConcurrentDictionary<string, HttpClient> dictionary = new ConcurrentDictionary<string, HttpClient>(); private HttpClient GetHttpClient(string url) { var uri = new Uri(url); var key = uri.Scheme + uri.Host; //if (!dictionary.Keys.Contains(key)) return dictionary.GetOrAdd(key, new HttpClient()); //return dictionary[key]; } /// <summary> /// Post请求 /// </summary> /// <param name="url">url地址</param> /// <param name="jsonString">请求参数(Json字符串)</param> /// <param name="headers">webapi做用户认证</param> /// <returns></returns> public async Task<HttpResponseMessage> PostAsync(string url, string jsonString, Dictionary<string, string> headers = null) { if (string.IsNullOrWhiteSpace(jsonString)) jsonString = "{}"; StringContent content = new StringContent(jsonString); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); if (headers != null && headers.Count > 0) { //如果有headers认证等信息,则每个请求实例一个HttpClient using (HttpClient http = new HttpClient()) { foreach (var item in headers) { http.DefaultRequestHeaders.Remove(item.Key); http.DefaultRequestHeaders.TryAddWithoutValidation(item.Key, item.Value); } return await http.PostAsync(new Uri(url), content); } } else { return await GetHttpClient(url).PostAsync(new Uri(url), content); } } /// <summary> /// Post请求 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url">url地址</param> /// <param name="content">请求参数</param> /// <param name="headers">webapi做用户认证</param> /// <returns></returns> public async Task<HttpResponseMessage> PostAsync<T>(string url, T content, Dictionary<string, string> headers = null) where T : class { return await PostAsync(url, JsonConvert.SerializeObject(content), headers); } /// <summary> /// Get请求 /// </summary> /// <param name="url">url地址</param> /// <param name="headers">webapi做用户认证</param> /// <returns></returns> public async Task<HttpResponseMessage> GetAsync(string url, Dictionary<string, string> headers = null) { if (headers != null && headers.Count > 0) { //如果有headers认证等信息,则每个请求实例一个HttpClient using (HttpClient http = new HttpClient()) { foreach (var item in headers) { http.DefaultRequestHeaders.Remove(item.Key); http.DefaultRequestHeaders.TryAddWithoutValidation(item.Key, item.Value); } return await http.GetAsync(url); } } else { return await GetHttpClient(url).GetAsync(url); } } /// <summary> /// Put请求 /// </summary> /// <param name="url">url地址</param> /// <param name="jsonString">请求参数(Json字符串)</param> /// <param name="headers">webapi做用户认证</param> /// <returns></returns> public async Task<HttpResponseMessage> PutAsync(string url, string jsonString, Dictionary<string, string> headers = null) { if (string.IsNullOrWhiteSpace(jsonString)) jsonString = "{}"; StringContent content = new StringContent(jsonString); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); if (headers != null && headers.Count > 0) { //如果有headers认证等信息,则每个请求实例一个HttpClient using (HttpClient http = new HttpClient()) { foreach (var item in headers) { http.DefaultRequestHeaders.Remove(item.Key); http.DefaultRequestHeaders.TryAddWithoutValidation(item.Key, item.Value); } return await http.PutAsync(url, content); } } else { return await GetHttpClient(url).PutAsync(url, content); } } /// <summary> /// Put请求 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url">url地址</param> /// <param name="content">请求参数</param> /// <param name="headers">webapi做用户认证</param> /// <returns></returns> public async Task<HttpResponseMessage> PutAsync<T>(string url, T content, Dictionary<string, string> headers = null) { return await PutAsync(url, JsonConvert.SerializeObject(content), headers); } /// <summary> /// Delete请求 /// </summary> /// <param name="url"></param> /// <param name="headers">webapi做用户认证</param> /// <returns></returns> public async Task<HttpResponseMessage> DeleteAsync(string url, Dictionary<string, string> headers = null) { if (headers != null && headers.Count > 0) { //如果有headers认证等信息,则每个请求实例一个HttpClient using (HttpClient http = new HttpClient()) { foreach (var item in headers) { http.DefaultRequestHeaders.Remove(item.Key); http.DefaultRequestHeaders.TryAddWithoutValidation(item.Key, item.Value); } return await http.DeleteAsync(url); } } else { return await GetHttpClient(url).DeleteAsync(url); } } } } Wms/Utility/Job/HttpJob.cs
New file @@ -0,0 +1,86 @@ using Newtonsoft.Json; using Quartz; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Web; using Utility.Tools; using Utility.Entity; using Serilog; using Talk.Extensions; namespace Utility.Job { public class HttpJob : JobBase<LogUrlModel>, IJob { public HttpJob(ILogger logger) : base(new LogUrlModel(), logger) { } public override async Task NextExecute(IJobExecutionContext context) { //获取相关参数 var requestUrl = context.JobDetail.JobDataMap.GetString(Constant.REQUESTURL)?.Trim(); requestUrl = requestUrl?.IndexOf("http") == 0 ? requestUrl : "http://" + requestUrl; var requestParameters = context.JobDetail.JobDataMap.GetString(Constant.REQUESTPARAMETERS); var headersString = context.JobDetail.JobDataMap.GetString(Constant.HEADERS); var headers = headersString != null ? JsonConvert.DeserializeObject<Dictionary<string, string>>(headersString?.Trim()) : null; var requestType = (RequestTypeEnum)int.Parse(context.JobDetail.JobDataMap.GetString(Constant.REQUESTTYPE)); LogInfo.Url = requestUrl; LogInfo.RequestType = requestType.ToString(); LogInfo.Parameters = requestParameters; HttpResponseMessage response = new HttpResponseMessage(); var http = HttpHelper.Instance; switch (requestType) { case RequestTypeEnum.Get: response = await http.GetAsync(requestUrl, headers); break; case RequestTypeEnum.Post: response = await http.PostAsync(requestUrl, requestParameters, headers); break; case RequestTypeEnum.Put: response = await http.PutAsync(requestUrl, requestParameters, headers); break; case RequestTypeEnum.Delete: response = await http.DeleteAsync(requestUrl, headers); break; } var result = HttpUtility.HtmlEncode(await response.Content.ReadAsStringAsync()); LogInfo.Result = $"<span class='result'>{result.MaxLeft(1000)}</span>"; if (!response.IsSuccessStatusCode) { LogInfo.ErrorMsg = $"<span class='error'>{result.MaxLeft(3000)}</span>"; context.JobDetail.JobDataMap[Constant.EXCEPTION] = $"<div class='err-time'>{LogInfo.BeginTime}</div>{JsonConvert.SerializeObject(LogInfo)}"; } else { //这里需要和请求方约定好返回结果约定为HttpResultModel模型 var httpResult = JsonConvert.DeserializeObject<HttpResultModel>(HttpUtility.HtmlDecode(result)); if (!httpResult.IsSuccess) { LogInfo.ErrorMsg = $"<span class='error'>{httpResult.ErrorMsg}</span>"; context.JobDetail.JobDataMap[Constant.EXCEPTION] = $"<div class='err-time'>{LogInfo.BeginTime}</div>{JsonConvert.SerializeObject(LogInfo)}"; } } } } /// <summary> /// Job任务结果 /// </summary> public class HttpResultModel { /// <summary> /// 请求是否成功 /// </summary> public bool IsSuccess { get; set; } = true; /// <summary> /// 异常消息 /// </summary> public string ErrorMsg { get; set; } } } Wms/Utility/Job/JobBase.cs
New file @@ -0,0 +1,93 @@ using Newtonsoft.Json; using Quartz; using Serilog; using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Threading.Tasks; using Utility.Entity; namespace Utility.Job { [DisallowConcurrentExecution] [PersistJobDataAfterExecution] public abstract class JobBase<T> where T : LogModel, new() { protected readonly int maxLogCount = 20;//最多保存日志数量 protected readonly int warnTime = 20;//接口请求超过多少秒记录警告日志 protected Stopwatch stopwatch = new Stopwatch(); protected T LogInfo { get; private set; } private ILogger _logger; public JobBase(T logInfo,ILogger logger) { LogInfo = logInfo; _logger = logger; } public async Task Execute(IJobExecutionContext context) { //如果结束时间超过当前时间,则暂停当前任务。 var endTime = context.JobDetail.JobDataMap.GetString("EndAt"); if (!string.IsNullOrWhiteSpace(endTime) && DateTime.Parse(endTime) <= DateTime.Now) { await context.Scheduler.PauseJob(new JobKey(context.JobDetail.Key.Name, context.JobDetail.Key.Group)); return; } //记录执行次数 var runNumber = context.JobDetail.JobDataMap.GetLong(Constant.RUNNUMBER); context.JobDetail.JobDataMap[Constant.RUNNUMBER] = ++runNumber; var logs = context.JobDetail.JobDataMap[Constant.LOGLIST] as List<string> ?? new List<string>(); if (logs.Count >= maxLogCount) logs.RemoveRange(0, logs.Count - maxLogCount); stopwatch.Restart(); // 开始监视代码运行时间 try { LogInfo.BeginTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); LogInfo.JobName = $"{context.JobDetail.Key.Group}.{context.JobDetail.Key.Name}"; await NextExecute(context); } catch (Exception ex) { LogInfo.ErrorMsg = $"<span class='error'>{ex.Message}</span>"; context.JobDetail.JobDataMap[Constant.EXCEPTION] = $"<div class='err-time'>{LogInfo.BeginTime}</div>{JsonConvert.SerializeObject(LogInfo)}"; } finally { stopwatch.Stop(); // 停止监视 double seconds = stopwatch.Elapsed.TotalSeconds; //总秒数 LogInfo.EndTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); if (seconds >= 1) LogInfo.ExecuteTime = seconds + "秒"; else LogInfo.ExecuteTime = stopwatch.Elapsed.TotalMilliseconds + "毫秒"; var classErr = string.IsNullOrWhiteSpace(LogInfo.ErrorMsg) ? "" : "error"; logs.Add($"<p class='msgList {classErr}'><span class='time'>{LogInfo.BeginTime} 至 {LogInfo.EndTime} 【耗时】{LogInfo.ExecuteTime}</span>{JsonConvert.SerializeObject(LogInfo)}</p>"); context.JobDetail.JobDataMap[Constant.LOGLIST] = logs; } } public abstract Task NextExecute(IJobExecutionContext context); public async Task WarningAsync(string title, string msg) { _logger.Warning(msg); } public async Task InformationAsync(string title, string msg) { _logger.Information(msg); } public async Task ErrorAsync(string title, Exception ex, string msg) { _logger.Error(ex, msg); } } } Wms/Utility/Job/SchedulerCenter.cs
New file @@ -0,0 +1,509 @@ using Quartz; using Quartz.Impl; using Quartz.Impl.AdoJobStore; using Quartz.Impl.AdoJobStore.Common; using Quartz.Impl.Matchers; using Quartz.Impl.Triggers; using Quartz.Simpl; using Quartz.Util; using Serilog; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.RazorPages; using Utility.Job; using System.Linq; using Utility.Entity; using Talk.Extensions; namespace Utility { public class SchedulerCenter { /// <summary> /// 数据连接 /// </summary> private IDbProvider dbProvider; /// <summary> /// ADO 数据类型 /// </summary> private string driverDelegateType; /// <summary> /// 调度器 /// </summary> private static IScheduler scheduler; public SchedulerCenter() { InitDriverDelegateType(); dbProvider = new DbProvider(ConfigurationManager.GetTryConfig("Quartz:dbProviderName"), ConfigurationManager.GetTryConfig("Quartz:connectionString")); } /// <summary> /// 初始化DriverDelegateType /// </summary> private void InitDriverDelegateType() { switch (ConfigurationManager.GetTryConfig("Quartz:dbProviderName")) { case "SQLite-Microsoft": case "SQLite": driverDelegateType = typeof(SQLiteDelegate).AssemblyQualifiedName; break; case "MySql": driverDelegateType = typeof(MySQLDelegate).AssemblyQualifiedName; break; case "OracleODPManaged": driverDelegateType = typeof(OracleDelegate).AssemblyQualifiedName; break; case "SqlServer": case "SQLServerMOT": driverDelegateType = typeof(SqlServerDelegate).AssemblyQualifiedName; break; case "Npgsql": driverDelegateType = typeof(PostgreSQLDelegate).AssemblyQualifiedName; break; case "Firebird": driverDelegateType = typeof(FirebirdDelegate).AssemblyQualifiedName; break; default: throw new Exception("错误数据库类型"); } } /// <summary> /// 初始化Scheduler /// </summary> private async Task InitSchedulerAsync() { if (scheduler == null) { DBConnectionManager.Instance.AddConnectionProvider("default", dbProvider); var serializer = new JsonObjectSerializer(); serializer.Initialize(); var jobStore = new JobStoreTX { DataSource = "default", TablePrefix = "QRTZ_", InstanceId = "AUTO", DriverDelegateType = driverDelegateType, ObjectSerializer = serializer, }; DirectSchedulerFactory.Instance.CreateScheduler("bennyScheduler", "AUTO", new DefaultThreadPool(), jobStore); scheduler = await SchedulerRepository.Instance.Lookup("bennyScheduler"); } } /// <summary> /// 开启调度器 /// </summary> /// <returns></returns> public async Task<bool> StartScheduleAsync() { //初始化Scheduler await InitSchedulerAsync(); //开启调度器 if (scheduler.InStandbyMode) { await scheduler.Start(); Log.Information("任务调度启动!"); } return scheduler.InStandbyMode; } /// <summary> /// 添加一个工作调度 /// </summary> /// <param name="entity"></param> /// <param name="runNumber"></param> /// <returns></returns> public async Task<string> AddScheduleJobAsync(ScheduleEntity entity, long? runNumber = null) { //检查任务是否已存在 var jobKey = new JobKey(entity.JobName, entity.JobGroup); if (await scheduler.CheckExists(jobKey)) { throw Oops.Bah("任务已存在"); } //http请求配置 var httpDir = new Dictionary<string, string>() { { Constant.EndAt, entity.EndTime.ToString()}, { Constant.JobTypeEnum, ((int)entity.JobType).ToString()}, }; if (runNumber.HasValue) httpDir.Add(Constant.RUNNUMBER, runNumber.ToString()); IJobConfigurator jobConfigurator = null; if (entity.JobType == JobTypeEnum.Url) { jobConfigurator = JobBuilder.Create<HttpJob>(); httpDir.Add(Constant.REQUESTURL, entity.RequestUrl); httpDir.Add(Constant.HEADERS, entity.Headers); httpDir.Add(Constant.REQUESTPARAMETERS, entity.RequestParameters); httpDir.Add(Constant.REQUESTTYPE, ((int)entity.RequestType).ToString()); } // 定义这个工作,并将其绑定到我们的IJob实现类 IJobDetail job = jobConfigurator .SetJobData(new JobDataMap(httpDir)) .WithDescription(entity.Description) .WithIdentity(entity.JobName, entity.JobGroup) .Build(); // 创建触发器 ITrigger trigger; //校验是否正确的执行周期表达式 if (entity.TriggerType == TriggerTypeEnum.Cron)//CronExpression.IsValidExpression(entity.Cron)) { trigger = CreateCronTrigger(entity); } else { trigger = CreateSimpleTrigger(entity); } // 告诉Quartz使用我们的触发器来安排作业 await scheduler.ScheduleJob(job, trigger); return "添加成功"; } /// <summary> /// 暂停/删除 指定的计划 /// </summary> /// <param name="jobGroup">任务分组</param> /// <param name="jobName">任务名称</param> /// <param name="isDelete">停止并删除任务</param> /// <returns></returns> public async Task<string> StopOrDelScheduleJobAsync(string jobGroup, string jobName, bool isDelete = false) { try { await scheduler.PauseJob(new JobKey(jobName, jobGroup)); if (isDelete) { await scheduler.DeleteJob(new JobKey(jobName, jobGroup)); return "删除任务计划成功!"; } else { return "停止任务计划成功!"; } } catch (Exception ex) { Log.Error(string.Format("停止任务计划失败!{0}", ex)); return "停止任务计划失败" + ex.Message; } } /// <summary> /// 恢复运行暂停的任务 /// </summary> /// <param name="jobName">任务名称</param> /// <param name="jobGroup">任务分组</param> public async Task<string> ResumeJobAsync(string jobGroup, string jobName) { try { //检查任务是否存在 var jobKey = new JobKey(jobName, jobGroup); if (await scheduler.CheckExists(jobKey)) { var jobDetail = await scheduler.GetJobDetail(jobKey); var endTime = jobDetail.JobDataMap.GetString("EndAt"); if (!string.IsNullOrWhiteSpace(endTime) && DateTime.Parse(endTime) <= DateTime.Now) { return "Job的结束时间已过期。"; } else { //任务已经存在则暂停任务 await scheduler.ResumeJob(jobKey); Log.Information(string.Format("任务“{0}”恢复运行", jobName)); return "恢复任务计划成功!"; } } else { return "任务不存在"; } } catch (Exception ex) { Log.Error(string.Format("恢复任务失败!{0}", ex)); return "恢复任务计划失败!"; } } /// <summary> /// 查询任务 /// </summary> /// <param name="jobGroup"></param> /// <param name="jobName"></param> /// <returns></returns> public async Task<ScheduleEntity> QueryJobAsync(string jobGroup, string jobName) { var entity = new ScheduleEntity(); var jobKey = new JobKey(jobName, jobGroup); var jobDetail = await scheduler.GetJobDetail(jobKey); var triggersList = await scheduler.GetTriggersOfJob(jobKey); var triggers = triggersList.AsEnumerable().FirstOrDefault(); var intervalSeconds = (triggers as SimpleTriggerImpl)?.RepeatInterval.TotalSeconds; var endTime = jobDetail.JobDataMap.GetString("EndAt"); entity.BeginTime = triggers.StartTimeUtc.LocalDateTime; if (!string.IsNullOrWhiteSpace(endTime)) entity.EndTime = DateTime.Parse(endTime); if (intervalSeconds.HasValue) entity.IntervalMilliseconds = Convert.ToInt32(intervalSeconds.Value); entity.JobGroup = jobGroup; entity.JobName = jobName; entity.Cron = (triggers as CronTriggerImpl)?.CronExpressionString; entity.RunTimes = (triggers as SimpleTriggerImpl)?.RepeatCount; entity.TriggerType = triggers is SimpleTriggerImpl ? TriggerTypeEnum.Simple : TriggerTypeEnum.Cron; entity.Description = jobDetail.Description; //旧代码没有保存JobTypeEnum,所以None可以默认为Url。 entity.JobType = (JobTypeEnum)int.Parse(jobDetail.JobDataMap.GetString(Constant.JobTypeEnum) ?? "1"); switch (entity.JobType) { case JobTypeEnum.None: break; case JobTypeEnum.Url: entity.RequestUrl = jobDetail.JobDataMap.GetString(Constant.REQUESTURL); entity.RequestType = (RequestTypeEnum)int.Parse(jobDetail.JobDataMap.GetString(Constant.REQUESTTYPE)); entity.RequestParameters = jobDetail.JobDataMap.GetString(Constant.REQUESTPARAMETERS); entity.Headers = jobDetail.JobDataMap.GetString(Constant.HEADERS); break; case JobTypeEnum.BuiltIn: break; case JobTypeEnum.Hotreload: break; default: break; } return entity; } /// <summary> /// 立即执行 /// </summary> /// <param name="jobKey"></param> /// <returns></returns> public async Task<bool> TriggerJobAsync(JobKey jobKey) { await scheduler.TriggerJob(jobKey); return true; } /// <summary> /// 获取job日志 /// </summary> /// <param name="jobKey"></param> /// <returns></returns> public async Task<List<string>> GetJobLogsAsync(JobKey jobKey) { var jobDetail = await scheduler.GetJobDetail(jobKey); return jobDetail.JobDataMap[Constant.LOGLIST] as List<string>; } /// <summary> /// 获取运行次数 /// </summary> /// <param name="jobKey"></param> /// <returns></returns> public async Task<long> GetRunNumberAsync(JobKey jobKey) { var jobDetail = await scheduler.GetJobDetail(jobKey); return jobDetail.JobDataMap.GetLong(Constant.RUNNUMBER); } /// <summary> /// 获取所有Job(详情信息 - 初始化页面调用) /// </summary> /// <returns></returns> public async Task<List<JobInfoEntity>> GetAllJobAsync() { List<JobKey> jboKeyList = new List<JobKey>(); List<JobInfoEntity> jobInfoList = new List<JobInfoEntity>(); var groupNames = await scheduler.GetJobGroupNames(); foreach (var groupName in groupNames.OrderBy(t => t)) { jboKeyList.AddRange(await scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(groupName))); jobInfoList.Add(new JobInfoEntity() { GroupName = groupName }); } foreach (var jobKey in jboKeyList.OrderBy(t => t.Name)) { var jobDetail = await scheduler.GetJobDetail(jobKey); var triggersList = await scheduler.GetTriggersOfJob(jobKey); var triggers = triggersList.AsEnumerable().FirstOrDefault(); var interval = string.Empty; if (triggers is SimpleTriggerImpl) interval = (triggers as SimpleTriggerImpl)?.RepeatInterval.ToString(); else interval = (triggers as CronTriggerImpl)?.CronExpressionString; foreach (var jobInfo in jobInfoList) { if (jobInfo.GroupName == jobKey.Group) { //旧代码没有保存JobTypeEnum,所以None可以默认为Url。 var jobType = (JobTypeEnum)jobDetail.JobDataMap.GetLong(Constant.JobTypeEnum); jobType = jobType == JobTypeEnum.None ? JobTypeEnum.Url : jobType; var triggerAddress = string.Empty; if (jobType == JobTypeEnum.Url) triggerAddress = jobDetail.JobDataMap.GetString(Constant.REQUESTURL); else if (jobType == JobTypeEnum.BuiltIn) triggerAddress = jobDetail.JobDataMap.GetString(Constant.BuiltIn); jobInfo.JobInfoList.Add(new JobInfo() { Name = jobKey.Name, LastErrMsg = jobDetail.JobDataMap.GetString(Constant.EXCEPTION), TriggerAddress = triggerAddress, TriggerState = await scheduler.GetTriggerState(triggers.Key), PreviousFireTime = triggers.GetPreviousFireTimeUtc()?.LocalDateTime, NextFireTime = triggers.GetNextFireTimeUtc()?.LocalDateTime, BeginTime = triggers.StartTimeUtc.LocalDateTime, Interval = interval, EndTime = triggers.EndTimeUtc?.LocalDateTime, Description = jobDetail.Description, RequestType = jobDetail.JobDataMap.GetString(Constant.REQUESTTYPE), RunNumber = jobDetail.JobDataMap.GetLong(Constant.RUNNUMBER), JobType = (long)jobType //(triggers as SimpleTriggerImpl)?.TimesTriggered //CronTriggerImpl 中没有 TimesTriggered 所以自己RUNNUMBER记录 }); continue; } } } return jobInfoList; } /// <summary> /// 移除异常信息 /// 因为只能在IJob持久化操作JobDataMap,所有这里直接暴力操作数据库。 /// </summary> /// <param name="jobGroup"></param> /// <param name="jobName"></param> /// <returns></returns> //public async Task<bool> RemoveErrLog(string jobGroup, string jobName) //{ // IRepositorie logRepositorie = RepositorieFactory.CreateRepositorie(driverDelegateType, dbProvider); // if (logRepositorie == null) return false; // await logRepositorie.RemoveErrLogAsync(jobGroup, jobName); // var jobKey = new JobKey(jobName, jobGroup); // var jobDetail = await scheduler.GetJobDetail(jobKey); // jobDetail.JobDataMap[Constant.EXCEPTION] = string.Empty; // return true; //} /// <summary> /// 获取所有Job信息(简要信息 - 刷新数据的时候使用) /// </summary> /// <returns></returns> public async Task<List<JobBriefInfoEntity>> GetAllJobBriefInfoAsync() { List<JobKey> jboKeyList = new List<JobKey>(); List<JobBriefInfoEntity> jobInfoList = new List<JobBriefInfoEntity>(); var groupNames = await scheduler.GetJobGroupNames(); foreach (var groupName in groupNames.OrderBy(t => t)) { jboKeyList.AddRange(await scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(groupName))); jobInfoList.Add(new JobBriefInfoEntity() { GroupName = groupName }); } foreach (var jobKey in jboKeyList.OrderBy(t => t.Name)) { var jobDetail = await scheduler.GetJobDetail(jobKey); var triggersList = await scheduler.GetTriggersOfJob(jobKey); var triggers = triggersList.AsEnumerable().FirstOrDefault(); foreach (var jobInfo in jobInfoList) { if (jobInfo.GroupName == jobKey.Group) { jobInfo.JobInfoList.Add(new JobBriefInfo() { Name = jobKey.Name, LastErrMsg = jobDetail?.JobDataMap.GetString(Constant.EXCEPTION), TriggerState = await scheduler.GetTriggerState(triggers.Key), PreviousFireTime = triggers.GetPreviousFireTimeUtc()?.LocalDateTime, NextFireTime = triggers.GetNextFireTimeUtc()?.LocalDateTime, RunNumber = jobDetail?.JobDataMap.GetLong(Constant.RUNNUMBER) ?? 0 }); continue; } } } return jobInfoList; } /// <summary> /// 停止任务调度 /// </summary> public async Task<bool> StopScheduleAsync() { //判断调度是否已经关闭 if (!scheduler.InStandbyMode) { //等待任务运行完成 await scheduler.Standby(); //TODO 注意:Shutdown后Start会报错,所以这里使用暂停。 Log.Information("任务调度暂停!"); } return !scheduler.InStandbyMode; } /// <summary> /// 创建类型Simple的触发器 /// </summary> /// <param name="entity"></param> /// <returns></returns> private ITrigger CreateSimpleTrigger(ScheduleEntity entity) { //作业触发器 if (entity.RunTimes.HasValue && entity.RunTimes > 0) { return TriggerBuilder.Create() .WithIdentity(entity.JobName, entity.JobGroup) .StartAt(entity.BeginTime)//开始时间 //.EndAt(entity.EndTime)//结束数据 .WithSimpleSchedule(x => { x.WithInterval(TimeSpan.FromMilliseconds(entity.IntervalMilliseconds.Value)) // 执行时间间隔,单位毫秒 .WithRepeatCount(entity.RunTimes.Value)//执行次数、默认从0开始 .WithMisfireHandlingInstructionFireNow(); }) .ForJob(entity.JobName, entity.JobGroup)//作业名称 .Build(); } else { return TriggerBuilder.Create() .WithIdentity(entity.JobName, entity.JobGroup) .StartAt(entity.BeginTime)//开始时间 //.EndAt(entity.EndTime)//结束数据 .WithSimpleSchedule(x => { x.WithInterval(TimeSpan.FromMilliseconds(entity.IntervalMilliseconds.Value)) // 执行时间间隔,单位毫秒 .RepeatForever() // 无限循环 .WithMisfireHandlingInstructionFireNow(); }) .ForJob(entity.JobName, entity.JobGroup) // 作业名称 .Build(); } } /// <summary> /// 创建类型Cron的触发器 /// </summary> /// <param name="entity"></param> /// <returns></returns> private ITrigger CreateCronTrigger(ScheduleEntity entity) { // 作业触发器 return TriggerBuilder.Create() .WithIdentity(entity.JobName, entity.JobGroup) .StartAt(entity.BeginTime)//开始时间 //.EndAt(entity.EndTime)//结束时间 .WithCronSchedule(entity.Cron, cronScheduleBuilder => cronScheduleBuilder.WithMisfireHandlingInstructionFireAndProceed())//指定cron表达式 .ForJob(entity.JobName, entity.JobGroup)//作业名称 .Build(); } } } Wms/Utility/Job/TestJob.cs
New file @@ -0,0 +1,21 @@ using Quartz; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Utility { public class TestJob : IJob { public async Task Execute(IJobExecutionContext context) { Console.WriteLine($"【任务执行】:{DateTime.Now}"); Console.WriteLine($"【触发时间】:{context.ScheduledFireTimeUtc?.LocalDateTime}"); Console.WriteLine($"【下次触发时间】:{context.NextFireTimeUtc?.LocalDateTime}"); await Task.CompletedTask; } } } Wms/Utility/Utility.csproj
@@ -19,6 +19,8 @@ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Quartz" Version="3.9.0" /> <PackageReference Include="Quartz.Serialization.Json" Version="3.9.0" /> <PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" /> @@ -26,6 +28,7 @@ <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="SqlSugarCore" Version="5.1.3.43" /> <PackageReference Include="System.Drawing.Common" Version="6.0.0" /> <PackageReference Include="Talk.Extensions" Version="1.0.1.84" /> <PackageReference Include="ZXing.Net" Version="0.16.7" /> </ItemGroup> Wms/Wms/Controllers/JobController.cs
New file @@ -0,0 +1,179 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Quartz; using System.Collections.Generic; using System.Threading.Tasks; using Utility.Entity; using Utility; namespace Wms.Controllers { /// <summary> /// 任务调度 /// </summary> [Route("api/[controller]/[Action]")] [EnableCors("AllowSameDomain")] //允许跨域 [ServiceFilter(typeof(ApiResponseActionFilter))] public class JobController : ControllerBase { private SchedulerCenter scheduler; /// <summary> /// 任务调度对象 /// </summary> /// <param name="schedulerCenter"></param> public JobController(SchedulerCenter schedulerCenter) { scheduler = schedulerCenter; } /// <summary> /// 添加任务 /// </summary> /// <param name="entity"></param> /// <returns></returns> [HttpPost] public async Task<string> AddJob([FromBody] ScheduleEntity entity) { if (entity.TriggerType == TriggerTypeEnum.Cron && entity.Cron == "* * * * * ?") { return "不允许过频繁执行任务!"; } return await scheduler.AddScheduleJobAsync(entity); } /// <summary> /// 暂停任务 /// </summary> /// <returns></returns> [HttpPost] public async Task<string> StopJob([FromBody] JobKey job) { return await scheduler.StopOrDelScheduleJobAsync(job.Group, job.Name); } /// <summary> /// 删除任务 /// </summary> /// <returns></returns> [HttpPost] public async Task<string> RemoveJob([FromBody] JobKey job) { return await scheduler.StopOrDelScheduleJobAsync(job.Group, job.Name, true); } /// <summary> /// 恢复运行暂停的任务 /// </summary> /// <returns></returns> [HttpPost] public async Task<string> ResumeJob([FromBody] JobKey job) { return await scheduler.ResumeJobAsync(job.Group, job.Name); } /// <summary> /// 查询任务 /// </summary> /// <returns></returns> [HttpPost] public async Task<ScheduleEntity> QueryJob([FromBody] JobKey job) { return await scheduler.QueryJobAsync(job.Group, job.Name); } /// <summary> /// 修改 /// </summary> /// <param name="entity"></param> /// <returns></returns> [HttpPost] public async Task<string> ModifyJob([FromBody] ModifyJobInput entity) { var jobKey = new JobKey(entity.OldScheduleEntity.JobName, entity.OldScheduleEntity.JobGroup); var runNumber = await scheduler.GetRunNumberAsync(jobKey); await scheduler.StopOrDelScheduleJobAsync(entity.OldScheduleEntity.JobGroup, entity.OldScheduleEntity.JobName, true); await scheduler.AddScheduleJobAsync(entity.NewScheduleEntity, runNumber); return "修改计划任务成功!"; } /// <summary> /// 立即执行 /// </summary> /// <param name="job"></param> /// <returns></returns> [HttpPost] public async Task<bool> TriggerJob([FromBody] JobKey job) { await scheduler.TriggerJobAsync(job); return true; } /// <summary> /// 获取job日志 /// </summary> /// <param name="jobKey"></param> /// <returns></returns> [HttpPost] public async Task<List<string>> GetJobLogs([FromBody] JobKey jobKey) { var logs = await scheduler.GetJobLogsAsync(jobKey); logs?.Reverse(); return logs; } /// <summary> /// 启动调度 /// </summary> /// <returns></returns> [HttpGet] public async Task<bool> StartSchedule() { return await scheduler.StartScheduleAsync(); } /// <summary> /// 停止调度 /// </summary> /// <returns></returns> [HttpGet] public async Task<bool> StopSchedule() { return await scheduler.StopScheduleAsync(); } /// <summary> /// 获取所有任务 /// </summary> /// <returns></returns> [HttpGet] public async Task<List<JobInfoEntity>> GetAllJob() { return await scheduler.GetAllJobAsync(); } /// <summary> /// 获取所有Job信息(简要信息 - 刷新数据的时候使用) /// </summary> /// <returns></returns> [HttpGet] public async Task<List<JobBriefInfoEntity>> GetAllJobBriefInfo() { return await scheduler.GetAllJobBriefInfoAsync(); } ///// <summary> ///// 移除异常信息 ///// </summary> ///// <param name="jobKey"></param> ///// <returns></returns> //[HttpPost] //public async Task<bool> RemoveErrLog([FromBody] JobKey jobKey) //{ // return await scheduler.RemoveErrLog(jobKey.Group, jobKey.Name); //} } } Wms/Wms/Startup.cs
@@ -21,6 +21,7 @@ using Microsoft.Extensions.Options; using Microsoft.AspNetCore.Http; using SqlSugar; using Utility.Job; namespace Wms { @@ -128,6 +129,9 @@ services.AddScoped<UnitOfWorkAttribute>(); services.AddSingleton<ISqlSugarClient>(DataContext.Db); // 单例注册 services.AddTransient<IUnitOfWork, SqlSugarUnitOfWork>(); // 事务与工作单元注册 services.AddHostedService<HostedService>(); services.AddSingleton<SchedulerCenter>(); } public void ConfigureContainer(ContainerBuilder builder) { Wms/Wms/Wms.csproj
@@ -28,6 +28,7 @@ <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.32" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Quartz" Version="3.9.0" /> <PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" /> Wms/Wms/appsettings.json
@@ -40,6 +40,10 @@ "SignConfig": { "AppKey": "90170307d4184844ac2a26b431f79980", //验签 "Minutes": 5 //验签时间 5分钟 }, "Quartz": { "dbProviderName": "SqlServer", "connectionString": "Server=192.168.62.200;Database=QRTZ;User ID=sa;Password=sql2019;Integrated Security=False;" }