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; }
}
}
}