1.车间综合看板接口调整
2.报工记录子表和不良记录表写入报工人员,班组编码
3.修改调整定时任务写入到数据库,并实现消息推送
已添加3个文件
已删除1个文件
已修改15个文件
1838 ■■■■■ 文件已修改
VueWebCoreApi/Controllers/TaskBackGroundController.cs 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/DLL/DAL/KanBanManagerentDAL.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/DLL/DAL/WorkOrderDAL.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/HttpManager.cs 181 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/HttpResultfulJob.cs 152 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/IOCJobFactory.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/JobAction.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/JobActionLog.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/QuartzDapperHelper.cs 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/QuartzFileInfo.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/QuartzNETExtension.cs 632 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/QuartzRepository.cs 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/QuartzSearchData.cs 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/TaskLog.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Quartz/TaskOptions.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/SignalR/ChatHub.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/SignalR/UserIdsStore.cs 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Startup.cs 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/appsettings.json 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
VueWebCoreApi/Controllers/TaskBackGroundController.cs
@@ -16,122 +16,161 @@
    [Route("api/[controller]")]
    public class TaskBackGroundController : Controller
    {
        //定义全局信息返回变量
        ToMessage mes = new ToMessage();
        private readonly ISchedulerFactory _schedulerFactory;
        private readonly IJobFactory _jobFactory;
        public TaskBackGroundController(ISchedulerFactory schedulerFactory, IJobFactory jobFactory)
        private readonly QuartzRepository _quartzRepo;
        public TaskBackGroundController(ISchedulerFactory schedulerFactory,
                                       IJobFactory jobFactory,
                                       QuartzRepository quartzRepo)
        {
            this._jobFactory = jobFactory;
            this._schedulerFactory = schedulerFactory;
            _schedulerFactory = schedulerFactory;
            _jobFactory = jobFactory;
            _quartzRepo = quartzRepo;
        }
        /// <summary>
        /// èŽ·å–æ‰€æœ‰çš„ä½œä¸š
        /// </summary>
        /// <returns></returns>
        [Route(template: "GetJobs")]
        [Route("GetJobs")]
        [HttpGet]
        public async Task<IActionResult> GetJobs()
        {
            try
            {
            mes.code = "200";
            mes.message = "查询成功!";
            mes.data = await _schedulerFactory.GetJobs();
                mes.data = await _schedulerFactory.GetJobs(_quartzRepo);
            }
            catch (Exception ex)
            {
                mes.code = "300";
                mes.message = "查询失败:" + ex.Message;
                mes.data = null;
            }
            return Json(mes);
        }
        /// <summary>
        /// èŽ·å–ä½œä¸šè¿è¡Œæ—¥å¿—
        /// </summary>
        /// <param name="taskName"></param>
        /// <param name="groupName"></param>
        /// <param name="page"></param>
        /// <returns></returns>
        [Route(template: "GetRunLog")]
        [Route("GetRunLog")]
        [HttpGet]
        public IActionResult GetRunLog(string taskName, string groupName, int page = 1)
        public async Task<IActionResult> GetRunLog(string taskName, string groupName, int page = 1)
        {
            // å…¥å‚校验
            if (string.IsNullOrEmpty(taskName) || string.IsNullOrEmpty(groupName))
            {
                mes.code = "300";
                mes.message = "任务名和分组名不能为空!";
                mes.data = null;
                return Json(mes);
            }
            try
        {
            mes.code = "200";
            mes.data = FileQuartz.GetJobRunLog(taskName, groupName, page);
                mes.message = "查询成功!";
                mes.data = await _quartzRepo.GetJobRunLogAsync(taskName, groupName, page);
            }
            catch (Exception ex)
            {
                mes.code = "300";
                mes.message = "查询失败:" + ex.Message;
                mes.data = null;
            }
            return Json(mes);
        }
        /// <summary>
        /// æ·»åŠ ä»»åŠ¡
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        [Route(template: "Add")]
        [Route("Add")]
        [HttpPost]
        //[TaskAuthor]
        public async Task<IActionResult> Add(TaskOptions taskOptions)
        {
            return Json(await taskOptions.AddJob(_schedulerFactory, jobFactory: _jobFactory));
            if (string.IsNullOrEmpty(taskOptions.TaskName) || string.IsNullOrEmpty(taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务名和分组名不能为空!";
                return Json(mes);
            }
            return Json(await taskOptions.AddJob(_schedulerFactory, false, _jobFactory, _quartzRepo));
        }
        /// <summary>
        /// åˆ é™¤ä»»åŠ¡
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        [Route(template: "Remove")]
        [Route("Remove")]
        [HttpPost]
        //[TaskAuthor]
        public async Task<IActionResult> Remove(TaskOptions taskOptions)
        {
            return Json(await _schedulerFactory.Remove(taskOptions));
            if (string.IsNullOrEmpty(taskOptions.TaskName) || string.IsNullOrEmpty(taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务名和分组名不能为空!";
                return Json(mes);
            }
            return Json(await _schedulerFactory.Remove(taskOptions, _quartzRepo));
        }
        /// <summary>
        /// ä¿®æ”¹ä»»åŠ¡
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        [Route(template: "Update")]
        [Route("Update")]
        [HttpPost]
        //[TaskAuthor]
        public async Task<IActionResult> Update(TaskOptions taskOptions)
        {
            return Json(await _schedulerFactory.Update(taskOptions));
            if (string.IsNullOrEmpty(taskOptions.TaskName) || string.IsNullOrEmpty(taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务名和分组名不能为空!";
                return Json(mes);
            }
            return Json(await _schedulerFactory.Update(taskOptions, _quartzRepo));
        }
        /// <summary>
        /// æš‚停任务
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        [Route(template: "Pause")]
        [Route("Pause")]
        [HttpPost]
        //[TaskAuthor]
        public async Task<IActionResult> Pause(TaskOptions taskOptions)
        {
            return Json(await _schedulerFactory.Pause(taskOptions));
            if (string.IsNullOrEmpty(taskOptions.TaskName) || string.IsNullOrEmpty(taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务名和分组名不能为空!";
                return Json(mes);
            }
            // æ ¡éªŒä»»åŠ¡æ˜¯å¦å­˜åœ¨
            if (!await _quartzRepo.TaskExistsAsync(taskOptions.TaskName, taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务不存在!";
                return Json(mes);
            }
            return Json(await _schedulerFactory.Pause(taskOptions, _quartzRepo));
        }
        /// <summary>
        /// å¼€å¯ä»»åŠ¡
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        [Route(template: "Start")]
        [Route("Start")]
        [HttpPost]
        //[TaskAuthor]
        public async Task<IActionResult> Start(TaskOptions taskOptions)
        {
            return Json(await _schedulerFactory.Start(taskOptions));
            if (string.IsNullOrEmpty(taskOptions.TaskName) || string.IsNullOrEmpty(taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务名和分组名不能为空!";
                return Json(mes);
            }
            if (!await _quartzRepo.TaskExistsAsync(taskOptions.TaskName, taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务不存在!";
                return Json(mes);
            }
            return Json(await _schedulerFactory.Start(taskOptions, _quartzRepo));
        }
        /// <summary>
        /// ç«‹å³æ‰§è¡Œ
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        [Route(template: "Run")]
        [Route("Run")]
        [HttpPost]
        //[TaskAuthor]
        public async Task<IActionResult> Run(TaskOptions taskOptions)
        {
            return Json(await _schedulerFactory.Run(taskOptions));
            if (string.IsNullOrEmpty(taskOptions.TaskName) || string.IsNullOrEmpty(taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务名和分组名不能为空!";
                return Json(mes);
            }
            if (!await _quartzRepo.TaskExistsAsync(taskOptions.TaskName, taskOptions.GroupName))
            {
                mes.code = "300";
                mes.message = "任务不存在!";
                return Json(mes);
            }
            return Json(await _schedulerFactory.Run(taskOptions, _quartzRepo));
        }
    }
}
VueWebCoreApi/DLL/DAL/KanBanManagerentDAL.cs
@@ -214,17 +214,13 @@
        public static ToMessage WorkShopCompreRightBottom()
        {
            var sql = "";
            Dictionary<object, object> dir = new Dictionary<object, object>();
            try
            {
                sql = @"select * from h_v_JLKanBan_WorkShopCompreRightBottom";
                var data = DapperHelper.selecttable(sql);
                dir.Add("Y", data.Rows[0]["BaseQuantity"].ToString());
                dir.Add("B", data.Rows[1]["BaseQuantity"].ToString());
                dir.Add("C", data.Rows[2]["BaseQuantity"].ToString());
                mes.code = "200";
                mes.message = "查询成功!";
                mes.data = dir;
                mes.data = data;
            }
            catch (Exception e)
            {
VueWebCoreApi/DLL/DAL/WorkOrderDAL.cs
@@ -931,7 +931,7 @@
                var total = 0; //总条数
                var sql = @"select A.id, A.status,A.wotype,A.wo_code,A.materiel_code as partcode,B.partname,B.partspec,A.idTopInventory,A.TopInventoryCode,A.TopInventoryName,A.route_code,B.default_route,R.name as route_name,A.plan_qty,A.wkshp_code,C.torg_name as wkshp_name,
                            A.stck_code,D.name as stck_name,A.plan_startdate,A.plan_enddate,A.piroque,A.sourceid,A.m_po,A.saleOrderDeliveryDate,A.saleOrderCode,U.username as lm_user,A.lm_date,A.data_sources,A.isstep,A.clerkuser,
                            B.priuserdefnvc1,B.priuserdefnvc2,B.priuserdefnvc3,B.priuserdefnvc4,B.priuserdefnvc5,B.priuserdefnvc6,A.printcount
                            B.priuserdefnvc1,B.priuserdefnvc2,B.priuserdefnvc3,B.priuserdefnvc4,B.priuserdefnvc5,B.priuserdefnvc6,A.printcount,W.memo,W.bomsubspec
                            from TK_Wrk_Man A
                            left join TKimp_Ewo W on A.m_po=W.wo and A.materiel_code=W.materiel_code and A.sbid=W.sbid
                            left join TMateriel_Info B on A.materiel_code=B.partcode
@@ -1019,7 +1019,7 @@
                var total = 0; //总条数
                var sql = @"select A.id, A.status,A.wotype,A.wo_code,A.materiel_code as partcode,B.partname,B.partspec,A.idTopInventory,A.TopInventoryCode,A.TopInventoryName,A.plan_qty,A.wkshp_code,C.torg_name as wkshp_name,
                            A.stck_code,D.name as stck_name,A.plan_startdate,A.plan_enddate,A.piroque,A.sourceid,A.m_po,A.saleOrderDeliveryDate,W.saleOrderCode,U.username as lm_user,A.lm_date,S.laborbad_qty,S.materielbad_qty,
                            B.priuserdefnvc1,B.priuserdefnvc2,B.priuserdefnvc3,B.priuserdefnvc4,B.priuserdefnvc5,B.priuserdefnvc6
                            B.priuserdefnvc1,B.priuserdefnvc2,B.priuserdefnvc3,B.priuserdefnvc4,B.priuserdefnvc5,B.priuserdefnvc6,W.memo,W.bomsubspec
                            from TK_Wrk_Man A
                            left join TKimp_Ewo W on A.m_po=W.wo and A.materiel_code=W.materiel_code and A.sbid=W.sbid
                            left join (select wo_code,isnull(sum(laborbad_qty),0) as laborbad_qty,isnull(sum(materielbad_qty),0) as materielbad_qty from  TK_Wrk_Step where (laborbad_qty+materielbad_qty)>0 group by wo_code) S on A.wo_code=S.wo_code
@@ -2883,9 +2883,9 @@
                        //写入缺陷记录表
                        for (int i = 0; i < groupedItems.Count; i++)
                        {
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code, remarks = remarks, style = "B", lm_user = us.usercode, lm_date = date } });
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,report_person,usergroup_code,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@report_person,@usergroup_code,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code,report_person=reportuser,usergroup_code= usergroupcode, remarks = remarks, style = "B", lm_user = us.usercode, lm_date = date } });
                        }
                    }
@@ -2928,9 +2928,9 @@
                        //写入缺陷记录表
                        for (int i = 0; i < groupedItems.Count; i++)
                        {
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code, remarks = remarks, style = "B", lm_user = us.usercode, lm_date = date } });
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,report_person,usergroup_code,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@report_person,@usergroup_code,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code, report_person=reportuser, usergroup_code =usergroupcode, remarks = remarks, style = "B", lm_user = us.usercode, lm_date = date } });
                        }
                    }
@@ -3160,9 +3160,9 @@
                        //写入缺陷记录表
                        for (int i = 0; i < groupedItems.Count; i++)
                        {
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code, remarks = remarks, style = "S", lm_user = us.usercode, lm_date = date } });
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,report_person,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@report_person,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code, report_person=inuser, remarks = remarks, style = "S", lm_user = us.usercode, lm_date = date } });
                        }
                    }
@@ -3187,9 +3187,9 @@
                        //写入缺陷记录表
                        for (int i = 0; i < groupedItems.Count; i++)
                        {
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code, remarks = remarks, style = "S", lm_user = us.usercode, lm_date = date } });
                            sql = @"insert into  CSR_WorkRecord_Defect(record_id,wo_code,partnumber,step_seq,step_code,defect_qty,defect_pendqty,defect_code,report_person,remarks,style,lm_user,lm_date)
                                values(@record_id,@wo_code,@partcode,@stepseq,@stepcode,@ngqty,@defect_pendqty,@defect_code,@report_person,@remarks,@style,@lm_user,@lm_date)";
                            list.Add(new { str = sql, parm = new { record_id = int.Parse(dt.Rows[0]["ID"].ToString()), wo_code = mesordercode, partcode = partcode, stepseq = stepseq, stepcode = stepcode, ngqty = groupedItems[i].badqty, defect_pendqty = groupedItems[i].badqty, defect_code = groupedItems[i].defect_code, report_person=inuser, remarks = remarks, style = "S", lm_user = us.usercode, lm_date = date } });
                        }
                    }
@@ -4293,7 +4293,7 @@
                        //回写对应的报工记录子表合格数量、不良数量、报废数量
                        sql = @"update TK_Wrk_RecordSub set report_qty=report_qty+@repair_qty,ng_qty=ng_qty+@ng_qty,laborbad_qty=laborbad_qty+@laborbad_qty,materielbad_qty=materielbad_qty+@materielbad_qty,
                            updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                            report_person=@report_person,usergroup_code=@usergroup_code,updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                            where  m_id=@m_id and id=@id and style='B'";
                        list.Add(new
                        {
@@ -4307,6 +4307,8 @@
                                laborbad_qty = this_laborbad_dvalue,
                                materielbad_qty = this_materielbad_dvalue,
                                //bad_money = decimal.Parse(json[i].badmoney_dvalue),
                                report_person = json[0].usercode,
                                usergroup_code = json[0].groupcode,
                                updatereportuser = us.usercode,
                                updatereportdate = date
                            }
@@ -4353,7 +4355,7 @@
                        {
                            //回写不良
                            sql = @"update CSR_WorkRecord_Defect set defect_qty=defect_qty+@ng_qty,defect_pendqty=defect_pendqty+@ng_qty,laborbad_qty=laborbad_qty+@laborbad_qty,materielbad_qty=materielbad_qty+@materielbad_qty,
                                updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                                report_person=@report_person,usergroup_code=@usergroup_code,updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                                where wo_code=@wo_code and step_code=@step_code and id=@ng_id and record_id=@record_id and style='B'";
                            list.Add(new
                            {
@@ -4367,6 +4369,8 @@
                                    step_code = json[0].step_code,
                                    ng_id = int.Parse(json[0].children[i].ng_id),
                                    record_id = json[0].id,
                                    report_person = json[0].usercode,
                                    usergroup_code = json[0].groupcode,
                                    updatereportuser = us.usercode,
                                    updatereportdate = date
                                }
@@ -4395,7 +4399,7 @@
                        //回写对应的外协记录子表收料数量、不良数量、报废数量
                        sql = @"update TK_Wrk_OutRecordSub set sqty=sqty+@repair_qty,ng_qty=ng_qty+@ng_qty,laborbad_qty=laborbad_qty+@laborbad_qty,materielbad_qty=materielbad_qty+@materielbad_qty,
                            updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                            out_person=@out_person,updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                            where  m_id=@m_id and id=@id and style='S' and wx_code=@wx_code";
                        list.Add(new
                        {
@@ -4410,6 +4414,7 @@
                                materielbad_qty = this_materielbad_dvalue,
                                wx_code = json[0].wxcode,
                                //bad_money = decimal.Parse(json[i].badmoney_dvalue),
                                out_person = json[0].usercode,
                                updatereportuser = us.usercode,
                                updatereportdate = date
                            }
@@ -4457,7 +4462,7 @@
                        {
                            //回写不良
                            sql = @"update CSR_WorkRecord_Defect set defect_qty=defect_qty+@ng_qty,defect_pendqty=defect_pendqty+@ng_qty,laborbad_qty=laborbad_qty+@laborbad_qty,materielbad_qty=materielbad_qty+@materielbad_qty,
                                updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                                report_person=@report_person,usergroup_code=@usergroup_code,updatereportuser=@updatereportuser,updatereportdate=@updatereportdate
                                where wo_code=@wo_code and step_code=@step_code and id=@ng_id and record_id=@record_id and style='S'";
                            list.Add(new
                            {
@@ -4471,6 +4476,8 @@
                                    step_code = json[0].step_code,
                                    ng_id = int.Parse(json[0].children[i].ng_id),
                                    record_id = json[0].id,
                                    report_person = json[0].usercode,
                                    usergroup_code = json[0].groupcode,
                                    updatereportuser = us.usercode,
                                    updatereportdate = date
                                }
VueWebCoreApi/Quartz/HttpManager.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace VueWebCoreApi.Quartz
@@ -12,62 +13,182 @@
    {
        public static string GetUserIP(IHttpContextAccessor httpContextAccessor)
        {
            var Request = httpContextAccessor.HttpContext.Request;
            string realIP = null;
            string forwarded = null;
            string remoteIpAddress = httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
            if (Request.Headers.ContainsKey("X-Real-IP"))
            if (httpContextAccessor?.HttpContext == null)
                return string.Empty;
            var request = httpContextAccessor.HttpContext.Request;
            string remoteIpAddress = httpContextAccessor.HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
            if (request.Headers.ContainsKey("X-Real-IP"))
            {
                realIP = Request.Headers["X-Real-IP"].ToString();
                if (realIP != remoteIpAddress)
                var realIp = request.Headers["X-Real-IP"].ToString();
                if (!string.IsNullOrEmpty(realIp) && realIp != remoteIpAddress)
                {
                    remoteIpAddress = realIP;
                    remoteIpAddress = realIp;
                }
            }
            if (Request.Headers.ContainsKey("X-Forwarded-For"))
            if (request.Headers.ContainsKey("X-Forwarded-For"))
            {
                forwarded = Request.Headers["X-Forwarded-For"].ToString();
                if (forwarded != remoteIpAddress)
                var forwarded = request.Headers["X-Forwarded-For"].ToString();
                if (!string.IsNullOrEmpty(forwarded) && forwarded != remoteIpAddress)
                {
                    remoteIpAddress = forwarded;
                }
            }
            return remoteIpAddress;
        }
        public static async Task<string> HttpSendAsync(this IHttpClientFactory httpClientFactory, HttpMethod method, string url, Dictionary<string, string> headers = null)
        public static async Task<string> HttpSendAsyncString(this IHttpClientFactory httpClientFactory, HttpMethod method, string url, string parameters = null, Dictionary<string, string> headers = null)
        {
            if (httpClientFactory == null)
                throw new ArgumentNullException(nameof(httpClientFactory));
            if (method == null)
                throw new ArgumentNullException(nameof(method));
            if (string.IsNullOrEmpty(url))
                throw new ArgumentNullException(nameof(url));
            var client = httpClientFactory.CreateClient();
            var content = new StringContent("");
            //content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            //content.Headers.ContentType.CharSet = "utf-8";
            var request = new HttpRequestMessage(method, url)
            string finalUrl = url;
            HttpContent content = new StringContent("");
            try
            {
                Content = content
            };
                // ====================== GET è¯·æ±‚:拼接参数到 URL ======================
                if (method == HttpMethod.Get && !string.IsNullOrWhiteSpace(parameters))
                {
                    finalUrl = url.Contains("?")
                        ? $"{url}&{parameters}"
                        : $"{url}?{parameters}";
                }
                // ====================== POST è¯·æ±‚:参数放入 Body ======================
                if (method == HttpMethod.Post && !string.IsNullOrWhiteSpace(parameters))
                {
                    content = new StringContent(
                        parameters,
                        System.Text.Encoding.UTF8,
                        "application/x-www-form-urlencoded");
                }
                var request = new HttpRequestMessage(method, finalUrl) { Content = content };
                // è¿½åŠ è¯·æ±‚å¤´
            if (headers != null)
            {
                foreach (var header in headers)
                {
                    request.Headers.Add(header.Key, header.Value);
                }
            }
            try
                        if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value))
            {
                HttpResponseMessage httpResponseMessage = await client.SendAsync(request);
                            content.Headers.TryAddWithoutValidation(header.Key, header.Value);
                        }
                    }
                }
                var result = await httpResponseMessage.Content
                    .ReadAsStringAsync();
                return result;
                using (var response = await client.SendAsync(request))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return ex.Message;
                return $"请求失败:{ex.Message}";
            }
        }
        public static async Task<HttpResult> HttpSendAsync(this IHttpClientFactory httpClientFactory,HttpMethod method,string url,string parameters = null,Dictionary<string, string> headers = null)
        {
            if (httpClientFactory == null)
                throw new ArgumentNullException(nameof(httpClientFactory));
            if (method == null)
                throw new ArgumentNullException(nameof(method));
            if (string.IsNullOrEmpty(url))
                throw new ArgumentNullException(nameof(url));
            var client = httpClientFactory.CreateClient();
            string finalUrl = url;
            HttpContent content = new StringContent("");
            try
            {
                // GET æ‹¼æŽ¥å‚æ•°
                if (method == HttpMethod.Get && !string.IsNullOrWhiteSpace(parameters))
                {
                    finalUrl = url.Contains("?") ? $"{url}&{parameters}" : $"{url}?{parameters}";
                }
                // POST å¤„理参数
                if (method == HttpMethod.Post && !string.IsNullOrWhiteSpace(parameters))
                {
                    content = new StringContent(parameters, Encoding.UTF8, "application/x-www-form-urlencoded");
                }
                var request = new HttpRequestMessage(method, finalUrl) { Content = content };
                // è¯·æ±‚头
                if (headers != null)
                {
                    foreach (var header in headers)
                    {
                        if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value))
                        {
                            content.Headers.TryAddWithoutValidation(header.Key, header.Value);
                        }
                    }
                }
                using (var response = await client.SendAsync(request))
                {
                    // è¯»å–返回内容
                    string responseContent = await response.Content.ReadAsStringAsync();
                    return new HttpResult
                    {
                        IsSuccess = response.IsSuccessStatusCode, // æ˜¯å¦æˆåŠŸ
                        StatusCode = (int)response.StatusCode,    // çŠ¶æ€ç  200/404/500
                        Content = responseContent,                // è¿”回内容
                        ErrorMsg = response.IsSuccessStatusCode ? "" : $"HTTP请求失败,状态码:{(int)response.StatusCode}"
                    };
                }
            }
            catch (Exception ex)
            {
                // ç½‘络异常、超时等
                return new HttpResult
                {
                    IsSuccess = false,
                    StatusCode = 0, // ç½‘络异常状态码设为0
                    Content = "",
                    ErrorMsg = ex.Message
                };
            }
        }
        /// <summary>
        /// HTTP è¯·æ±‚统一返回模型
        /// </summary>
        public class HttpResult
        {
            /// <summary>
            /// æ˜¯å¦è¯·æ±‚成功(200-299)
            /// </summary>
            public bool IsSuccess { get; set; }
            /// <summary>
            /// HTTP çŠ¶æ€ç ï¼ˆæ•°å­—ï¼š200,404,500)
            /// </summary>
            public int StatusCode { get; set; }
            /// <summary>
            /// è¿”回内容
            /// </summary>
            public string Content { get; set; }
            /// <summary>
            /// é”™è¯¯ä¿¡æ¯
            /// </summary>
            public string ErrorMsg { get; set; }
        }
    }
}
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}");
        }
    }
}
VueWebCoreApi/Quartz/IOCJobFactory.cs
@@ -16,12 +16,14 @@
        }
        public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            // ä»ŽDI容器获取Job实例(支持构造函数注入)
            return _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob;
        }
        public void ReturnJob(IJob job)
        {
            // é‡Šæ”¾èµ„源(若需要)
            (job as IDisposable)?.Dispose();
        }
    }
VueWebCoreApi/Quartz/JobAction.cs
@@ -11,8 +11,8 @@
        åˆ é™¤ = 2,
        ä¿®æ”¹ = 3,
        æš‚停 = 4,
        åœæ­¢,
        å¼€å¯,
        ç«‹å³æ‰§è¡Œ
        åœæ­¢ = 5,
        å¼€å¯ = 6,
        ç«‹å³æ‰§è¡Œ = 7
    }
}
VueWebCoreApi/Quartz/JobActionLog.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace VueWebCoreApi.Quartz
{
    public class JobActionLog
    {
        public long Id { get; set; }
        public string ActionType { get; set; }
        public string TaskName { get; set; }
        public string GroupName { get; set; }
        public string Content { get; set; }
        public DateTime CreateTime { get; set; }
    }
}
VueWebCoreApi/Quartz/QuartzDapperHelper.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,135 @@
using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using VueWebCoreApi.Tools;
namespace VueWebCoreApi.Quartz
{
    public static class DapperHelper
    {
        // é…ç½®æ–‡ä»¶è¯»å–,建议放到appsettings.json
        private static readonly string _connectionString = AppSetting.GetAppSetting("DBServer");
        /// <summary>
        /// èŽ·å–æ•°æ®åº“è¿žæŽ¥
        /// </summary>
        public static IDbConnection GetConnection()
        {
            var conn = new SqlConnection(_connectionString);
            if (conn.State != ConnectionState.Open)
                conn.Open();
            return conn;
        }
        /// <summary>
        /// æ‰§è¡ŒæŸ¥è¯¢è¿”回DataTable
        /// </summary>
        public static DataTable SelectTable(string sql, object param = null)
        {
            using (var conn = GetConnection())
            {
                var reader = conn.ExecuteReader(sql, param);
                var dt = new DataTable();
                dt.Load(reader);
                return dt;
            }
        }
        /// <summary>
        /// æ‰§è¡Œå¢žåˆ æ”¹
        /// </summary>
        public static int Execute(string sql, object param = null)
        {
            using (var conn = GetConnection())
            {
                return conn.Execute(sql, param);
            }
        }
        /// <summary>
        /// å¼‚步执行增删改
        /// </summary>
        public static async Task<int> ExecuteAsync(string sql, object param = null)
        {
            using (var conn = GetConnection())
            {
                return await conn.ExecuteAsync(sql, param);
            }
        }
        /// <summary>
        /// æ‰§è¡Œäº‹åŠ¡
        /// </summary>
        public static bool DoTransaction(List<(string Sql, object Param)> list)
        {
            using (var conn = GetConnection())
            {
                using (var tran = conn.BeginTransaction())
                {
                    try
                    {
                        foreach (var item in list)
                        {
                            conn.Execute(item.Sql, item.Param, tran);
                        }
                        tran.Commit();
                        return true;
                    }
                    catch
                    {
                        tran.Rollback();
                        return false;
                    }
                }
            }
        }
        /// <summary>
        /// æŸ¥è¯¢åˆ—表
        /// </summary>
        public static List<T> Query<T>(string sql, object param = null)
        {
            using (var conn = GetConnection())
            {
                return conn.Query<T>(sql, param).ToList();
            }
        }
        /// <summary>
        /// å¼‚步查询列表
        /// </summary>
        public static async Task<List<T>> QueryAsync<T>(string sql, object param = null)
        {
            using (var conn = GetConnection())
            {
                var result = await conn.QueryAsync<T>(sql, param);
                return result.ToList();
            }
        }
        /// <summary>
        /// æŸ¥è¯¢å•个对象
        /// </summary>
        public static T QueryFirstOrDefault<T>(string sql, object param = null)
        {
            using (var conn = GetConnection())
            {
                return conn.QueryFirstOrDefault<T>(sql, param);
            }
        }
        /// <summary>
        /// å¼‚步查询单个对象
        /// </summary>
        public static async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object param = null)
        {
            using (var conn = GetConnection())
            {
                return await conn.QueryFirstOrDefaultAsync<T>(sql, param);
            }
        }
    }
}
VueWebCoreApi/Quartz/QuartzFileInfo.cs
@@ -5,6 +5,9 @@
namespace VueWebCoreApi.Quartz
{
    /// <summary>
    /// Quartz文件存储常量配置
    /// </summary>
    public class QuartzFileInfo
    {
        /// <summary>
VueWebCoreApi/Quartz/QuartzNETExtension.cs
@@ -17,110 +17,142 @@
namespace VueWebCoreApi.Extensions
{
    /// <summary>
    /// Quartz.NET æ‰©å±•方法类
    /// å°è£…Quartz任务的初始化、增删改查、启停、立即执行等核心操作
    /// è§£å†³äº†Scheduler启动、内存与数据库状态同步、Cron表达式兼容、Key匹配等核心问题
    /// </summary>
    public static class QuartzNETExtension
    {
        public static ToMessage mes = new ToMessage(); //定义全局返回信息对象
        /// <summary>
        /// å…¨å±€è¿”回信息对象(统一接口返回格式)
        /// åŒ…含code(状态码)、count(数量)、message(提示信息)、data(数据)
        /// </summary>
        public static ToMessage mes = new ToMessage();
        /// <summary>
        /// å†…存级任务列表缓存
        /// ç”¨äºŽå‡å°‘数据库查询,同时保证内存与数据库状态一致
        /// </summary>
        private static List<TaskOptions> _taskList = new List<TaskOptions>();
        /// <summary>
        /// åˆå§‹åŒ–作业
        /// æ‰©å±•IApplicationBuilder,初始化Quartz任务调度器并加载所有任务
        /// ã€æ ¸å¿ƒä¿®å¤ã€‘解决Quartz默认不启动Scheduler的问题
        /// </summary>
        /// <param name="applicationBuilder"></param>
        /// <param name="env"></param>
        /// <returns></returns>
        public static IApplicationBuilder UseQuartz(this IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
        /// <param name="app">ASP.NET Core应用构建器</param>
        /// <param name="env">Web主机环境</param>
        /// <returns>应用构建器(链式调用)</returns>
        public static IApplicationBuilder UseQuartz(this IApplicationBuilder app, IWebHostEnvironment env)
        {
            IServiceProvider services = applicationBuilder.ApplicationServices;
            // ä»ŽDI容器获取依赖服务
            var services = app.ApplicationServices;
            // Quartz调度器工厂
            var schedulerFactory = services.GetService<ISchedulerFactory>();
            // è‡ªå®šä¹‰Quartz仓储(数据库操作)
            var quartzRepo = services.GetService<QuartzRepository>();
            ISchedulerFactory _schedulerFactory = services.GetService<ISchedulerFactory>();
            string path = FileQuartz.CreateQuartzRootPath(env);
            //查找任务列表
            var data = QuartzSearchData.QuartzSearch();
            if (data.Rows.Count <= 0)
            // 1. æ ¸å¿ƒä¿®å¤ï¼šQuartz默认不会自动启动Scheduler,必须显式启动
            var scheduler = schedulerFactory.GetScheduler().GetAwaiter().GetResult();
            if (!scheduler.IsStarted)
            {
                FileHelper.WriteFile(FileQuartz.LogPath, "start.txt", $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")},没有默认配置任务\r\n", true);
                return applicationBuilder;
                scheduler.Start().GetAwaiter().GetResult();
            }
            string jobConfig = JsonConvert.SerializeObject(data);
            //string jobConfig = FileHelper.ReadFile(path + QuartzFileInfo.JobConfigFileName);
            //if (string.IsNullOrEmpty(jobConfig))
            //{
            //    FileHelper.WriteFile(FileQuartz.LogPath, "start.txt", $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")},没有默认配置任务\r\n", true);
            //    return applicationBuilder;
            //}
            // 2. ä»Žæ•°æ®åº“加载所有配置的任务
            var taskList = quartzRepo.GetAllTasksAsync().GetAwaiter().GetResult();
            if (taskList.Count == 0)
            {
                //无任务时记录启动日志
                quartzRepo.WriteStartLogAsync($"{DateTime.Now:yyyy-MM-dd HH:mm:ss},没有默认配置任务").GetAwaiter().GetResult();
                return app;
            }
            // 3. éåŽ†ä»»åŠ¡å¹¶åˆå§‹åŒ–ï¼ˆè®°å½•å¤±è´¥æ•°å’Œå¼‚å¸¸ä¿¡æ¯ï¼‰
            int errorCount = 0;
            string errorMsg = "";
            TaskOptions options = null;
            string errorMsg = string.Empty;
            // åˆå§‹åŒ–内存任务列表
            _taskList = taskList;
            foreach (var task in taskList)
            {
            try
            {
                _taskList = JsonConvert.DeserializeObject<List<TaskOptions>>(jobConfig);
                _taskList.ForEach(x =>
                {
                    options = x;
                    var result = x.AddJob(_schedulerFactory, true, jobFactory: services.GetService<IJobFactory>()).GetAwaiter().GetResult();
                });
                    //var result = task.AddJob(schedulerFactory, true, services.GetService<IJobFactory>()).GetAwaiter().GetResult();
                    // è°ƒç”¨TaskOptions的AddJob方法添加任务到调度器
                    // ä¼ å…¥ä»“储用于数据库操作,传入JobFactory用于自定义Job实例化
                    var result = task.AddJob(schedulerFactory, true, services.GetService<IJobFactory>(), quartzRepo).GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                errorCount = +1;
                errorMsg += $"作业:{options?.TaskName},异常:{ex.Message}";
                    // è®°å½•单个任务初始化失败信息
                    errorCount++;
                    errorMsg += $"作业:{task.TaskName},异常:{ex.Message};";
            }
            string content = $"成功:{   _taskList.Count - errorCount}个,失败{errorCount}个,异常:{errorMsg}\r\n";
            FileQuartz.WriteStartLog(content);
            }
            // 4. è®°å½•整体初始化结果日志
            var content = $"成功:{taskList.Count - errorCount}个,失败{errorCount}个,异常:{errorMsg}";
            quartzRepo.WriteStartLogAsync($"{DateTime.Now:yyyy-MM-dd HH:mm:ss},{content}").GetAwaiter().GetResult();
            return applicationBuilder;
            return app;
        }
        /// <summary>
        /// èŽ·å–æ‰€æœ‰çš„ä½œä¸š
        /// èŽ·å–è°ƒåº¦å™¨ä¸­æ‰€æœ‰ä»»åŠ¡ï¼Œå¹¶åŒæ­¥å†…å­˜/数据库状态(核心修复:解决内存列表与数据库不一致问题)
        /// </summary>
        /// <param name="schedulerFactory"></param>
        /// <returns></returns>
        public static async Task<List<TaskOptions>> GetJobs(this ISchedulerFactory schedulerFactory)
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>最新的任务列表</returns>
        public static async Task<List<TaskOptions>> GetJobs(this ISchedulerFactory schedulerFactory, QuartzRepository quartzRepo)
        {
            List<TaskOptions> list = new List<TaskOptions>();
            var list = new List<TaskOptions>();
            try
            {
                IScheduler _scheduler = await schedulerFactory.GetScheduler();
                var groups = await _scheduler.GetJobGroupNames();
                var scheduler = await schedulerFactory.GetScheduler();
                // ç¡®ä¿Scheduler处于启动状态(防御性编程)
                if (!scheduler.IsStarted) await scheduler.Start();
                // 1. èŽ·å–æ‰€æœ‰ä»»åŠ¡åˆ†ç»„ï¼ˆQuartz任务按分组管理)
                var groups = await scheduler.GetJobGroupNames();
                foreach (var groupName in groups)
                {
                    foreach (var jobKey in await _scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(groupName)))
                    // 2. èŽ·å–åˆ†ç»„ä¸‹æ‰€æœ‰JobKey(任务唯一标识:名称+分组)
                    var jobKeys = await scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(groupName));
                    foreach (var jobKey in jobKeys)
                    {
                        TaskOptions taskOptions = _taskList.Where(x => x.GroupName == jobKey.Group && x.TaskName == jobKey.Name)
                            .FirstOrDefault();
                        if (taskOptions == null)
                            continue;
                        var triggers = await _scheduler.GetTriggersOfJob(jobKey);
                        foreach (ITrigger trigger in triggers)
                        // 3. æ ¸å¿ƒä¿®å¤ï¼šä¼˜å…ˆä»Žæ•°æ®åº“获取最新任务(避免内存数据过期)
                        var dbTask = await quartzRepo.TaskExists(jobKey.Name, jobKey.Group);
                        // æ•°æ®åº“无数据时,降级从内存列表获取
                        var task = dbTask.FirstOrDefault() ?? _taskList.FirstOrDefault(x => x.GroupName == jobKey.Group && x.TaskName == jobKey.Name);
                        // è¿‡æ»¤æ— æ•ˆä»»åŠ¡
                        if (task == null) continue;
                        // 4. èŽ·å–ä»»åŠ¡å…³è”çš„è§¦å‘å™¨ï¼ˆQuartz任务通过触发器触发执行)
                        var triggers = await scheduler.GetTriggersOfJob(jobKey);
                        foreach (var trigger in triggers)
                        {
                            DateTimeOffset? dateTimeOffset = trigger.GetPreviousFireTimeUtc();
                            if (dateTimeOffset != null)
                            // 5. åŒæ­¥è§¦å‘器实际状态到任务对象,并更新到数据库
                            task.Status = (int)await scheduler.GetTriggerState(trigger.Key);
                            await quartzRepo.UpdateTaskAsync(task); // çŠ¶æ€åŒæ­¥åˆ°åº“
                            // 6. åŒæ­¥ä»»åŠ¡æœ€åŽæ‰§è¡Œæ—¶é—´
                            if (trigger.GetPreviousFireTimeUtc() != null)
                            {
                                taskOptions.LastRunTime = Convert.ToDateTime(dateTimeOffset.ToString());
                                //更改最后执行时间
                                bool aa = QuartzSearchData.QuartzUpdate(taskOptions);
                                // ä¼˜å…ˆä»Žè§¦å‘器获取最后执行时间(UTC转本地时间)
                                task.LastRunTime = trigger.GetPreviousFireTimeUtc()?.LocalDateTime;
                                await quartzRepo.UpdateTaskLastRunTimeAsync(task.TaskName, task.GroupName, task.LastRunTime.Value);
                            }
                            else
                            {
                                var runlog = FileQuartz.GetJobRunLog(taskOptions.TaskName, taskOptions.GroupName, 1, 2);
                                if (runlog.Count > 0)
                                // è§¦å‘器无记录时,从执行日志中获取最后执行时间
                                var logs = await quartzRepo.GetJobRunLogAsync(task.TaskName, task.GroupName, 1, 1);
                                if (logs.Count > 0 && DateTime.TryParse(logs[0].BeginDate, out var lastRunTime))
                                {
                                    DateTime.TryParse(runlog[0].BeginDate, out DateTime lastRunTime);
                                    taskOptions.LastRunTime = lastRunTime;
                                    //更改最后执行时间
                                    bool aa = QuartzSearchData.QuartzUpdate(taskOptions);
                                    task.LastRunTime = lastRunTime;
                                    await quartzRepo.UpdateTaskLastRunTimeAsync(task.TaskName, task.GroupName, lastRunTime);
                                }
                            }
                        }
                        list.Add(taskOptions);
                        list.Add(task);
                    }
                }
                // 7. åŒæ­¥å†…存列表为最新数据(保证后续操作使用最新状态)
                _taskList = list;
            }
            catch (Exception ex)
            {
@@ -128,92 +160,78 @@
                mes.count = 0;
                mes.message = ex.Message + ex.StackTrace;
                mes.data = null;
                FileQuartz.WriteStartLog("获取作业异常:" + ex.Message + ex.StackTrace);
                await quartzRepo.WriteStartLogAsync($"获取作业异常:{ex.Message}{ex.StackTrace}");
                return new List<TaskOptions>();
            }
            return list;
        }
        /// <summary>
        /// æ·»åŠ ä½œä¸š
        /// æ–°å¢žQuartz任务(初始化/手动添加通用)
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <param name="schedulerFactory"></param>
        /// <param name="init">是否初始化,否=需要重新生成配置文件,是=不重新生成配置文件</param>
        /// <returns></returns>
        public static async Task<object> AddJob(this TaskOptions taskOptions, ISchedulerFactory schedulerFactory, bool init = false, IJobFactory jobFactory = null)
        /// <param name="task">任务配置项</param>
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="init">是否为初始化阶段(初始化时不重复入库)</param>
        /// <param name="jobFactory">Job工厂(自定义Job实例化逻辑)</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>统一返回结果(ToMessage)</returns>
        public static async Task<object> AddJob(this TaskOptions task, ISchedulerFactory schedulerFactory, bool init = false, IJobFactory jobFactory = null, QuartzRepository quartzRepo = null)
        {
            try
            {
                //处理前端Corn表达式最后一个月的星期天表达式缺陷问题
                string resultString = "";
                string[] str = taskOptions.Interval.Split(" ");
                if ((str[3].Equals("1L") || str[3].Equals("2L") || str[3].Equals("3L") || str[3].Equals("4L") || str[3].Equals("5L") || str[3].Equals("6L") || str[3].Equals("7L")) && str[5].Equals("?"))
                // 1. ä¿®å¤å‰ç«¯ä¼ å…¥çš„Cron表达式兼容性问题
                task.Interval = FixCronExpression(task.Interval);
                // 2. éªŒè¯Cron表达式有效性
                var (isValid, msg) = task.Interval.IsValidExpression();
                if (!isValid)
                    return new ToMessage { code = "300", count = 0, message = msg, data = null };
                // 3. éžåˆå§‹åŒ–阶段:校验任务是否已存在(避免重复添加)
                if (!init && await quartzRepo.TaskExistsAsync(task.TaskName, task.GroupName))
                {
                    str[5] = str[3];
                    str[3] = "?";
                    for (int i = 0; i < str.Length; i++)
                    {
                        resultString += str[i] + " ";
                    return new ToMessage { code = "300", count = 0, message = $"作业:{task.TaskName},分组:{task.GroupName}已经存在", data = null };
                    }
                    taskOptions.Interval = resultString;
                }
                (bool, string) validExpression = taskOptions.Interval.IsValidExpression();
                if (!validExpression.Item1)
                    return new { code = 300,count=0,Message=validExpression.Item2,data="null" };
                (bool, object) result = taskOptions.Exists(init);
                if (!result.Item1)
                    return result.Item2;
                // 4. éžåˆå§‹åŒ–阶段:新增任务到内存+数据库+记录操作日志
                if (!init)
                {
                    //将历史任务及新增加任务天加到List集合
                    _taskList.Add(taskOptions);
                    //将集合数据添加到json文件(覆盖式)
                    FileQuartz.WriteJobConfig(_taskList);
                    //将当前新任务添加到数据表
                    bool aa = QuartzSearchData.QuartzCreate(taskOptions);
                    _taskList.Add(task);// å†…存添加
                    await quartzRepo.CreateTaskAsync(task);// æ•°æ®åº“添加
                    await quartzRepo.AddJobActionLogAsync(JobAction.新增.ToString(), task.TaskName, task.GroupName, "新增任务成功");// æ“ä½œæ—¥å¿—
                }
                IJobDetail job = JobBuilder.Create<HttpResultfulJob>().WithIdentity(taskOptions.TaskName, taskOptions.GroupName).Build();
                ITrigger trigger = TriggerBuilder.Create()
                   .WithIdentity(taskOptions.TaskName, taskOptions.GroupName)
                   .StartNow()
                   .WithDescription(taskOptions.Describe)
                   .WithCronSchedule(taskOptions.Interval)
                // 5. æž„建Quartz Job(任务执行体)
                var job = JobBuilder.Create<HttpResultfulJob>()// HttpResultfulJob:自定义的HTTP请求型Job
                    .WithIdentity(task.TaskName, task.GroupName)// è®¾ç½®Job唯一标识(名称+分组)
                   .Build();
                IScheduler scheduler = await schedulerFactory.GetScheduler();
                // 6. æž„建Cron触发器(按Cron表达式触发)
                var trigger = TriggerBuilder.Create()
                    .WithIdentity(task.TaskName, task.GroupName)// è§¦å‘器唯一标识(与Job保持一致)
                    .StartNow()// ç«‹å³å¯åЍ
                    .WithDescription(task.Describe)// ä»»åŠ¡æè¿°
                    .WithCronSchedule(task.Interval)// ç»‘定Cron表达式
                    .Build();
                if (jobFactory == null)
                {
                    try
                    {
                        jobFactory = HttpContext.Current.RequestServices.GetService<IJobFactory>();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"创建任务[{taskOptions.TaskName}]异常,{ex.Message}");
                    }
                }
                // 7. èŽ·å–è°ƒåº¦å™¨å¹¶ç¡®ä¿å¯åŠ¨
                var scheduler = await schedulerFactory.GetScheduler();
                if (!scheduler.IsStarted) await scheduler.Start(); // ç¡®ä¿å¯åЍ
                // 8. è®¾ç½®è‡ªå®šä¹‰JobFactory(如需控制Job的依赖注入/实例化)
                if (jobFactory != null)
                {
                    scheduler.JobFactory = jobFactory;
                }
                // 9. å°†Job和触发器绑定到调度器
                await scheduler.ScheduleJob(job, trigger);
                if (taskOptions.Status == (int)TriggerState.Normal)
                // 10. æ ¹æ®ä»»åŠ¡çŠ¶æ€è®¾ç½®è§¦å‘å™¨å¯åœ
                if (task.Status == (int)TriggerState.Normal)
                {
                    await scheduler.Start();
                    await scheduler.ResumeTrigger(trigger.Key);// æ˜¾å¼æ¢å¤ï¼ˆç¡®ä¿è§¦å‘器正常运行)
                }
                else
                {
                    await scheduler.PauseJob(job.Key);
                    //  await schedulerFactory.Pause(taskOptions);
                    FileQuartz.WriteStartLog($"作业:{taskOptions.TaskName},分组:{taskOptions.GroupName},新建时未启动原因,状态为:{taskOptions.Status}");
                    await scheduler.PauseJob(job.Key);// æš‚停任务
                    await quartzRepo.WriteStartLogAsync($"作业:{task.TaskName},分组:{task.GroupName},新建时未启动原因,状态为:{task.Status}");
                }
                if (!init)
                    FileQuartz.WriteJobAction(JobAction.新增, taskOptions.TaskName, taskOptions.GroupName);
                mes.code = "200";
                mes.count = 0;
                mes.message ="执行成功!";
@@ -221,221 +239,167 @@
            }
            catch (Exception ex)
            {
                mes.code = "300";
                mes.count = 0;
                mes.message = ex.Message;
                mes.data = null;
                return new { status = false, msg = ex.Message };
                return new ToMessage { code = "300", count = 0, message = ex.Message, data = null };
            }
            return mes;
        }
        /// <summary>
        /// ç§»é™¤ä½œä¸š
        /// ç§»é™¤ï¼ˆåˆ é™¤ï¼‰ä»»åŠ¡
        /// </summary>
        /// <param name="schedulerFactory"></param>
        /// <param name="taskName"></param>
        /// <param name="groupName"></param>
        /// <returns></returns>
        public static Task<object> Remove(this ISchedulerFactory schedulerFactory, TaskOptions taskOptions)
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="task">任务配置</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>操作结果</returns>
        public static Task<object> Remove(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
        {
            return schedulerFactory.TriggerAction(taskOptions.TaskName, taskOptions.GroupName, JobAction.删除, taskOptions);
            return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.删除, task, quartzRepo);
        }
        /// <summary>
        /// æ›´æ–°ä½œä¸š
        /// ä¿®æ”¹ä»»åŠ¡é…ç½®
        /// </summary>
        /// <param name="schedulerFactory"></param>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        public static Task<object> Update(this ISchedulerFactory schedulerFactory, TaskOptions taskOptions)
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="task">新的任务配置</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>操作结果</returns>
        public static Task<object> Update(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
        {
            return schedulerFactory.TriggerAction(taskOptions.TaskName, taskOptions.GroupName, JobAction.修改, taskOptions);
            return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.修改, task, quartzRepo);
        }
        /// <summary>
        /// æš‚停作业
        /// æš‚停任务
        /// </summary>
        /// <param name="schedulerFactory"></param>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        public static Task<object> Pause(this ISchedulerFactory schedulerFactory, TaskOptions taskOptions)
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="task">任务配置</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>操作结果</returns>
        public static Task<object> Pause(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
        {
            return schedulerFactory.TriggerAction(taskOptions.TaskName, taskOptions.GroupName, JobAction.暂停, taskOptions);
            return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.暂停, task, quartzRepo);
        }
        /// <summary>
        /// å¯åŠ¨ä½œä¸š
        /// å¯åŠ¨ï¼ˆæ¢å¤ï¼‰ä»»åŠ¡
        /// </summary>
        /// <param name="schedulerFactory"></param>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        public static Task<object> Start(this ISchedulerFactory schedulerFactory, TaskOptions taskOptions)
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="task">任务配置</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>操作结果</returns>
        public static Task<object> Start(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
        {
            return schedulerFactory.TriggerAction(taskOptions.TaskName, taskOptions.GroupName, JobAction.开启, taskOptions);
            return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.开启, task, quartzRepo);
        }
        /// <summary>
        /// ç«‹å³æ‰§è¡Œä¸€æ¬¡ä½œä¸š
        /// ç«‹å³æ‰§è¡Œä»»åŠ¡ï¼ˆæ— è§†Cron表达式)
        /// </summary>
        /// <param name="schedulerFactory"></param>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        public static Task<object> Run(this ISchedulerFactory schedulerFactory, TaskOptions taskOptions)
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="task">任务配置</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>操作结果</returns>
        public static Task<object> Run(this ISchedulerFactory schedulerFactory, TaskOptions task, QuartzRepository quartzRepo)
        {
            return schedulerFactory.TriggerAction(taskOptions.TaskName, taskOptions.GroupName, JobAction.立即执行, taskOptions);
        }
        public static object ModifyTaskEntity(this TaskOptions taskOptions, ISchedulerFactory schedulerFactory, JobAction action)
        {
            TaskOptions options = null;
            object result = null;
            switch (action)
            {
                case JobAction.删除:
                    for (int i = 0; i < _taskList.Count; i++)
                    {
                        options = _taskList[i];
                        if (options.TaskName == taskOptions.TaskName && options.GroupName == taskOptions.GroupName)
                        {
                            _taskList.RemoveAt(i);
                            //删除任务
                            bool aa = QuartzSearchData.QuartzDelete(options);
                        }
                    }
                    break;
                case JobAction.修改:
                    options = _taskList.Where(x => x.TaskName == taskOptions.TaskName && x.GroupName == taskOptions.GroupName).FirstOrDefault();
                    //移除以前的配置
                    if (options != null)
                    {
                        _taskList.Remove(options);
                        //删除任务
                        bool aa = QuartzSearchData.QuartzDelete(options);
                    }
                    //生成任务并添加新配置
                    result = taskOptions.AddJob(schedulerFactory, false).GetAwaiter().GetResult();
                    break;
                case JobAction.暂停:
                case JobAction.开启:
                case JobAction.停止:
                case JobAction.立即执行:
                    options = _taskList.Where(x => x.TaskName == taskOptions.TaskName && x.GroupName == taskOptions.GroupName).FirstOrDefault();
                    if (action == JobAction.暂停)
                    {
                        options.Status = (int)TriggerState.Paused;
                        //更新任务状态
                        bool aa= QuartzSearchData.QuartzUpdate(options);
                    }
                    else if (action == JobAction.停止)
                    {
                        options.Status = (int)action;
                        //更新任务状态
                        bool aa = QuartzSearchData.QuartzUpdate(options);
                    }
                    else
                    {
                        options.Status = (int)TriggerState.Normal;
                        //更新任务状态
                        bool aa = QuartzSearchData.QuartzUpdate(options);
                    }
                    break;
            }
            //生成配置文件
            FileQuartz.WriteJobConfig(_taskList);
            FileQuartz.WriteJobAction(action, taskOptions.TaskName, taskOptions.GroupName, "操作对象:" + JsonConvert.SerializeObject(taskOptions));
            return result;
            return TriggerAction(schedulerFactory, task.TaskName, task.GroupName, JobAction.立即执行, task, quartzRepo);
        }
        /// <summary>
        /// è§¦å‘新增、删除、修改、暂停、启用、立即执行事件
        /// ä»»åŠ¡æ“ä½œæ ¸å¿ƒé€»è¾‘ï¼ˆæ ¸å¿ƒä¿®å¤ï¼šKey匹配+状态同步+Scheduler启动校验)
        /// å°è£…删除/修改/暂停/开启/立即执行的通用逻辑,避免代码冗余
        /// </summary>
        /// <param name="schedulerFactory"></param>
        /// <param name="taskName"></param>
        /// <param name="groupName"></param>
        /// <param name="action"></param>
        /// <param name="taskOptions"></param>
        /// <returns></returns>
        public static async Task<object> TriggerAction(this ISchedulerFactory schedulerFactory, string taskName, string groupName, JobAction action, TaskOptions taskOptions = null)
        /// <param name="schedulerFactory">调度器工厂</param>
        /// <param name="taskName">任务名称</param>
        /// <param name="groupName">分组名称</param>
        /// <param name="action">操作类型(删除/修改/暂停/开启/立即执行)</param>
        /// <param name="task">任务配置</param>
        /// <param name="quartzRepo">Quartz仓储</param>
        /// <returns>操作结果</returns>
        private static async Task<object> TriggerAction(this ISchedulerFactory schedulerFactory, string taskName, string groupName, JobAction action, TaskOptions task, QuartzRepository quartzRepo)
        {
            string errorMsg = "";
            try
            {
                //处理前端Corn表达式最后一个月的星期天表达式缺陷问题
                string resultString = "";
                string[] str = taskOptions.Interval.Split(" ");
                if ((str[3].Equals("1L") || str[3].Equals("2L") || str[3].Equals("3L") || str[3].Equals("4L") || str[3].Equals("5L") || str[3].Equals("6L") || str[3].Equals("7L")) && str[5].Equals("?"))
                {
                    str[5] = str[3];
                    str[3] = "?";
                    for (int i = 0; i < str.Length; i++)
                    {
                        resultString += str[i] + " ";
                    }
                    taskOptions.Interval = resultString;
                }
                IScheduler scheduler = await schedulerFactory.GetScheduler();
                List<JobKey> jobKeys = scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(groupName)).Result.ToList();
                if (jobKeys == null || jobKeys.Count() == 0)
                {
                    errorMsg = $"未找到分组[{groupName}]";
                    return new { code = 300, count = 0, Message = errorMsg, data = "null" };
                    //return new { status = false, msg = errorMsg };
                }
                JobKey jobKey = jobKeys.Where(s => scheduler.GetTriggersOfJob(s).Result.Any(x => (x as CronTriggerImpl).Name == taskName)).FirstOrDefault();
                if (jobKey == null)
                {
                    errorMsg = $"未找到触发器[{taskName}]";
                    return new { code = 300, count = 0, Message = errorMsg, data = "null" };
                }
                var triggers = await scheduler.GetTriggersOfJob(jobKey);
                ITrigger trigger = triggers?.Where(x => (x as CronTriggerImpl).Name == taskName).FirstOrDefault();
                // 1. é¢„处理Cron表达式(修复前端传入的表达式)
                if (task != null) task.Interval = FixCronExpression(task.Interval);
                if (trigger == null)
                // 2. èŽ·å–è°ƒåº¦å™¨å¹¶ç¡®ä¿å¯åŠ¨ï¼ˆæ ¸å¿ƒä¿®å¤1:全局Scheduler启动校验)
                var scheduler = await schedulerFactory.GetScheduler();
                // æ ¸å¿ƒä¿®å¤1:确保Scheduler全局启动
                if (!scheduler.IsStarted) await scheduler.Start();
                // 3. æ ¸å¿ƒä¿®å¤2:正确构建JobKey(任务名+分组),解决Key匹配失败问题
                var jobKey = new JobKey(taskName, groupName);
                if (!await scheduler.CheckExists(jobKey))
                {
                    errorMsg = $"未找到触发器[{taskName}]";
                    return new { code = 300, count = 0, Message = errorMsg, data = "null" };
                    return new ToMessage { code = "300", count = 0, message = $"未找到任务[{taskName}-{groupName}]", data = null };
                }
                // 4. èŽ·å–ä»»åŠ¡å…³è”çš„è§¦å‘å™¨ï¼ˆæ— è§¦å‘å™¨åˆ™ä»»åŠ¡æ— æ³•æ‰§è¡Œï¼‰
                var triggers = await scheduler.GetTriggersOfJob(jobKey);
                if (triggers == null || !triggers.Any())
                {
                    return new ToMessage { code = "300", count = 0, message = $"任务[{taskName}-{groupName}]无触发器", data = null };
                }
                // å–第一个触发器(单任务默认绑定一个触发器)
                var trigger = triggers.First();
                object result = null;
                // 5. æ ¹æ®æ“ä½œç±»åž‹æ‰§è¡Œä¸åŒé€»è¾‘
                switch (action)
                {
                    case JobAction.删除:
                        // æš‚停触发器 -> è§£ç»‘触发器 -> åˆ é™¤Job -> åŒæ­¥å†…å­˜+数据库 -> è®°å½•日志
                        await scheduler.PauseTrigger(trigger.Key);
                        await scheduler.UnscheduleJob(trigger.Key);
                        await scheduler.DeleteJob(jobKey);
                        _taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
                        await quartzRepo.DeleteTaskAsync(taskName, groupName);
                        await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "删除任务成功");
                        break;
                    case JobAction.修改:
                        // ä¿®æ”¹é€»è¾‘:先删除旧任务 -> é‡æ–°æ·»åŠ æ–°é…ç½® -> è®°å½•日志
                        await scheduler.PauseTrigger(trigger.Key);
                        await scheduler.UnscheduleJob(trigger.Key);// ç§»é™¤è§¦å‘器
                        await scheduler.DeleteJob(trigger.JobKey);
                        result = taskOptions.ModifyTaskEntity(schedulerFactory, action);
                        await scheduler.UnscheduleJob(trigger.Key);
                        await scheduler.DeleteJob(jobKey);
                        _taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
                        await quartzRepo.DeleteTaskAsync(taskName, groupName);
                        result = await task.AddJob(schedulerFactory, false, null, quartzRepo);// é‡æ–°æ·»åŠ ä»»åŠ¡
                        await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, JsonConvert.SerializeObject(task));
                        break;
                    case JobAction.暂停:
                    case JobAction.停止:
                    case JobAction.开启:
                        result = taskOptions.ModifyTaskEntity(schedulerFactory, action);
                        if (action == JobAction.暂停)
                        {
                        // æš‚停触发器 -> æ›´æ–°ä»»åŠ¡çŠ¶æ€ -> åŒæ­¥å†…å­˜+数据库 -> è®°å½•日志
                            await scheduler.PauseTrigger(trigger.Key);
                        }
                        else if (action == JobAction.开启)
                        {
                            await scheduler.ResumeTrigger(trigger.Key);
                            //   await scheduler.RescheduleJob(trigger.Key, trigger);
                        }
                        else
                        {
                            await scheduler.Shutdown();
                        }
                        task.Status = (int)TriggerState.Paused;
                        await quartzRepo.UpdateTaskAsync(task);
                        _taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
                        _taskList.Add(task); // åŒæ­¥å†…存状态
                        await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "暂停任务成功");
                        break;
                    case JobAction.立即执行:
                        if (taskOptions != null && taskOptions.Status != (int)TriggerState.Normal)
                        {
                            result = taskOptions.ModifyTaskEntity(schedulerFactory, JobAction.开启);
                    case JobAction.开启:
                        // æ¢å¤è§¦å‘器 -> æ›´æ–°ä»»åŠ¡çŠ¶æ€ -> åŒæ­¥å†…å­˜+数据库 -> è®°å½•日志
                             await scheduler.ResumeTrigger(trigger.Key);
                        task.Status = (int)TriggerState.Normal;
                        await quartzRepo.UpdateTaskAsync(task);
                        _taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
                        _taskList.Add(task); // åŒæ­¥å†…存状态
                        await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "开启任务成功");
                        break;
                        
                    case JobAction.立即执行:
                        // ç«‹å³æ‰§è¡Œå‰ç¡®ä¿ä»»åŠ¡å¤„äºŽå¯åŠ¨çŠ¶æ€ -> è§¦å‘Job立即执行 -> è®°å½•日志
                        if (task.Status != (int)TriggerState.Normal)
                        {
                            task.Status = (int)TriggerState.Normal;
                            await quartzRepo.UpdateTaskAsync(task);
                            _taskList.RemoveAll(x => x.TaskName == taskName && x.GroupName == groupName);
                            _taskList.Add(task);
                            await scheduler.ResumeTrigger(trigger.Key);
                        }
                        else {
                            await scheduler.TriggerJob(jobKey);
                        }
                        await scheduler.TriggerJob(jobKey); // ç«‹å³è§¦å‘任务执行
                        await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, "立即执行任务成功");
                        break;
                }
                mes.code = "200";
@@ -443,70 +407,78 @@
                mes.message = $"作业{action.ToString()}成功";
                mes.data = null;
                return result ?? mes;
                //return result ?? new { status = true, msg = $"作业{action.ToString()}成功" };
            }
            catch (Exception ex)
            {
                errorMsg = ex.Message;
                mes.code = "300";
                mes.count = 0;
                mes.message = errorMsg;
                mes.data = null;
                return mes;
                //return new { status = false, msg = ex.Message };
            }
            finally
            {
                FileQuartz.WriteJobAction(action, taskName, groupName, errorMsg);
                await quartzRepo.AddJobActionLogAsync(action.ToString(), taskName, groupName, $"操作失败:{ex.Message}");
                return new ToMessage { code = "300", count = 0, message = ex.Message, data = null };
            }
        }
        /// <summary>
        ///
        /// ä¿®å¤å‰ç«¯ä¼ å…¥çš„Cron表达式缺陷
        /// å…¼å®¹ASP.NET Core 3.1中Cron表达式的Day/Week字段写法问题
        /// </summary>
        /// <param name="context"></param>通过作业上下文获取作业对应的配置参数
        /// <returns></returns>
        public static TaskOptions GetTaskOptions(this IJobExecutionContext context)
        /// <param name="cron">原始Cron表达式</param>
        /// <returns>修复后的Cron表达式</returns>
        private static string FixCronExpression(string cron)
        {
            AbstractTrigger trigger = (context as JobExecutionContextImpl).Trigger as AbstractTrigger;
            TaskOptions taskOptions = _taskList.Where(x => x.TaskName == trigger.Name && x.GroupName == trigger.Group).FirstOrDefault();
            return taskOptions ?? _taskList.Where(x => x.TaskName == trigger.JobName && x.GroupName == trigger.JobGroup).FirstOrDefault();
            if (string.IsNullOrEmpty(cron))
                return cron;
            // æ‹†åˆ†Cron表达式(标准Cron:秒 åˆ† æ—¶ æ—¥ æœˆ å‘¨ å¹´ï¼ˆå¯é€‰ï¼‰ï¼‰
            var str = cron.Split(" ", StringSplitOptions.RemoveEmptyEntries);
            if (str.Length < 6)
                return cron;
            // å…¼å®¹ ASP.NET Core 3.1 çš„写法(替换 or è¯­æ³•)
            // ä¿®å¤åœºæ™¯ï¼šDay字段为1L/2L...7L ä¸” å‘¨å­—段为? æ—¶ï¼Œäº¤æ¢Day和周字段
            // åŽŸå› ï¼šå‰ç«¯å¯èƒ½æ··æ·†äº†Day/周字段的写法,Quartz对L(最后一天)的解析有特定规则
            var dayValue = str[3];// ç¬¬4个字段:Day
            if ((dayValue == "1L" || dayValue == "2L" || dayValue == "3L" ||
                 dayValue == "4L" || dayValue == "5L" || dayValue == "6L" || dayValue == "7L") && str[5] == "?")
            {
                str[5] = str[3]; // å‘¨å­—段 = åŽŸDay字段
                str[3] = "?";    // Day字段 = ?
                return string.Join(" ", str);
            }
            return cron;
        }
        /// <summary>
        /// ä½œä¸šæ˜¯å¦å­˜åœ¨
        /// éªŒè¯Cron表达式是否有效
        /// </summary>
        /// <param name="taskOptions"></param>
        /// <param name="init">初始化的不需要判断</param>
        /// <returns></returns>
        public static (bool, object) Exists(this TaskOptions taskOptions, bool init)
        {
            if (!init && _taskList.Any(x => x.TaskName == taskOptions.TaskName && x.GroupName == taskOptions.GroupName))
            {
                return (false,
                    new
                    {
                        status = false,
                        msg = $"作业:{taskOptions.TaskName},分组:{taskOptions.GroupName}已经存在"
                    });
            }
            return (true, null);
        }
        /// <param name="cronExpression">Cron表达式</param>
        /// <returns>是否有效 + é”™è¯¯ä¿¡æ¯</returns>
        public static (bool, string) IsValidExpression(this string cronExpression)
        {
            try
            {
                CronTriggerImpl trigger = new CronTriggerImpl();
                // é€šè¿‡Quartz内置的CronTriggerImpl验证表达式
                var trigger = new CronTriggerImpl();
                trigger.CronExpressionString = cronExpression;
                DateTimeOffset? date = trigger.ComputeFirstFireTimeUtc(null);
                return (date != null, date == null ? $"请确认表达式{cronExpression}是否正确!" : "");
                // è®¡ç®—第一个触发时间:null表示表达式无效
                var date = trigger.ComputeFirstFireTimeUtc(null);
                return (date != null, date == null ? $"表达式{cronExpression}无效!" : string.Empty);
            }
            catch (Exception e)
            {
                return (false, $"请确认表达式{cronExpression}是否正确!{e.Message}");
            }
                return (false, $"表达式{cronExpression}无效!{e.Message}");
        }
    }
        /// <summary>
        /// ä»ŽJob执行上下文获取任务配置
        /// ç”¨äºŽJob执行时获取最新的任务参数
        /// </summary>
        /// <param name="context">Job执行上下文</param>
        /// <returns>任务配置项</returns>
        public static TaskOptions GetTaskOptions(this IJobExecutionContext context)
        {
            var jobKey = context.JobDetail.Key;
            // ä»Žå†…存列表中获取匹配的任务(Job执行时优先用内存,减少数据库查询)
            return _taskList.FirstOrDefault(x => x.TaskName == jobKey.Name && x.GroupName == jobKey.Group);
        }
    }
}
VueWebCoreApi/Quartz/QuartzRepository.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace VueWebCoreApi.Quartz
{
    public class QuartzRepository
    {
        #region ä»»åŠ¡åŸºç¡€æ“ä½œ
        public async Task<List<TaskOptions>> GetAllTasksAsync()
        {
            var sql = @"SELECT TaskName,GroupName,Interval,ApiUrl,AuthKey,AuthValue,Describe,MessagePush,PushUserCode,RequestType,LastRunTime,Status,CreateAuthor,CreateTime,UpdateTime
                        FROM ScheduleInfo";
            return await DapperHelper.QueryAsync<TaskOptions>(sql);
        }
        public async Task<bool> CreateTaskAsync(TaskOptions task)
        {
            var sql = @"INSERT INTO ScheduleInfo(TaskName,GroupName,Interval,ApiUrl,RequestParameters,AuthKey,AuthValue,Describe,MessagePush,PushUserCode,RequestType,LastRunTime,Status,CreateAuthor,CreateTime,UpdateTime)
                        VALUES(@TaskName,@GroupName,@Interval,@ApiUrl,@RequestParameters,@AuthKey,@AuthValue,@Describe,@MessagePush,@PushUserCode,@RequestType,@LastRunTime,@Status,'9999',GETDATE(),GETDATE())";
            var rows = await DapperHelper.ExecuteAsync(sql, task);
            return rows > 0;
        }
        /// <summary>
        /// ä¿®å¤ï¼šå¢žåŠ æ›´æ–°ç»“æžœæ ¡éªŒï¼Œè¿”å›žå…·ä½“ä¿¡æ¯
        /// </summary>
        public async Task<(bool Success, string Msg)> UpdateTaskWithCheckAsync(TaskOptions task)
        {
            if (string.IsNullOrEmpty(task.TaskName) || string.IsNullOrEmpty(task.GroupName))
            {
                return (false, "任务名/分组名不能为空");
            }
            // å…ˆæ ¡éªŒä»»åŠ¡æ˜¯å¦å­˜åœ¨
            if (!await TaskExistsAsync(task.TaskName, task.GroupName))
            {
                return (false, $"任务[{task.TaskName}-{task.GroupName}]不存在");
            }
            var sql = @"UPDATE ScheduleInfo
                        SET Interval=@Interval,ApiUrl=@ApiUrl,RequestParameters=@RequestParameters,AuthKey=@AuthKey,AuthValue=@AuthValue,Describe=@Describe,
                        MessagePush=@MessagePush,PushUserCode=@PushUserCode,RequestType=@RequestType,LastRunTime=@LastRunTime,Status=@Status,UpdateTime=GETDATE()
                        WHERE TaskName=@TaskName AND GroupName=@GroupName";
            var rows = await DapperHelper.ExecuteAsync(sql, task);
            return rows > 0 ? (true, "更新成功") : (false, "更新失败(无数据行更新)");
        }
        // å…¼å®¹åŽŸæœ‰æ–¹æ³•
        public async Task<bool> UpdateTaskAsync(TaskOptions task)
        {
            var (success, _) = await UpdateTaskWithCheckAsync(task);
            return success;
        }
        public async Task<bool> DeleteTaskAsync(string taskName, string groupName)
        {
            var sql = "DELETE FROM ScheduleInfo WHERE TaskName=@TaskName AND GroupName=@GroupName";
            var rows = await DapperHelper.ExecuteAsync(sql, new { TaskName = taskName, GroupName = groupName });
            return rows > 0;
        }
        public async Task<List<TaskOptions>> TaskExists(string taskName, string groupName)
        {
            var sql = "SELECT * FROM ScheduleInfo WHERE TaskName=@TaskName AND GroupName=@GroupName";
            return await DapperHelper.QueryAsync<TaskOptions>(sql, new { TaskName = taskName, GroupName = groupName });
        }
        public async Task<bool> TaskExistsAsync(string taskName, string groupName)
        {
            var sql = "SELECT 1 FROM ScheduleInfo WHERE TaskName=@TaskName AND GroupName=@GroupName";
            var result = await DapperHelper.QueryFirstOrDefaultAsync<int?>(sql, new { TaskName = taskName, GroupName = groupName });
            return result.HasValue;
        }
        public async Task<bool> UpdateTaskLastRunTimeAsync(string taskName, string groupName, DateTime lastRunTime)
        {
            var sql = "UPDATE ScheduleInfo SET LastRunTime=@LastRunTime,UpdateTime=GETDATE() WHERE TaskName=@TaskName AND GroupName=@GroupName";
            var rows = await DapperHelper.ExecuteAsync(sql, new { TaskName = taskName, GroupName = groupName, LastRunTime = lastRunTime });
            return rows > 0;
        }
        #endregion
        #region ä»»åŠ¡è¿è¡Œæ—¥å¿—æ“ä½œ
        public async Task<bool> AddJobRunLogAsync(string taskName, string groupName, DateTime beginDate, DateTime endDate, string msg,string MessagePush,string PushUserCode)
        {
            var sql = @"INSERT INTO JobRunLog(TaskName,GroupName,BeginDate,EndDate,Msg,CreateTime,MessagePush,PushUserCode)
                        VALUES(@TaskName,@GroupName,@BeginDate,@EndDate,@Msg,GETDATE(),@MessagePush,@PushUserCode)";
            var param = new
            {
                TaskName = taskName,
                GroupName = groupName,
                BeginDate = beginDate,
                EndDate = endDate,
                Msg = msg,
                MessagePush= MessagePush,
                PushUserCode= PushUserCode
            };
            var rows = await DapperHelper.ExecuteAsync(sql, param);
            return rows > 0;
        }
        /// <summary>
        /// ä¿®å¤ï¼šåˆ†é¡µå‚数可配置,避免写死100
        /// </summary>
        public async Task<List<TaskLog>> GetJobRunLogAsync(string taskName, string groupName, int page, int pageSize = 20)
        {
            var sql = $@"SELECT Id,TaskName,GroupName,CONVERT(VARCHAR(20),BeginDate,120) AS BeginDate,
                             CONVERT(VARCHAR(20),EndDate,120) AS EndDate,Msg,CreateTime,MessagePush,PushUserCode
                        FROM JobRunLog
                        WHERE TaskName=@TaskName AND GroupName=@GroupName
                        ORDER BY CreateTime DESC
                        OFFSET {(page - 1) * pageSize} ROWS FETCH NEXT {pageSize} ROWS ONLY";
            return await DapperHelper.QueryAsync<TaskLog>(sql, new { TaskName = taskName, GroupName = groupName });
        }
        #endregion
        #region ä»»åŠ¡æ“ä½œæ—¥å¿—æ“ä½œ
        public async Task<bool> AddJobActionLogAsync(string actionType, string taskName, string groupName, string content)
        {
            var sql = @"INSERT INTO JobActionLog(ActionType,TaskName,GroupName,Content,CreateTime)
                        VALUES(@ActionType,@TaskName,@GroupName,@Content,GETDATE())";
            var param = new
            {
                ActionType = actionType,
                TaskName = taskName,
                GroupName = groupName,
                Content = content
            };
            var rows = await DapperHelper.ExecuteAsync(sql, param);
            return rows > 0;
        }
        #endregion
        #region ç³»ç»Ÿå¯åŠ¨æ—¥å¿—æ“ä½œ
        public async Task<bool> WriteStartLogAsync(string content)
        {
            var sql = "INSERT INTO SystemStartLog(Content,CreateTime) VALUES(@Content,GETDATE())";
            var rows = await DapperHelper.ExecuteAsync(sql, new { Content = content });
            return rows > 0;
        }
        #endregion
    }
}
VueWebCoreApi/Quartz/QuartzSearchData.cs
ÎļþÒÑɾ³ý
VueWebCoreApi/Quartz/TaskLog.cs
@@ -7,8 +7,12 @@
{
    public class TaskLog
    {
        public long Id { get; set; }
        public string TaskName { get; set; }
        public string GroupName { get; set; }
        public string BeginDate { get; set; }
        public string EndDate { get; set; }
        public string Msg { get; set; }
        public DateTime CreateTime { get; set; }
    }
}
VueWebCoreApi/Quartz/TaskOptions.cs
@@ -7,15 +7,57 @@
{
    public class TaskOptions
    {
        /// <summary>
        /// ä»»åŠ¡åç§°
        /// </summary>
        public string TaskName { get; set; }
        /// <summary>
        /// ä»»åŠ¡ç»„åç§°
        /// </summary>
        public string GroupName { get; set; }
        /// <summary>
        /// Cron表达式(任务执行时间规则)
        /// </summary>
        public string Interval { get; set; }
        /// <summary>
        /// ä»»åŠ¡è§¦å‘çš„æŽ¥å£åœ°å€ï¼ˆGET/POST请求地址)
        /// </summary>
        public string ApiUrl { get; set; }
        /// <summary>
        /// è¯·æ±‚参数
        /// </summary>
        public string RequestParameters { get; set; }
        /// <summary>
        /// æŽ¥å£è¯·æ±‚授权Key(请求头参数)
        /// </summary>
        public string AuthKey { get; set; }
        /// <summary>
        /// æŽ¥å£è¯·æ±‚授权Value(请求头参数)
        /// </summary>
        public string AuthValue { get; set; }
        /// <summary>
        /// ä»»åŠ¡æè¿°/备注(说明任务用途)
        /// </summary>
        public string Describe { get; set; }
        /// <summary>
        /// è¯·æ±‚方式:GET/POST
        /// </summary>
        public string RequestType { get; set; }
        public DateTime? LastRunTime { get; set; }
        /// <summary>
        /// æ˜¯å¦æ¶ˆæ¯æŽ¨é€ï¼šY/N
        /// </summary>
        public string MessagePush { get; set; }
        /// <summary>
        /// æ¶ˆæ¯æŽ¨é€ç”¨æˆ·ç¼–号集合
        /// </summary>
        public string PushUserCode { get; set; }
        /// <summary>
        /// ä»»åŠ¡çŠ¶æ€
        /// </summary>
        public int Status { get; set; }
        /// <summary>
        /// æœ€åŽæ‰§è¡Œæ—¶é—´
        /// </summary>
        public DateTime? LastRunTime { get; set; }
    }
}
VueWebCoreApi/SignalR/ChatHub.cs
@@ -19,11 +19,13 @@
        private readonly IHttpContextAccessor _httpContextAccessor;
        private ILog log = LogManager.GetLogger(Startup.repository.Name, typeof(ChatHub));
        ILogger<ChatHub> _logger;
        public ChatHub(ILogger<ChatHub> logger, IHttpContextAccessor httpContextAccessor)
        {
            _logger = logger;
            _httpContextAccessor = httpContextAccessor;
        }
        /// <summary>
        /// å®¢æˆ·ç«¯è¿žæŽ¥æœåŠ¡ç«¯
        /// </summary>
@@ -35,6 +37,7 @@
            _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Connection Server!");
            return base.OnConnectedAsync();
        }
        /// <summary>
        /// å®¢æˆ·ç«¯æ–­å¼€è¿žæŽ¥
        /// </summary>
@@ -43,14 +46,12 @@
        public override Task OnDisconnectedAsync(Exception exception)
        {
            var id = Context.ConnectionId;
            // åˆ é™¤ç”¨æˆ·ID
            UserIdsStore.Ids.Remove(id);
            // ä½¿ç”¨çº¿ç¨‹å®‰å…¨æ–¹æ³•移除连接
            UserIdsStore.RemoveUser(id);
            _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Close Connection Server!");
            return base.OnDisconnectedAsync(exception);
        }
        /**
         * æµ‹è¯•
         * */
        /// <summary>
        /// ç»™æ‰€æœ‰å®¢æˆ·ç«¯å‘送消息
        /// </summary>
@@ -61,27 +62,31 @@
        }
        /// <summary>
        /// æ·»åŠ åˆ°åœ¨çº¿ç”¨æˆ·åˆ—è¡¨
        /// æ·»åŠ åˆ°åœ¨çº¿ç”¨æˆ·åˆ—è¡¨ï¼ˆçº¿ç¨‹å®‰å…¨ç‰ˆï¼‰
        /// </summary>
        /// <param name="usercode"></param>
        /// <param name="usercode">用户编码</param>
        /// <returns></returns>
        public async Task AddUser(string usercode)
        {
            string cid = Context.ConnectionId;
            if (!UserIdsStore.Ids.ContainsValue(usercode))
            if (string.IsNullOrEmpty(usercode))
            {
                await Task.Run(() => UserIdsStore.Ids.Add(cid, usercode));
            }
            else
            {
                //lambada表达式:根据值去键名Key
                string key = UserIdsStore.Ids.FirstOrDefault(d => d.Value == usercode).Key.ToString();
                // åˆ›å»ºä¸€ä¸ªæ–°çš„键值对
                UserIdsStore.Ids.Add(cid, usercode);
                // ç§»é™¤æ—§çš„键值对
                UserIdsStore.Ids.Remove(key);
                _logger.LogWarning("AddUser方法接收的usercode为空,忽略操作");
                return;
            }
            
            string cid = Context.ConnectionId;
            try
            {
                // è°ƒç”¨çº¿ç¨‹å®‰å…¨çš„æ·»åŠ /更新方法
                UserIdsStore.AddOrUpdateUser(cid, usercode);
                _logger.LogInformation($"用户[{usercode}]的连接[{cid}]已注册到在线列表");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"添加用户[{usercode}]连接[{cid}]失败");
                throw; // æŠ›å‡ºå¼‚常让客户端感知错误
            }
            await Task.CompletedTask; // æ— å¼‚步操作时返回已完成任务
        }
    }
}
VueWebCoreApi/SignalR/UserIdsStore.cs
@@ -6,10 +6,96 @@
namespace VueWebCoreApi.SignalR
{
    /// <summary>
    /// ç”¨æˆ·id集合
    /// ç”¨æˆ·è¿žæŽ¥ID与用户编码映射存储(线程安全版)
    /// è§£å†³å¤šçº¿ç¨‹å¹¶å‘读写导致的字典数据错乱问题
    /// </summary>
    public static class UserIdsStore
    {
        public static Dictionary<string, string> Ids = new Dictionary<string, string>();
        // æ›¿æ¢ä¸ºçº¿ç¨‹å®‰å…¨çš„并发字典
        private static readonly Dictionary<string, string> _ids = new Dictionary<string, string>();
        // åŠ é”ä¿è¯å­—å…¸æ“ä½œçš„åŽŸå­æ€§ï¼ˆæ¯”ConcurrentDictionary更灵活控制批量操作)
        private static readonly object _lockObj = new object();
        /// <summary>
        /// å¯¹å¤–暴露的连接映射(只读,避免外部直接修改)
        /// </summary>
        public static IReadOnlyDictionary<string, string> Ids
        {
            get
            {
                lock (_lockObj)
                {
                    // è¿”回新字典,避免外部拿到引用后修改
                    return new Dictionary<string, string>(_ids);
                }
            }
        }
        /// <summary>
        /// æ·»åŠ /更新用户连接(原子操作)
        /// </summary>
        /// <param name="connectionId">SignalR连接ID</param>
        /// <param name="userCode">用户编码</param>
        public static void AddOrUpdateUser(string connectionId, string userCode)
        {
            if (string.IsNullOrEmpty(connectionId) || string.IsNullOrEmpty(userCode))
                throw new ArgumentNullException("连接ID和用户编码不能为空");
            lock (_lockObj)
            {
                // å…ˆç§»é™¤è¯¥ç”¨æˆ·çš„æ—§è¿žæŽ¥ï¼ˆä¸€ä¸ªç”¨æˆ·åªä¿ç•™æœ€æ–°çš„连接)
                var oldConnIds = _ids.Where(kv => kv.Value == userCode).Select(kv => kv.Key).ToList();
                foreach (var oldConnId in oldConnIds)
                {
                    _ids.Remove(oldConnId);
                }
                // æ·»åŠ æ–°è¿žæŽ¥ï¼ˆè¦†ç›–åŒåconnectionId,避免重复)
                if (_ids.ContainsKey(connectionId))
                {
                    _ids[connectionId] = userCode;
                }
                else
                {
                    _ids.Add(connectionId, userCode);
                }
            }
        }
        /// <summary>
        /// ç§»é™¤æŒ‡å®šè¿žæŽ¥ID的映射
        /// </summary>
        /// <param name="connectionId">SignalR连接ID</param>
        public static void RemoveUser(string connectionId)
        {
            if (string.IsNullOrEmpty(connectionId))
                return;
            lock (_lockObj)
            {
                if (_ids.ContainsKey(connectionId))
                {
                    _ids.Remove(connectionId);
                }
            }
        }
        /// <summary>
        /// æ ¹æ®ç”¨æˆ·ç¼–码获取所有在线连接ID(一个用户可能多端登录时返回多个)
        /// </summary>
        /// <param name="userCodes">用户编码列表</param>
        /// <returns>连接ID列表</returns>
        public static List<string> GetConnectionIdsByUserCodes(List<string> userCodes)
        {
            if (userCodes == null || userCodes.Count == 0)
                return new List<string>();
            lock (_lockObj)
            {
                return _ids.Where(kv => userCodes.Contains(kv.Value))
                           .Select(kv => kv.Key)
                           .Distinct() // åŽ»é‡ï¼Œé¿å…é‡å¤æŽ¨é€
                           .ToList();
            }
        }
    }
}
VueWebCoreApi/Startup.cs
@@ -93,23 +93,38 @@
            XmlConfigurator.Configure(repository, new FileInfo("Config/log4net.config"));
            BasicConfigurator.Configure(repository);
            services.AddControllersWithViews();
            //Quartz
            services.AddHttpClient();
            // æ³¨å†ŒHttpContextAccessor
            services.AddHttpContextAccessor();
            services.AddSingleton<IPathProvider, PathProvider>();
            services.AddTransient<HttpResultfulJob>();
            services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
            services.AddSingleton<IJobFactory, IOCJobFactory>();
            // æ³¨å†ŒHttpClient
            services.AddHttpClient();
            // æ³¨å†ŒQuartz(补充IOCJobFactory)
            services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
            services.AddSingleton<IJobFactory>(sp => new IOCJobFactory(sp)); // å…³é”®ï¼šæ³¨å†Œè‡ªå®šä¹‰JobFactory
            services.AddSingleton<HttpResultfulJob>();
            services.AddSingleton<QuartzRepository>();
            // æ³¨å†Œè·¯å¾„提供器
            services.AddScoped<IPathProvider, PathProvider>();
            //跨域设置
            //services.AddCors(options =>
            //{
            //    options.AddPolicy(
            //        "cors",
            //        set =>
            //        {
            //            set.SetIsOriginAllowed(origin => true).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
            //        });
            //});
            services.AddCors(options =>
            {
                options.AddPolicy(
                    "cors",
                    set =>
                    {
                        set.SetIsOriginAllowed(origin => true).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
                        set.AllowAnyOrigin()
                           .AllowAnyHeader()
                           .AllowAnyMethod();
                    });
            });
            //设置SignalR
@@ -164,11 +179,9 @@
            });
            #endregion
            // å…ˆé‡å®šå‘ HTTPS(如果需要)
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            //添加静态文件中间件
            app.UseDefaultFiles();
            //添加授权文件
@@ -185,6 +198,11 @@
            app.UseStaticFiles();
            //跨域
            app.UseCors("cors");
            //路由
            app.UseRouting();
            //授权
            app.UseAuthorization();
            //用户session服务
            app.UseSession();
            //cookies
VueWebCoreApi/appsettings.json
@@ -9,13 +9,13 @@
  "AllowedHosts": "*",
  //服务器环境
  "ConnectionStrings": {
    "DBServer": "Data Source=121.196.36.24,1533;Initial Catalog=vmes_yy;User ID=sa;Password=xkd@20230101;pooling=false",
    "DBServer": "Data Source=121.196.36.24,1533;Initial Catalog=vmes_alkv2;User ID=sa;Password=xkd@20230101;pooling=false",
    //企业编码
    "CompanyCode": "000",
    //企业名称
    "CompanyName": "浙江寅寅科技有限公司",
    "CompanyName": "浙江阿鲁克健身器材有限公司",
    //redis企业配置
    "Enterprise": "NewYYMES",
    "Enterprise": "NewAlkV2MES",
    //redis数据库链接配置ip
    "RedisConnIp": "127.0.0.1",
    //redis数据库链接配置端口号