VueWebCoreApi/Quartz/HttpResultfulJob.cs
@@ -12,83 +12,157 @@
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}");
        }
    }
}