using log4net; using Microsoft.AspNetCore.SignalR; using Quartz; using Quartz.Impl; using Quartz.Impl.Triggers; using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using VueWebCoreApi.Extensions; using VueWebCoreApi.SignalR; using VueWebCoreApi.Tools; using static VueWebCoreApi.Quartz.HttpManager; namespace VueWebCoreApi.Quartz { /// /// 定时任务执行类:Quartz到时间后,自动运行这个类的Execute方法 /// 作用:发送HTTP请求、记录任务日志、推送执行结果 /// public class HttpResultfulJob : IJob { // 日志记录工具(log4net) private readonly ILog _log = LogManager.GetLogger(Startup.repository.Name, typeof(ChatHub)); // SignalR 实时推送工具(给前端推送消息) private readonly IHubContext _hubContext; // HttpClient 工厂:用于发送 HTTP 请求 private readonly IHttpClientFactory _httpClientFactory; // 数据库仓储:用于记录任务运行日志、更新最后执行时间 private readonly QuartzRepository _quartzRepo; /// /// 构造函数:依赖注入(自动赋值所有需要的服务) /// public HttpResultfulJob(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory, IHubContext hubContext, QuartzRepository quartzRepo) { _httpClientFactory = httpClientFactory; _hubContext = hubContext; _quartzRepo = quartzRepo; } /// /// 【核心方法】 /// Quartz 调度器根据 Cron 表达式,到时间后**自动调用**这个方法 /// 作用:执行具体的任务逻辑(发送HTTP请求) /// public async Task Execute(IJobExecutionContext context) { // 记录任务开始执行时间 var beginTime = DateTime.Now; // 从 Quartz 上下文获取当前要执行的任务配置(名称、分组、URL、Cron等) var taskOptions = context.GetTaskOptions(); // 存储 HTTP 请求返回的结果信息 string httpMessage = string.Empty; HttpResult result = new HttpResult(); // ========================================== // 第一步:校验任务是否存在 // ========================================== if (taskOptions == null) { // 如果任务为空,记录错误日志到数据库 var trigger = context.Trigger; await _quartzRepo.AddJobRunLogAsync(trigger.Key.Name, trigger.Key.Group, beginTime, DateTime.Now, "未找到作业或可能被移除", taskOptions.MessagePush, taskOptions.PushUserCode); return; } // 系统日志输出:任务开始执行 _log.Info($"作业[{taskOptions.TaskName}]开始:{beginTime:yyyy-MM-dd HH:mm:ss}"); // ========================================== // 第二步:校验任务是否配置了请求地址 // ========================================== if (string.IsNullOrEmpty(taskOptions.ApiUrl) || taskOptions.ApiUrl == "/") { // 未配置API地址,记录日志 await _quartzRepo.AddJobRunLogAsync(taskOptions.TaskName, taskOptions.GroupName, beginTime, DateTime.Now, "未配置URL", taskOptions.MessagePush, taskOptions.PushUserCode); return; } // ========================================== // 第三步:发送 HTTP 请求(核心业务) // ========================================== try { // 存储请求头(身份验证信息) var headers = new Dictionary(); // 如果配置了 AuthKey 和 AuthValue,添加到请求头 if (!string.IsNullOrEmpty(taskOptions.AuthKey) && !string.IsNullOrEmpty(taskOptions.AuthValue)) { headers.Add(taskOptions.AuthKey.Trim(), taskOptions.AuthValue.Trim()); } // 判断请求方式: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) { // 如果请求异常(网络错误、接口报错),记录异常信息 result.ErrorMsg = ex.Message; _log.Error($"作业[{taskOptions.TaskName}]执行异常:{ex.Message}", ex); } try { // 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) { //写入任务子表 } if (taskOptions.MessagePush == "N"||taskOptions.PushUserCode==null) //是否需要推送 { _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}"); } } }