Asoka.Wang aac34433fa 123
2025-09-04 19:14:24 +08:00

1091 lines
54 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// 班次规则校验引擎
/// 【业务用途】:专门负责班次规则的批次感知验证,支持遗传算法和常规任务分配的规则检查
/// 【核心功能】整合9种班次规则验证支持历史数据+批次上下文的双重验证模式
/// 【架构设计】借鉴WorkOrderService中ValidateShiftRuleWithBatchContextAsync的实现方式
/// 【性能优化】:基于规则类型的优先级验证,缓存优化,批量验证支持
/// </summary>
public class ShiftRuleValidationEngine
{
private readonly ILogger<ShiftRuleValidationEngine> _logger;
/// <summary>
/// 【缓存优化】班次规则验证结果缓存
/// Key: PersonnelId_TaskId_RuleType, Value: ValidationResult
/// </summary>
private readonly Dictionary<string, ShiftRuleValidationResult> _validationCache = new();
/// <summary>
/// 【性能监控】验证统计信息
/// </summary>
private readonly Dictionary<string, int> _validationStats = new();
public ShiftRuleValidationEngine(
ILogger<ShiftRuleValidationEngine> logger)
{
_logger = logger;
_logger.LogInformation("班次规则校验引擎初始化完成支持9种规则类型的批次感知验证使用缓存替代班次服务");
}
/// <summary>
/// 【核心验证方法】增强版批次感知班次规则验证
/// 【业务逻辑】整合9种班次规则的完整验证支持历史数据+批次上下文的双重验证模式
/// 【验证策略】:优先验证批次上下文冲突,然后结合历史数据进行完整性检查
/// 【性能优化】:基于规则类型的优先级验证,快速识别高风险冲突
/// 【深度思考】:该方法是遗传算法适应度计算的关键组件,必须确保验证的准确性和性能
/// </summary>
/// <param name="personnelId">验证的人员ID</param>
/// <param name="currentTask">当前待验证的任务</param>
/// <param name="batchContext">批次上下文(不包含当前任务的其他相关任务)</param>
/// <returns>发现的班次规则冲突列表</returns>
public async Task<List<string>> ValidateShiftRulesWithEnhancedBatchContextAsync(
long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> batchContext, GlobalAllocationContext context)
{
var conflicts = new List<string>();
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<Task<List<string>>>();
var ruleValidationResults = new Dictionary<string, List<string>>();
// 【并行验证优化】:对独立的规则进行并行验证以提升性能
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<string> { $"人员{personnelId}任务{currentTask.WorkOrderCode}班次规则验证异常: {ex.Message}" };
}
}
/// <summary>
/// 【规则验证核心】验证特定班次规则类型
/// 【架构设计】:采用策略模式,根据规则类型分发到具体的验证方法
/// 【业务覆盖】支持规则3-9的完整验证覆盖
/// </summary>
/// <param name="ruleType">规则类型标识</param>
/// <param name="personnelId">人员ID</param>
/// <param name="currentTask">当前任务</param>
/// <param name="batchContext">批次上下文</param>
/// <returns>该规则类型的冲突列表</returns>
private async Task<List<string>> ValidateSpecificShiftRuleAsync(
string ruleType, long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> 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<string> conflicts = new List<string>();
// 【规则分发】根据规则类型分发到具体的验证逻辑
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<string> { $"规则{ruleType}验证异常: {ex.Message}" };
}
}
/// <summary>
/// 【规则实现3&4】班次连续性规则验证一/二班不连续,二/三班不连续)
/// 【业务逻辑】:同一人员同一天不能有冲突的班次安排
/// 【深度思考】:需要综合考虑批次内任务和数据库历史任务,确保无遗漏
/// 【验证范围】:批次内同日冲突 + 数据库历史同日冲突
/// </summary>
private async Task<List<string>> ValidateShiftSequenceRuleWithBatchContextAsync(
string ruleName, long personnelId, WorkOrderEntity currentTask,
string firstShiftName, string secondShiftName, List<WorkOrderEntity> batchContext, GlobalAllocationContext context)
{
var conflicts = new List<string>();
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<string> { $"{ruleName}验证异常: {ex.Message}" };
}
}
/// <summary>
/// 【批次内冲突检查】验证批次上下文中的班次冲突
/// </summary>
private async Task ValidateBatchContextShiftConflicts(
List<string> conflicts, string ruleName, long personnelId, WorkOrderEntity currentTask,
List<WorkOrderEntity> 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);
}
}
}
}
/// <summary>
/// 【历史数据冲突检查】验证历史数据中的班次冲突
/// </summary>
private async Task ValidateHistoricalDataShiftConflicts(
List<string> 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<WorkOrderEntity>();
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);
}
}
}
}
/// <summary>
/// 【规则实现5】周日/下周六连续规则验证
/// 【业务逻辑】:防止跨周连续工作,检查周日->下周六的7天跨度
/// 【深度思考】:批次内可能同时包含这两个时间点的任务,需要双向检查
/// </summary>
private async Task<List<string>> ValidateSundayToSaturdayRuleWithBatchContextAsync(
string ruleName, long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> batchContext, GlobalAllocationContext context)
{
var conflicts = new List<string>();
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<string> { $"{ruleName}验证异常: {ex.Message}" };
}
}
/// <summary>
/// 【规则实现6】周六/周日连续规则验证
/// 【业务逻辑】:防止周末连续工作,批次内可能同时包含同一人员的周六和周日任务
/// </summary>
private async Task<List<string>> ValidateSaturdayToSundayRuleWithBatchContextAsync(
string ruleName, long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> batchContext, GlobalAllocationContext context)
{
var conflicts = new List<string>();
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<string> { $"{ruleName}验证异常: {ex.Message}" };
}
}
/// <summary>
/// 【规则实现7】最大连续天数规则验证
/// 【业务逻辑】:最严格的连续工作限制,基于批次+历史数据计算真实的连续工作天数序列
/// </summary>
private async Task<List<string>> ValidateMaxConsecutiveDaysRuleWithBatchContextAsync(
string ruleName, long personnelId, WorkOrderEntity currentTask, int maxDays,
List<WorkOrderEntity> batchContext, GlobalAllocationContext context)
{
var conflicts = new List<string>();
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<string> { $"{ruleName}验证异常: {ex.Message}" };
}
}
/// <summary>
/// 【规则实现8&9】班次后休息规则验证二班后休息三班后休息
/// 【业务逻辑】:指定班次后次日必须休息,需要双向检查前一天和后一天
/// </summary>
private async Task<List<string>> ValidateRestAfterShiftRuleWithBatchContextAsync(
string ruleName, long personnelId, WorkOrderEntity currentTask, string shiftName,
List<WorkOrderEntity> batchContext, GlobalAllocationContext context)
{
var conflicts = new List<string>();
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<string> { $"{ruleName}验证异常: {ex.Message}" };
}
}
private async Task<List<string>> ValidatePersonShiftRuleWithBatchContextAsync(
string ruleName, long personnelId, WorkOrderEntity currentTask, string shiftName,
List<WorkOrderEntity> batchContext, GlobalAllocationContext context)
{
var conflicts = new List<string>();
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<string> { $"{ruleName}验证异常: {ex.Message}" };
}
}
/// <summary>
/// 【辅助方法】检查前一天的休息规则
/// </summary>
private async Task CheckPreviousDayRestRule(List<string> conflicts, string ruleName,
long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> 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);
}
}
/// <summary>
/// 【辅助方法】检查后一天的休息规则
/// </summary>
private async Task CheckNextDayRestRule(List<string> conflicts, string ruleName,
long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> 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);
}
}
}
/// <summary>
/// 【缓存管理】清理过期的缓存条目
/// </summary>
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);
}
/// <summary>
/// 【统计更新】更新验证统计信息
/// </summary>
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;
}
/// <summary>
/// 【日志记录】记录冲突详情
/// </summary>
private void LogConflictDetails(long personnelId, string taskCode, List<string> allConflicts, Dictionary<string, List<string>> 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]);
}
}
}
/// <summary>
/// 【配置方法】获取启用的班次规则类型列表
/// </summary>
private IEnumerable<string> GetEnabledShiftRuleTypes(GlobalAllocationContext context)
{
return context.ShiftRulesMapping.OrderBy(a => a.RuleType).Where(a=> a.IsEnabled == true).Select(a => a.RuleType).ToArray();
}
/// <summary>
/// 【辅助方法】检查下周六冲突 - 周日/下周六连续规则的核心逻辑
/// </summary>
private async Task CheckNextSaturdayConflicts(List<string> conflicts, string ruleName,
long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> 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;
}
/// <summary>
/// 【辅助方法】检查上周日冲突 - 周日/下周六连续规则的反向逻辑
/// </summary>
private async Task CheckLastSundayConflicts(List<string> conflicts, string ruleName,
long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> 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;
}
/// <summary>
/// 【辅助方法】检查相邻日期冲突 - 周六/周日连续规则的通用逻辑
/// </summary>
private async Task CheckAdjacentDayConflicts(List<string> conflicts, string ruleName,
long personnelId, WorkOrderEntity currentTask, List<WorkOrderEntity> 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;
}
/// <summary>
/// 【辅助方法】检查历史数据前一天任务 - 班次后休息规则的历史数据检查
/// </summary>
private async Task CheckHistoricalPreviousDayTask(List<string> 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);
}
}
}
/// <summary>
/// 【辅助方法】检查历史数据后一天任务 - 班次后休息规则的历史数据检查
/// </summary>
private async Task CheckHistoricalNextDayTask(List<string> 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;
}
/// <summary>
/// 【核心算法】计算批次感知的连续工作天数 - 最大连续天数规则的核心实现
/// 【业务逻辑】:整合批次内任务和历史数据,计算真实的连续工作天数
/// 【深度思考】:需要处理批次内任务的时序关系,确保连续性计算的准确性
/// </summary>
private async Task<int> CalculateConsecutiveWorkDaysWithBatchContextAsync(
long personnelId, DateTime targetDate, List<WorkOrderEntity> 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<WorkOrderEntity>();
// 【时序数据构建】创建包含批次任务和历史任务的完整工作日期集合
var allWorkDates = new HashSet<DateTime>();
// 添加批次内任务日期
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; // 异常时返回最小值,避免过度限制
}
}
/// <summary>
/// 【深度实现】检查人员在指定日期的班次不可用性
/// 【业务思考】整合GlobalAllocationContext中的班次不可用性缓存支持高性能查询
/// 【数据来源】DateShiftUnavailablePersonnel + PersonnelUnavailabilityIndex + UnavailabilityDetails
/// 【查询策略】优先使用PersonnelUnavailabilityIndex进行O(1)查询,再检查详细信息
/// 【约束类型】:区分硬约束(请假、医疗)和软约束(个人意愿、培训)
/// </summary>
/// <param name="personnelId">人员ID</param>
/// <param name="checkDate">检查日期</param>
/// <param name="context">全局分配上下文</param>
/// <returns>是否不可用true表示不可用false表示可用</returns>
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;
}
}
/// <summary>
/// 【辅助方法】检查硬约束是否存在
/// 【业务逻辑】通过UnavailabilityDetails查询具体的不可用原因区分硬约束和软约束
/// 【约束类型】:硬约束(请假、医疗)必须遵守,软约束(个人意愿、培训)可协调
/// </summary>
private bool CheckForHardConstraints(long personnelId, DateTime targetDate, HashSet<long> 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; // 异常情况下保守处理,认为存在硬约束
}
}
/// <summary>
/// 【辅助方法】后备查询使用DateShiftUnavailablePersonnel验证
/// 【查询策略】当PersonnelUnavailabilityIndex缺失时的后备验证机制
/// 【性能考虑】:虽然性能略低于索引查询,但确保数据一致性
/// </summary>
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; // 异常情况下假设可用,避免过度限制
}
}
/// <summary>
/// 【验证结果数据结构】班次规则验证结果
/// </summary>
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<string> Conflicts { get; set; } = new();
public DateTime ValidatedAt { get; set; }
}
}
}