using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using ZhonTai.Admin.Core.Dto; using ZhonTai.Admin.Services; using NPP.SmartSchedue.Api.Contracts.Services.Work; using NPP.SmartSchedue.Api.Contracts.Services.Work.Input; using NPP.SmartSchedue.Api.Contracts.Services.Work.Output; using NPP.SmartSchedue.Api.Repositories.Work; using NPP.SmartSchedue.Api.Contracts.Domain.Work; using NPP.SmartSchedue.Api.Contracts.Core.Enums; using NPP.SmartSchedue.Api.Contracts.Services.Time; using NPP.SmartSchedue.Api.Contracts.Services.Time.Output; using NPP.SmartSchedue.Api.Contracts.Services.Personnel; using NPP.SmartSchedue.Api.Contracts.Services.Integration.Internal; using ZhonTai.DynamicApi; using ZhonTai.DynamicApi.Attributes; namespace NPP.SmartSchedue.Api.Services.Work; /// /// 工作任务服务 /// [DynamicApi(Area = "app")] public class WorkOrderService : BaseService, IWorkOrderService, IDynamicApi { private readonly WorkOrderRepository _workOrderRepository; private readonly WorkOrderFLPersonnelRepository _workOrderFlPersonnelRepository; private readonly IEmployeeLeaveService _employeeLeaveService; private readonly IPersonnelWorkLimitService _personnelWorkLimitService; private readonly IShiftService _shiftService; private readonly IShiftRuleMappingService _shiftRuleMappingService; private readonly IShiftRuleService _shiftRuleService; private readonly IShiftUnavailabilityService _shiftUnavailabilityService; private readonly ILogger _logger; public WorkOrderService( WorkOrderRepository workOrderRepository, WorkOrderFLPersonnelRepository workOrderFlPersonnelRepository, IEmployeeLeaveService employeeLeaveService, IPersonnelWorkLimitService personnelWorkLimitService, IShiftService shiftService, IShiftRuleMappingService shiftRuleMappingService, IShiftRuleService shiftRuleService, IShiftUnavailabilityService shiftUnavailabilityService, ILogger logger) { _workOrderRepository = workOrderRepository; _workOrderFlPersonnelRepository = workOrderFlPersonnelRepository; _employeeLeaveService = employeeLeaveService; _personnelWorkLimitService = personnelWorkLimitService; _shiftService = shiftService; _shiftRuleMappingService = shiftRuleMappingService; _shiftRuleService = shiftRuleService; _shiftUnavailabilityService = shiftUnavailabilityService; _logger = logger; } /// /// 查询 /// /// /// public async Task GetAsync(long id) { var output = await _workOrderRepository.Select .WhereDynamic(id) .ToOneAsync(); if (output != null) { // 获取关联的FL人员信息 var flPersonnels = await _workOrderFlPersonnelRepository.Select .Where(a => a.WorkOrderId == id) .ToListAsync(); output.FLPersonnels = flPersonnels.Select(fp => new FLPersonnelInfo { FLPersonnelId = fp.FLPersonnelId, FLPersonnelName = fp.FLPersonnelName }).ToList(); } return output; } /// /// 查询分页 /// /// /// [HttpPost] public async Task> GetPageAsync(PageInput input) { var list = await _workOrderRepository.Select .WhereIf(!string.IsNullOrEmpty(input.Filter?.ProjectNumber), a => a.ProjectNumber.Contains(input.Filter.ProjectNumber)) .WhereIf(!string.IsNullOrEmpty(input.Filter?.WorkOrderCode), a => a.WorkOrderCode.Contains(input.Filter.WorkOrderCode)) .WhereIf(input.Filter?.Status.HasValue == true, a => a.Status == (int)(WorkOrderStatusEnum)input.Filter.Status.Value) .WhereIf(input.Filter?.StartDate.HasValue == true, a => a.WorkOrderDate >= input.Filter.StartDate.Value) .WhereIf(input.Filter?.EndDate.HasValue == true, a => a.WorkOrderDate <= input.Filter.EndDate.Value) .Count(out var total) .OrderByDescending(a => a.Id) .Page(input.CurrentPage, input.PageSize) .ToListAsync(); // 为分页结果添加FL人员信息 if (list?.Any() == true) { var workOrderIds = list.Select(x => x.Id).ToList(); var flPersonnelsList = await _workOrderFlPersonnelRepository.Select .Where(a => workOrderIds.Contains(a.WorkOrderId)) .ToListAsync(); var flPersonnelsDict = flPersonnelsList .GroupBy(fp => fp.WorkOrderId) .ToDictionary( g => g.Key, g => g.Select(fp => new FLPersonnelInfo { FLPersonnelId = fp.FLPersonnelId, FLPersonnelName = fp.FLPersonnelName }).ToList() ); foreach (var item in list) { if (flPersonnelsDict.TryGetValue(item.Id, out var flPersonnels)) { item.FLPersonnels = flPersonnels; } else { item.FLPersonnels = new List(); } } } var data = new PageOutput() { List = list, Total = total }; return data; } /// /// 添加 /// /// /// public async Task AddAsync(WorkOrderAddInput input) { return await AddSingleWorkOrderAsync(input); } /// /// 批量添加 /// /// /// [HttpPost] public async Task BatchAddAsync(BatchWorkOrderAddInput input) { var result = new BatchWorkOrderAddOutput { TotalCount = input.WorkOrders?.Count ?? 0 }; if (input.WorkOrders?.Any() != true) { result.IsSuccess = false; result.Message = "工作任务列表不能为空"; return result; } var successIds = new List(); var failedItems = new List(); // 开始批量处理 for (int i = 0; i < input.WorkOrders.Count; i++) { var workOrder = input.WorkOrders[i]; try { // 添加单个任务 var workOrderId = await AddSingleWorkOrderAsync(workOrder); successIds.Add(workOrderId); } catch (Exception ex) { failedItems.Add(new BatchWorkOrderError { Index = i, WorkOrderCode = $"{workOrder.ProjectNumber}_{workOrder.ShiftCode}_{workOrder.ProcessCode}" ?? $"第{i + 1}个任务", ErrorMessage = ex.Message, ErrorDetail = ex.ToString() }); // 如果启用失败回滚,则回滚已添加的任务 if (input.RollbackOnFailure) { foreach (var successId in successIds) { try { await DeleteAsync(successId); } catch { // 回滚失败,记录日志但不影响主流程 } } result.IsSuccess = false; result.Message = $"批量添加失败,已回滚所有操作。错误: {ex.Message}"; return result; } } } // 设置结果 result.SuccessIds = successIds; result.FailedItems = failedItems; result.SuccessCount = successIds.Count; result.FailedCount = failedItems.Count; result.IsSuccess = result.SuccessCount > 0; result.Message = $"批量添加完成。成功: {result.SuccessCount},失败: {result.FailedCount}"; return result; } /// /// 修改 /// /// /// public async Task UpdateAsync(WorkOrderUpdateInput input) { var entity = await _workOrderRepository.GetAsync(input.Id); Mapper.Map(input, entity); await ConvertWorkOrderShift(entity); await _workOrderRepository.UpdateAsync(entity); // 更新FL人员关联 await UpdateWorkOrderFLPersonnelsAsync(input.Id, input.FLPersonnels ?? new List()); } /// /// 删除 /// /// /// public async Task DeleteAsync(long id) { // 删除FL人员关联 await _workOrderFlPersonnelRepository.DeleteAsync(a => a.WorkOrderId == id); // 删除工作任务 await _workOrderRepository.DeleteAsync(id); } /// /// 软删除 /// /// /// public async Task SoftDeleteAsync(long id) { await _workOrderRepository.SoftDeleteAsync(id); } /// /// 批量软删除 /// /// /// public async Task BatchSoftDeleteAsync(long[] ids) { await _workOrderRepository.SoftDeleteAsync(ids); } /// /// 更新任务状态 /// /// /// /// public async Task UpdateStatusAsync(long id, WorkOrderStatusEnum status) { var entity = await _workOrderRepository.GetAsync(id); if (entity != null) { entity.Status = (int)status; await _workOrderRepository.UpdateAsync(entity); } } /// /// 开始任务 /// /// /// public async Task StartAsync(long id) { var entity = await _workOrderRepository.GetAsync(id); if (entity != null) { entity.Status = (int)WorkOrderStatusEnum.InProgress; entity.ActualStartTime = DateTime.Now; await _workOrderRepository.UpdateAsync(entity); } } /// /// 完成任务 /// /// /// /// public async Task CompleteAsync(long id, decimal? actualWorkHours = null) { var entity = await _workOrderRepository.GetAsync(id); if (entity != null) { entity.Status = (int)WorkOrderStatusEnum.Completed; entity.ActualEndTime = DateTime.Now; if (actualWorkHours.HasValue) { entity.ActualWorkHours = actualWorkHours.Value; } else if (entity.ActualStartTime.HasValue) { // 自动计算工时 var timeSpan = DateTime.Now - entity.ActualStartTime.Value; entity.ActualWorkHours = (decimal)timeSpan.TotalHours; } await _workOrderRepository.UpdateAsync(entity); } } #region 私有方法 /// /// 添加单个工作任务(内部方法) /// /// /// private async Task AddSingleWorkOrderAsync(WorkOrderAddInput input) { var result = new WorkOrderEntity(); for (int i = 1; i <= input.RequiredPersonnel; i++) { var entity = Mapper.Map(input); // 生成任务代码:项目号_班次code_工序code if (string.IsNullOrEmpty(entity.WorkOrderCode)) { entity.WorkOrderCode = $"{entity.ProjectNumber}_{entity.ShiftCode}_{entity.ProcessCode}"; } // 设置默认状态 if (entity.Status == 0) { entity.Status = (int)WorkOrderStatusEnum.PendingSubmit; } if (input.RequiredPersonnel > 1 && i == 1) { entity.NeedPostHead = true; } // 获取任务班次信息 await ConvertWorkOrderShift(entity); result = await _workOrderRepository.InsertAsync(entity); // 添加FL人员关联 if (input.FLPersonnels?.Any() == true) { await AddWorkOrderFLPersonnelsAsync(result.Id, input.FLPersonnels); } } return result.Id; } /// /// 添加工作任务FL人员关联 /// /// /// /// private async Task AddWorkOrderFLPersonnelsAsync(long workOrderId, List flPersonnels) { if (flPersonnels?.Any() == true) { var entities = flPersonnels.Select(fp => new WorkOrderFLPersonnelEntity { WorkOrderId = workOrderId, FLPersonnelId = fp.FLPersonnelId, FLPersonnelName = fp.FLPersonnelName, CreatedTime = DateTime.Now }).ToList(); await _workOrderFlPersonnelRepository.InsertAsync(entities); } } /// /// 更新工作任务FL人员关联 /// /// /// private async Task UpdateWorkOrderFLPersonnelsAsync(long workOrderId, List flPersonnels) { // 删除现有关联 await _workOrderFlPersonnelRepository.DeleteAsync(a => a.WorkOrderId == workOrderId); // 添加新关联 await AddWorkOrderFLPersonnelsAsync(workOrderId, flPersonnels); } /// /// 转换工作任务班次信息,计算计划开始和结束时间 /// 【核心业务逻辑】:将班次的时间配置与任务日期结合,生成具体的执行时间段 /// 【深度思考】:该方法解决了班次时间模板与具体任务执行日期的映射问题, /// 特别处理了跨天班次的复杂场景,确保时间计算的准确性 /// /// 待转换的工作任务实体 /// 转换后的工作任务实体,包含计算好的计划开始和结束时间 private async Task ConvertWorkOrderShift(WorkOrderEntity workOrderEntity) { // 【业务逻辑】:根据任务的班次ID获取班次定义信息 // 班次定义包含: StartTime、EndTime、班次名称等基础配置 var shift = await _shiftService.GetAsync(workOrderEntity.ShiftId ?? 0); if (shift != null) { // 【时间计算核心】:将任务执行日期与班次时间模板结合 // 关键设计:只取日期部分,避免时间部分干扰班次时间的准确计算 var workDate = workOrderEntity.WorkOrderDate.Date; // 取日期部分,确保时间为00:00:00 // 【计划时间生成】:组合任务日期和班次时间,生成具体的执行时间点 // 例:任务日期2024-01-15 + 班次开始08:00 = 2024-01-15 08:00:00 workOrderEntity.PlannedStartTime = workDate.Add(shift.StartTime); workOrderEntity.PlannedEndTime = workDate.Add(shift.EndTime); // 【跨天班次特殊处理】:处理夜班等跨越午夜的班次场景 // 深度业务思考:如夜班22:00-06:00,结束时间小于开始时间表示跨天 // 解决方案:将结束时间推迟一天,确保时间逻辑正确 // 示例:2024-01-15 22:00 - 2024-01-16 06:00 if (shift.EndTime < shift.StartTime) { // 跨天班次:结束时间需要加一天 // 业务场景:夜班从今天22:00工作到明天06:00 workOrderEntity.PlannedEndTime = workOrderEntity.PlannedEndTime.AddDays(1); } } // 【异常处理】:如果班次信息不存在,保持原有的时间设置 // 这种设计避免了因班次配置缺失而导致任务创建失败的情况 return workOrderEntity; } #endregion #region 任务自检功能 /// /// 单个任务自检 /// /// /// public async Task ValidateWorkOrderAsync(SingleWorkOrderValidationInput input) { var workOrder = await _workOrderRepository.GetAsync(input.WorkOrderId); if (workOrder == null) { return new WorkOrderValidationOutput { WorkOrderId = input.WorkOrderId, IsValid = false, Errors = new List { new WorkOrderValidationError { ErrorType = ValidationErrorType.TaskTimeConflict, Message = "任务不存在" } } }; } var result = new WorkOrderValidationOutput { WorkOrderId = input.WorkOrderId, WorkOrderCode = workOrder.WorkOrderCode, IsValid = true }; // 如果任务已分配实施人员,则检查人员空闲状态 if (workOrder.AssignedPersonnelId.HasValue) { // 检查分配人员的空闲状态 var personnelResult = await CheckPersonnelAvailabilityWithBatchContextAsync( workOrder.AssignedPersonnelId.Value, workOrder.AssignedPersonnelName ?? $"人员{workOrder.AssignedPersonnelId}", workOrder, new List() { workOrder }); result.PersonnelResults.Add(personnelResult); if (!personnelResult.IsAvailable) { result.IsValid = false; // 添加具体的错误信息 foreach (var detail in personnelResult.ConflictDetails) { result.Errors.Add(new WorkOrderValidationError { ErrorType = GetErrorTypeFromDetail(detail), Message = detail, PersonnelId = workOrder.AssignedPersonnelId.Value, PersonnelName = workOrder.AssignedPersonnelName }); } } } else { // 任务未分配实施人员,自检通过,但添加信息说明 result.PersonnelResults.Add(new PersonnelAvailabilityResult { PersonnelId = 0, PersonnelName = "未分配", IsAvailable = true, ConflictDetails = new List { "任务尚未分配实施人员" } }); } // 如果自检通过且需要更新状态为待整合 if (result.IsValid && input.UpdateStatusToPendingIntegration) { await UpdateStatusAsync(workOrder.Id, WorkOrderStatusEnum.PendingIntegration); } return result; } /// /// 多个任务自检 /// /// /// public async Task ValidateMultipleWorkOrdersAsync(WorkOrderValidationInput input) { var result = new MultipleWorkOrderValidationOutput { IsValid = true }; // 获取所有待验证的任务信息(用于上下文计算) var allWorkOrders = await _workOrderRepository.Select .Where(wo => input.WorkOrderIds.Contains(wo.Id)) .ToListAsync(); // 检查每个任务(考虑批次内其他任务的影响) foreach (var workOrderId in input.WorkOrderIds) { var currentWorkOrder = allWorkOrders.FirstOrDefault(wo => wo.Id == workOrderId); if (currentWorkOrder == null) { result.WorkOrderResults.Add(new WorkOrderValidationOutput { WorkOrderId = workOrderId, IsValid = false, Errors = new List { new NPP.SmartSchedue.Api.Contracts.Services.Work.Output.WorkOrderValidationError { ErrorType = ValidationErrorType.TaskTimeConflict, Message = "任务不存在" } } }); result.IsValid = false; continue; } // 获取批次内其他任务作为上下文 var batchContext = allWorkOrders.Where(wo => wo.Id != workOrderId).ToList(); var singleResult = await ValidateWorkOrderWithBatchContextAsync(currentWorkOrder, batchContext); result.WorkOrderResults.Add(singleResult); if (!singleResult.IsValid) { result.IsValid = false; } } // 如果所有任务自检通过且需要更新状态为待整合 if (result.IsValid && input.UpdateStatusToPendingIntegration) { try { var updateTasks = new List(); foreach (var workOrderId in input.WorkOrderIds) { updateTasks.Add(UpdateStatusAsync(workOrderId, WorkOrderStatusEnum.PendingIntegration)); } // 并行执行状态更新,提高性能 await Task.WhenAll(updateTasks); // 验证所有任务状态确实已更新 var updatedWorkOrders = await _workOrderRepository.Where(wo => input.WorkOrderIds.Contains(wo.Id)) .ToListAsync(); var failedUpdates = updatedWorkOrders .Where(wo => wo.Status != (int)WorkOrderStatusEnum.PendingIntegration) .ToList(); if (failedUpdates.Any()) { throw new InvalidOperationException( $"状态更新失败,涉及任务ID: {string.Join(",", failedUpdates.Select(wo => wo.Id))}"); } _logger.LogInformation($"成功批量更新任务状态为待整合,任务数量: {input.WorkOrderIds?.Count ?? 0}"); } catch (Exception ex) { _logger.LogError(ex, $"批量更新任务状态失败,任务ID: {string.Join(",", input.WorkOrderIds ?? new List())}"); throw new InvalidOperationException($"批量状态更新失败: {ex.Message}", ex); } } return result; } /// /// 带批次上下文的单任务验证(考虑本批次其他任务的影响) /// /// 当前任务 /// 批次内其他任务 /// private async Task ValidateWorkOrderWithBatchContextAsync(WorkOrderEntity workOrder, List batchContext) { var result = new WorkOrderValidationOutput { WorkOrderId = workOrder.Id, WorkOrderCode = workOrder.WorkOrderCode, IsValid = true }; // 如果任务已分配实施人员,则检查人员空闲状态(包含批次上下文) if (workOrder.AssignedPersonnelId.HasValue) { // 检查分配人员的空闲状态(考虑批次上下文) var personnelResult = await CheckPersonnelAvailabilityWithBatchContextAsync( workOrder.AssignedPersonnelId.Value, workOrder.AssignedPersonnelName ?? $"人员{workOrder.AssignedPersonnelId}", workOrder, batchContext); result.PersonnelResults.Add(personnelResult); if (!personnelResult.IsAvailable) { result.IsValid = false; // 添加具体的错误信息 foreach (var detail in personnelResult.ConflictDetails) { result.Errors.Add(new NPP.SmartSchedue.Api.Contracts.Services.Work.Output.WorkOrderValidationError { ErrorType = GetErrorTypeFromDetail(detail), Message = detail, PersonnelId = workOrder.AssignedPersonnelId.Value, PersonnelName = workOrder.AssignedPersonnelName }); } } } else { // 任务未分配实施人员,自检通过,但添加信息说明 result.PersonnelResults.Add(new PersonnelAvailabilityResult { PersonnelId = 0, PersonnelName = "未分配", IsAvailable = true, ConflictDetails = new List { "任务尚未分配实施人员" } }); } return result; } /// /// 带批次上下文的人员空闲状态检查 /// private async Task CheckPersonnelAvailabilityWithBatchContextAsync( long personnelId, string personnelName, WorkOrderEntity workOrder, List batchContext) { var result = new PersonnelAvailabilityResult { PersonnelId = personnelId, PersonnelName = personnelName, IsAvailable = true }; var hasLeaveConflict = !await _shiftUnavailabilityService.CheckUnavailablePersonnelByShift( workOrder.WorkOrderDate, workOrder.ShiftId ?? 0, workOrder.AssignedPersonnelId ?? 0); if (hasLeaveConflict) { result.HasLeaveConflict = true; result.IsAvailable = false; result.ConflictDetails.Add($"人员 {personnelName} 在任务时间段的班次存在其他意愿"); } // 检查任务时间冲突(包含批次上下文) var taskConflicts = await CheckTaskTimeConflictsWithBatchContextAsync(personnelId, workOrder, workOrder.PlannedStartTime, workOrder.PlannedEndTime, batchContext); if (taskConflicts.Any()) { result.HasTaskTimeConflict = true; result.IsAvailable = false; result.ConflictDetails.AddRange(taskConflicts); } // 检查班次规则冲突(包含批次上下文) var shiftRuleConflicts = await CheckShiftRuleConflictsWithBatchContextAsync(personnelId, workOrder, workOrder.PlannedStartTime, workOrder.PlannedEndTime, batchContext); if (shiftRuleConflicts.Any()) { result.IsAvailable = false; result.ConflictDetails.AddRange(shiftRuleConflicts); } else { result.ConflictDetails.Add("班次规则检查:通过"); } return result; } /// /// 带批次上下文的任务时间冲突检查(包含精确的时间段重叠检查) /// private async Task> CheckTaskTimeConflictsWithBatchContextAsync(long personnelId, WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List batchContext) { return await CheckTaskTimeConflictsWithBatchContextAsync(personnelId, workOrder, workStartTime, workEndTime, batchContext, null); } /// /// 带批次上下文和缓存优化的任务时间冲突检查(包含精确的时间段重叠检查) /// 【性能优化】:优先使用PersonnelHistoryTasks缓存,避免重复数据库查询 /// /// 人员ID /// 工作任务 /// 工作开始时间 /// 工作结束时间 /// 批次上下文中的其他任务 /// 人员历史任务缓存(可选,优先使用缓存避免数据库查询) /// 冲突详情列表 private async Task> CheckTaskTimeConflictsWithBatchContextAsync(long personnelId, WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List batchContext, Dictionary>? personnelHistoryCache) { var conflicts = new List(); // 获取当前任务的班次信息 var currentShift = await _shiftService.GetAsync(workOrder.ShiftId ?? 0); if (currentShift == null) { conflicts.Add("当前任务班次信息不存在"); return conflicts; } // 1. 检查与数据库历史任务的冲突(包含时间段重叠检查) var dbConflictingTasks = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.Id != workOrder.Id) // 排除自己 .Where(wo => wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .ToListAsync(); foreach (var conflict in dbConflictingTasks) { // 优化后的时间冲突检查:只检查真正的时间重叠,移除同日期强约束 // 深度业务思考:不同日期的任务允许分配给同一人员,只要时间段不重叠 var conflictShift = await _shiftService.GetAsync(conflict.ShiftId ?? 0); if (conflictShift != null) { var conflictStartTime = conflict.WorkOrderDate.Date.Add(conflictShift.StartTime); var conflictEndTime = conflict.WorkOrderDate.Date.Add(conflictShift.EndTime); // 处理跨天班次:如果结束时间小于开始时间,说明跨天 if (conflictShift.EndTime < conflictShift.StartTime) { conflictEndTime = conflictEndTime.AddDays(1); } // 精确检查时间段重叠:只有真正时间重叠才算冲突 if (IsTimeRangeOverlap(workStartTime, workEndTime, conflictStartTime, conflictEndTime)) { conflicts.Add( $"与数据库任务 '{conflict.WorkOrderCode}' ({conflictShift.Name}: {conflictShift.StartTime:hh\\:mm}-{conflictShift.EndTime:hh\\:mm}) 时间重叠,日期: {conflict.WorkOrderDate:yyyy-MM-dd}"); } } else { // 如果没有班次信息,仍然需要检查同日期冲突作为兜底 if (conflict.WorkOrderDate.Date == workOrder.WorkOrderDate.Date) { conflicts.Add( $"与数据库任务 '{conflict.WorkOrderCode}' 在同一天存在冲突 (日期: {conflict.WorkOrderDate:yyyy-MM-dd}),缺少班次信息无法精确检查"); } } } // 2. 检查与批次内其他任务的冲突(精确时间段重叠检查) foreach (var conflict in batchContext.Where(wo => wo.AssignedPersonnelId == personnelId && wo.Id != workOrder.Id)) { // 优化后的批次内冲突检查:只检查真正的时间重叠,支持不同日期同人员分配 var conflictShift = await _shiftService.GetAsync(conflict.ShiftId ?? 0); if (conflictShift != null) { var conflictStartTime = conflict.WorkOrderDate.Date.Add(conflictShift.StartTime); var conflictEndTime = conflict.WorkOrderDate.Date.Add(conflictShift.EndTime); // 处理跨天班次 if (conflictShift.EndTime < conflictShift.StartTime) { conflictEndTime = conflictEndTime.AddDays(1); } // 精确检查时间段重叠:只有真正时间重叠才算冲突 if (IsTimeRangeOverlap(workStartTime, workEndTime, conflictStartTime, conflictEndTime)) { conflicts.Add( $"与批次内任务 '{conflict.WorkOrderCode}' ({conflictShift.Name}: {conflictShift.StartTime:hh\\:mm}-{conflictShift.EndTime:hh\\:mm}) 时间重叠,日期: {conflict.WorkOrderDate:yyyy-MM-dd}"); } } else { // 如果没有班次信息,检查同日期冲突作为兜底 if (conflict.WorkOrderDate.Date == workOrder.WorkOrderDate.Date) { conflicts.Add( $"与批次内任务 '{conflict.WorkOrderCode}' 在同一天存在冲突 (日期: {conflict.WorkOrderDate:yyyy-MM-dd}),缺少班次信息无法精确检查"); } } } return conflicts; } /// /// 带批次上下文的工作时间限制冲突检查 /// private async Task> CheckWorkLimitConflictsWithBatchContextAsync(long personnelId, WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List batchContext) { var conflicts = new List(); try { // 获取人员工作时间限制设置 var workLimit = await _personnelWorkLimitService.GetPersonnelWorkLimitAsync(personnelId); if (workLimit == null) { // 如果没有设置工作时间限制,则认为通过 return conflicts; } // 检查连续工作天数限制(需要考虑批次内任务的顺序影响) if (workLimit.MaxContinuousWorkDays.HasValue) { var consecutiveDays = await CalculateConsecutiveWorkDaysWithBatchContextAsync(personnelId, workOrder.WorkOrderDate, batchContext); if (consecutiveDays >= workLimit.MaxContinuousWorkDays.Value) { conflicts.Add($"连续工作 {consecutiveDays} 天超过限制 {workLimit.MaxContinuousWorkDays.Value} 天(包含批次内任务影响)"); } } // 检查当周最大排班次数限制(需要考虑批次内同周任务) if (workLimit.MaxShiftsPerWeek.HasValue) { var weekStart = workOrder.WorkOrderDate.Date.AddDays(-(int)workOrder.WorkOrderDate.DayOfWeek); var weekEnd = weekStart.AddDays(7); // 数据库中的同周任务数量 var weeklyTaskCount = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.Id != workOrder.Id) .Where(wo => wo.WorkOrderDate.Date >= weekStart && wo.WorkOrderDate.Date < weekEnd) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .CountAsync(); // 批次内的同周任务数量(排除当前任务) var batchWeeklyTaskCount = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date >= weekStart && wo.WorkOrderDate.Date < weekEnd) .Count(); // 总数包含当前任务,所以+1 var totalWeeklyTasks = weeklyTaskCount + batchWeeklyTaskCount + 1; if (totalWeeklyTasks > workLimit.MaxShiftsPerWeek.Value) { conflicts.Add( $"本周排班次数 {totalWeeklyTasks} 次超过限制 {workLimit.MaxShiftsPerWeek.Value} 次(包含批次内任务 {batchWeeklyTaskCount} 次)"); } } // 检查每日工作量限制(整合原跨任务检查的逻辑) if (workOrder.EstimatedHours.HasValue && workOrder.EstimatedHours.Value > 0) { var currentDate = workOrder.WorkOrderDate.Date; // 计算数据库中同一天的总工作量 var dbDailyWorkHours = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.Id != workOrder.Id) .Where(wo => wo.WorkOrderDate.Date == currentDate) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .Where(wo => wo.EstimatedHours.HasValue && wo.EstimatedHours.Value > 0) .SumAsync(wo => wo.EstimatedHours.Value); // 计算批次内同一天的总工作量 var batchDailyWorkHours = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == currentDate) .Where(wo => wo.EstimatedHours.HasValue && wo.EstimatedHours.Value > 0) .Sum(wo => wo.EstimatedHours.Value); // 总工作量 = 数据库 + 批次内 + 当前任务 var totalDailyWorkHours = dbDailyWorkHours + batchDailyWorkHours + workOrder.EstimatedHours.Value; // 假设每日最大工作时间为12小时(可配置化) const decimal MaxDailyWorkHours = 12m; if (totalDailyWorkHours > MaxDailyWorkHours) { var batchTaskCodes = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == currentDate) .Select(wo => wo.WorkOrderCode) .ToList(); var batchInfo = batchTaskCodes.Any() ? $"(包含批次内任务: {string.Join(", ", batchTaskCodes)})" : ""; conflicts.Add($"每日工作量 {totalDailyWorkHours:F1}小时 超过限制 {MaxDailyWorkHours}小时 {batchInfo}"); } } } catch (Exception ex) { conflicts.Add($"工作时间限制检查异常: {ex.Message}"); } return conflicts; } /// /// 带批次上下文的班次规则冲突检查 /// private async Task> CheckShiftRuleConflictsWithBatchContextAsync(long personnelId, WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List batchContext) { var conflicts = new List(); try { var shiftRules = await _shiftRuleService.GetListAsync(); foreach (var shiftRule in shiftRules) { // 根据规则类型执行不同的检查逻辑(考虑批次上下文) var ruleConflicts = await ValidateShiftRuleWithBatchContextAsync(personnelId, workOrder, shiftRule, workStartTime, workEndTime, batchContext); if (ruleConflicts.Any()) { conflicts.AddRange(ruleConflicts); } } } catch (Exception ex) { conflicts.Add($"班次规则检查异常: {ex.Message}"); } return conflicts; } /// /// 计算连续工作天数(考虑批次上下文) /// private async Task CalculateConsecutiveWorkDaysWithBatchContextAsync(long personnelId, DateTime targetDate, List batchContext) { var consecutiveDays = 1; // 包含目标日期 // 获取批次内该人员的任务,按日期排序 var personnelBatchTasks = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .OrderBy(wo => wo.WorkOrderDate.Date) .ToList(); // 创建一个包含批次任务的日期集合 var batchWorkDates = new HashSet(personnelBatchTasks.Select(t => t.WorkOrderDate.Date)); batchWorkDates.Add(targetDate.Date); // 包含当前任务日期 var currentDate = targetDate.Date.AddDays(-1); // 从目标日期前一天开始往前检查 // 往前检查连续工作天数 while (true) { bool hasWork = false; // 检查批次内是否有工作 if (batchWorkDates.Contains(currentDate)) { hasWork = true; } else { // 检查数据库中是否有工作 hasWork = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == currentDate) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .AnyAsync(); } if (!hasWork) { break; // 没有工作任务,中断连续天数 } // 检查是否有请假 var hasLeave = await _employeeLeaveService.HasApprovedLeaveInTimeRangeAsync( personnelId, currentDate, currentDate.AddDays(1)); if (hasLeave) { break; // 有请假,中断连续天数 } consecutiveDays++; currentDate = currentDate.AddDays(-1); // 避免无限循环,最多检查30天 if (consecutiveDays > 30) { break; } } return consecutiveDays; } /// /// 带批次上下文的班次规则验证 /// private async Task> ValidateShiftRuleWithBatchContextAsync( long personnelId, WorkOrderEntity workOrder, ShiftRuleGetPageOutput shiftRule, DateTime workStartTime, DateTime workEndTime, List batchContext) { var conflicts = new List(); try { // 根据规则类型执行不同的验证逻辑 switch (shiftRule?.RuleType) { case "3": // 一/二班不连续 conflicts.AddRange(await ValidateShiftSequenceRuleWithBatchContext(shiftRule.RuleName, personnelId, workOrder, "一", "二", batchContext)); break; case "4": // 二/三班不连续 conflicts.AddRange(await ValidateShiftSequenceRuleWithBatchContext(shiftRule.RuleName, personnelId, workOrder, "二", "三", batchContext)); break; case "5": // 不能这周日/下周六连 conflicts.AddRange(await ValidateSundayToSaturdayRuleWithBatchContext( shiftRule.RuleName,personnelId, workOrder, batchContext)); break; case "6": // 不能本周六/本周日连 conflicts.AddRange(await ValidateSaturdayToSundayRuleWithBatchContext(shiftRule.RuleName, personnelId, workOrder, batchContext)); break; case "7": // 不能连续7天排班 conflicts.AddRange(await ValidateMaxConsecutiveDaysRuleWithBatchContext(shiftRule.RuleName,personnelId, workOrder, 7, batchContext)); break; case "8": // 三班后一天不排班 conflicts.AddRange(await ValidateRestAfterShiftRuleWithBatchContext(shiftRule.RuleName,personnelId, workOrder, "三", batchContext)); break; case "9": // 二班后一天不排班 conflicts.AddRange(await ValidateRestAfterShiftRuleWithBatchContext(shiftRule.RuleName,personnelId, workOrder, "二", batchContext)); break; default: // 对于未知规则类型,记录警告但不阻止任务 break; } } catch (Exception ex) { conflicts.Add($"规则 '{shiftRule?.RuleName}' 验证异常: {ex.Message}"); } return conflicts; } /// /// 带批次上下文的班次后休息规则验证(示例实现) /// private async Task> ValidateRestAfterShiftRuleWithBatchContext(string ruleName, long personnelId, WorkOrderEntity workOrder, string shiftName, List batchContext) { var conflicts = new List(); try { var currentDate = workOrder.WorkOrderDate.Date; // 检查前一天是否是指定班次(包含批次上下文和数据库) var previousDay = currentDate.AddDays(-1); // 先检查批次内是否有前一天的指定班次任务 var batchPreviousTask = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == previousDay) .FirstOrDefault(); if (batchPreviousTask != null) { var batchPreviousShift = await _shiftService.GetAsync(batchPreviousTask.ShiftId ?? 0); if (batchPreviousShift != null && batchPreviousShift.Name?.Contains(shiftName) == true) { conflicts.Add( $"违反批次内班次后休息规则 '{ruleName}': 批次内任务 '{batchPreviousTask.WorkOrderCode}' {shiftName}班后一天不能安排工作"); } } else { // 检查数据库中前一天是否有指定班次 var dbPreviousTask = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == previousDay) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .FirstAsync(); if (dbPreviousTask != null) { var dbPreviousShift = await _shiftService.GetAsync(dbPreviousTask.ShiftId ?? 0); if (dbPreviousShift != null && dbPreviousShift.Name?.Contains(shiftName) == true) { conflicts.Add( $"违反班次后休息规则 '{ruleName}': 数据库任务 '{dbPreviousTask.WorkOrderCode}' {shiftName}班后一天不能安排工作"); } } } // 检查当前任务是指定班次,确保后一天不排班(包含批次上下文) var currentShift = await _shiftService.GetAsync(workOrder.ShiftId ?? 0); if (currentShift != null && 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) { conflicts.Add($"违反批次内班次后休息规则 '{ruleName}': {shiftName}班后一天不能安排批次内任务"); } else { // 检查数据库中是否有后一天的任务 var dbNextTask = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == nextDay) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .AnyAsync(); if (dbNextTask) { conflicts.Add($"违反班次后休息规则 '{ruleName}': {shiftName}班后一天不能安排工作(与数据库任务冲突)"); } } } } catch (Exception ex) { conflicts.Add($"班次后休息规则验证异常: {ex.Message}"); } return conflicts; } /// /// 带批次上下文的班次连续性规则验证(规则3和4:一/二班不连续以及二/三班不连续) /// 深度思考:同一人员同一天不能有冲突的班次安排,需要综合考虑批次内任务和数据库历史任务 /// private async Task> ValidateShiftSequenceRuleWithBatchContext(string ruleName, long personnelId, WorkOrderEntity workOrder, string firstShiftName, string secondShiftName, List batchContext) { var conflicts = new List(); try { var currentShift = await _shiftService.GetAsync(workOrder.ShiftId ?? 0); if (currentShift == null) return conflicts; var currentDate = workOrder.WorkOrderDate.Date; // 检查当前班次是否为规则中的班次之一 bool currentIsFirstShift = currentShift.Name?.Contains(firstShiftName) == true; bool currentIsSecondShift = currentShift.Name?.Contains(secondShiftName) == true; if (!currentIsFirstShift && !currentIsSecondShift) { return conflicts; // 当前班次不是规则关注的班次 } // 1. 检查批次内同一天是否有冲突班次 var batchSameDayTasks = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == currentDate) .ToList(); foreach (var batchTask in batchSameDayTasks) { var batchShift = await _shiftService.GetAsync(batchTask.ShiftId ?? 0); if (batchShift == null) continue; bool batchIsFirstShift = batchShift.Name?.Contains(firstShiftName) == true; bool batchIsSecondShift = batchShift.Name?.Contains(secondShiftName) == true; // 检查班次冲突:当前是一班且批次内有二班,或当前是二班且批次内有一班 if ((currentIsFirstShift && batchIsSecondShift) || (currentIsSecondShift && batchIsFirstShift)) { conflicts.Add( $"违反批次内班次连续性规则 '{ruleName}': 同一天内不能同时安排{firstShiftName}班和{secondShiftName}班 (与批次内任务 '{batchTask.WorkOrderCode}' {batchShift.Name} 冲突)"); } } // 2. 检查数据库中同一天是否有冲突班次 var dbSameDayTasks = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.Id != workOrder.Id) .Where(wo => wo.WorkOrderDate.Date == currentDate) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .ToListAsync(); foreach (var dbTask in dbSameDayTasks) { var dbShift = await _shiftService.GetAsync(dbTask.ShiftId ?? 0); if (dbShift == null) continue; bool dbIsFirstShift = dbShift.Name?.Contains(firstShiftName) == true; bool dbIsSecondShift = dbShift.Name?.Contains(secondShiftName) == true; // 检查班次冲突 if ((currentIsFirstShift && dbIsSecondShift) || (currentIsSecondShift && dbIsFirstShift)) { conflicts.Add( $"违反班次连续性规则 '{ruleName}': 同一天内不能同时安排{firstShiftName}班和{secondShiftName}班 (与数据库任务 '{dbTask.WorkOrderCode}' {dbShift.Name} 冲突)"); } } } catch (Exception ex) { conflicts.Add($"班次连续性规则验证异常: {ex.Message}"); } return conflicts; } /// /// 带批次上下文的这周日/下周六连续规则验证(规则5) /// 深度思考:防止跨周连续工作,需要检查周日->下周六的7天跨度,批次内可能同时包含这两个时间点的任务 /// private async Task> ValidateSundayToSaturdayRuleWithBatchContext(string ruleName, long personnelId, WorkOrderEntity workOrder, List batchContext) { var conflicts = new List(); try { var currentDate = workOrder.WorkOrderDate.Date; // 情况1:当前任务是周日,检查下周六是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Sunday) { var nextSaturday = currentDate.AddDays(6); // 周日 + 6天 = 下周六 // 先检查批次内是否有下周六的任务 var batchNextSaturday = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == nextSaturday) .FirstOrDefault(); if (batchNextSaturday != null) { conflicts.Add( $"违反批次内周日/周六连续规则 '{ruleName}': 不能在这周日和下周六安排工作 (与批次内任务 '{batchNextSaturday.WorkOrderCode}' 冲突)"); } else { // 检查数据库中下周六是否有任务 var dbNextSaturday = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == nextSaturday) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.PendingAssignment || wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .AnyAsync(); if (dbNextSaturday) { conflicts.Add($"违反周日/周六连续规则 '{ruleName}': 不能在这周日和下周六安排工作 (与数据库任务冲突)"); } } } // 情况2:当前任务是周六,检查上周日是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Saturday) { var lastSunday = currentDate.AddDays(-6); // 周六 - 6天 = 上周日 // 先检查批次内是否有上周日的任务 var batchLastSunday = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == lastSunday) .FirstOrDefault(); if (batchLastSunday != null) { conflicts.Add( $"违反批次内周日/周六连续规则 '{ruleName}': 不能在上周日和这周六安排工作 (与批次内任务 '{batchLastSunday.WorkOrderCode}' 冲突)"); } else { // 检查数据库中上周日是否有任务 var dbLastSunday = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == lastSunday) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.PendingAssignment || wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .AnyAsync(); if (dbLastSunday) { conflicts.Add($"违反周日/周六连续规则 '{ruleName}': 不能在上周日和这周六安排工作 (与数据库任务冲突)"); } } } } catch (Exception ex) { conflicts.Add($"周日/周六连续规则验证异常: {ex.Message}"); } return conflicts; } /// /// 带批次上下文的本周六/本周日连续规则验证(规则6) /// 深度思考:防止周末连续工作,批次内可能同时包含同一人员的周六和周日任务 /// private async Task> ValidateSaturdayToSundayRuleWithBatchContext(string ruleName, long personnelId, WorkOrderEntity workOrder, List batchContext) { var conflicts = new List(); try { var currentDate = workOrder.WorkOrderDate.Date; // 情况1:当前是周六,检查周日是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Saturday) { var sunday = currentDate.AddDays(1); // 先检查批次内是否有周日任务 var batchSunday = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == sunday) .FirstOrDefault(); if (batchSunday != null) { conflicts.Add( $"违反批次内周六周日连续规则 '{ruleName}': 不能在周六和周日连续安排工作 (与批次内任务 '{batchSunday.WorkOrderCode}' 冲突)"); } else { // 检查数据库中周日是否有任务 var dbSunday = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == sunday) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.PendingAssignment || wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .AnyAsync(); if (dbSunday) { conflicts.Add($"违反周六周日连续规则 '{ruleName}': 不能在周六和周日连续安排工作 (与数据库任务冲突)"); } } } // 情况2:当前是周日,检查周六是否有排班 if (currentDate.DayOfWeek == DayOfWeek.Sunday) { var saturday = currentDate.AddDays(-1); // 先检查批次内是否有周六任务 var batchSaturday = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == saturday) .FirstOrDefault(); if (batchSaturday != null) { conflicts.Add( $"违反批次内周六周日连续规则 '{ruleName}': 不能在周六和周日连续安排工作 (与批次内任务 '{batchSaturday.WorkOrderCode}' 冲突)"); } else { // 检查数据库中周六是否有任务 var dbSaturday = await _workOrderRepository.Select .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => wo.WorkOrderDate.Date == saturday) .Where(wo => wo.Status == (int)WorkOrderStatusEnum.PendingAssignment || wo.Status == (int)WorkOrderStatusEnum.Assigned || wo.Status == (int)WorkOrderStatusEnum.InProgress || wo.Status == (int)WorkOrderStatusEnum.Completed) .AnyAsync(); if (dbSaturday) { conflicts.Add($"违反周六周日连续规则 '{ruleName}': 不能在周六和周日连续安排工作 (与数据库任务冲突)"); } } } } catch (Exception ex) { conflicts.Add($"周六周日连续规则验证异常: {ex.Message}"); } return conflicts; } /// /// 带批次上下文的最大连续天数规则验证(规则7:不能连续7天排班) /// 深度思考:这是最严格的连续工作限制,需要基于批次+历史数据计算真实的连续工作天数序列 /// private async Task> ValidateMaxConsecutiveDaysRuleWithBatchContext(string ruleName, long personnelId, WorkOrderEntity workOrder, int maxDays, List batchContext) { var conflicts = new List(); try { // 使用已实现的带批次上下文的连续工作天数计算方法 var consecutiveDays = await CalculateConsecutiveWorkDaysWithBatchContextAsync(personnelId, workOrder.WorkOrderDate, batchContext); if (consecutiveDays >= maxDays) { // 获取相关的批次内任务信息用于详细说明 var relatedBatchTasks = batchContext .Where(wo => wo.AssignedPersonnelId == personnelId) .Where(wo => Math.Abs((wo.WorkOrderDate.Date - workOrder.WorkOrderDate.Date).Days) <= maxDays) .Select(wo => wo.WorkOrderCode) .ToList(); var batchTasksInfo = relatedBatchTasks.Any() ? $"(涉及批次内任务: {string.Join(", ", relatedBatchTasks)})" : ""; conflicts.Add( $"违反连续排班天数限制规则 '{ruleName}': 连续工作 {consecutiveDays} 天超过限制 {maxDays} 天 {batchTasksInfo}"); } } catch (Exception ex) { conflicts.Add($"连续排班天数限制规则验证异常: {ex.Message}"); } return conflicts; } /// /// 检查时间段是否重叠 /// 修复边界情况处理:相邻时间点不应被视为重叠,支持跨天班次检查 /// 优化同一天多任务场景:增加缓冲时间机制,提升排班灵活性 /// /// 第一个时间段的开始时间 /// 第一个时间段的结束时间 /// 第二个时间段的开始时间 /// 第二个时间段的结束时间 /// 如果时间段重叠返回true,否则返回false private bool IsTimeRangeOverlap(DateTime start1, DateTime end1, DateTime start2, DateTime end2) { // 【新增】缓冲时间配置:允许班次间有30分钟缓冲,支持同一天多任务灵活调度 const int bufferMinutes = 30; // 处理跨天班次:如果结束时间小于开始时间,说明跨天 var normalizedEnd1 = end1 < start1 ? end1.AddDays(1) : end1; var normalizedEnd2 = end2 < start2 ? end2.AddDays(1) : end2; var normalizedStart1 = start1; var normalizedStart2 = end2 < start2 ? start2.AddDays(-1) : start2; // 【优化】应用缓冲时间:在时间比较时减少缓冲时间,允许合理的班次间隔 var bufferedEnd1 = normalizedEnd1.AddMinutes(-bufferMinutes); var bufferedStart1 = normalizedStart1.AddMinutes(bufferMinutes); var bufferedEnd2 = normalizedEnd2.AddMinutes(-bufferMinutes); var bufferedStart2 = normalizedStart2.AddMinutes(bufferMinutes); // 【深度业务思考】: // 1. 一班(08:00-17:30) + 二班(14:00-22:00) 原本重叠14:00-17:30(3.5小时) // 2. 应用30分钟缓冲后:一班实际到17:00,二班实际从14:30开始 // 3. 重叠时间减少为14:30-17:00(2.5小时),仍然冲突但影响降低 // 4. 如果是一班(08:00-14:00) + 二班(14:30-22:00),则完全无冲突 // 标准重叠检查:考虑缓冲时间后判断是否仍有重叠 bool hasOverlap = bufferedStart1 < normalizedEnd2 && normalizedStart2 < bufferedEnd1; // 【同一天多任务特殊处理】:如果是同一天且时间接近,允许适度重叠 if (normalizedStart1.Date == normalizedStart2.Date) { var overlapMinutes = CalculateOverlapMinutes(normalizedStart1, normalizedEnd1, normalizedStart2, normalizedEnd2); // 【业务规则】:同一天任务重叠时间小于2小时时,降低冲突等级(警告而非阻止) if (overlapMinutes > 0 && overlapMinutes <= 120) // 2小时以内的重叠 { _logger.LogWarning($"同一天任务存在 {overlapMinutes} 分钟重叠,但在可接受范围内(≤2小时)"); return false; // 不视为严重冲突,允许排班 } } return hasOverlap; } /// /// 计算两个时间段的重叠分钟数 /// 用于同一天多任务场景的精确重叠时间计算 /// /// 第一个时间段开始 /// 第一个时间段结束 /// 第二个时间段开始 /// 第二个时间段结束 /// 重叠分钟数,无重叠返回0 private double CalculateOverlapMinutes(DateTime start1, DateTime end1, DateTime start2, DateTime end2) { var overlapStart = start1 > start2 ? start1 : start2; var overlapEnd = end1 < end2 ? end1 : end2; if (overlapStart < overlapEnd) { return (overlapEnd - overlapStart).TotalMinutes; } return 0; // 无重叠 } /// /// 根据冲突详情获取错误类型 /// /// /// private ValidationErrorType GetErrorTypeFromDetail(string detail) { if (detail.Contains("请假")) return ValidationErrorType.PersonnelLeaveConflict; else if (detail.Contains("任务") && detail.Contains("冲突")) return ValidationErrorType.TaskTimeConflict; else if (detail.Contains("连续工作") || detail.Contains("工作时间")) return ValidationErrorType.ContinuousWorkTimeExceeded; else return ValidationErrorType.WorkLimitConflict; } /// /// 根据用户ID查询任务列表 /// 只返回员工相关的任务状态:已分配、进行中、已完成 /// /// 用户ID /// public async Task> GetByUserIdAsync(long userId) { var workOrders = await _workOrderRepository.Select .Where(a => a.AssignedPersonnelId == userId) .Where(a => a.Status == (int)WorkOrderStatusEnum.Assigned || a.Status == (int)WorkOrderStatusEnum.InProgress || a.Status == (int)WorkOrderStatusEnum.Completed) .OrderBy(a => a.Status == (int)WorkOrderStatusEnum.InProgress ? 0 : a.Status == (int)WorkOrderStatusEnum.Assigned ? 1 : 2) .OrderByDescending(a => a.WorkOrderDate) .OrderByDescending(a => a.Id) .ToListAsync(); if (workOrders?.Any() == true) { var workOrderIds = workOrders.Select(x => x.Id).ToList(); var flPersonnelsList = await _workOrderFlPersonnelRepository.Select .Where(a => workOrderIds.Contains(a.WorkOrderId)) .ToListAsync(); var flPersonnelsDict = flPersonnelsList .GroupBy(fp => fp.WorkOrderId) .ToDictionary( g => g.Key, g => g.Select(fp => new FLPersonnelInfo { FLPersonnelId = fp.FLPersonnelId, FLPersonnelName = fp.FLPersonnelName }).ToList() ); foreach (var item in workOrders) { if (flPersonnelsDict.TryGetValue(item.Id, out var flPersonnels)) { item.FLPersonnels = flPersonnels; } else { item.FLPersonnels = new List(); } } } return workOrders ?? new List(); } #endregion }