1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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
{
    /// <summary>
    /// 定时任务执行类:Quartz到时间后,自动运行这个类的Execute方法
    /// 作用:发送HTTP请求、记录任务日志、推送执行结果
    /// </summary>
    public class HttpResultfulJob : IJob
    {
        // 日志记录工具(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;
 
        /// <summary>
        /// 构造函数:依赖注入(自动赋值所有需要的服务)
        /// </summary>
        public HttpResultfulJob(IServiceProvider serviceProvider,
                                IHttpClientFactory httpClientFactory,
                                IHubContext<ChatHub, IChatClient> hubContext,
                                QuartzRepository quartzRepo)
        {
            _httpClientFactory = httpClientFactory;
            _hubContext = hubContext;
            _quartzRepo = quartzRepo;
        }
 
        /// <summary>
        /// 【核心方法】
        /// Quartz 调度器根据 Cron 表达式,到时间后**自动调用**这个方法
        /// 作用:执行具体的任务逻辑(发送HTTP请求)
        /// </summary>
        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<string, string>();
                // 如果配置了 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}");
        }
    }
}