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

1717 lines
71 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.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;
/// <summary>
/// 工作任务服务
/// </summary>
[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<WorkOrderService> _logger;
public WorkOrderService(
WorkOrderRepository workOrderRepository,
WorkOrderFLPersonnelRepository workOrderFlPersonnelRepository,
IEmployeeLeaveService employeeLeaveService,
IPersonnelWorkLimitService personnelWorkLimitService,
IShiftService shiftService,
IShiftRuleMappingService shiftRuleMappingService,
IShiftRuleService shiftRuleService,
IShiftUnavailabilityService shiftUnavailabilityService,
ILogger<WorkOrderService> logger)
{
_workOrderRepository = workOrderRepository;
_workOrderFlPersonnelRepository = workOrderFlPersonnelRepository;
_employeeLeaveService = employeeLeaveService;
_personnelWorkLimitService = personnelWorkLimitService;
_shiftService = shiftService;
_shiftRuleMappingService = shiftRuleMappingService;
_shiftRuleService = shiftRuleService;
_shiftUnavailabilityService = shiftUnavailabilityService;
_logger = logger;
}
/// <summary>
/// 查询
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<WorkOrderGetOutput> GetAsync(long id)
{
var output = await _workOrderRepository.Select
.WhereDynamic(id)
.ToOneAsync<WorkOrderGetOutput>();
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;
}
/// <summary>
/// 查询分页
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
public async Task<PageOutput<WorkOrderGetPageOutput>> GetPageAsync(PageInput<WorkOrderGetPageInput> 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<WorkOrderGetPageOutput>();
// 为分页结果添加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<FLPersonnelInfo>();
}
}
}
var data = new PageOutput<WorkOrderGetPageOutput>()
{
List = list,
Total = total
};
return data;
}
/// <summary>
/// 添加
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<long> AddAsync(WorkOrderAddInput input)
{
return await AddSingleWorkOrderAsync(input);
}
/// <summary>
/// 批量添加
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
public async Task<BatchWorkOrderAddOutput> 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<long>();
var failedItems = new List<BatchWorkOrderError>();
// 开始批量处理
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;
}
/// <summary>
/// 修改
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
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<FLPersonnelInfo>());
}
/// <summary>
/// 删除
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task DeleteAsync(long id)
{
// 删除FL人员关联
await _workOrderFlPersonnelRepository.DeleteAsync(a => a.WorkOrderId == id);
// 删除工作任务
await _workOrderRepository.DeleteAsync(id);
}
/// <summary>
/// 软删除
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task SoftDeleteAsync(long id)
{
await _workOrderRepository.SoftDeleteAsync(id);
}
/// <summary>
/// 批量软删除
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
public async Task BatchSoftDeleteAsync(long[] ids)
{
await _workOrderRepository.SoftDeleteAsync(ids);
}
/// <summary>
/// 更新任务状态
/// </summary>
/// <param name="id"></param>
/// <param name="status"></param>
/// <returns></returns>
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);
}
}
/// <summary>
/// 开始任务
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
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);
}
}
/// <summary>
/// 完成任务
/// </summary>
/// <param name="id"></param>
/// <param name="actualWorkHours"></param>
/// <returns></returns>
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
/// <summary>
/// 添加单个工作任务(内部方法)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private async Task<long> AddSingleWorkOrderAsync(WorkOrderAddInput input)
{
var result = new WorkOrderEntity();
for (int i = 1; i <= input.RequiredPersonnel; i++)
{
var entity = Mapper.Map<WorkOrderEntity>(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;
}
/// <summary>
/// 添加工作任务FL人员关联
/// </summary>
/// <param name="workOrderId"></param>
/// <param name="flPersonnels"></param>
/// <returns></returns>
private async Task AddWorkOrderFLPersonnelsAsync(long workOrderId, List<FLPersonnelInfo> 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);
}
}
/// <summary>
/// 更新工作任务FL人员关联
/// </summary>
/// <param name="workOrderId"></param>
/// <param name="flPersonnels"></param>
private async Task UpdateWorkOrderFLPersonnelsAsync(long workOrderId, List<FLPersonnelInfo> flPersonnels)
{
// 删除现有关联
await _workOrderFlPersonnelRepository.DeleteAsync(a => a.WorkOrderId == workOrderId);
// 添加新关联
await AddWorkOrderFLPersonnelsAsync(workOrderId, flPersonnels);
}
/// <summary>
/// 转换工作任务班次信息,计算计划开始和结束时间
/// 【核心业务逻辑】:将班次的时间配置与任务日期结合,生成具体的执行时间段
/// 【深度思考】:该方法解决了班次时间模板与具体任务执行日期的映射问题,
/// 特别处理了跨天班次的复杂场景,确保时间计算的准确性
/// </summary>
/// <param name="workOrderEntity">待转换的工作任务实体</param>
/// <returns>转换后的工作任务实体,包含计算好的计划开始和结束时间</returns>
private async Task<WorkOrderEntity> 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
/// <summary>
/// 单个任务自检
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<WorkOrderValidationOutput> ValidateWorkOrderAsync(SingleWorkOrderValidationInput input)
{
var workOrder = await _workOrderRepository.GetAsync(input.WorkOrderId);
if (workOrder == null)
{
return new WorkOrderValidationOutput
{
WorkOrderId = input.WorkOrderId,
IsValid = false,
Errors = new List<WorkOrderValidationError>
{
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<WorkOrderEntity>() { 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<string> { "任务尚未分配实施人员" }
});
}
// 如果自检通过且需要更新状态为待整合
if (result.IsValid && input.UpdateStatusToPendingIntegration)
{
await UpdateStatusAsync(workOrder.Id, WorkOrderStatusEnum.PendingIntegration);
}
return result;
}
/// <summary>
/// 多个任务自检
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<MultipleWorkOrderValidationOutput> 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<NPP.SmartSchedue.Api.Contracts.Services.Work.Output.WorkOrderValidationError>
{
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<Task>();
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<long>())}");
throw new InvalidOperationException($"批量状态更新失败: {ex.Message}", ex);
}
}
return result;
}
/// <summary>
/// 带批次上下文的单任务验证(考虑本批次其他任务的影响)
/// </summary>
/// <param name="workOrder">当前任务</param>
/// <param name="batchContext">批次内其他任务</param>
/// <returns></returns>
private async Task<WorkOrderValidationOutput> ValidateWorkOrderWithBatchContextAsync(WorkOrderEntity workOrder,
List<WorkOrderEntity> 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<string> { "任务尚未分配实施人员" }
});
}
return result;
}
/// <summary>
/// 带批次上下文的人员空闲状态检查
/// </summary>
private async Task<PersonnelAvailabilityResult> CheckPersonnelAvailabilityWithBatchContextAsync(
long personnelId,
string personnelName,
WorkOrderEntity workOrder,
List<WorkOrderEntity> 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;
}
/// <summary>
/// 带批次上下文的任务时间冲突检查(包含精确的时间段重叠检查)
/// </summary>
private async Task<List<string>> CheckTaskTimeConflictsWithBatchContextAsync(long personnelId,
WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List<WorkOrderEntity> batchContext)
{
return await CheckTaskTimeConflictsWithBatchContextAsync(personnelId, workOrder, workStartTime, workEndTime,
batchContext, null);
}
/// <summary>
/// 带批次上下文和缓存优化的任务时间冲突检查(包含精确的时间段重叠检查)
/// 【性能优化】优先使用PersonnelHistoryTasks缓存避免重复数据库查询
/// </summary>
/// <param name="personnelId">人员ID</param>
/// <param name="workOrder">工作任务</param>
/// <param name="workStartTime">工作开始时间</param>
/// <param name="workEndTime">工作结束时间</param>
/// <param name="batchContext">批次上下文中的其他任务</param>
/// <param name="personnelHistoryCache">人员历史任务缓存(可选,优先使用缓存避免数据库查询)</param>
/// <returns>冲突详情列表</returns>
private async Task<List<string>> CheckTaskTimeConflictsWithBatchContextAsync(long personnelId,
WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List<WorkOrderEntity> batchContext,
Dictionary<long, List<WorkOrderHistoryItem>>? personnelHistoryCache)
{
var conflicts = new List<string>();
// 获取当前任务的班次信息
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;
}
/// <summary>
/// 带批次上下文的工作时间限制冲突检查
/// </summary>
private async Task<List<string>> CheckWorkLimitConflictsWithBatchContextAsync(long personnelId,
WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
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;
}
/// <summary>
/// 带批次上下文的班次规则冲突检查
/// </summary>
private async Task<List<string>> CheckShiftRuleConflictsWithBatchContextAsync(long personnelId,
WorkOrderEntity workOrder, DateTime workStartTime, DateTime workEndTime, List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
try
{
var shiftRules = await _shiftRuleService.GetListAsync();
foreach (var shiftRule in shiftRules)
{
if (shiftRule.IsEnabled)
{
// 根据规则类型执行不同的检查逻辑(考虑批次上下文)
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;
}
/// <summary>
/// 计算连续工作天数(考虑批次上下文)
/// </summary>
private async Task<int> CalculateConsecutiveWorkDaysWithBatchContextAsync(long personnelId, DateTime targetDate,
List<WorkOrderEntity> batchContext)
{
var consecutiveDays = 1; // 包含目标日期
// 获取批次内该人员的任务,按日期排序
var personnelBatchTasks = batchContext
.Where(wo => wo.AssignedPersonnelId == personnelId)
.OrderBy(wo => wo.WorkOrderDate.Date)
.ToList();
// 创建一个包含批次任务的日期集合
var batchWorkDates = new HashSet<DateTime>(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;
}
/// <summary>
/// 带批次上下文的班次规则验证
/// </summary>
private async Task<List<string>> ValidateShiftRuleWithBatchContextAsync(
long personnelId,
WorkOrderEntity workOrder,
ShiftRuleGetPageOutput shiftRule,
DateTime workStartTime,
DateTime workEndTime,
List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
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;
}
/// <summary>
/// 带批次上下文的班次后休息规则验证(示例实现)
/// </summary>
private async Task<List<string>> ValidateRestAfterShiftRuleWithBatchContext(string ruleName,
long personnelId, WorkOrderEntity workOrder, string shiftName, List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
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;
}
/// <summary>
/// 带批次上下文的班次连续性规则验证规则3和4一/二班不连续以及二/三班不连续)
/// 深度思考:同一人员同一天不能有冲突的班次安排,需要综合考虑批次内任务和数据库历史任务
/// </summary>
private async Task<List<string>> ValidateShiftSequenceRuleWithBatchContext(string ruleName,
long personnelId, WorkOrderEntity workOrder, string firstShiftName, string secondShiftName,
List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
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;
}
/// <summary>
/// 带批次上下文的这周日/下周六连续规则验证规则5
/// 深度思考:防止跨周连续工作,需要检查周日->下周六的7天跨度批次内可能同时包含这两个时间点的任务
/// </summary>
private async Task<List<string>> ValidateSundayToSaturdayRuleWithBatchContext(string ruleName,
long personnelId, WorkOrderEntity workOrder, List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
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;
}
/// <summary>
/// 带批次上下文的本周六/本周日连续规则验证规则6
/// 深度思考:防止周末连续工作,批次内可能同时包含同一人员的周六和周日任务
/// </summary>
private async Task<List<string>> ValidateSaturdayToSundayRuleWithBatchContext(string ruleName,
long personnelId, WorkOrderEntity workOrder, List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
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;
}
/// <summary>
/// 带批次上下文的最大连续天数规则验证规则7不能连续7天排班
/// 深度思考:这是最严格的连续工作限制,需要基于批次+历史数据计算真实的连续工作天数序列
/// </summary>
private async Task<List<string>> ValidateMaxConsecutiveDaysRuleWithBatchContext(string ruleName,
long personnelId, WorkOrderEntity workOrder, int maxDays,
List<WorkOrderEntity> batchContext)
{
var conflicts = new List<string>();
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;
}
/// <summary>
/// 检查时间段是否重叠
/// 修复边界情况处理:相邻时间点不应被视为重叠,支持跨天班次检查
/// 优化同一天多任务场景:增加缓冲时间机制,提升排班灵活性
/// </summary>
/// <param name="start1">第一个时间段的开始时间</param>
/// <param name="end1">第一个时间段的结束时间</param>
/// <param name="start2">第二个时间段的开始时间</param>
/// <param name="end2">第二个时间段的结束时间</param>
/// <returns>如果时间段重叠返回true否则返回false</returns>
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:303.5小时)
// 2. 应用30分钟缓冲后一班实际到17:00二班实际从14:30开始
// 3. 重叠时间减少为14:30-17:002.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;
}
/// <summary>
/// 计算两个时间段的重叠分钟数
/// 用于同一天多任务场景的精确重叠时间计算
/// </summary>
/// <param name="start1">第一个时间段开始</param>
/// <param name="end1">第一个时间段结束</param>
/// <param name="start2">第二个时间段开始</param>
/// <param name="end2">第二个时间段结束</param>
/// <returns>重叠分钟数无重叠返回0</returns>
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; // 无重叠
}
/// <summary>
/// 根据冲突详情获取错误类型
/// </summary>
/// <param name="detail"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 根据用户ID查询任务列表
/// 只返回员工相关的任务状态:已分配、进行中、已完成
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns></returns>
public async Task<List<WorkOrderGetOutput>> 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<WorkOrderGetOutput>();
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<FLPersonnelInfo>();
}
}
}
return workOrders ?? new List<WorkOrderGetOutput>();
}
#endregion
}