| | |
| | | using VueWebCoreApi.Extensions; |
| | | using VueWebCoreApi.SignalR; |
| | | using VueWebCoreApi.Tools; |
| | | using static VueWebCoreApi.Quartz.HttpManager; |
| | | |
| | | namespace VueWebCoreApi.Quartz |
| | | { |
| | | /// <summary> |
| | | /// 定时任务执行类:Quartz到时间后,自动运行这个类的Execute方法 |
| | | /// 作用:发送HTTP请求、记录任务日志、推送执行结果 |
| | | /// </summary> |
| | | public class HttpResultfulJob : IJob |
| | | { |
| | | private ILog log = LogManager.GetLogger(Startup.repository.Name, typeof(ChatHub)); |
| | | // 日志记录工具(log4net) |
| | | private readonly ILog _log = LogManager.GetLogger(Startup.repository.Name, typeof(ChatHub)); |
| | | // SignalR 实时推送工具(给前端推送消息) |
| | | private readonly IHubContext<ChatHub, IChatClient> _hubContext; |
| | | // HttpClient 工厂:用于发送 HTTP 请求 |
| | | private readonly IHttpClientFactory _httpClientFactory; |
| | | // 数据库仓储:用于记录任务运行日志、更新最后执行时间 |
| | | private readonly QuartzRepository _quartzRepo; |
| | | |
| | | readonly IHttpClientFactory httpClientFactory; |
| | | /// <summary> |
| | | /// 2020.05.31增加构造方法 |
| | | /// 构造函数:依赖注入(自动赋值所有需要的服务) |
| | | /// </summary> |
| | | /// <param name="serviceProvider"></param> |
| | | /// <param name="httpClientFactory"></param> |
| | | public HttpResultfulJob(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory, IHubContext<ChatHub, IChatClient> hubContext) |
| | | public HttpResultfulJob(IServiceProvider serviceProvider, |
| | | IHttpClientFactory httpClientFactory, |
| | | IHubContext<ChatHub, IChatClient> hubContext, |
| | | QuartzRepository quartzRepo) |
| | | { |
| | | this.httpClientFactory = httpClientFactory; |
| | | _httpClientFactory = httpClientFactory; |
| | | _hubContext = hubContext; |
| | | //serviceProvider.GetService() |
| | | _quartzRepo = quartzRepo; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 【核心方法】 |
| | | /// Quartz 调度器根据 Cron 表达式,到时间后**自动调用**这个方法 |
| | | /// 作用:执行具体的任务逻辑(发送HTTP请求) |
| | | /// </summary> |
| | | public async Task Execute(IJobExecutionContext context) |
| | | { |
| | | DateTime dateTime = DateTime.Now; |
| | | TaskOptions taskOptions = context.GetTaskOptions(); |
| | | string httpMessage = ""; |
| | | AbstractTrigger trigger = (context as JobExecutionContextImpl).Trigger as AbstractTrigger; |
| | | // 记录任务开始执行时间 |
| | | var beginTime = DateTime.Now; |
| | | // 从 Quartz 上下文获取当前要执行的任务配置(名称、分组、URL、Cron等) |
| | | var taskOptions = context.GetTaskOptions(); |
| | | // 存储 HTTP 请求返回的结果信息 |
| | | string httpMessage = string.Empty; |
| | | HttpResult result = new HttpResult(); |
| | | |
| | | // ========================================== |
| | | // 第一步:校验任务是否存在 |
| | | // ========================================== |
| | | if (taskOptions == null) |
| | | { |
| | | FileHelper.WriteFile(FileQuartz.LogPath + trigger.Group, $"{trigger.Name}.txt", "未到找作业或可能被移除", true); |
| | | // 如果任务为空,记录错误日志到数据库 |
| | | var trigger = context.Trigger; |
| | | await _quartzRepo.AddJobRunLogAsync(trigger.Key.Name, trigger.Key.Group, beginTime, DateTime.Now, "未找到作业或可能被移除", taskOptions.MessagePush, taskOptions.PushUserCode); |
| | | return; |
| | | } |
| | | Console.WriteLine($"作业[{taskOptions.TaskName}]开始:{ DateTime.Now.ToString("yyyy-MM-dd HH:mm:sss")}"); |
| | | // 系统日志输出:任务开始执行 |
| | | _log.Info($"作业[{taskOptions.TaskName}]开始:{beginTime:yyyy-MM-dd HH:mm:ss}"); |
| | | |
| | | // ========================================== |
| | | // 第二步:校验任务是否配置了请求地址 |
| | | // ========================================== |
| | | if (string.IsNullOrEmpty(taskOptions.ApiUrl) || taskOptions.ApiUrl == "/") |
| | | { |
| | | FileHelper.WriteFile(FileQuartz.LogPath + trigger.Group, $"{trigger.Name}.txt", $"{ DateTime.Now.ToString("yyyy-MM-dd HH:mm:sss")}未配置url,", true); |
| | | // 未配置API地址,记录日志 |
| | | await _quartzRepo.AddJobRunLogAsync(taskOptions.TaskName, taskOptions.GroupName, beginTime, DateTime.Now, "未配置URL", taskOptions.MessagePush, taskOptions.PushUserCode); |
| | | return; |
| | | } |
| | | |
| | | // ========================================== |
| | | // 第三步:发送 HTTP 请求(核心业务) |
| | | // ========================================== |
| | | try |
| | | { |
| | | Dictionary<string, string> header = new Dictionary<string, string>(); |
| | | // 存储请求头(身份验证信息) |
| | | var headers = new Dictionary<string, string>(); |
| | | // 如果配置了 AuthKey 和 AuthValue,添加到请求头 |
| | | if (!string.IsNullOrEmpty(taskOptions.AuthKey) && !string.IsNullOrEmpty(taskOptions.AuthValue)) |
| | | { |
| | | header.Add(taskOptions.AuthKey.Trim(), taskOptions.AuthValue.Trim()); |
| | | headers.Add(taskOptions.AuthKey.Trim(), taskOptions.AuthValue.Trim()); |
| | | } |
| | | |
| | | httpMessage = await httpClientFactory.HttpSendAsync(taskOptions.RequestType?.ToLower() == "get" ? HttpMethod.Get : HttpMethod.Post, taskOptions.ApiUrl, header); |
| | | // 判断请求方式:GET 或 POST(默认POST) |
| | | var method = taskOptions.RequestType?.ToLower() == "get" ? HttpMethod.Get : HttpMethod.Post; |
| | | // 调用工具类,发送 HTTP 请求,并获取返回结果 |
| | | result = await _httpClientFactory.HttpSendAsync(method, taskOptions.ApiUrl, taskOptions.RequestParameters, headers); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | httpMessage = ex.Message; |
| | | // 如果请求异常(网络错误、接口报错),记录异常信息 |
| | | result.ErrorMsg = ex.Message; |
| | | _log.Error($"作业[{taskOptions.TaskName}]执行异常:{ex.Message}", ex); |
| | | } |
| | | |
| | | try |
| | | { |
| | | string logContent = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}_{dateTime.ToString("yyyy-MM-dd HH:mm:ss")}_{(string.IsNullOrEmpty(httpMessage) ? "OK" : httpMessage)}\r\n"; |
| | | FileHelper.WriteFile(FileQuartz.LogPath + taskOptions.GroupName + "\\", $"{taskOptions.TaskName}.txt", logContent, true); |
| | | //await _hubContext.Clients.All.SendAsync("SendMessage系统通知:"+$"最新消息{DateTime.Now}"); |
| | | //await _hubContext.Clients.All.SendAll(logContent); |
| | | //查找系统用户 |
| | | var sql = @"select usercode as code,username as name |
| | | from TUser |
| | | where is_delete='0' and enable='Y' and password='123'"; |
| | | var data = DapperHelper.selecttable(sql); |
| | | var departmentIDs = data.AsEnumerable().Select(x => x.Field<string>("code")).ToList();//获取推送人员编码 |
| | | var FindPublicBaseDic = UserIdsStore.Ids.Where(d => departmentIDs.Contains(d.Value)).Select(x => x.Key).ToList(); //匹配已经登录的推送人员connectionIds |
| | | if (FindPublicBaseDic.Count > 0) |
| | | // 1. 把本次执行记录写入 SQL Server 数据库(任务运行日志表) |
| | | await _quartzRepo.AddJobRunLogAsync(taskOptions.TaskName, taskOptions.GroupName, beginTime, DateTime.Now,result.StatusCode==200?result.Content.ToString():result.ErrorMsg, taskOptions.MessagePush, taskOptions.PushUserCode); |
| | | // 2. 更新任务表中的【最后执行时间】 |
| | | await _quartzRepo.UpdateTaskLastRunTimeAsync(taskOptions.TaskName, taskOptions.GroupName, beginTime); |
| | | |
| | | // ========================================== |
| | | // 以下是 SignalR 实时推送(给前端页面发消息) |
| | | // ========================================== |
| | | //判断请求是否成功 |
| | | if (result.IsSuccess) |
| | | { |
| | | //推送指定用户 |
| | | await _hubContext.Clients.Clients(FindPublicBaseDic).SendCustomUserMessage(logContent); |
| | | //写入任务子表 |
| | | } |
| | | } |
| | | catch (Exception) |
| | | if (taskOptions.MessagePush == "N"||taskOptions.PushUserCode==null) //是否需要推送 |
| | | { |
| | | } |
| | | Console.WriteLine(trigger.FullName + " " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:sss") + " " + httpMessage); |
| | | _log.Info($"作业[{taskOptions.TaskName}]无需要推送的用户,跳过SignalR推送"); |
| | | return; |
| | | } |
| | | //步骤2:提取用户编码(去重 + 空值过滤) |
| | | var departmentIDs = taskOptions.PushUserCode.Split(',').ToList(); |
| | | |
| | | if (departmentIDs.Count == 0) |
| | | { |
| | | _log.Info($"作业[{taskOptions.TaskName}]查询到的用户编码为空,跳过SignalR推送"); |
| | | return; |
| | | } |
| | | // 步骤3:获取在线用户的连接ID(使用线程安全方法) |
| | | var targetConnIds = UserIdsStore.GetConnectionIdsByUserCodes(departmentIDs); |
| | | if (targetConnIds.Count == 0) |
| | | { |
| | | _log.Info($"作业[{taskOptions.TaskName}]目标用户均不在线,跳过SignalR推送"); |
| | | return; |
| | | } |
| | | // 步骤4:构造推送消息(标准化格式) |
| | | var logContent = $"【{taskOptions.TaskName}】执行结果:{beginTime:yyyy-MM-dd HH:mm:ss} 至 {DateTime.Now:yyyy-MM-dd HH:mm:ss} | 结果:{(string.IsNullOrEmpty(httpMessage) ? "执行成功" : httpMessage)}"; |
| | | // 步骤5:批量推送(增加超时+异常捕获) |
| | | _log.Info($"作业[{taskOptions.TaskName}]开始向[{targetConnIds.Count}]个在线连接推送消息:{logContent}"); |
| | | // 设置推送超时时间(避免长时间阻塞) |
| | | var pushTask = _hubContext.Clients.Clients(targetConnIds).SendCustomUserMessage(logContent); |
| | | if (await Task.WhenAny(pushTask, Task.Delay(5000)) == pushTask) |
| | | { |
| | | await pushTask; // 推送成功 |
| | | _log.Info($"作业[{taskOptions.TaskName}]SignalR推送完成,目标连接数:{targetConnIds.Count}"); |
| | | } |
| | | else |
| | | { |
| | | _log.Warn($"作业[{taskOptions.TaskName}]SignalR推送超时(5秒),目标连接数:{targetConnIds.Count}"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | // 记录日志写入或推送失败的异常 |
| | | _log.Error($"作业[{taskOptions.TaskName}]日志写入/SignalR推送异常:{ex.Message}", ex); |
| | | } |
| | | // 系统日志输出:任务执行结束 |
| | | _log.Info($"作业[{taskOptions.TaskName}]结束:{DateTime.Now:yyyy-MM-dd HH:mm:ss} 结果:{result.ErrorMsg}"); |
| | | } |
| | | } |
| | | } |