| @@ -20,45 +20,66 @@ namespace Himp.TaskScheduling | |||||
| [HttpGet(Name = "Init")] | [HttpGet(Name = "Init")] | ||||
| public bool Init() | public bool Init() | ||||
| { | { | ||||
| // 转码任务 - 每小时执行一次 | |||||
| RecurringJob.AddOrUpdate<TCode1V1Job>("转码-全水码1对1分析码", | RecurringJob.AddOrUpdate<TCode1V1Job>("转码-全水码1对1分析码", | ||||
| job => job.StartAsync(new CancellationToken()), "0 * * * *"); | job => job.StartAsync(new CancellationToken()), "0 * * * *"); | ||||
| // 制样相关任务 - 错开执行时间,避免冲突 | |||||
| RecurringJob.AddOrUpdate<ZYOperateRecordJob>("制样-制样操作记录信息任务", | RecurringJob.AddOrUpdate<ZYOperateRecordJob>("制样-制样操作记录信息任务", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "0,10,20,30,40,50 * * * *"); // 每10分钟,0分开始 | |||||
| RecurringJob.AddOrUpdate<SamplePreparationRecWorker>("制样-制样结果转标准化验任务", | RecurringJob.AddOrUpdate<SamplePreparationRecWorker>("制样-制样结果转标准化验任务", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "2,12,22,32,42,52 * * * *"); // 每10分钟,2分开始 | |||||
| RecurringJob.AddOrUpdate<SampleSpotCheckJob>("制样化验-抽样分样任务", | RecurringJob.AddOrUpdate<SampleSpotCheckJob>("制样化验-抽样分样任务", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "4,14,24,34,44,54 * * * *"); // 每10分钟,4分开始 | |||||
| RecurringJob.AddOrUpdate<AutoSamplingToJob>("制样-自动制样任务下发", | RecurringJob.AddOrUpdate<AutoSamplingToJob>("制样-自动制样任务下发", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "6,16,26,36,46,56 * * * *"); // 每10分钟,6分开始 | |||||
| // 弃样任务 - 每天下午17点执行一次 | |||||
| RecurringJob.AddOrUpdate<AutoDiscardedSamplingJob>("制样-自动弃样任务", | RecurringJob.AddOrUpdate<AutoDiscardedSamplingJob>("制样-自动弃样任务", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "0 17 * * *"); | |||||
| // 数据同步任务 - 错开执行时间 | |||||
| RecurringJob.AddOrUpdate<TestResultSyncStep1Job>("制样-制样数据及全水台账同步", | RecurringJob.AddOrUpdate<TestResultSyncStep1Job>("制样-制样数据及全水台账同步", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "1,11,21,31,41,51 * * * *"); // 每10分钟,1分开始 | |||||
| RecurringJob.AddOrUpdate<TestResultSyncStep2Job>("化验-化验数据同步", | RecurringJob.AddOrUpdate<TestResultSyncStep2Job>("化验-化验数据同步", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "3,13,23,33,43,53 * * * *"); // 每10分钟,3分开始 | |||||
| // 存样柜数据同步 - 保持较高频率但错开时间 | |||||
| RecurringJob.AddOrUpdate<BoltInfo1Job>("存样1-存样柜数据同步", | RecurringJob.AddOrUpdate<BoltInfo1Job>("存样1-存样柜数据同步", | ||||
| job => job.StartAsync(new CancellationToken()), "*/2 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "0,5,10,15,20,25,30,35,40,45,50,55 * * * *"); // 每5分钟,0分开始 | |||||
| RecurringJob.AddOrUpdate<BoltInfo2Job>("存样2-存样柜数据同步", | RecurringJob.AddOrUpdate<BoltInfo2Job>("存样2-存样柜数据同步", | ||||
| job => job.StartAsync(new CancellationToken()), "*/2 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "2,7,12,17,22,27,32,37,42,47,52,57 * * * *"); // 每5分钟,2分开始 | |||||
| // 化验相关任务 - 关键任务,错开执行时间避免冲突 | |||||
| RecurringJob.AddOrUpdate<AuditStandardTestTaskWorker>("化验-自动审核标准化验任务", | RecurringJob.AddOrUpdate<AuditStandardTestTaskWorker>("化验-自动审核标准化验任务", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "5,15,25,35,45,55 * * * *"); // 每10分钟,5分开始 | |||||
| // 化验设备数据收集任务 - 错开执行时间 | |||||
| RecurringJob.AddOrUpdate<TBSampleJob>("化验-机器人测水_化验结果", | RecurringJob.AddOrUpdate<TBSampleJob>("化验-机器人测水_化验结果", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "1,6,11,16,21,26,31,36,41,46,51,56 * * * *"); // 每5分钟,1分开始 | |||||
| RecurringJob.AddOrUpdate<RILS1810ViewMJob>("化验-水分数据", | RecurringJob.AddOrUpdate<RILS1810ViewMJob>("化验-水分数据", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "0,8,16,24,32,40,48,56 * * * *"); // 每8分钟 | |||||
| RecurringJob.AddOrUpdate<RILS1810ViewAJob>("化验-灰分化验结果", | RecurringJob.AddOrUpdate<RILS1810ViewAJob>("化验-灰分化验结果", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "1,9,17,25,33,41,49,57 * * * *"); // 每8分钟,1分开始 | |||||
| RecurringJob.AddOrUpdate<RILS1810ViewVJob>("化验-挥发分数据", | RecurringJob.AddOrUpdate<RILS1810ViewVJob>("化验-挥发分数据", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "2,10,18,26,34,42,50,58 * * * *"); // 每8分钟,2分开始 | |||||
| RecurringJob.AddOrUpdate<RILS1810ViewSJob>("化验-硫数据", | RecurringJob.AddOrUpdate<RILS1810ViewSJob>("化验-硫数据", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "3,11,19,27,35,43,51,59 * * * *"); // 每8分钟,3分开始 | |||||
| RecurringJob.AddOrUpdate<RILS1810ViewCalorJob>("化验-量热仪数据", | RecurringJob.AddOrUpdate<RILS1810ViewCalorJob>("化验-量热仪数据", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "4,12,20,28,36,44,52 * * * *"); // 每8分钟,4分开始 | |||||
| RecurringJob.AddOrUpdate<RILS1810ViewChnJob>("化验-碳氢氮数据", | RecurringJob.AddOrUpdate<RILS1810ViewChnJob>("化验-碳氢氮数据", | ||||
| job => job.StartAsync(new CancellationToken()), "*/5 * * * *"); | |||||
| job => job.StartAsync(new CancellationToken()), "5,13,21,29,37,45,53 * * * *"); // 每8分钟,5分开始 | |||||
| return true; | return true; | ||||
| } | } | ||||
| @@ -9,8 +9,8 @@ namespace Himp.TaskScheduling.Hangfire.Jobs | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// 自动弃样任务 | /// 自动弃样任务 | ||||
| /// 0 7 * * * | |||||
| /// 每天早上七点 | |||||
| /// 0 17 * * * | |||||
| /// 每天下午17点 | |||||
| /// </summary> | /// </summary> | ||||
| public class AutoDiscardedSamplingJob : IHostedService | public class AutoDiscardedSamplingJob : IHostedService | ||||
| { | { | ||||
| @@ -14,16 +14,28 @@ namespace Himp.TaskScheduling | |||||
| public const string GET_STANDARD_TEST_TASK_BEFORE_NOON_SQL = @" | public const string GET_STANDARD_TEST_TASK_BEFORE_NOON_SQL = @" | ||||
| SELECT * | SELECT * | ||||
| FROM [TaskScheduling].[TS_StandardTestTasks] | |||||
| WHERE ExecStas = N'未就绪' | |||||
| AND ((TestCode LIKE N'%Z%') OR (TestCode LIKE N'%RC%' AND Sampletype = '61')) | |||||
| FROM [TaskScheduling].[TS_StandardTestTasks] st | |||||
| WHERE st.ExecStas = N'未就绪' | |||||
| AND ((st.TestCode LIKE N'%Z%') OR (st.TestCode LIKE N'%RC%' AND st.Sampletype = '61')) | |||||
| AND NOT EXISTS ( | |||||
| SELECT 1 FROM [TaskScheduling].[TS_SampleTestTasks] stt | |||||
| WHERE stt.SampleCode = st.SampleCode | |||||
| AND stt.TestCode = st.TestCode | |||||
| AND stt.BottomCode = st.BottomCode | |||||
| ) | |||||
| "; | "; | ||||
| public const string GET_STANDARD_TEST_TASK_AFTER_NOON_SQL = @" | public const string GET_STANDARD_TEST_TASK_AFTER_NOON_SQL = @" | ||||
| SELECT * | SELECT * | ||||
| FROM [TaskScheduling].[TS_StandardTestTasks] | |||||
| WHERE ExecStas = N'未就绪' | |||||
| AND (TestCode LIKE N'%RC%' AND Sampletype = '21') | |||||
| FROM [TaskScheduling].[TS_StandardTestTasks] st | |||||
| WHERE st.ExecStas = N'未就绪' | |||||
| AND (st.TestCode LIKE N'%RC%' AND st.Sampletype = '21') | |||||
| AND NOT EXISTS ( | |||||
| SELECT 1 FROM [TaskScheduling].[TS_SampleTestTasks] stt | |||||
| WHERE stt.SampleCode = st.SampleCode | |||||
| AND stt.TestCode = st.TestCode | |||||
| AND stt.BottomCode = st.BottomCode | |||||
| ) | |||||
| "; | "; | ||||
| public const string UPDATE_STANDARD_TEST_TASK_SQL = @" | public const string UPDATE_STANDARD_TEST_TASK_SQL = @" | ||||
| @@ -69,6 +81,14 @@ namespace Himp.TaskScheduling | |||||
| WHERE TestItems = 'CHN,Mad,A,V,Q,S' AND CONVERT(VARCHAR(10), TaskAssgnTime, 120) = CONVERT(VARCHAR(10), GETDATE(), 120) | WHERE TestItems = 'CHN,Mad,A,V,Q,S' AND CONVERT(VARCHAR(10), TaskAssgnTime, 120) = CONVERT(VARCHAR(10), GETDATE(), 120) | ||||
| "; | "; | ||||
| public const string EXIST_SAMPLE_TEST_TASK_SQL = @" | |||||
| SELECT CASE WHEN | |||||
| EXISTS (SELECT 1 FROM [TaskScheduling].[TS_SampleTestTasks] | |||||
| WHERE SampleCode = @SampleCode AND TestCode = @TestCode AND BottomCode = @BottomCode) | |||||
| THEN 1 | |||||
| ELSE 0 | |||||
| END"; | |||||
| #endregion | #endregion | ||||
| #region 字段 | #region 字段 | ||||
| @@ -76,6 +96,7 @@ namespace Himp.TaskScheduling | |||||
| private readonly ILogger<AuditStandardTestTaskWorker> _logger; | private readonly ILogger<AuditStandardTestTaskWorker> _logger; | ||||
| private readonly ConnectionStringOption _connectionStrings; | private readonly ConnectionStringOption _connectionStrings; | ||||
| private readonly TimeSpan _period = TimeSpan.FromMinutes(1); // 每10分钟执行一次 | private readonly TimeSpan _period = TimeSpan.FromMinutes(1); // 每10分钟执行一次 | ||||
| private static readonly SemaphoreSlim _processLock = new SemaphoreSlim(1, 1); // 应用级别的锁 | |||||
| #endregion | #endregion | ||||
| @@ -141,96 +162,153 @@ namespace Himp.TaskScheduling | |||||
| /// <returns></returns> | /// <returns></returns> | ||||
| private async Task ProcessTasksAsync() | private async Task ProcessTasksAsync() | ||||
| { | { | ||||
| _logger.LogInformation("--------自动审核标准化验任务开始执行--------"); | |||||
| Console.WriteLine("\n========== 自动审核标准化验任务开始执行 ==========\n"); | |||||
| // 尝试获取锁,如果获取不到说明有其他实例正在执行,直接返回 | |||||
| if (!await _processLock.WaitAsync(TimeSpan.FromSeconds(1))) | |||||
| { | |||||
| _logger.LogInformation("--------其他实例正在执行自动审核标准化验任务,跳过本次执行--------"); | |||||
| Console.WriteLine("\n========== 其他实例正在执行,跳过本次执行 ==========\n"); | |||||
| return; | |||||
| } | |||||
| try | try | ||||
| { | { | ||||
| // 检查当前时间是否超过中午12点 | |||||
| var currentTime = DateTime.Now; | |||||
| string sqlToUse; | |||||
| _logger.LogInformation("--------自动审核标准化验任务开始执行--------"); | |||||
| Console.WriteLine("\n========== 自动审核标准化验任务开始执行 ==========\n"); | |||||
| if (currentTime.Hour >= 12) | |||||
| try | |||||
| { | { | ||||
| _logger.LogInformation("--------当前时间已超过中午12点,执行下午任务--------"); | |||||
| Console.WriteLine("\n========== 当前时间已超过中午12点,执行下午任务 ==========\n"); | |||||
| sqlToUse = GET_STANDARD_TEST_TASK_AFTER_NOON_SQL; | |||||
| } | |||||
| else | |||||
| { | |||||
| _logger.LogInformation("--------当前时间未超过中午12点,执行上午任务--------"); | |||||
| Console.WriteLine("\n========== 当前时间未超过中午12点,执行上午任务 ==========\n"); | |||||
| sqlToUse = GET_STANDARD_TEST_TASK_BEFORE_NOON_SQL; | |||||
| } | |||||
| // 检查当前时间是否超过中午12点 | |||||
| var currentTime = DateTime.Now; | |||||
| string sqlToUse; | |||||
| using (IDbConnection connection = new SqlConnection(_connectionStrings.RL_Web)) | |||||
| { | |||||
| try | |||||
| if (currentTime.Hour >= 12) | |||||
| { | { | ||||
| Console.WriteLine("\n========== 尝试连接数据库 ==========\n"); | |||||
| _logger.LogInformation("--------当前时间已超过中午12点,执行下午任务--------"); | |||||
| Console.WriteLine("\n========== 当前时间已超过中午12点,执行下午任务 ==========\n"); | |||||
| sqlToUse = GET_STANDARD_TEST_TASK_AFTER_NOON_SQL; | |||||
| } | |||||
| else | |||||
| { | |||||
| _logger.LogInformation("--------当前时间未超过中午12点,执行上午任务--------"); | |||||
| Console.WriteLine("\n========== 当前时间未超过中午12点,执行上午任务 ==========\n"); | |||||
| sqlToUse = GET_STANDARD_TEST_TASK_BEFORE_NOON_SQL; | |||||
| } | |||||
| // 检查连接字符串是否为空 | |||||
| if (string.IsNullOrEmpty(_connectionStrings.RL_Web)) | |||||
| using (IDbConnection connection = new SqlConnection(_connectionStrings.RL_Web)) | |||||
| { | |||||
| try | |||||
| { | { | ||||
| throw new InvalidOperationException("RL_Web 连接字符串为空,请检查配置文件"); | |||||
| } | |||||
| Console.WriteLine("\n========== 尝试连接数据库 ==========\n"); | |||||
| Console.WriteLine($"\n========== 使用连接字符串: {_connectionStrings.RL_Web} ==========\n"); | |||||
| connection.Open(); | |||||
| Console.WriteLine("\n========== 数据库连接成功 ==========\n"); | |||||
| // 检查连接字符串是否为空 | |||||
| if (string.IsNullOrEmpty(_connectionStrings.RL_Web)) | |||||
| { | |||||
| throw new InvalidOperationException("RL_Web 连接字符串为空,请检查配置文件"); | |||||
| } | |||||
| // 读取数据 | |||||
| Console.WriteLine($"\n========== 执行SQL查询: {sqlToUse.Replace("\n", " ")} ==========\n"); | |||||
| var standardTestTaskResultList = connection.Query<StandardTestTask>(sqlToUse); | |||||
| Console.WriteLine($"\n========== 使用连接字符串: {_connectionStrings.RL_Web} ==========\n"); | |||||
| connection.Open(); | |||||
| Console.WriteLine("\n========== 数据库连接成功 ==========\n"); | |||||
| _logger.LogInformation($"--------查询到 {standardTestTaskResultList.Count()} 条待处理任务--------"); | |||||
| Console.WriteLine($"\n========== 查询到 {standardTestTaskResultList.Count()} 条待处理任务 ==========\n"); | |||||
| // 读取数据 | |||||
| Console.WriteLine($"\n========== 执行SQL查询: {sqlToUse.Replace("\n", " ")} ==========\n"); | |||||
| var standardTestTaskResultList = connection.Query<StandardTestTask>(sqlToUse); | |||||
| foreach (var item in standardTestTaskResultList) | |||||
| { | |||||
| Console.WriteLine($"\n========== 处理任务: SampleCode={item.SampleCode}, TestCode={item.TestCode} ==========\n"); | |||||
| var testItems = ""; | |||||
| var chnCount = connection.QuerySingle<int>(EXIST_STANDARD_TEST_TASK_CH_SQL); | |||||
| _logger.LogInformation($"--------查询到 {standardTestTaskResultList.Count()} 条待处理任务--------"); | |||||
| Console.WriteLine($"\n========== 查询到 {standardTestTaskResultList.Count()} 条待处理任务 ==========\n"); | |||||
| if (item.Sampletype == "21") | |||||
| foreach (var item in standardTestTaskResultList) | |||||
| { | { | ||||
| testItems = "Mad,A,V,Q,S"; | |||||
| } | |||||
| else if (item.Sampletype == "61") | |||||
| { | |||||
| testItems = "Mt"; | |||||
| } | |||||
| Console.WriteLine($"\n========== 处理任务: SampleCode={item.SampleCode}, TestCode={item.TestCode} ==========\n"); | |||||
| // 开始数据库事务,确保原子性操作 | |||||
| using (var transaction = connection.BeginTransaction()) | |||||
| { | |||||
| try | |||||
| { | |||||
| // 第一步:先更新标准测试任务状态为"已审核",防止重复处理 | |||||
| Console.WriteLine("\n========== 先更新标准任务状态为已审核 ==========\n"); | |||||
| var updateStas = await connection.ExecuteAsync(UPDATE_STANDARD_TEST_TASK_SQL, | |||||
| new { ID = item.Id }, transaction); | |||||
| Console.WriteLine($"\n========== 标准任务状态更新结果={updateStas} ==========\n"); | |||||
| // 第二步:检查是否已存在相同的样本测试任务 | |||||
| bool taskExists = await connection.ExecuteScalarAsync<bool>(EXIST_SAMPLE_TEST_TASK_SQL, | |||||
| new | |||||
| { | |||||
| SampleCode = item.SampleCode, | |||||
| TestCode = item.TestCode, | |||||
| BottomCode = item.BottomCode | |||||
| }, transaction); | |||||
| if (!taskExists) | |||||
| { | |||||
| // 第三步:插入样本测试任务 | |||||
| var testItems = ""; | |||||
| var chnCount = connection.QuerySingle<int>(EXIST_STANDARD_TEST_TASK_CH_SQL, transaction: transaction); | |||||
| SampleTestTask sampleTestTask = new SampleTestTask(item.SampleCode | |||||
| , item.TestCode | |||||
| , item.BottomCode | |||||
| , item.Sampletype | |||||
| , item.TestCnt | |||||
| , testItems | |||||
| , 0); | |||||
| Console.WriteLine("\n========== 执行插入操作 ==========\n"); | |||||
| var insertStas = await connection.ExecuteAsync(INSERT_STANDARD_TEST_TASK_SQL, sampleTestTask); | |||||
| Console.WriteLine("\n========== 执行更新操作 ==========\n"); | |||||
| var updateStas = await connection.ExecuteAsync(UPDATE_STANDARD_TEST_TASK_SQL, new { ID = item.Id }); | |||||
| Console.WriteLine($"\n========== 任务处理完成: 插入结果={insertStas}, 更新结果={updateStas} ==========\n"); | |||||
| if (item.Sampletype == "21") | |||||
| { | |||||
| testItems = "Mad,A,V,Q,S"; | |||||
| } | |||||
| else if (item.Sampletype == "61") | |||||
| { | |||||
| testItems = "Mt"; | |||||
| } | |||||
| SampleTestTask sampleTestTask = new SampleTestTask(item.SampleCode | |||||
| , item.TestCode | |||||
| , item.BottomCode | |||||
| , item.Sampletype | |||||
| , item.TestCnt | |||||
| , testItems | |||||
| , 0); | |||||
| Console.WriteLine("\n========== 执行插入样本测试任务操作 ==========\n"); | |||||
| var insertStas = await connection.ExecuteAsync(INSERT_STANDARD_TEST_TASK_SQL, sampleTestTask, transaction); | |||||
| Console.WriteLine($"\n========== 样本测试任务插入结果={insertStas} ==========\n"); | |||||
| } | |||||
| else | |||||
| { | |||||
| Console.WriteLine($"\n========== 样本测试任务已存在,跳过插入: {item.SampleCode} ==========\n"); | |||||
| } | |||||
| // 提交事务 | |||||
| transaction.Commit(); | |||||
| Console.WriteLine($"\n========== 任务处理完成并提交事务: {item.SampleCode} ==========\n"); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| // 回滚事务 | |||||
| transaction.Rollback(); | |||||
| Console.WriteLine($"\n========== 处理任务出错,回滚事务: {item.SampleCode}, 错误: {ex.Message} ==========\n"); | |||||
| _logger.LogError(ex, $"处理任务出错: {item.SampleCode}"); | |||||
| throw; // 重新抛出异常 | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| _logger.LogError(ex, "数据库操作异常"); | |||||
| Console.WriteLine($"\n========== 数据库操作异常: {ex.Message} ==========\n"); | |||||
| } | } | ||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| _logger.LogError(ex, "数据库操作异常"); | |||||
| Console.WriteLine($"\n========== 数据库操作异常: {ex.Message} ==========\n"); | |||||
| } | } | ||||
| } | } | ||||
| catch (Exception e) | |||||
| { | |||||
| _logger.LogError(e, "异常:自动审核标准化验任务"); | |||||
| Console.WriteLine($"\n========== 异常:自动审核标准化验任务: {e.Message} ==========\n"); | |||||
| } | |||||
| _logger.LogInformation("--------自动审核标准化验任务完成--------"); | |||||
| Console.WriteLine("\n========== 自动审核标准化验任务完成 ==========\n"); | |||||
| } | } | ||||
| catch (Exception e) | |||||
| finally | |||||
| { | { | ||||
| _logger.LogError(e, "异常:自动审核标准化验任务"); | |||||
| Console.WriteLine($"\n========== 异常:自动审核标准化验任务: {e.Message} ==========\n"); | |||||
| // 确保释放锁 | |||||
| _processLock.Release(); | |||||
| } | } | ||||
| _logger.LogInformation("--------自动审核标准化验任务完成--------"); | |||||
| Console.WriteLine("\n========== 自动审核标准化验任务完成 ==========\n"); | |||||
| } | } | ||||
| #endregion | #endregion | ||||
| @@ -103,6 +103,14 @@ namespace Himp.TaskScheduling.Hangfire | |||||
| ) | ) | ||||
| "; | "; | ||||
| public const string EXIST_STANDARD_TEST_TASK_SQL = @" | |||||
| SELECT CASE WHEN | |||||
| EXISTS (SELECT 1 FROM [TaskScheduling].[TS_StandardTestTasks] | |||||
| WHERE SampleCode = @SampleCode AND TestCode = @TestCode AND BottomCode = @BottomCode) | |||||
| THEN 1 | |||||
| ELSE 0 | |||||
| END"; | |||||
| public const string UPDATE_SAMPLE_PREPARATION_STAS_SQL = @" | public const string UPDATE_SAMPLE_PREPARATION_STAS_SQL = @" | ||||
| UPDATE | UPDATE | ||||
| [dbo].[ZY_RECORD_TB] | [dbo].[ZY_RECORD_TB] | ||||
| @@ -243,35 +251,67 @@ namespace Himp.TaskScheduling.Hangfire | |||||
| if (data.SampleType == 1) | if (data.SampleType == 1) | ||||
| { | { | ||||
| var intStas = await connection.ExecuteAsync(INSERT_STANDARD_TEST_TASK_SQL, | |||||
| // 检查是否已存在相同的标准测试任务 | |||||
| bool taskExists = await connection.ExecuteScalarAsync<bool>(EXIST_STANDARD_TEST_TASK_SQL, | |||||
| new | new | ||||
| { | { | ||||
| SampleCode = data.SampleID, | SampleCode = data.SampleID, | ||||
| TestCode = randomCode, | TestCode = randomCode, | ||||
| BottomCode = data.PackCode, | |||||
| Sampletype = "61", | |||||
| TaskType = 0, | |||||
| SampeWay = "自动", | |||||
| TestCnt = 2, | |||||
| ExecStas = "未就绪" | |||||
| BottomCode = data.PackCode | |||||
| }); | }); | ||||
| _logger.LogInformation($"生成标准化验任务:{data.PackCode}:{intStas}"); | |||||
| if (!taskExists) | |||||
| { | |||||
| var intStas = await connection.ExecuteAsync(INSERT_STANDARD_TEST_TASK_SQL, | |||||
| new | |||||
| { | |||||
| SampleCode = data.SampleID, | |||||
| TestCode = randomCode, | |||||
| BottomCode = data.PackCode, | |||||
| Sampletype = "61", | |||||
| TaskType = 0, | |||||
| SampeWay = "自动", | |||||
| TestCnt = 2, | |||||
| ExecStas = "未就绪" | |||||
| }); | |||||
| _logger.LogInformation($"生成标准化验任务:{data.PackCode}:{intStas}"); | |||||
| } | |||||
| else | |||||
| { | |||||
| _logger.LogInformation($"标准化验任务已存在,跳过插入:{data.PackCode}"); | |||||
| } | |||||
| } | } | ||||
| else if (data.SampleType == 4) | else if (data.SampleType == 4) | ||||
| { | { | ||||
| var intStas = await connection.ExecuteAsync(INSERT_STANDARD_TEST_TASK_SQL, | |||||
| // 检查是否已存在相同的标准测试任务 | |||||
| bool taskExists = await connection.ExecuteScalarAsync<bool>(EXIST_STANDARD_TEST_TASK_SQL, | |||||
| new | new | ||||
| { | { | ||||
| SampleCode = data.SampleID, | SampleCode = data.SampleID, | ||||
| TestCode = randomCode, | TestCode = randomCode, | ||||
| BottomCode = data.PackCode, | |||||
| Sampletype = "21", | |||||
| TaskType = 0, | |||||
| SampeWay = "自动", | |||||
| TestCnt = 2, | |||||
| ExecStas = "未就绪" | |||||
| BottomCode = data.PackCode | |||||
| }); | }); | ||||
| _logger.LogInformation($"生成标准化验任务:{data.PackCode}:{intStas}"); | |||||
| if (!taskExists) | |||||
| { | |||||
| var intStas = await connection.ExecuteAsync(INSERT_STANDARD_TEST_TASK_SQL, | |||||
| new | |||||
| { | |||||
| SampleCode = data.SampleID, | |||||
| TestCode = randomCode, | |||||
| BottomCode = data.PackCode, | |||||
| Sampletype = "21", | |||||
| TaskType = 0, | |||||
| SampeWay = "自动", | |||||
| TestCnt = 2, | |||||
| ExecStas = "未就绪" | |||||
| }); | |||||
| _logger.LogInformation($"生成标准化验任务:{data.PackCode}:{intStas}"); | |||||
| } | |||||
| else | |||||
| { | |||||
| _logger.LogInformation($"标准化验任务已存在,跳过插入:{data.PackCode}"); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,112 @@ | |||||
| # 任务调度优化说明 | |||||
| ## 问题描述 | |||||
| 原系统中 `INSERT_STANDARD_TEST_TASK_SQL` 执行4-5遍的问题,主要由以下原因造成: | |||||
| 1. 多个任务都设置为每5分钟执行一次,频率过高 | |||||
| 2. 任务执行时间重叠,同时处理相同数据 | |||||
| 3. 缺乏重复检查机制,导致重复插入 | |||||
| 4. **AuditStandardTestTaskWorker存在严重竞态条件**:先插入后更新状态,导致多个实例同时处理相同数据 | |||||
| ## 优化方案 | |||||
| ### 1. 调度频率优化 | |||||
| 将各类任务的执行频率进行分类调整: | |||||
| #### 制样相关任务(从每5分钟改为每10分钟,错开执行) | |||||
| - `ZYOperateRecordJob`: `0,10,20,30,40,50 * * * *` (每10分钟,0分开始) | |||||
| - `SamplePreparationRecWorker`: `2,12,22,32,42,52 * * * *` (每10分钟,2分开始) | |||||
| - `SampleSpotCheckJob`: `4,14,24,34,44,54 * * * *` (每10分钟,4分开始) | |||||
| - `AutoSamplingToJob`: `6,16,26,36,46,56 * * * *` (每10分钟,6分开始) | |||||
| #### 数据同步任务(从每5分钟改为每10分钟,错开执行) | |||||
| - `TestResultSyncStep1Job`: `1,11,21,31,41,51 * * * *` (每10分钟,1分开始) | |||||
| - `TestResultSyncStep2Job`: `3,13,23,33,43,53 * * * *` (每10分钟,3分开始) | |||||
| #### 化验审核任务(从每5分钟改为每10分钟) | |||||
| - `AuditStandardTestTaskWorker`: `5,15,25,35,45,55 * * * *` (每10分钟,5分开始) | |||||
| #### 存样柜同步(从每2分钟改为每5分钟,错开执行) | |||||
| - `BoltInfo1Job`: `0,5,10,15,20,25,30,35,40,45,50,55 * * * *` (每5分钟,0分开始) | |||||
| - `BoltInfo2Job`: `2,7,12,17,22,27,32,37,42,47,52,57 * * * *` (每5分钟,2分开始) | |||||
| #### 化验设备数据收集(改为每8分钟,错开执行) | |||||
| - `RILS1810ViewMJob`: `0,8,16,24,32,40,48,56 * * * *` (每8分钟) | |||||
| - `RILS1810ViewAJob`: `1,9,17,25,33,41,49,57 * * * *` (每8分钟,1分开始) | |||||
| - `RILS1810ViewVJob`: `2,10,18,26,34,42,50,58 * * * *` (每8分钟,2分开始) | |||||
| - `RILS1810ViewSJob`: `3,11,19,27,35,43,51,59 * * * *` (每8分钟,3分开始) | |||||
| - `RILS1810ViewCalorJob`: `4,12,20,28,36,44,52 * * * *` (每8分钟,4分开始) | |||||
| - `RILS1810ViewChnJob`: `5,13,21,29,37,45,53 * * * *` (每8分钟,5分开始) | |||||
| #### 其他任务保持不变 | |||||
| - `TCode1V1Job`: `0 * * * *` (每小时执行) | |||||
| - `AutoDiscardedSamplingJob`: `0 17 * * *` (每天下午17点) | |||||
| - `TBSampleJob`: `1,6,11,16,21,26,31,36,41,46,51,56 * * * *` (每5分钟,1分开始) | |||||
| ### 2. 重复检查机制优化 | |||||
| #### SamplePreparationRecWorker 优化 | |||||
| - 添加 `EXIST_STANDARD_TEST_TASK_SQL` 检查语句 | |||||
| - 在插入前检查 `SampleCode`, `TestCode`, `BottomCode` 组合是否已存在 | |||||
| - 避免重复插入标准测试任务 | |||||
| #### AuditStandardTestTaskWorker 竞态条件修复(关键修复) | |||||
| **问题根因**:原逻辑是先插入样本测试任务,再更新标准测试任务状态。在插入完成后、更新状态前的短暂时间内,其他实例仍能查询到"未就绪"状态的相同记录,导致重复插入。 | |||||
| **修复措施**: | |||||
| 1. **修改执行顺序**:先更新标准测试任务状态为"已审核",再插入样本测试任务 | |||||
| 2. **添加数据库事务**:确保更新和插入操作的原子性 | |||||
| 3. **优化查询SQL**:在查询阶段就排除已存在样本测试任务的记录 | |||||
| ```sql | |||||
| -- 新增 NOT EXISTS 条件,排除已处理的记录 | |||||
| AND NOT EXISTS ( | |||||
| SELECT 1 FROM [TaskScheduling].[TS_SampleTestTasks] stt | |||||
| WHERE stt.SampleCode = st.SampleCode | |||||
| AND stt.TestCode = st.TestCode | |||||
| AND stt.BottomCode = st.BottomCode | |||||
| ) | |||||
| ``` | |||||
| 4. **添加应用级锁**:使用 `SemaphoreSlim` 确保同时只有一个实例在执行 | |||||
| 5. **添加 `EXIST_SAMPLE_TEST_TASK_SQL` 检查**:在事务内再次检查是否已存在相同任务 | |||||
| **修复后的执行流程**: | |||||
| 1. 获取应用级锁(防止多实例同时执行) | |||||
| 2. 查询待处理任务(已排除重复记录) | |||||
| 3. 开始数据库事务 | |||||
| 4. 先更新标准测试任务状态为"已审核" | |||||
| 5. 检查样本测试任务是否已存在 | |||||
| 6. 如不存在则插入样本测试任务 | |||||
| 7. 提交事务 | |||||
| 8. 释放应用级锁 | |||||
| ### 3. 并发控制优化 | |||||
| - **应用级锁机制**:防止多个任务实例同时处理相同数据 | |||||
| - **数据库事务**:确保相关操作的原子性 | |||||
| - **状态优先更新**:先占用资源(更新状态),再执行业务逻辑 | |||||
| ## 优化效果 | |||||
| ### 性能提升 | |||||
| 1. **执行频率降低**: 大部分任务从每5分钟改为每8-10分钟执行 | |||||
| 2. **避免冲突**: 错开执行时间,避免多任务同时处理相同数据 | |||||
| 3. **减少重复**: 添加重复检查机制,避免重复插入 | |||||
| 4. **消除竞态条件**: 修复 AuditStandardTestTaskWorker 的并发问题 | |||||
| ### 系统负载降低 | |||||
| 1. **数据库连接数减少**: 同时执行的任务数量减少 | |||||
| 2. **CPU使用率降低**: 任务执行频率降低 | |||||
| 3. **内存占用减少**: 错开执行避免内存峰值 | |||||
| 4. **锁竞争减少**: 应用级锁和优化的执行顺序减少数据库锁竞争 | |||||
| ### 数据一致性提升 | |||||
| 1. **避免重复数据**: 重复检查机制确保数据唯一性 | |||||
| 2. **减少锁竞争**: 错开执行减少数据库锁竞争 | |||||
| 3. **提高稳定性**: 降低并发冲突风险 | |||||
| 4. **事务原子性**: 确保相关操作要么全部成功,要么全部回滚 | |||||
| ## 注意事项 | |||||
| 1. 优化后需要重新部署应用程序 | |||||
| 2. 建议先在测试环境验证优化效果 | |||||
| 3. 监控系统运行状况,确保数据处理及时性 | |||||
| 4. 如有特殊业务需求,可适当调整执行频率 | |||||
| 5. **重点关注 AuditStandardTestTaskWorker 的运行日志**,确认竞态条件已彻底解决 | |||||
| 6. 监控数据库事务执行情况,确保不会因为长事务影响性能 | |||||