using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NPP.SmartSchedue.Api.Contracts.Services.Integration.Internal; using NPP.SmartSchedue.Api.Contracts.Domain.Work; using NPP.SmartSchedue.Api.Contracts.Services.Time.Output; namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms { /// /// 班次规则校验引擎 /// 【业务用途】:专门负责班次规则的批次感知验证,支持遗传算法和常规任务分配的规则检查 /// 【核心功能】:整合9种班次规则验证,支持历史数据+批次上下文的双重验证模式 /// 【架构设计】:借鉴WorkOrderService中ValidateShiftRuleWithBatchContextAsync的实现方式 /// 【性能优化】:基于规则类型的优先级验证,缓存优化,批量验证支持 /// public class ShiftRuleValidationEngine { private readonly ILogger _logger; /// /// 【缓存优化】班次规则验证结果缓存 /// Key: PersonnelId_TaskId_RuleType, Value: ValidationResult /// private readonly Dictionary _validationCache = new(); /// /// 【性能监控】验证统计信息 /// private readonly Dictionary _validationStats = new(); public ShiftRuleValidationEngine( ILogger logger) { _logger = logger; _logger.LogInformation("班次规则校验引擎初始化完成,支持9种规则类型的批次感知验证,使用缓存替代班次服务"); } /// /// 【核心验证方法】增强版批次感知班次规则验证 /// 【业务逻辑】:整合9种班次规则的完整验证,支持历史数据+批次上下文的双重验证模式 /// 【验证策略】:优先验证批次上下文冲突,然后结合历史数据进行完整性检查 /// 【性能优化】:基于规则类型的优先级验证,快速识别高风险冲突 /// 【深度思考】:该方法是遗传算法适应度计算的关键组件,必须确保验证的准确性和性能 /// /// 验证的人员ID /// 当前待验证的任务 /// 批次上下文(不包含当前任务的其他相关任务) /// 发现的班次规则冲突列表 public async Task> ValidateShiftRulesWithEnhancedBatchContextAsync( long personnelId, WorkOrderEntity currentTask, List batchContext, GlobalAllocationContext context) { var conflicts = new List(); var validationStartTime = DateTime.Now; try { // 【前置检查】验证输入参数有效性 if (!currentTask.ShiftId.HasValue) { _logger.LogWarning("【批次感知验证跳过】任务{TaskCode}缺少班次ID", currentTask.WorkOrderCode); return conflicts; } var currentTaskHash = $"{personnelId}_{currentTask.Id}_{currentTask.ShiftId}"; _logger.LogDebug("【增强版班次规则验证开始】人员{PersonnelId}任务{TaskCode}({Date})班次{ShiftId},批次上下文{BatchCount}个任务", personnelId, currentTask.WorkOrderCode, currentTask.WorkOrderDate.ToString("yyyy-MM-dd"), currentTask.ShiftId, batchContext.Count); // 【规则验证主循环】遍历所有启用的班次规则进行验证 var validationTasks = new List>>(); var ruleValidationResults = new Dictionary>(); // 【并行验证优化】:对独立的规则进行并行验证以提升性能 foreach (var ruleType in GetEnabledShiftRuleTypes(context)) { var validationTask = ValidateSpecificShiftRuleAsync( ruleType, personnelId, currentTask, batchContext, context); validationTasks.Add(validationTask); } // 等待所有规则验证完成 var allRuleResults = await Task.WhenAll(validationTasks); // 【结果整合】合并所有规则的验证结果 for (int i = 0; i < allRuleResults.Length; i++) { var ruleType = GetEnabledShiftRuleTypes(context).ToList()[i]; var ruleConflicts = allRuleResults[i]; if (ruleConflicts.Any()) { conflicts.AddRange(ruleConflicts); ruleValidationResults[ruleType] = ruleConflicts; } } // 【性能统计】记录验证性能和结果统计 var validationDuration = DateTime.Now - validationStartTime; UpdateValidationStatistics(currentTaskHash, conflicts.Count, validationDuration); _logger.LogInformation("【增强版班次规则验证完成】人员{PersonnelId}任务{TaskCode}共检查{RuleCount}种规则,发现{ConflictCount}个冲突,耗时{Duration}ms", personnelId, currentTask.WorkOrderCode, GetEnabledShiftRuleTypes(context).Count(), conflicts.Count, validationDuration.TotalMilliseconds); // 【详细冲突日志】记录前5个具体冲突详情 LogConflictDetails(personnelId, currentTask.WorkOrderCode, conflicts, ruleValidationResults); return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【增强版班次规则验证异常】人员{PersonnelId}任务{TaskCode}验证失败", personnelId, currentTask.WorkOrderCode); // 【异常处理】异常情况下返回通用冲突信息,确保系统稳定性 return new List { $"人员{personnelId}任务{currentTask.WorkOrderCode}班次规则验证异常: {ex.Message}" }; } } /// /// 【规则验证核心】验证特定班次规则类型 /// 【架构设计】:采用策略模式,根据规则类型分发到具体的验证方法 /// 【业务覆盖】:支持规则3-9的完整验证覆盖 /// /// 规则类型标识 /// 人员ID /// 当前任务 /// 批次上下文 /// 该规则类型的冲突列表 private async Task> ValidateSpecificShiftRuleAsync( string ruleType, long personnelId, WorkOrderEntity currentTask, List batchContext, GlobalAllocationContext context) { try { // 【缓存检查】首先检查缓存中是否有该规则的验证结果 var cacheKey = $"{personnelId}_{currentTask.Id}_{ruleType}"; if (_validationCache.TryGetValue(cacheKey, out var cachedResult)) { _logger.LogTrace("【缓存命中】规则{RuleType}验证结果来自缓存", ruleType); return cachedResult.Conflicts; } List conflicts = new List(); // 【规则分发】根据规则类型分发到具体的验证逻辑 switch (ruleType) { case "3": // 一/二班不连续 conflicts = await ValidateShiftSequenceRuleWithBatchContextAsync( "一/二班不连续规则", personnelId, currentTask, "一", "二", batchContext, context); break; case "4": // 二/三班不连续 conflicts = await ValidateShiftSequenceRuleWithBatchContextAsync( "二/三班不连续规则", personnelId, currentTask, "二", "三", batchContext, context); break; case "5": // 不能这周日/下周六连 conflicts = await ValidateSundayToSaturdayRuleWithBatchContextAsync( "周日/周六连续规则", personnelId, currentTask, batchContext, context); break; case "6": // 不能本周六/本周日连 conflicts = await ValidateSaturdayToSundayRuleWithBatchContextAsync( "周六/周日连续规则", personnelId, currentTask, batchContext, context); break; case "7": // 不能连续7天排班 conflicts = await ValidateMaxConsecutiveDaysRuleWithBatchContextAsync( "连续7天排班限制规则", personnelId, currentTask, 7, batchContext, context); break; case "8": // 三班后一天不排班 conflicts = await ValidateRestAfterShiftRuleWithBatchContextAsync( "三班后休息规则", personnelId, currentTask, "三", batchContext, context); break; case "9": // 二班后一天不排班 conflicts = await ValidateRestAfterShiftRuleWithBatchContextAsync( "二班后休息规则", personnelId, currentTask, "二", batchContext, context); break; case "12": // 当天最多排一个班次 conflicts = await ValidatePersonShiftRuleWithBatchContextAsync( "一人当天只排一个班次", personnelId, currentTask, "二", batchContext, context); break; default: // 【扩展性设计】未知规则类型记录警告但不阻止任务 _logger.LogWarning("【未知规则类型】规则类型{RuleType}暂未实现验证逻辑", ruleType); break; } // 【缓存更新】将验证结果存入缓存 var validationResult = new ShiftRuleValidationResult { RuleType = ruleType, PersonnelId = personnelId, TaskId = currentTask.Id, IsValid = !conflicts.Any(), Conflicts = conflicts, ValidatedAt = DateTime.Now }; // 【内存管理】限制缓存大小,避免内存泄漏 if (_validationCache.Count > 1000) { ClearOldCacheEntries(); } _validationCache[cacheKey] = validationResult; return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【规则验证异常】规则类型{RuleType}验证异常", ruleType); return new List { $"规则{ruleType}验证异常: {ex.Message}" }; } } /// /// 【规则实现3&4】班次连续性规则验证(一/二班不连续,二/三班不连续) /// 【业务逻辑】:同一人员同一天不能有冲突的班次安排 /// 【深度思考】:需要综合考虑批次内任务和数据库历史任务,确保无遗漏 /// 【验证范围】:批次内同日冲突 + 数据库历史同日冲突 /// private async Task> ValidateShiftSequenceRuleWithBatchContextAsync( string ruleName, long personnelId, WorkOrderEntity currentTask, string firstShiftName, string secondShiftName, List batchContext, GlobalAllocationContext context) { var conflicts = new List(); try { // 【班次信息获取】获取当前任务的班次信息 var currentShift = context.ShiftInformationCache.Where(a => a.Id == currentTask.ShiftId).FirstOrDefault(); if (currentShift == null) { _logger.LogWarning("【班次规则】任务{TaskCode}的班次{ShiftId}信息不存在", currentTask.WorkOrderCode, currentTask.ShiftId); return conflicts; } var currentDate = currentTask.WorkOrderDate.Date; // 【班次类型判断】检查当前班次是否为规则关注的班次之一 bool currentIsFirstShift = currentShift.Name?.Contains(firstShiftName) == true; bool currentIsSecondShift = currentShift.Name?.Contains(secondShiftName) == true; if (!currentIsFirstShift && !currentIsSecondShift) { // 当前班次不是规则关注的班次,无需验证 return conflicts; } _logger.LogDebug("【班次连续性检查】人员{PersonnelId}任务{TaskCode}当前为{ShiftType},检查与{OtherShiftType}的冲突", personnelId, currentTask.WorkOrderCode, currentIsFirstShift ? firstShiftName : secondShiftName, currentIsFirstShift ? secondShiftName : firstShiftName); // 【批次内冲突检查】检查批次内同一天是否有冲突班次 await ValidateBatchContextShiftConflicts(conflicts, ruleName, personnelId, currentTask, batchContext, currentDate, currentIsFirstShift, currentIsSecondShift, firstShiftName, secondShiftName, context); // 【历史数据冲突检查】检查数据库中同一天是否有冲突班次 await ValidateHistoricalDataShiftConflicts(conflicts, ruleName, personnelId, currentTask, currentDate, currentIsFirstShift, currentIsSecondShift, firstShiftName, secondShiftName, context); return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【班次连续性规则验证异常】规则{RuleName}", ruleName); return new List { $"{ruleName}验证异常: {ex.Message}" }; } } /// /// 【批次内冲突检查】验证批次上下文中的班次冲突 /// private async Task ValidateBatchContextShiftConflicts( List conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, DateTime currentDate, bool currentIsFirstShift, bool currentIsSecondShift, string firstShiftName, string secondShiftName, GlobalAllocationContext context) { // 获取批次内同一天同一人员的其他任务 var batchSameDayTasks = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == currentDate) .ToList(); foreach (var batchTask in batchSameDayTasks) { if (batchTask.ShiftId.HasValue) { var batchShift = context.ShiftInformationCache.Where(a => a.Id == batchTask.ShiftId.Value) .FirstOrDefault(); if (batchShift == null) continue; bool batchIsFirstShift = batchShift.Name?.Contains(firstShiftName) == true; bool batchIsSecondShift = batchShift.Name?.Contains(secondShiftName) == true; // 【冲突检测】检查班次冲突:当前是一班且批次内有二班,或当前是二班且批次内有一班 if ((currentIsFirstShift && batchIsSecondShift) || (currentIsSecondShift && batchIsFirstShift)) { var conflictMessage = $"违反批次内{ruleName}: 同一天内不能同时安排{firstShiftName}班和{secondShiftName}班 " + $"(与批次内任务 '{batchTask.WorkOrderCode}' {batchShift.Name} 冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【批次内班次冲突】{Conflict}", conflictMessage); } } } } /// /// 【历史数据冲突检查】验证历史数据中的班次冲突 /// private async Task ValidateHistoricalDataShiftConflicts( List conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask, DateTime currentDate, bool currentIsFirstShift, bool currentIsSecondShift, string firstShiftName, string secondShiftName, GlobalAllocationContext context) { // 【历史数据查询优化】:使用上下文中的任务数据而非直接数据库查询 var historicalSameDayTasks = context.Tasks?.Where(wo => wo.AssignedPersonnelId == personnelId && wo.Id != currentTask.Id && wo.WorkOrderDate.Date == currentDate).ToList() ?? new List(); foreach (var dbTask in historicalSameDayTasks) { if (dbTask.ShiftId.HasValue) { var dbShift = context.ShiftInformationCache.Where(a => a.Id == dbTask.ShiftId.Value) .FirstOrDefault(); if (dbShift == null) continue; bool dbIsFirstShift = dbShift.Name?.Contains(firstShiftName) == true; bool dbIsSecondShift = dbShift.Name?.Contains(secondShiftName) == true; // 【历史冲突检测】检查与历史数据的班次冲突 if ((currentIsFirstShift && dbIsSecondShift) || (currentIsSecondShift && dbIsFirstShift)) { var conflictMessage = $"违反{ruleName}: 同一天内不能同时安排{firstShiftName}班和{secondShiftName}班 " + $"(与历史任务 '{dbTask.WorkOrderCode}' {dbShift.Name} 冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【历史数据班次冲突】{Conflict}", conflictMessage); } } } } /// /// 【规则实现5】周日/下周六连续规则验证 /// 【业务逻辑】:防止跨周连续工作,检查周日->下周六的7天跨度 /// 【深度思考】:批次内可能同时包含这两个时间点的任务,需要双向检查 /// private async Task> ValidateSundayToSaturdayRuleWithBatchContextAsync( string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, GlobalAllocationContext context) { var conflicts = new List(); await Task.CompletedTask; try { var currentDate = currentTask.WorkOrderDate.Date; // 【情况1】当前任务是周日,检查下周六是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Sunday) { var nextSaturday = currentDate.AddDays(6); // 周日 + 6天 = 下周六 await CheckNextSaturdayConflicts(conflicts, ruleName, personnelId, currentTask, batchContext, nextSaturday, context); } // 【情况2】当前任务是周六,检查上周日是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Saturday) { var lastSunday = currentDate.AddDays(-6); // 周六 - 6天 = 上周日 await CheckLastSundayConflicts(conflicts, ruleName, personnelId, currentTask, batchContext, lastSunday, context); } return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【周日/周六连续规则验证异常】规则{RuleName}", ruleName); return new List { $"{ruleName}验证异常: {ex.Message}" }; } } /// /// 【规则实现6】周六/周日连续规则验证 /// 【业务逻辑】:防止周末连续工作,批次内可能同时包含同一人员的周六和周日任务 /// private async Task> ValidateSaturdayToSundayRuleWithBatchContextAsync( string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, GlobalAllocationContext context) { var conflicts = new List(); await Task.CompletedTask; try { var currentDate = currentTask.WorkOrderDate.Date; // 【情况1】当前是周六,检查周日是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Saturday) { var sunday = currentDate.AddDays(1); await CheckAdjacentDayConflicts(conflicts, ruleName, personnelId, currentTask, batchContext, sunday, "周六", "周日", context); } // 【情况2】当前是周日,检查周六是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Sunday) { var saturday = currentDate.AddDays(-1); await CheckAdjacentDayConflicts(conflicts, ruleName, personnelId, currentTask, batchContext, saturday, "周日", "周六", context); } return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【周六/周日连续规则验证异常】规则{RuleName}", ruleName); return new List { $"{ruleName}验证异常: {ex.Message}" }; } } /// /// 【规则实现7】最大连续天数规则验证 /// 【业务逻辑】:最严格的连续工作限制,基于批次+历史数据计算真实的连续工作天数序列 /// private async Task> ValidateMaxConsecutiveDaysRuleWithBatchContextAsync( string ruleName, long personnelId, WorkOrderEntity currentTask, int maxDays, List batchContext, GlobalAllocationContext context) { var conflicts = new List(); try { // 【连续天数计算】使用批次感知的连续工作天数计算 var consecutiveDays = await CalculateConsecutiveWorkDaysWithBatchContextAsync( personnelId, currentTask.WorkOrderDate, batchContext, context); if (consecutiveDays >= maxDays) { // 【相关任务信息】获取相关的批次内任务信息用于详细说明 var relatedBatchTasks = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => Math.Abs((wo.WorkOrderDate.Date - currentTask.WorkOrderDate.Date).Days) <= maxDays) .Select(wo => wo.WorkOrderCode) .ToList(); var batchTasksInfo = relatedBatchTasks.Any() ? $"(涉及批次内任务: {string.Join(", ", relatedBatchTasks)})" : ""; var conflictMessage = $"违反{ruleName}: 连续工作 {consecutiveDays} 天超过限制 {maxDays} 天 {batchTasksInfo}"; conflicts.Add(conflictMessage); _logger.LogError("【连续天数规则冲突】{Conflict}", conflictMessage); } return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【连续天数规则验证异常】规则{RuleName}", ruleName); return new List { $"{ruleName}验证异常: {ex.Message}" }; } } /// /// 【规则实现8&9】班次后休息规则验证(二班后休息,三班后休息) /// 【业务逻辑】:指定班次后次日必须休息,需要双向检查前一天和后一天 /// private async Task> ValidateRestAfterShiftRuleWithBatchContextAsync( string ruleName, long personnelId, WorkOrderEntity currentTask, string shiftName, List batchContext, GlobalAllocationContext context) { var conflicts = new List(); try { var currentDate = currentTask.WorkOrderDate.Date; // 【双向检查1】检查前一天是否是指定班次 await CheckPreviousDayRestRule(conflicts, ruleName, personnelId, currentTask, batchContext, shiftName, currentDate, context); // 【双向检查2】检查当前是指定班次时后一天是否安排了工作 await CheckNextDayRestRule(conflicts, ruleName, personnelId, currentTask, batchContext, shiftName, currentDate, context); return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【班次后休息规则验证异常】规则{RuleName}", ruleName); return new List { $"{ruleName}验证异常: {ex.Message}" }; } } private async Task> ValidatePersonShiftRuleWithBatchContextAsync( string ruleName, long personnelId, WorkOrderEntity currentTask, string shiftName, List batchContext, GlobalAllocationContext context) { var conflicts = new List(); try { var currentDate = currentTask.WorkOrderDate.Date; var batchNextTask = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == currentDate) .Count(); if (batchNextTask > 1) { var conflictMessage = $"违反批次内{ruleName}"; conflicts.Add(conflictMessage); _logger.LogError("【批次内安排冲突】{Conflict}", conflictMessage); } return conflicts; } catch (Exception ex) { _logger.LogError(ex, "【班次后休息规则验证异常】规则{RuleName}", ruleName); return new List { $"{ruleName}验证异常: {ex.Message}" }; } } /// /// 【辅助方法】检查前一天的休息规则 /// private async Task CheckPreviousDayRestRule(List conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, string shiftName, DateTime currentDate, GlobalAllocationContext context) { var previousDay = currentDate.AddDays(-1); // 【批次内检查】先检查批次内是否有前一天的指定班次任务 var batchPreviousTask = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == previousDay) .FirstOrDefault(); if (batchPreviousTask?.ShiftId.HasValue == true) { var batchPreviousShift = context.ShiftInformationCache.Where(a => a.Id == batchPreviousTask.ShiftId.Value) .FirstOrDefault(); if (batchPreviousShift?.Name?.Contains(shiftName) == true) { var conflictMessage = $"违反批次内{ruleName}: 批次内任务 '{batchPreviousTask.WorkOrderCode}' " + $"{shiftName}班后一天不能安排工作"; conflicts.Add(conflictMessage); _logger.LogError("【批次内休息规则冲突】{Conflict}", conflictMessage); } } else { // 【历史数据检查】检查历史数据中前一天是否有指定班次 await CheckHistoricalPreviousDayTask(conflicts, ruleName, personnelId, previousDay, shiftName, context); } } /// /// 【辅助方法】检查后一天的休息规则 /// private async Task CheckNextDayRestRule(List conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, string shiftName, DateTime currentDate, GlobalAllocationContext context) { // 【当前任务检查】检查当前任务是否为指定班次 var currentShift = context.ShiftInformationCache.Where(a => a.Id == (currentTask.ShiftId ?? 0)) .FirstOrDefault(); if (currentShift?.Name?.Contains(shiftName) == true) { var nextDay = currentDate.AddDays(1); // 【批次内后一天检查】检查批次内是否有后一天的任务 var batchNextTask = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == nextDay) .Any(); if (batchNextTask) { var conflictMessage = $"违反批次内{ruleName}: {shiftName}班后一天不能安排批次内任务"; conflicts.Add(conflictMessage); _logger.LogError("【批次内次日安排冲突】{Conflict}", conflictMessage); } else { // 【历史数据后一天检查】检查历史数据中是否有后一天的任务 await CheckHistoricalNextDayTask(conflicts, ruleName, personnelId, nextDay, shiftName, context); } } } /// /// 【缓存管理】清理过期的缓存条目 /// private void ClearOldCacheEntries() { var cutoffTime = DateTime.Now.AddMinutes(-30); // 清理30分钟前的缓存 var keysToRemove = _validationCache .Where(kvp => kvp.Value.ValidatedAt < cutoffTime) .Select(kvp => kvp.Key) .Take(200) // 每次最多清理200个 .ToList(); foreach (var key in keysToRemove) { _validationCache.Remove(key); } _logger.LogDebug("【缓存清理】清理了{Count}个过期的验证缓存项", keysToRemove.Count); } /// /// 【统计更新】更新验证统计信息 /// private void UpdateValidationStatistics(string taskHash, int conflictCount, TimeSpan duration) { var statsKey = conflictCount > 0 ? "ValidationWithConflicts" : "ValidationPassed"; _validationStats[statsKey] = _validationStats.GetValueOrDefault(statsKey, 0) + 1; _validationStats["TotalValidationTime"] = _validationStats.GetValueOrDefault("TotalValidationTime", 0) + (int)duration.TotalMilliseconds; } /// /// 【日志记录】记录冲突详情 /// private void LogConflictDetails(long personnelId, string taskCode, List allConflicts, Dictionary> ruleResults) { if (allConflicts.Any()) { _logger.LogError("【冲突汇总】人员{PersonnelId}任务{TaskCode}发现{TotalConflicts}个冲突", personnelId, taskCode, allConflicts.Count); // 按规则类型分组显示冲突 foreach (var rule in ruleResults) { _logger.LogError("【规则{RuleType}冲突】{ConflictCount}个: {Conflicts}", rule.Key, rule.Value.Count, string.Join("; ", rule.Value.Take(2))); } // 显示前5个具体冲突 for (int i = 0; i < Math.Min(5, allConflicts.Count); i++) { _logger.LogError("【冲突详情{Index}】{Conflict}", i + 1, allConflicts[i]); } } } /// /// 【配置方法】获取启用的班次规则类型列表 /// private IEnumerable GetEnabledShiftRuleTypes(GlobalAllocationContext context) { return context.ShiftRulesMapping.OrderBy(a => a.RuleType).Where(a=> a.IsEnabled == true).Select(a => a.RuleType).ToArray(); } /// /// 【辅助方法】检查下周六冲突 - 周日/下周六连续规则的核心逻辑 /// private async Task CheckNextSaturdayConflicts(List conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, DateTime nextSaturday, GlobalAllocationContext context) { // 【批次内检查】先检查批次内是否有下周六的任务 var batchNextSaturday = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == nextSaturday) .FirstOrDefault(); if (batchNextSaturday != null) { var conflictMessage = $"违反批次内{ruleName}: 不能在这周日和下周六安排工作 " + $"(与批次内任务 '{batchNextSaturday.WorkOrderCode}' 冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【批次内跨周冲突】{Conflict}", conflictMessage); } else { // 【历史数据检查】检查历史数据中下周六是否有任务 var dbNextSaturday = context.Tasks?.Any(wo => wo.AssignedPersonnelId == personnelId && wo.WorkOrderDate.Date == nextSaturday) == true; if (dbNextSaturday) { var conflictMessage = $"违反{ruleName}: 不能在这周日和下周六安排工作 (与历史数据任务冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【历史数据跨周冲突】{Conflict}", conflictMessage); } } await Task.CompletedTask; } /// /// 【辅助方法】检查上周日冲突 - 周日/下周六连续规则的反向逻辑 /// private async Task CheckLastSundayConflicts(List conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, DateTime lastSunday, GlobalAllocationContext context) { // 【批次内检查】先检查批次内是否有上周日的任务 var batchLastSunday = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == lastSunday) .FirstOrDefault(); if (batchLastSunday != null) { var conflictMessage = $"违反批次内{ruleName}: 不能在上周日和这周六安排工作 " + $"(与批次内任务 '{batchLastSunday.WorkOrderCode}' 冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【批次内反向跨周冲突】{Conflict}", conflictMessage); } else { // 【历史数据检查】检查历史数据中上周日是否有任务 var dbLastSunday = context.Tasks?.Any(wo => wo.AssignedPersonnelId == personnelId && wo.WorkOrderDate.Date == lastSunday) == true; if (dbLastSunday) { var conflictMessage = $"违反{ruleName}: 不能在上周日和这周六安排工作 (与历史数据任务冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【历史数据反向跨周冲突】{Conflict}", conflictMessage); } } await Task.CompletedTask; } /// /// 【辅助方法】检查相邻日期冲突 - 周六/周日连续规则的通用逻辑 /// private async Task CheckAdjacentDayConflicts(List conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask, List batchContext, DateTime adjacentDay, string currentDayName, string adjacentDayName, GlobalAllocationContext context) { // 【批次内相邻日检查】先检查批次内相邻日期任务 var batchAdjacentTask = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == adjacentDay) .FirstOrDefault(); if (batchAdjacentTask != null) { var conflictMessage = $"违反批次内{ruleName}: 不能在{currentDayName}和{adjacentDayName}连续安排工作 " + $"(与批次内任务 '{batchAdjacentTask.WorkOrderCode}' 冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【批次内相邻日冲突】{Conflict}", conflictMessage); } else { // 【历史数据相邻日检查】检查历史数据中相邻日期是否有任务 var dbAdjacentTask = context.Tasks?.Any(wo => wo.AssignedPersonnelId == personnelId && wo.WorkOrderDate.Date == adjacentDay) == true; if (dbAdjacentTask) { var conflictMessage = $"违反{ruleName}: 不能在{currentDayName}和{adjacentDayName}连续安排工作 (与历史数据任务冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【历史数据相邻日冲突】{Conflict}", conflictMessage); } } await Task.CompletedTask; } /// /// 【辅助方法】检查历史数据前一天任务 - 班次后休息规则的历史数据检查 /// private async Task CheckHistoricalPreviousDayTask(List conflicts, string ruleName, long personnelId, DateTime previousDay, string shiftName, GlobalAllocationContext context) { var dbPreviousTask = context.Tasks?.FirstOrDefault(wo => wo.AssignedPersonnelId == personnelId && wo.WorkOrderDate.Date == previousDay); if (dbPreviousTask?.ShiftId.HasValue == true) { var dbPreviousShift = context.ShiftInformationCache.Where(a => a.Id == dbPreviousTask.ShiftId.Value) .FirstOrDefault(); if (dbPreviousShift?.Name?.Contains(shiftName) == true) { var conflictMessage = $"违反{ruleName}: 历史数据任务 '{dbPreviousTask.WorkOrderCode}' " + $"{shiftName}班后一天不能安排工作"; conflicts.Add(conflictMessage); _logger.LogError("【历史数据前日班次冲突】{Conflict}", conflictMessage); } } } /// /// 【辅助方法】检查历史数据后一天任务 - 班次后休息规则的历史数据检查 /// private async Task CheckHistoricalNextDayTask(List conflicts, string ruleName, long personnelId, DateTime nextDay, string shiftName, GlobalAllocationContext context) { var dbNextTask = context.Tasks?.Any(wo => wo.AssignedPersonnelId == personnelId && wo.WorkOrderDate.Date == nextDay) == true; if (dbNextTask) { var conflictMessage = $"违反{ruleName}: {shiftName}班后一天不能安排工作(与历史数据任务冲突)"; conflicts.Add(conflictMessage); _logger.LogError("【历史数据次日安排冲突】{Conflict}", conflictMessage); } await Task.CompletedTask; } /// /// 【核心算法】计算批次感知的连续工作天数 - 最大连续天数规则的核心实现 /// 【业务逻辑】:整合批次内任务和历史数据,计算真实的连续工作天数 /// 【深度思考】:需要处理批次内任务的时序关系,确保连续性计算的准确性 /// private async Task CalculateConsecutiveWorkDaysWithBatchContextAsync( long personnelId, DateTime targetDate, List batchContext, GlobalAllocationContext context) { await Task.CompletedTask; try { var consecutiveDays = 1; // 包含目标日期 // 【批次任务整合】获取批次内该人员的任务,按日期排序 var personnelBatchTasks = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .OrderBy(wo => wo.WorkOrderDate.Date) .ToList(); // 【历史任务整合】获取历史上下文中该人员的任务 var personnelHistoryTasks = context.Tasks?.Where(wo => wo.AssignedPersonnelId == personnelId && !personnelBatchTasks.Any(bt => bt.Id == wo.Id)) // 避免重复 .ToList() ?? new List(); // 【时序数据构建】创建包含批次任务和历史任务的完整工作日期集合 var allWorkDates = new HashSet(); // 添加批次内任务日期 foreach (var task in personnelBatchTasks) { allWorkDates.Add(task.WorkOrderDate.Date); } // 添加历史任务日期 foreach (var task in personnelHistoryTasks) { allWorkDates.Add(task.WorkOrderDate.Date); } // 包含当前目标日期 allWorkDates.Add(targetDate.Date); // 【连续性计算】从目标日期往前检查连续工作天数 var currentDate = targetDate.Date.AddDays(-1); // 从目标日期前一天开始往前检查 while (true) { // 【工作日检查】检查当前日期是否有工作安排 bool hasWork = allWorkDates.Contains(currentDate); if (!hasWork) { break; // 没有工作任务,中断连续天数 } // 【班次不可用性检查】检查是否有班次不可用情况 - 深度实现 var isUnavailable = CheckPersonnelShiftUnavailability(personnelId, currentDate, context); _logger.LogTrace("【班次不可用性检查】人员{PersonnelId}在{Date:yyyy-MM-dd}的可用状态:{IsAvailable}", personnelId, currentDate, !isUnavailable); if (isUnavailable) { break; // 班次不可用,中断连续天数 } consecutiveDays++; currentDate = currentDate.AddDays(-1); // 【安全限制】避免无限循环,最多检查60天 if (consecutiveDays > 60) { _logger.LogWarning("【连续天数计算】人员{PersonnelId}连续工作天数计算超过60天限制", personnelId); break; } } _logger.LogDebug("【连续天数计算完成】人员{PersonnelId}目标日期{TargetDate:yyyy-MM-dd}连续工作{ConsecutiveDays}天", personnelId, targetDate, consecutiveDays); return consecutiveDays; } catch (Exception ex) { _logger.LogError(ex, "【连续天数计算异常】人员{PersonnelId}目标日期{TargetDate}", personnelId, targetDate); return 1; // 异常时返回最小值,避免过度限制 } } /// /// 【深度实现】检查人员在指定日期的班次不可用性 /// 【业务思考】:整合GlobalAllocationContext中的班次不可用性缓存,支持高性能查询 /// 【数据来源】:DateShiftUnavailablePersonnel + PersonnelUnavailabilityIndex + UnavailabilityDetails /// 【查询策略】:优先使用PersonnelUnavailabilityIndex进行O(1)查询,再检查详细信息 /// 【约束类型】:区分硬约束(请假、医疗)和软约束(个人意愿、培训) /// /// 人员ID /// 检查日期 /// 全局分配上下文 /// 是否不可用(true表示不可用,false表示可用) private bool CheckPersonnelShiftUnavailability(long personnelId, DateTime checkDate, GlobalAllocationContext context) { try { var targetDate = checkDate.Date; // 【查询策略1】优先使用PersonnelUnavailabilityIndex进行O(1)查询 if (context.PersonnelUnavailabilityIndex.TryGetValue(personnelId, out var unavailabilityDates)) { // 检查该人员在目标日期是否有不可用记录 if (unavailabilityDates.TryGetValue(targetDate, out var unavailableShifts)) { // 只要有任意班次不可用,就认为该日不可用(保守策略) if (unavailableShifts.Any()) { _logger.LogDebug("【班次不可用性命中】人员{PersonnelId}在{Date:yyyy-MM-dd}有{ShiftCount}个班次不可用", personnelId, checkDate, unavailableShifts.Count); // 【详细信息查询】获取不可用的具体原因和约束类型 var hardConstraintExists = CheckForHardConstraints(personnelId, targetDate, unavailableShifts, context); if (hardConstraintExists) { _logger.LogDebug("【硬约束检测】人员{PersonnelId}在{Date:yyyy-MM-dd}存在硬约束不可用情况", personnelId, checkDate); return true; // 硬约束不可用 } else { _logger.LogTrace("【软约束检测】人员{PersonnelId}在{Date:yyyy-MM-dd}仅有软约束不可用,可协调", personnelId, checkDate); return false; // 软约束可协调,认为可用 } } } } // 【查询策略2】后备查询:使用DateShiftUnavailablePersonnel验证 var fallbackUnavailable = CheckDateShiftUnavailabilityFallback(personnelId, targetDate, context); if (fallbackUnavailable) { _logger.LogDebug("【后备查询命中】人员{PersonnelId}在{Date:yyyy-MM-dd}通过后备查询发现不可用", personnelId, checkDate); return true; } // 【可用结论】所有查询都未发现不可用情况 _logger.LogTrace("【班次可用性验证通过】人员{PersonnelId}在{Date:yyyy-MM-dd}可用", personnelId, checkDate); return false; } catch (Exception ex) { _logger.LogError(ex, "【班次不可用性检查异常】人员{PersonnelId}日期{CheckDate:yyyy-MM-dd}", personnelId, checkDate); // 【异常处理】异常情况下保守处理,假设可用(避免过度限制) return false; } } /// /// 【辅助方法】检查硬约束是否存在 /// 【业务逻辑】:通过UnavailabilityDetails查询具体的不可用原因,区分硬约束和软约束 /// 【约束类型】:硬约束(请假、医疗)必须遵守,软约束(个人意愿、培训)可协调 /// private bool CheckForHardConstraints(long personnelId, DateTime targetDate, HashSet unavailableShifts, GlobalAllocationContext context) { try { // 遍历所有不可用的班次,检查是否存在硬约束 foreach (var shiftId in unavailableShifts) { var detailKey = $"{targetDate:yyyyMMdd}_{shiftId}_{personnelId}"; if (context.UnavailabilityDetails.TryGetValue(detailKey, out var unavailabilityDetail)) { // 检查是否为硬约束类型 if (unavailabilityDetail.IsHardConstraint) { _logger.LogDebug("【硬约束发现】人员{PersonnelId}日期{Date:yyyy-MM-dd}班次{ShiftId}:{ReasonType}({ConstraintScore})", personnelId, targetDate, shiftId, unavailabilityDetail.ReasonType, unavailabilityDetail.ConstraintScore); return true; } else { _logger.LogTrace("【软约束检测】人员{PersonnelId}日期{Date:yyyy-MM-dd}班次{ShiftId}:{ReasonType}(可协调)", personnelId, targetDate, shiftId, unavailabilityDetail.ReasonType); } } } return false; // 未发现硬约束 } catch (Exception ex) { _logger.LogError(ex, "【硬约束检查异常】人员{PersonnelId}日期{Date:yyyy-MM-dd}", personnelId, targetDate); return true; // 异常情况下保守处理,认为存在硬约束 } } /// /// 【辅助方法】后备查询:使用DateShiftUnavailablePersonnel验证 /// 【查询策略】:当PersonnelUnavailabilityIndex缺失时的后备验证机制 /// 【性能考虑】:虽然性能略低于索引查询,但确保数据一致性 /// private bool CheckDateShiftUnavailabilityFallback(long personnelId, DateTime targetDate, GlobalAllocationContext context) { try { // 获取该日期所有可能的班次ID(这里简化处理,实际应该从系统配置获取) // 为了演示,我们检查一些常见的班次ID模式 var commonShiftIds = new[] { 1L, 2L, 3L }; // 一班、二班、三班 foreach (var shiftId in commonShiftIds) { var dateShiftKey = $"{targetDate:yyyyMMdd}_{shiftId}"; if (context.DateShiftUnavailablePersonnel.TryGetValue(dateShiftKey, out var unavailablePersonnels)) { if (unavailablePersonnels.Contains(personnelId)) { _logger.LogTrace("【后备查询命中】人员{PersonnelId}在{Date:yyyy-MM-dd}班次{ShiftId}不可用", personnelId, targetDate, shiftId); // 进一步检查是否为硬约束 var detailKey = $"{dateShiftKey}_{personnelId}"; if (context.UnavailabilityDetails.TryGetValue(detailKey, out var detail)) { return detail.IsHardConstraint; } else { return true; // 无详细信息时保守处理 } } } } return false; // 后备查询未发现不可用情况 } catch (Exception ex) { _logger.LogError(ex, "【后备查询异常】人员{PersonnelId}日期{Date:yyyy-MM-dd}", personnelId, targetDate); return false; // 异常情况下假设可用,避免过度限制 } } /// /// 【验证结果数据结构】班次规则验证结果 /// private class ShiftRuleValidationResult { public string RuleType { get; set; } = string.Empty; public long PersonnelId { get; set; } public long TaskId { get; set; } public bool IsValid { get; set; } public List Conflicts { get; set; } = new(); public DateTime ValidatedAt { get; set; } } } }