4241 lines
188 KiB
C#
4241 lines
188 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.Extensions.Logging;
|
||
using ZhonTai.Admin.Services;
|
||
using ZhonTai.DynamicApi;
|
||
using ZhonTai.DynamicApi.Attributes;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Integration;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Integration.Input;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Integration.Output;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Work;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Personnel;
|
||
using NPP.SmartSchedue.Api.Contracts.Core.Enums;
|
||
using NPP.SmartSchedue.Api.Contracts.Domain.Work;
|
||
using NPP.SmartSchedue.Api.Contracts.Domain.Personnel;
|
||
using NPP.SmartSchedue.Api.Services.Integration.Models;
|
||
using NPP.SmartSchedue.Api.Contracts.Domain.Time;
|
||
using NPP.SmartSchedue.Api.Repositories.Work;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Integration.Models;
|
||
using Microsoft.Extensions.Caching.Memory;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Time;
|
||
using SkiaSharp;
|
||
|
||
namespace NPP.SmartSchedue.Api.Services.Integration
|
||
{
|
||
/// <summary>
|
||
/// 智能人员分配服务
|
||
/// 实现基于五层决策模型的智能分配算法:资格过滤→约束评估→优化决策→结果生成→统一验证
|
||
/// 深度业务思考:支持复杂的多维度约束和智能优化,确保分配结果的合理性和公平性
|
||
/// </summary>
|
||
[DynamicApi(Area = "app")]
|
||
public partial class PersonnelAllocationService : BaseService, IPersonnelAllocationService
|
||
{
|
||
private readonly WorkOrderRepository _workOrderRepository;
|
||
private readonly IPersonService _personService;
|
||
private readonly IPersonnelWorkLimitService _personnelWorkLimitService;
|
||
private readonly IPersonnelQualificationService _personnelQualificationService;
|
||
private readonly IShiftService _shiftService;
|
||
private readonly IEmployeeLeaveService _employeeLeaveService;
|
||
private readonly ILogger<PersonnelAllocationService> _logger;
|
||
private readonly IMemoryCache _memoryCache;
|
||
|
||
// 性能监控
|
||
private readonly ActivitySource _activitySource;
|
||
|
||
// 权重配置(可配置化)
|
||
private readonly OptimizationWeights _defaultWeights = OptimizationWeights.CreateEfficiencyFirstConfiguration();
|
||
|
||
public PersonnelAllocationService(
|
||
WorkOrderRepository workOrderRepository,
|
||
IPersonService personService,
|
||
IPersonnelWorkLimitService personnelWorkLimitService,
|
||
IPersonnelQualificationService personnelQualificationService,
|
||
IShiftService shiftService,
|
||
IEmployeeLeaveService employeeLeaveService,
|
||
ILogger<PersonnelAllocationService> logger,
|
||
IMemoryCache memoryCache)
|
||
{
|
||
_workOrderRepository = workOrderRepository;
|
||
_personService = personService;
|
||
_personnelWorkLimitService = personnelWorkLimitService;
|
||
_personnelQualificationService = personnelQualificationService;
|
||
_shiftService = shiftService;
|
||
_employeeLeaveService = employeeLeaveService;
|
||
_logger = logger;
|
||
_memoryCache = memoryCache;
|
||
|
||
// 初始化性能监控
|
||
_activitySource = new ActivitySource("NPP.SmartSchedule.PersonnelAllocation");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 智能人员分配核心方法
|
||
/// 基于五层决策模型的完整实现:资格过滤→约束评估→优化决策→结果生成→验证
|
||
/// 深度业务思考:考虑所有约束条件,确保分配结果既满足业务规则又实现最优化
|
||
/// </summary>
|
||
[HttpPost]
|
||
public async Task<PersonnelAllocationResult> AllocatePersonnelSmartlyAsync(PersonnelAllocationInput input)
|
||
{
|
||
using var activity = _activitySource?.StartActivity("AllocatePersonnelSmart");
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
try
|
||
{
|
||
_logger.LogInformation("开始智能人员分配,任务数量:{TaskCount},策略:{Strategy}",
|
||
input.Tasks?.Count ?? input.TaskIds?.Count ?? 0, input.Strategy);
|
||
|
||
// 输入验证和数据准备
|
||
var validationResult = await ValidateAndPrepareInputAsync(input);
|
||
if (!validationResult.IsValid)
|
||
{
|
||
return CreateFailureResult(validationResult.ErrorMessage);
|
||
}
|
||
|
||
// 构建分配上下文
|
||
var context = await BuildAllocationContextAsync(input, validationResult.WorkOrders);
|
||
activity?.SetTag("task.count", context.WorkOrders.Count);
|
||
activity?.SetTag("personnel.pool.size", context.AvailablePersonnel.Count);
|
||
|
||
// 第一层:资格过滤处理
|
||
var qualificationResult = await ExecuteQualificationFilterAsync(context);
|
||
if (!qualificationResult.IsSuccess)
|
||
{
|
||
_logger.LogWarning("资格过滤失败:{Message}", qualificationResult.Message);
|
||
return CreatePartialFailureResult(context, "资格过滤阶段失败");
|
||
}
|
||
|
||
// 第二层:约束评估处理
|
||
var constraintResult = await ExecuteConstraintEvaluationAsync(context);
|
||
if (!constraintResult.IsSuccess)
|
||
{
|
||
_logger.LogWarning("约束评估失败:{Message}", constraintResult.Message);
|
||
return CreatePartialFailureResult(context, "约束评估阶段失败");
|
||
}
|
||
|
||
// 第三层:优化决策处理
|
||
var optimizationResult = await ExecuteOptimizationDecisionAsync(context);
|
||
if (!optimizationResult.IsSuccess)
|
||
{
|
||
_logger.LogWarning("优化决策失败:{Message}", optimizationResult.Message);
|
||
return CreatePartialFailureResult(context, "优化决策阶段失败");
|
||
}
|
||
|
||
// 第四层:结果生成处理
|
||
var generationResult = await ExecuteResultGenerationAsync(context);
|
||
if (!generationResult.IsSuccess)
|
||
{
|
||
_logger.LogError("结果生成失败:{Message}", generationResult.Message);
|
||
return CreateFailureResult("结果生成阶段失败");
|
||
}
|
||
|
||
// 第五层:最终验证和质量评估
|
||
var finalResult = await ExecuteFinalValidationAsync(context,
|
||
generationResult.ResultData as AllocationGenerationResult);
|
||
|
||
stopwatch.Stop();
|
||
activity?.SetTag("execution.time.ms", stopwatch.ElapsedMilliseconds);
|
||
activity?.SetTag("allocation.success", finalResult.IsSuccess);
|
||
|
||
_logger.LogInformation("智能人员分配完成,耗时:{ElapsedMs}ms,成功:{Success}",
|
||
stopwatch.ElapsedMilliseconds, finalResult.IsSuccess);
|
||
|
||
return finalResult;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stopwatch.Stop();
|
||
if (activity != null)
|
||
{
|
||
activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.Message);
|
||
}
|
||
|
||
_logger.LogError(ex, "智能人员分配异常,耗时:{ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
|
||
|
||
return CreateFailureResult($"系统异常:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取任务的候选人员列表(带评分和推理)
|
||
/// </summary>
|
||
public async Task<List<PersonnelCandidate>> GetCandidatesAsync(long workOrderId)
|
||
{
|
||
try
|
||
{
|
||
var workOrder = await _workOrderRepository.GetAsync(workOrderId);
|
||
if (workOrder == null)
|
||
{
|
||
_logger.LogWarning("工作任务不存在:{WorkOrderId}", workOrderId);
|
||
return new List<PersonnelCandidate>();
|
||
}
|
||
|
||
// 构建单任务分配上下文
|
||
var context = await BuildSingleTaskContextAsync(workOrder);
|
||
|
||
// 执行资格过滤和约束评估
|
||
await ExecuteQualificationFilterAsync(context);
|
||
await ExecuteConstraintEvaluationAsync(context);
|
||
await ExecuteOptimizationDecisionAsync(context);
|
||
|
||
// 返回候选人员列表
|
||
if (context.FilteredCandidates.TryGetValue(workOrderId, out var candidates))
|
||
{
|
||
return candidates.OrderByDescending(c => c.TotalScore).ToList();
|
||
}
|
||
|
||
return new List<PersonnelCandidate>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "获取候选人员异常,任务ID:{WorkOrderId}", workOrderId);
|
||
return new List<PersonnelCandidate>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量获取任务的候选人员映射(性能优化)
|
||
/// </summary>
|
||
public async Task<Dictionary<long, List<PersonnelCandidate>>> GetBatchCandidatesAsync(List<long> workOrderIds)
|
||
{
|
||
try
|
||
{
|
||
if (workOrderIds == null || !workOrderIds.Any())
|
||
{
|
||
return new Dictionary<long, List<PersonnelCandidate>>();
|
||
}
|
||
|
||
// 批量获取工作任务
|
||
var workOrders = await _workOrderRepository.Select
|
||
.Where(w => workOrderIds.Contains(w.Id))
|
||
.Include(w => w.ProcessEntity)
|
||
.ToListAsync();
|
||
|
||
if (!workOrders.Any())
|
||
{
|
||
return new Dictionary<long, List<PersonnelCandidate>>();
|
||
}
|
||
|
||
// 构建批量分配上下文
|
||
var input = new PersonnelAllocationInput
|
||
{
|
||
Tasks = workOrders,
|
||
Strategy = PersonnelAllocationStrategy.FairDistribution
|
||
};
|
||
|
||
var context = await BuildAllocationContextAsync(input, workOrders);
|
||
|
||
// 执行前三层处理
|
||
await ExecuteQualificationFilterAsync(context);
|
||
await ExecuteConstraintEvaluationAsync(context);
|
||
await ExecuteOptimizationDecisionAsync(context);
|
||
|
||
// 排序并返回结果
|
||
var result = new Dictionary<long, List<PersonnelCandidate>>();
|
||
foreach (var kvp in context.FilteredCandidates)
|
||
{
|
||
result[kvp.Key] = kvp.Value.OrderByDescending(c => c.TotalScore).ToList();
|
||
}
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "批量获取候选人员异常,任务数量:{TaskCount}", workOrderIds?.Count ?? 0);
|
||
return new Dictionary<long, List<PersonnelCandidate>>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证人员分配的可行性
|
||
/// </summary>
|
||
public async Task<PersonnelAllocationValidationResult> ValidateAllocationAsync(
|
||
PersonnelAllocationValidationInput validationInput)
|
||
{
|
||
try
|
||
{
|
||
var result = new PersonnelAllocationValidationResult();
|
||
var violations = new List<ValidationViolation>();
|
||
var warnings = new List<ValidationWarning>();
|
||
|
||
foreach (var assignment in validationInput.Assignments)
|
||
{
|
||
// 验证资质匹配
|
||
var qualificationValid =
|
||
await ValidatePersonnelQualificationAsync(assignment.WorkOrderId, assignment.PersonnelId);
|
||
if (!qualificationValid.IsValid)
|
||
{
|
||
violations.AddRange(qualificationValid.Violations);
|
||
}
|
||
|
||
// 验证时间冲突
|
||
var timeConflictValid = await ValidateTimeConflictAsync(assignment.PersonnelId,
|
||
assignment.AssignmentDate, assignment.ShiftId ?? 0);
|
||
if (!timeConflictValid.IsValid)
|
||
{
|
||
violations.AddRange(timeConflictValid.Violations);
|
||
}
|
||
|
||
// 验证工作限制
|
||
var workLimitValid =
|
||
await ValidateWorkLimitAsync(assignment.PersonnelId, assignment.AssignmentDate);
|
||
if (!workLimitValid.IsValid)
|
||
{
|
||
violations.AddRange(workLimitValid.Violations);
|
||
}
|
||
|
||
// 验证班次规则
|
||
if (validationInput.IncludeConflictDetection)
|
||
{
|
||
var shiftRuleValid = await ValidateShiftRuleAsync(assignment.PersonnelId,
|
||
assignment.AssignmentDate, assignment.ShiftId ?? 0);
|
||
if (!shiftRuleValid.IsValid)
|
||
{
|
||
violations.AddRange(shiftRuleValid.Violations);
|
||
}
|
||
}
|
||
}
|
||
|
||
result.IsValid = !violations.Any(v => v.Severity >= ViolationSeverity.Error);
|
||
result.Violations = violations;
|
||
result.Warnings = warnings;
|
||
result.PassedAssignmentCount = validationInput.Assignments.Count - result.FailedAssignmentCount;
|
||
result.FailedAssignmentCount = violations.Count(v => v.Severity >= ViolationSeverity.Error);
|
||
result.ValidationScore =
|
||
CalculateValidationScore(violations, warnings, validationInput.Assignments.Count);
|
||
result.ValidationReport = GenerateValidationReport(result);
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "人员分配验证异常");
|
||
return new PersonnelAllocationValidationResult
|
||
{
|
||
IsValid = false,
|
||
ValidationReport = $"验证异常:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取人员工作负载分析
|
||
/// </summary>
|
||
public async Task<List<PersonnelWorkloadAnalysis>> AnalyzePersonnelWorkloadAsync(List<long> personnelIds,
|
||
DateRange dateRange)
|
||
{
|
||
try
|
||
{
|
||
var result = new List<PersonnelWorkloadAnalysis>();
|
||
|
||
foreach (var personnelId in personnelIds)
|
||
{
|
||
// 获取指定时间范围内的任务分配
|
||
var assignments = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate >= dateRange.StartDate &&
|
||
w.WorkOrderDate <= dateRange.EndDate)
|
||
.ToListAsync();
|
||
|
||
// 计算工作负载
|
||
var workloadAnalysis = new PersonnelWorkloadAnalysis
|
||
{
|
||
PersonnelId = personnelId,
|
||
AssignedTaskCount = assignments.Count,
|
||
WorkDates = assignments.Select(a => a.WorkOrderDate).Distinct().ToList()
|
||
};
|
||
|
||
// 计算连续工作天数
|
||
workloadAnalysis.ContinuousWorkDays = CalculateContinuousWorkDays(workloadAnalysis.WorkDates);
|
||
|
||
// 估算总工时
|
||
workloadAnalysis.TotalEstimatedHours = assignments.Sum(a => a.EstimatedHours ?? 8.0m);
|
||
|
||
// 计算工作负载百分比(基于标准工时)
|
||
var standardHours = dateRange.Duration.Days * 8.0m; // 假设每天8小时标准工时
|
||
workloadAnalysis.WorkloadPercentage = standardHours > 0
|
||
? (double)(workloadAnalysis.TotalEstimatedHours / standardHours * 100)
|
||
: 0;
|
||
|
||
// 确定工作负载状态
|
||
workloadAnalysis.WorkloadStatus = workloadAnalysis.WorkloadPercentage switch
|
||
{
|
||
< 50 => "轻松",
|
||
< 80 => "适中",
|
||
< 100 => "繁重",
|
||
_ => "超负荷"
|
||
};
|
||
|
||
result.Add(workloadAnalysis);
|
||
}
|
||
|
||
return result.OrderByDescending(w => w.WorkloadPercentage).ToList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "人员工作负载分析异常");
|
||
return new List<PersonnelWorkloadAnalysis>();
|
||
}
|
||
}
|
||
|
||
#region 核心流程方法实现
|
||
|
||
/// <summary>
|
||
/// 验证和准备输入数据
|
||
/// 深度业务思考:确保输入数据的完整性和有效性,避免后续处理出现异常
|
||
/// </summary>
|
||
private async Task<InputValidationResult> ValidateAndPrepareInputAsync(PersonnelAllocationInput input)
|
||
{
|
||
var result = new InputValidationResult();
|
||
|
||
try
|
||
{
|
||
// 基础验证
|
||
if (input == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "输入参数不能为空";
|
||
return result;
|
||
}
|
||
|
||
// 获取工作任务
|
||
List<WorkOrderEntity> workOrders;
|
||
if (input.Tasks != null && input.Tasks.Any())
|
||
{
|
||
// 优先使用传入的Tasks对象(性能优化)
|
||
workOrders = input.Tasks;
|
||
_logger.LogInformation("使用传入的Tasks对象,任务数量:{TaskCount}", workOrders.Count);
|
||
}
|
||
else if (input.TaskIds != null && input.TaskIds.Any())
|
||
{
|
||
// 从数据库查询Tasks
|
||
workOrders = await _workOrderRepository.Select
|
||
.Where(w => input.TaskIds.Contains(w.Id))
|
||
.Include(w => w.ProcessEntity)
|
||
.ToListAsync();
|
||
|
||
if (!workOrders.Any())
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "指定的工作任务不存在";
|
||
return result;
|
||
}
|
||
|
||
_logger.LogInformation("从数据库查询Tasks,任务数量:{TaskCount}", workOrders.Count);
|
||
}
|
||
else
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "必须提供Tasks或TaskIds";
|
||
return result;
|
||
}
|
||
|
||
// 验证任务状态
|
||
var invalidTasks = workOrders.Where(w => w.Status != (int)WorkOrderStatusEnum.PendingIntegration).ToList();
|
||
if (invalidTasks.Any())
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"存在非待分配状态的任务:{string.Join(",", invalidTasks.Select(t => t.WorkOrderCode))}";
|
||
return result;
|
||
}
|
||
|
||
// 验证任务是否包含必要信息
|
||
var invalidProcessTasks = workOrders.Where(w => w.ProcessEntity == null).ToList();
|
||
if (invalidProcessTasks.Any())
|
||
{
|
||
_logger.LogWarning("存在工序信息缺失的任务:{TaskCodes}",
|
||
string.Join(",", invalidProcessTasks.Select(t => t.WorkOrderCode)));
|
||
}
|
||
|
||
result.IsValid = true;
|
||
result.WorkOrders = workOrders;
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "验证和准备输入数据异常");
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"输入验证异常:{ex.Message}";
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建分配上下文
|
||
/// 深度架构思考:集中管理分配过程中的所有状态和数据,支持复杂的多任务分配场景
|
||
/// </summary>
|
||
private async Task<AllocationContext> BuildAllocationContextAsync(PersonnelAllocationInput input,
|
||
List<WorkOrderEntity> workOrders)
|
||
{
|
||
try
|
||
{
|
||
var context = new AllocationContext
|
||
{
|
||
ContextId = Guid.NewGuid().ToString(),
|
||
CreatedAt = DateTime.UtcNow,
|
||
WorkOrders = workOrders,
|
||
Strategy = input.Strategy,
|
||
WeightConfiguration = _defaultWeights.ToDictionary()
|
||
};
|
||
|
||
// 获取可用人员池
|
||
context.AvailablePersonnel = await GetAvailablePersonnelPoolAsync();
|
||
|
||
// 过滤排除的人员
|
||
if (input.ExcludedPersonnelIds != null && input.ExcludedPersonnelIds.Any())
|
||
{
|
||
context.AvailablePersonnel = context.AvailablePersonnel
|
||
.Where(p => !input.ExcludedPersonnelIds.Contains(p.Id))
|
||
.ToList();
|
||
}
|
||
|
||
// 记录处理开始
|
||
context.ProcessingLog.Add($"构建分配上下文完成,上下文ID:{context.ContextId}");
|
||
context.ProcessingLog.Add($"任务数:{workOrders.Count},人员池大小:{context.AvailablePersonnel.Count}");
|
||
context.ProcessingLog.Add($"分配策略:{input.Strategy},排除人员:{input.ExcludedPersonnelIds?.Count ?? 0}个");
|
||
|
||
// 记录权重配置
|
||
var weightInfo = string.Join(";", context.WeightConfiguration.Select(kv => $"{kv.Key}={kv.Value:P0}"));
|
||
context.ProcessingLog.Add($"权重配置:{weightInfo}");
|
||
|
||
return context;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "构建分配上下文异常");
|
||
throw new InvalidOperationException($"构建分配上下文失败:{ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建单任务分配上下文(用于GetCandidatesAsync)
|
||
/// </summary>
|
||
private async Task<AllocationContext> BuildSingleTaskContextAsync(WorkOrderEntity workOrder)
|
||
{
|
||
try
|
||
{
|
||
var context = new AllocationContext
|
||
{
|
||
ContextId = Guid.NewGuid().ToString(),
|
||
CreatedAt = DateTime.UtcNow,
|
||
WorkOrders = new List<WorkOrderEntity> { workOrder },
|
||
Strategy = PersonnelAllocationStrategy.FairDistribution,
|
||
WeightConfiguration = _defaultWeights.ToDictionary()
|
||
};
|
||
|
||
context.AvailablePersonnel = await GetAvailablePersonnelPoolAsync();
|
||
context.ProcessingLog.Add($"构建单任务上下文完成,任务:{workOrder.WorkOrderCode}");
|
||
|
||
return context;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "构建单任务分配上下文异常,任务ID:{WorkOrderId}", workOrder.Id);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取完整人员池
|
||
/// 深度架构思考:严格遵循五层决策模型,获取全量人员池,由各决策层负责筛选
|
||
/// 业务逻辑:根据资质表获取所有人员并去重,确保决策模型的完整性和公正性
|
||
/// </summary>
|
||
private async Task<List<PersonnelEntity>> GetAvailablePersonnelPoolAsync()
|
||
{
|
||
try
|
||
{
|
||
// 获取所有人员池(从资质表关联获取并去重)
|
||
// 五层决策模型要求:人员池应该是完整的,筛选工作由各决策层完成
|
||
var personnelPoolOutputs = await _personService.GetAllPersonnelPoolAsync(includeQualifications: false);
|
||
|
||
if (personnelPoolOutputs == null || !personnelPoolOutputs.Any())
|
||
{
|
||
_logger.LogWarning("系统中无任何人员数据");
|
||
return new List<PersonnelEntity>();
|
||
}
|
||
|
||
// 转换为PersonnelEntity格式,只进行最基础的数据完整性过滤
|
||
var personnelEntities = new List<PersonnelEntity>();
|
||
foreach (var output in personnelPoolOutputs)
|
||
{
|
||
if (output != null && output.Id > 0)
|
||
{
|
||
var entity = new PersonnelEntity
|
||
{
|
||
Id = output.Id,
|
||
PersonnelCode = output.PersonnelCode,
|
||
PersonnelName = output.PersonnelName,
|
||
DepartmentId = output.DepartmentId,
|
||
DepartmentName = output.DepartmentName,
|
||
Position = output.Position,
|
||
IsActive = output.IsActive,
|
||
Contact = output.Contact,
|
||
Remarks = output.Remarks,
|
||
CreatedTime = output.CreatedTime,
|
||
ModifiedTime = output.ModifiedTime
|
||
};
|
||
personnelEntities.Add(entity);
|
||
}
|
||
}
|
||
|
||
_logger.LogInformation("获取完整人员池完成,原始数量:{Total},有效数量:{Valid}",
|
||
personnelPoolOutputs.Count, personnelEntities.Count);
|
||
|
||
_logger.LogDebug("人员池获取策略:从资质表关联获取全量人员并去重,由五层决策模型负责业务筛选");
|
||
|
||
return personnelEntities;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "获取人员池异常,将影响整个分配流程");
|
||
return new List<PersonnelEntity>();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 五层处理器方法实现
|
||
|
||
/// <summary>
|
||
/// 执行第一层:资格过滤处理
|
||
/// 🎯 架构说明:这是一个可插拔的处理器架构
|
||
/// 📌 设计理念:既要开箱即用(内置实现),又要支持企业定制(外部处理器)
|
||
/// </summary>
|
||
private async Task<ProcessorResult> ExecuteQualificationFilterAsync(AllocationContext context)
|
||
{
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
try
|
||
{
|
||
context.ProcessingLog.Add("开始资格过滤处理");
|
||
|
||
// ⚡ 内置资格过滤实现(默认实现,覆盖99%的业务场景)
|
||
// 📈 技术特点:高性能、业务完整、可靠稳定
|
||
var filteredCandidates = new Dictionary<long, List<PersonnelCandidate>>();
|
||
|
||
var totalProcessed = 0;
|
||
var totalQualified = 0;
|
||
var totalBasicEligible = 0;
|
||
|
||
foreach (var workOrder in context.WorkOrders)
|
||
{
|
||
var candidates = new List<PersonnelCandidate>();
|
||
|
||
// 获取工序资质要求
|
||
var requiredQualifications = GetRequiredQualifications(workOrder);
|
||
|
||
foreach (var personnel in context.AvailablePersonnel)
|
||
{
|
||
totalProcessed++;
|
||
|
||
// 第一层决策:基础条件筛选
|
||
// 1. 检查人员基础状态(在职、可用等)
|
||
var personnelStatusValid = await IsPersonnelBasicallyEligibleAsync(personnel);
|
||
if (!personnelStatusValid.IsEligible)
|
||
{
|
||
// 记录但不计入后续处理
|
||
_logger.LogDebug("人员 {PersonnelName}({PersonnelId}) 未通过基础资格检查:{Reasons}",
|
||
personnel.PersonnelName, personnel.Id,
|
||
string.Join(",", personnelStatusValid.EligibilityReasons));
|
||
continue;
|
||
}
|
||
|
||
totalBasicEligible++;
|
||
|
||
// 2. 检查资质匹配
|
||
var qualificationMatch =
|
||
await CheckQualificationMatchAsync(personnel.Id, requiredQualifications);
|
||
if (qualificationMatch.IsMatched)
|
||
{
|
||
var candidate = new PersonnelCandidate
|
||
{
|
||
PersonnelId = personnel.Id,
|
||
PersonnelName = personnel.PersonnelName,
|
||
QualificationScore = new QualificationScore
|
||
{
|
||
MatchScore = qualificationMatch.MatchScore,
|
||
MatchedQualifications = qualificationMatch.MatchedQualifications,
|
||
MissingQualifications = qualificationMatch.MissingQualifications,
|
||
IsQualificationValid = qualificationMatch.IsValid,
|
||
QualificationDetails = qualificationMatch.Details
|
||
}
|
||
};
|
||
|
||
candidates.Add(candidate);
|
||
totalQualified++;
|
||
}
|
||
}
|
||
|
||
filteredCandidates[workOrder.Id] = candidates;
|
||
context.ProcessingLog.Add($"任务 {workOrder.WorkOrderCode} 资格过滤完成,候选人:{candidates.Count}");
|
||
}
|
||
|
||
context.FilteredCandidates = filteredCandidates;
|
||
stopwatch.Stop();
|
||
|
||
context.ProcessingLog.Add(
|
||
$"资格过滤完成,总处理:{totalProcessed},基础合格:{totalBasicEligible},资质匹配:{totalQualified},耗时:{stopwatch.ElapsedMilliseconds}ms");
|
||
context.ProcessingLog.Add(
|
||
$"五层决策模型-第一层:从完整人员池({context.AvailablePersonnel.Count})中筛选出资质合格候选人({totalQualified})");
|
||
|
||
context.Metrics["QualificationFilter_ExecutionTime"] = stopwatch.ElapsedMilliseconds;
|
||
context.Metrics["QualificationFilter_ProcessedCount"] = totalProcessed;
|
||
context.Metrics["QualificationFilter_BasicEligibleCount"] = totalBasicEligible;
|
||
context.Metrics["QualificationFilter_QualifiedCount"] = totalQualified;
|
||
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = true,
|
||
ProcessorName = "QualificationFilter",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
SuccessCount = totalQualified,
|
||
ProcessedCount = totalProcessed,
|
||
Message = "资格过滤处理成功"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stopwatch.Stop();
|
||
_logger.LogError(ex, "资格过滤处理异常");
|
||
context.Errors.Add($"资格过滤异常:{ex.Message}");
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = false,
|
||
ProcessorName = "QualificationFilter",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
Message = $"资格过滤处理失败:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行第二层:约束评估处理
|
||
/// </summary>
|
||
private async Task<ProcessorResult> ExecuteConstraintEvaluationAsync(AllocationContext context)
|
||
{
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
try
|
||
{
|
||
context.ProcessingLog.Add("开始约束评估处理");
|
||
|
||
// ⚡ 内置约束评估实现(默认实现,包含完整的11种班次规则验证)
|
||
var constraintResults = new Dictionary<long, List<ConstraintEvaluationResult>>();
|
||
var totalEvaluated = 0;
|
||
var totalPassed = 0;
|
||
|
||
// 任务循环
|
||
foreach (var kvp in context.FilteredCandidates)
|
||
{
|
||
var workOrderId = kvp.Key;
|
||
var candidates = kvp.Value;
|
||
var workOrder = context.WorkOrders.First(w => w.Id == workOrderId);
|
||
|
||
var evaluationResults = new List<ConstraintEvaluationResult>();
|
||
|
||
// 人员循环
|
||
foreach (var candidate in candidates)
|
||
{
|
||
totalEvaluated++;
|
||
|
||
// 约束检查
|
||
var constraintCheck = await EvaluateCandidateConstraintsAsync(candidate, workOrder);
|
||
|
||
// 更新候选人约束评分
|
||
candidate.ConstraintScore = new ConstraintScore
|
||
{
|
||
ComplianceScore = constraintCheck.ConstraintScore,
|
||
TimeAvailabilityScore = constraintCheck.TimeAvailabilityScore ?? 0,
|
||
WorkLimitComplianceScore = constraintCheck.WorkLimitScore ?? 0,
|
||
ShiftRuleComplianceScore = constraintCheck.ShiftRuleScore ?? 0,
|
||
Violations = constraintCheck.Violations,
|
||
PassedConstraints = constraintCheck.PassedConstraints ?? new List<string>(),
|
||
ConstraintDetails = constraintCheck.EvaluationDetails
|
||
};
|
||
|
||
// 转换为Processors命名空间的类型
|
||
var processorsResult = new ConstraintEvaluationResult
|
||
{
|
||
Candidate = candidate,
|
||
IsValid = constraintCheck.IsValid,
|
||
ConstraintScore = constraintCheck.ConstraintScore,
|
||
Violations = constraintCheck.Violations?.Select(v => new ConstraintViolation
|
||
{
|
||
ViolationType = "General",
|
||
Description = v.Description,
|
||
Severity = ViolationSeverity.Warning
|
||
}).ToList() ?? new List<ConstraintViolation>(),
|
||
PassedConstraints = constraintCheck.PassedConstraints ?? new List<string>(),
|
||
EvaluationDetails = constraintCheck.EvaluationDetails,
|
||
RiskLevel = constraintCheck.ConstraintScore < 60 ? RiskLevel.High :
|
||
constraintCheck.ConstraintScore < 80 ? RiskLevel.Medium : RiskLevel.Low
|
||
};
|
||
|
||
evaluationResults.Add(processorsResult);
|
||
|
||
if (constraintCheck.IsValid)
|
||
{
|
||
totalPassed++;
|
||
}
|
||
}
|
||
|
||
constraintResults[workOrderId] = evaluationResults;
|
||
|
||
// 过滤掉不符合约束的候选人(约束符合度阈值60分)
|
||
context.FilteredCandidates[workOrderId] = candidates
|
||
.Where(c => c.ConstraintScore.ComplianceScore >= 60)
|
||
.ToList();
|
||
|
||
var passedCount = context.FilteredCandidates[workOrderId].Count;
|
||
context.ProcessingLog.Add(
|
||
$"任务 {workOrder.WorkOrderCode} 约束评估完成,通过:{passedCount}/{candidates.Count}");
|
||
}
|
||
|
||
context.ConstraintResults = constraintResults;
|
||
stopwatch.Stop();
|
||
|
||
context.ProcessingLog.Add(
|
||
$"约束评估完成,总评估:{totalEvaluated},通过:{totalPassed},耗时:{stopwatch.ElapsedMilliseconds}ms");
|
||
context.Metrics["ConstraintEvaluation_ExecutionTime"] = stopwatch.ElapsedMilliseconds;
|
||
context.Metrics["ConstraintEvaluation_EvaluatedCount"] = totalEvaluated;
|
||
context.Metrics["ConstraintEvaluation_PassedCount"] = totalPassed;
|
||
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = true,
|
||
ProcessorName = "ConstraintEvaluation",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
SuccessCount = totalPassed,
|
||
ProcessedCount = totalEvaluated,
|
||
Message = "约束评估处理成功"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stopwatch.Stop();
|
||
_logger.LogError(ex, "约束评估处理异常");
|
||
context.Errors.Add($"约束评估异常:{ex.Message}");
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = false,
|
||
ProcessorName = "ConstraintEvaluation",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
Message = $"约束评估处理失败:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行第三层:优化决策处理
|
||
/// </summary>
|
||
private async Task<ProcessorResult> ExecuteOptimizationDecisionAsync(AllocationContext context)
|
||
{
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
try
|
||
{
|
||
context.ProcessingLog.Add("开始优化决策处理");
|
||
|
||
// 内置优化决策实现
|
||
var optimizationResults = new Dictionary<long, List<OptimizationScoreResult>>();
|
||
var totalOptimized = 0;
|
||
|
||
foreach (var kvp in context.FilteredCandidates)
|
||
{
|
||
var workOrderId = kvp.Key;
|
||
var candidates = kvp.Value;
|
||
var workOrder = context.WorkOrders.First(w => w.Id == workOrderId);
|
||
|
||
var scoreResults = new List<OptimizationScoreResult>();
|
||
|
||
foreach (var candidate in candidates)
|
||
{
|
||
totalOptimized++;
|
||
|
||
// 计算优化评分
|
||
var optimizationScore = await CalculateOptimizationScoreAsync(candidate, workOrder, context);
|
||
|
||
// 更新候选人优化评分
|
||
candidate.OptimizationScore = new OptimizationScore
|
||
{
|
||
AssignedPersonnelScore =
|
||
optimizationScore.DimensionScores.GetValueOrDefault("AssignedPersonnel", 0),
|
||
|
||
ProjectFLScore = optimizationScore.DimensionScores.GetValueOrDefault("ProjectFL", 0),
|
||
|
||
WorkloadBalanceScore =
|
||
optimizationScore.DimensionScores.GetValueOrDefault("WorkloadBalance", 0),
|
||
|
||
ShiftContinuityScore =
|
||
optimizationScore.DimensionScores.GetValueOrDefault("ShiftContinuity", 0),
|
||
SkillMatchScore = optimizationScore.DimensionScores.GetValueOrDefault("SkillMatch", 0),
|
||
EfficiencyScore =
|
||
optimizationScore.DimensionScores.GetValueOrDefault("CollaborationHistory", 0),
|
||
OptimizationDetails = optimizationScore.OptimizationReason
|
||
};
|
||
|
||
candidate.DecisionScore = new DecisionScore
|
||
{
|
||
FinalScore = optimizationScore.OptimizationScore,
|
||
ConfidenceLevel = optimizationScore.Confidence,
|
||
RiskLevel = optimizationScore.RiskAssessment?.OverallRiskLevel ?? RiskLevel.Low,
|
||
DecisionReason = optimizationScore.OptimizationReason
|
||
};
|
||
|
||
// 计算综合总分
|
||
candidate.TotalScore = CalculateTotalScore(candidate, context.WeightConfiguration);
|
||
|
||
// 设置推荐等级和理由
|
||
candidate.RecommendationLevel = DetermineRecommendationLevel(candidate.TotalScore,
|
||
candidate.DecisionScore.ConfidenceLevel);
|
||
|
||
candidate.AllocationReason = GenerateAllocationReason(candidate);
|
||
|
||
scoreResults.Add(optimizationScore);
|
||
}
|
||
|
||
optimizationResults[workOrderId] = scoreResults;
|
||
|
||
// 对候选人按总分排序
|
||
context.FilteredCandidates[workOrderId] = candidates.OrderByDescending(c => c.TotalScore).ToList();
|
||
|
||
var bestScore = candidates.Any() ? candidates.Max(c => c.TotalScore) : 0;
|
||
context.ProcessingLog.Add($"任务 {workOrder.WorkOrderCode} 优化决策完成,最高评分:{bestScore:F1}");
|
||
}
|
||
|
||
context.OptimizationResults = optimizationResults;
|
||
stopwatch.Stop();
|
||
|
||
context.ProcessingLog.Add($"优化决策完成,总处理:{totalOptimized},耗时:{stopwatch.ElapsedMilliseconds}ms");
|
||
context.Metrics["OptimizationDecision_ExecutionTime"] = stopwatch.ElapsedMilliseconds;
|
||
context.Metrics["OptimizationDecision_OptimizedCount"] = totalOptimized;
|
||
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = true,
|
||
ProcessorName = "OptimizationDecision",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
SuccessCount = totalOptimized,
|
||
ProcessedCount = totalOptimized,
|
||
Message = "优化决策处理成功"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stopwatch.Stop();
|
||
_logger.LogError(ex, "优化决策处理异常");
|
||
context.Errors.Add($"优化决策异常:{ex.Message}");
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = false,
|
||
ProcessorName = "OptimizationDecision",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
Message = $"优化决策处理失败:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行第四层:结果生成处理
|
||
/// </summary>
|
||
private async Task<ProcessorResult> ExecuteResultGenerationAsync(AllocationContext context)
|
||
{
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
try
|
||
{
|
||
context.ProcessingLog.Add("开始结果生成处理");
|
||
|
||
// 内置结果生成实现
|
||
var allocationResult = new AllocationGenerationResult
|
||
{
|
||
SuccessfulAllocations = new Dictionary<long, PersonnelCandidate>(),
|
||
FailedAllocations = new List<FailedAllocation>(),
|
||
DetailedReport = new AllocationReport()
|
||
};
|
||
|
||
foreach (var kvp in context.FilteredCandidates)
|
||
{
|
||
var workOrderId = kvp.Key;
|
||
var candidates = kvp.Value.OrderByDescending(c => c.TotalScore).ToList();
|
||
var workOrder = context.WorkOrders.First(w => w.Id == workOrderId);
|
||
|
||
if (candidates.Any())
|
||
{
|
||
// 选择最佳候选人
|
||
var bestCandidate = candidates.First();
|
||
bestCandidate.IsOptimalChoice = true;
|
||
bestCandidate.Rank = 1;
|
||
|
||
// 为其他候选人设置排名
|
||
for (int i = 1; i < candidates.Count; i++)
|
||
{
|
||
candidates[i].Rank = i + 1;
|
||
}
|
||
|
||
allocationResult.SuccessfulAllocations[workOrderId] = bestCandidate;
|
||
context.ProcessingLog.Add(
|
||
$"任务 {workOrder.WorkOrderCode} 分配成功,选择:{bestCandidate.PersonnelName}({bestCandidate.TotalScore:F1}分)");
|
||
}
|
||
else
|
||
{
|
||
// 记录分配失败
|
||
var failedAllocation = new FailedAllocation
|
||
{
|
||
WorkOrderId = workOrderId,
|
||
FailureReason = "无符合条件的候选人员",
|
||
FailureType = AllocationFailureType.NoAvailablePersonnel,
|
||
SuggestedSolutions = GenerateFailureSuggestions(workOrder)
|
||
};
|
||
|
||
allocationResult.FailedAllocations.Add(failedAllocation);
|
||
context.ProcessingLog.Add(
|
||
$"任务 {workOrder.WorkOrderCode} 分配失败:{failedAllocation.FailureReason}");
|
||
}
|
||
}
|
||
|
||
// 计算整体质量评分和生成报告
|
||
allocationResult.OverallQualityScore = CalculateOverallQualityScore(allocationResult);
|
||
allocationResult.AllocationSummary = GenerateAllocationSummary(allocationResult, context);
|
||
allocationResult.DetailedReport = GenerateDetailedReport(allocationResult, context);
|
||
|
||
// 记录性能指标
|
||
allocationResult.PerformanceMetrics = new Dictionary<string, object>(context.Metrics);
|
||
|
||
stopwatch.Stop();
|
||
context.Metrics["ResultGeneration_ExecutionTime"] = stopwatch.ElapsedMilliseconds;
|
||
|
||
var successCount = allocationResult.SuccessfulAllocations.Count;
|
||
var failureCount = allocationResult.FailedAllocations.Count;
|
||
context.ProcessingLog.Add(
|
||
$"结果生成完成,成功:{successCount},失败:{failureCount},耗时:{stopwatch.ElapsedMilliseconds}ms");
|
||
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = true,
|
||
ProcessorName = "ResultGeneration",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
SuccessCount = successCount,
|
||
FailureCount = failureCount,
|
||
ProcessedCount = context.WorkOrders.Count,
|
||
Message = "结果生成处理成功",
|
||
ResultData = allocationResult
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stopwatch.Stop();
|
||
_logger.LogError(ex, "结果生成处理异常");
|
||
context.Errors.Add($"结果生成异常:{ex.Message}");
|
||
return new ProcessorResult
|
||
{
|
||
IsSuccess = false,
|
||
ProcessorName = "ResultGeneration",
|
||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
|
||
Message = $"结果生成处理失败:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行第五层:最终验证和质量评估
|
||
/// </summary>
|
||
private async Task<PersonnelAllocationResult> ExecuteFinalValidationAsync(AllocationContext context,
|
||
AllocationGenerationResult generationResult)
|
||
{
|
||
try
|
||
{
|
||
context.ProcessingLog.Add("开始最终验证和质量评估");
|
||
|
||
var finalResult = new PersonnelAllocationResult
|
||
{
|
||
SuccessfulMatches = new List<TaskPersonnelMatch>(),
|
||
FailedAllocations = new List<FailedPersonnelAllocation>(),
|
||
WorkloadAnalysis = new List<PersonnelWorkloadAnalysis>()
|
||
};
|
||
|
||
// 转换成功分配
|
||
foreach (var kvp in generationResult.SuccessfulAllocations)
|
||
{
|
||
var workOrder = context.WorkOrders.First(w => w.Id == kvp.Key);
|
||
var candidate = kvp.Value;
|
||
|
||
finalResult.SuccessfulMatches.Add(new TaskPersonnelMatch
|
||
{
|
||
TaskId = kvp.Key,
|
||
TaskCode = workOrder.WorkOrderCode,
|
||
PersonnelId = candidate.PersonnelId,
|
||
PersonnelName = candidate.PersonnelName,
|
||
MatchScore = (int)Math.Round(candidate.TotalScore),
|
||
MatchReason = candidate.AllocationReason ?? "综合评分最优",
|
||
QualificationMatches = candidate.QualificationScore.MatchedQualifications,
|
||
EstimatedEfficiency = CalculateEstimatedEfficiency(candidate)
|
||
});
|
||
}
|
||
|
||
// 转换失败分配
|
||
foreach (var failedAllocation in generationResult.FailedAllocations)
|
||
{
|
||
var workOrder = context.WorkOrders.First(w => w.Id == failedAllocation.WorkOrderId);
|
||
finalResult.FailedAllocations.Add(new FailedPersonnelAllocation
|
||
{
|
||
TaskId = failedAllocation.WorkOrderId,
|
||
TaskCode = workOrder.WorkOrderCode,
|
||
FailureReason = failedAllocation.FailureReason,
|
||
ConflictDetails = failedAllocation.SuggestedSolutions
|
||
});
|
||
}
|
||
|
||
// 生成工作负载分析
|
||
finalResult.WorkloadAnalysis =
|
||
await GenerateWorkloadAnalysisAsync(generationResult.SuccessfulAllocations);
|
||
|
||
// 计算公平性评分
|
||
finalResult.FairnessScore = CalculateFairnessScore(finalResult.WorkloadAnalysis);
|
||
|
||
// 设置其他属性
|
||
finalResult.AllocationSummary = generationResult.AllocationSummary;
|
||
finalResult.AllocationStrategy = context.Strategy.ToString();
|
||
finalResult.ProcessingDetails = string.Join("; ", context.ProcessingLog);
|
||
|
||
// 兼容性验证结果
|
||
finalResult.ValidationResult = new
|
||
{
|
||
OverallScore = generationResult.OverallQualityScore,
|
||
ProcessingMetrics = context.Metrics,
|
||
ValidationPassed = !context.Errors.Any(),
|
||
ProcessingTime = context.Metrics.GetValueOrDefault("Total_ExecutionTime", 0),
|
||
QualityAssessment = new
|
||
{
|
||
AverageScore = generationResult.SuccessfulAllocations.Any()
|
||
? generationResult.SuccessfulAllocations.Values.Average(c => c.TotalScore)
|
||
: 0,
|
||
SuccessRate = (double)generationResult.SuccessfulAllocations.Count / context.WorkOrders.Count *
|
||
100,
|
||
FairnessScore = finalResult.FairnessScore
|
||
}
|
||
};
|
||
|
||
context.ProcessingLog.Add(
|
||
$"最终验证完成,成功分配:{finalResult.SuccessfulMatches.Count},失败:{finalResult.FailedAllocations.Count},公平性评分:{finalResult.FairnessScore}");
|
||
|
||
return finalResult;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "最终验证异常");
|
||
return CreateFailureResult($"最终验证失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 执行第一层:资格过滤辅助方法
|
||
|
||
/// <summary>
|
||
/// 获取工序资质要求
|
||
/// </summary>
|
||
private string[] GetRequiredQualifications(WorkOrderEntity workOrder)
|
||
{
|
||
if (workOrder.ProcessEntity?.QualificationRequirements == null)
|
||
return new string[0];
|
||
|
||
return workOrder.ProcessEntity.QualificationRequirements
|
||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(q => q.Trim())
|
||
.Where(q => !string.IsNullOrEmpty(q))
|
||
.ToArray();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成分配理由
|
||
/// </summary>
|
||
private string GenerateAllocationReason(PersonnelCandidate candidate)
|
||
{
|
||
var reasons = new List<string>();
|
||
|
||
// 资质匹配
|
||
if (candidate.QualificationScore.MatchScore >= 90)
|
||
reasons.Add("资质完全匹配");
|
||
else if (candidate.QualificationScore.MatchScore >= 70)
|
||
reasons.Add("资质基本符合");
|
||
|
||
// 约束符合
|
||
if (candidate.ConstraintScore.ComplianceScore >= 90)
|
||
reasons.Add("约束条件优秀");
|
||
else if (candidate.ConstraintScore.ComplianceScore >= 70)
|
||
reasons.Add("约束条件良好");
|
||
|
||
// 优化评分
|
||
if (candidate.OptimizationScore != null)
|
||
{
|
||
if (candidate.OptimizationScore.AssignedPersonnelScore > 80)
|
||
reasons.Add("指定人员匹配");
|
||
if (candidate.OptimizationScore.ProjectFLScore > 80)
|
||
reasons.Add("项目FL人员");
|
||
if (candidate.OptimizationScore.WorkloadBalanceScore > 80)
|
||
reasons.Add("负载均衡良好");
|
||
}
|
||
|
||
// 综合评分
|
||
reasons.Add($"综合评分{candidate.TotalScore:F1}分");
|
||
|
||
return string.Join(";", reasons);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成失败建议
|
||
/// </summary>
|
||
private List<string> GenerateFailureSuggestions(WorkOrderEntity workOrder)
|
||
{
|
||
var suggestions = new List<string>();
|
||
|
||
// 基于工序特点给出建议
|
||
if (!string.IsNullOrEmpty(workOrder.ProcessEntity?.QualificationRequirements))
|
||
{
|
||
suggestions.Add("考虑放宽资质要求或安排人员培训");
|
||
}
|
||
|
||
suggestions.Add("调整任务时间安排");
|
||
suggestions.Add("增加临时人员或外包");
|
||
suggestions.Add("与其他项目协调人员调配");
|
||
|
||
return suggestions;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成详细报告
|
||
/// </summary>
|
||
private AllocationReport GenerateDetailedReport(AllocationGenerationResult result, AllocationContext context)
|
||
{
|
||
var report = new AllocationReport
|
||
{
|
||
SuccessfulAllocationCount = result.SuccessfulAllocations.Count,
|
||
FailedAllocationCount = result.FailedAllocations.Count,
|
||
SuccessRate = context.WorkOrders.Count > 0
|
||
? (double)result.SuccessfulAllocations.Count / context.WorkOrders.Count * 100
|
||
: 0
|
||
};
|
||
|
||
if (result.SuccessfulAllocations.Any())
|
||
{
|
||
report.AverageQualityScore = result.SuccessfulAllocations.Values.Average(c => c.TotalScore);
|
||
}
|
||
|
||
// 人员利用率分析
|
||
report.PersonnelUtilizationRate = result.SuccessfulAllocations.Values
|
||
.GroupBy(c => c.PersonnelId)
|
||
.ToDictionary(g => g.Key, g => (double)g.Count());
|
||
|
||
// 风险分析
|
||
report.RiskAnalysis = new RiskAnalysis
|
||
{
|
||
OverallRiskScore = result.SuccessfulAllocations.Values
|
||
.Where(c => c.DecisionScore?.RiskLevel != null)
|
||
.Average(c => (int)c.DecisionScore.RiskLevel) * 25, // 转换为百分比
|
||
|
||
RiskDistribution = result.SuccessfulAllocations.Values
|
||
.Where(c => c.DecisionScore?.RiskLevel != null)
|
||
.GroupBy(c => c.DecisionScore.RiskLevel)
|
||
.ToDictionary(g => g.Key, g => g.Count())
|
||
};
|
||
|
||
return report;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查人员基础资格(第一层决策的基础筛选)
|
||
/// 深度架构思考:在资格过滤层进行人员基础状态检查,符合五层决策模型设计
|
||
/// </summary>
|
||
private async Task<PersonnelEligibilityResult> IsPersonnelBasicallyEligibleAsync(PersonnelEntity personnel)
|
||
{
|
||
try
|
||
{
|
||
var result = new PersonnelEligibilityResult
|
||
{
|
||
PersonnelId = personnel.Id,
|
||
IsEligible = true,
|
||
EligibilityReasons = new List<string>()
|
||
};
|
||
|
||
// 1. 检查人员基础信息完整性
|
||
if (string.IsNullOrEmpty(personnel.PersonnelName))
|
||
{
|
||
result.IsEligible = false;
|
||
result.EligibilityReasons.Add("人员基础信息不完整");
|
||
return result;
|
||
}
|
||
|
||
// 通过基础资格检查
|
||
result.EligibilityReasons.Add("基础资格检查通过");
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查人员基础资格异常,人员ID:{PersonnelId}", personnel.Id);
|
||
return new PersonnelEligibilityResult
|
||
{
|
||
PersonnelId = personnel.Id,
|
||
IsEligible = false,
|
||
EligibilityReasons = new List<string> { $"基础资格检查异常:{ex.Message}" }
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查资质匹配
|
||
/// </summary>
|
||
private async Task<QualificationMatchResult> CheckQualificationMatchAsync(long personnelId,
|
||
string[] requiredQualifications)
|
||
{
|
||
try
|
||
{
|
||
if (requiredQualifications == null || !requiredQualifications.Any())
|
||
{
|
||
return new QualificationMatchResult
|
||
{
|
||
IsMatched = true,
|
||
IsValid = true,
|
||
MatchScore = 100,
|
||
MatchedQualifications = new List<string>(),
|
||
MissingQualifications = new List<string>(),
|
||
Details = "无特殊资质要求"
|
||
};
|
||
}
|
||
|
||
// 获取人员资质
|
||
var personnelQualifications = await GetPersonnelQualificationsAsync(personnelId);
|
||
var validQualificationIds = personnelQualifications
|
||
.Where(q => q.ExpiryDate == null || q.ExpiryDate > DateTime.Now)
|
||
.Select(q => q.QualificationId.ToString())
|
||
.ToList();
|
||
|
||
var matchedQualifications = requiredQualifications.Intersect(validQualificationIds).ToList();
|
||
var missingQualifications = requiredQualifications.Except(validQualificationIds).ToList();
|
||
|
||
var matchScore = requiredQualifications.Length > 0
|
||
? (double)matchedQualifications.Count / requiredQualifications.Length * 100
|
||
: 100;
|
||
|
||
return new QualificationMatchResult
|
||
{
|
||
IsMatched = matchedQualifications.Any() || !requiredQualifications.Any(),
|
||
IsValid = !missingQualifications.Any(),
|
||
MatchScore = matchScore,
|
||
MatchedQualifications = matchedQualifications,
|
||
MissingQualifications = missingQualifications,
|
||
Details =
|
||
$"匹配资质:{matchedQualifications.Count}/{requiredQualifications.Length},匹配度:{matchScore:F1}%"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查资质匹配异常,人员ID:{PersonnelId}", personnelId);
|
||
return new QualificationMatchResult
|
||
{
|
||
IsMatched = false,
|
||
IsValid = false,
|
||
MatchScore = 0,
|
||
MatchedQualifications = new List<string>(),
|
||
MissingQualifications = requiredQualifications?.ToList() ?? new List<string>(),
|
||
Details = $"资质检查异常:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取人员资质列表
|
||
/// </summary>
|
||
private async Task<List<NPP.SmartSchedue.Api.Contracts.Domain.Personnel.PersonnelQualificationEntity>>
|
||
GetPersonnelQualificationsAsync(long personnelId)
|
||
{
|
||
try
|
||
{
|
||
return await _personnelQualificationService.GetPersonnelQualificationsAsync(personnelId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "获取人员资质异常,人员ID:{PersonnelId}", personnelId);
|
||
return new List<NPP.SmartSchedue.Api.Contracts.Domain.Personnel.PersonnelQualificationEntity>();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 执行第二层:约束评估处理辅助方法
|
||
|
||
/// <summary>
|
||
/// 评估候选人约束条件
|
||
/// 深度业务思考:全面检查时间可用性、工作限制、班次规则等多维度约束
|
||
/// </summary>
|
||
private async Task<ConstraintEvaluationResult> EvaluateCandidateConstraintsAsync(PersonnelCandidate candidate,
|
||
WorkOrderEntity workOrder)
|
||
{
|
||
var result = new ConstraintEvaluationResult
|
||
{
|
||
Candidate = candidate,
|
||
IsValid = true,
|
||
PassedConstraints = new List<string>(),
|
||
Violations = new List<ConstraintViolation>()
|
||
};
|
||
|
||
try
|
||
{
|
||
var scores = new List<double>();
|
||
|
||
// 1. 时间可用性检查
|
||
var timeAvailability = await CheckTimeAvailabilityAsync(candidate.PersonnelId, workOrder.WorkOrderDate,
|
||
workOrder.ShiftId ?? 0);
|
||
|
||
result.TimeAvailabilityScore = timeAvailability.Score;
|
||
scores.Add(timeAvailability.Score);
|
||
|
||
if (timeAvailability.IsAvailable)
|
||
{
|
||
result.PassedConstraints.Add("时间可用性检查");
|
||
}
|
||
else
|
||
{
|
||
result.Violations.Add(new ConstraintViolation()
|
||
{
|
||
Severity = ViolationSeverity.High,
|
||
IsBlockingIssue = true,
|
||
CanOverrideWithApproval = false,
|
||
Description = $"时间不可用:{timeAvailability.Reason}"
|
||
});
|
||
|
||
if (timeAvailability.Score == 0)
|
||
{
|
||
result.IsValid = false;
|
||
}
|
||
}
|
||
|
||
// 2. 工作限制检查
|
||
var workLimitCheck = await CheckWorkLimitAsync(candidate.PersonnelId, workOrder.WorkOrderDate);
|
||
result.WorkLimitScore = workLimitCheck.Score;
|
||
scores.Add(workLimitCheck.Score);
|
||
|
||
if (workLimitCheck.IsCompliant)
|
||
{
|
||
result.PassedConstraints.Add("工作限制检查");
|
||
}
|
||
else
|
||
{
|
||
result.Violations.Add(new ConstraintViolation()
|
||
{
|
||
Severity = ViolationSeverity.High,
|
||
IsBlockingIssue = true,
|
||
CanOverrideWithApproval = false,
|
||
Description = $"工作限制违规:{workLimitCheck.Reason}"
|
||
});
|
||
|
||
if (timeAvailability.Score == 0)
|
||
{
|
||
result.IsValid = false;
|
||
}
|
||
|
||
if (workLimitCheck.Severity == "Critical")
|
||
{
|
||
result.IsValid = false;
|
||
}
|
||
}
|
||
|
||
// 3. 班次规则检查
|
||
var shiftRuleCheck = await CheckShiftRulesAsync(candidate.PersonnelId, workOrder.WorkOrderDate,
|
||
workOrder.ShiftId ?? 0);
|
||
|
||
result.ShiftRuleScore = shiftRuleCheck.Score;
|
||
scores.Add(shiftRuleCheck.Score);
|
||
|
||
if (shiftRuleCheck.IsCompliant)
|
||
{
|
||
result.PassedConstraints.Add("班次规则检查");
|
||
}
|
||
else
|
||
{
|
||
result.Violations.Add(new ConstraintViolation()
|
||
{
|
||
Severity = ViolationSeverity.Medium,
|
||
IsBlockingIssue = true,
|
||
CanOverrideWithApproval = false,
|
||
Description = $"班次规则违规:{shiftRuleCheck.Reason}"
|
||
});
|
||
|
||
if (shiftRuleCheck.IsCritical)
|
||
{
|
||
result.IsValid = false;
|
||
}
|
||
}
|
||
|
||
// 计算综合约束评分
|
||
result.ConstraintScore = scores.Any() ? scores.Average() : 0;
|
||
|
||
// 生成评估详情
|
||
result.EvaluationDetails = GenerateConstraintEvaluationDetails(result, scores);
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "约束评估异常,候选人:{PersonnelId},任务:{WorkOrderId}",
|
||
candidate.PersonnelId, workOrder.Id);
|
||
|
||
result.IsValid = false;
|
||
result.ConstraintScore = 0;
|
||
result.EvaluationDetails = $"约束评估异常:{ex.Message}";
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成约束评估详情
|
||
/// </summary>
|
||
private string GenerateConstraintEvaluationDetails(ConstraintEvaluationResult result, List<double> scores)
|
||
{
|
||
var details = new List<string>
|
||
{
|
||
$"约束评分:{result.ConstraintScore:F1}分",
|
||
$"通过检查:{result.PassedConstraints.Count}项",
|
||
$"违规项目:{result.Violations.Count}项"
|
||
};
|
||
|
||
if (result.TimeAvailabilityScore.HasValue)
|
||
details.Add($"时间可用性:{result.TimeAvailabilityScore.Value:F1}分");
|
||
if (result.WorkLimitScore.HasValue)
|
||
details.Add($"工作限制:{result.WorkLimitScore.Value:F1}分");
|
||
if (result.ShiftRuleScore.HasValue)
|
||
details.Add($"班次规则:{result.ShiftRuleScore.Value:F1}分");
|
||
|
||
// 评估等级
|
||
var evaluationLevel = result.ConstraintScore switch
|
||
{
|
||
>= 90 => "优秀",
|
||
>= 80 => "良好",
|
||
>= 70 => "一般",
|
||
>= 60 => "勉强",
|
||
_ => "不合格"
|
||
};
|
||
|
||
details.Add($"评估等级:{evaluationLevel}");
|
||
|
||
return string.Join(";", details);
|
||
}
|
||
|
||
#region 约束检查方法
|
||
|
||
/// <summary>
|
||
/// 检查时间可用性
|
||
/// </summary>
|
||
private async Task<TimeAvailabilityResult> CheckTimeAvailabilityAsync(long personnelId, DateTime workDate,
|
||
long shiftId)
|
||
{
|
||
try
|
||
{
|
||
// 检查是否在请假期间
|
||
var leaveInfo = await _employeeLeaveService.IsOnLeaveAsync(personnelId, workDate);
|
||
if (leaveInfo.IsOnLeave)
|
||
{
|
||
return new TimeAvailabilityResult
|
||
{
|
||
IsAvailable = false,
|
||
Score = 0,
|
||
Reason = $"人员在请假期间:{leaveInfo.LeaveType}"
|
||
};
|
||
}
|
||
|
||
// 检查是否已有其他任务分配
|
||
var existingTasks = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate.Date == workDate.Date &&
|
||
w.ShiftId == shiftId &&
|
||
w.Status > (int)WorkOrderStatusEnum.PendingReview)
|
||
.CountAsync();
|
||
|
||
if (existingTasks > 0)
|
||
{
|
||
return new TimeAvailabilityResult
|
||
{
|
||
IsAvailable = false,
|
||
Score = 0,
|
||
Reason = $"该时间段已有{existingTasks}个任务分配"
|
||
};
|
||
}
|
||
|
||
return new TimeAvailabilityResult
|
||
{
|
||
IsAvailable = true,
|
||
Score = 100,
|
||
Reason = "时间完全可用"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查时间可用性异常,人员ID:{PersonnelId},日期:{WorkDate}", personnelId, workDate);
|
||
return new TimeAvailabilityResult
|
||
{
|
||
IsAvailable = false,
|
||
Score = 0,
|
||
Reason = $"时间检查异常:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查工作限制
|
||
/// </summary>
|
||
private async Task<WorkLimitResult> CheckWorkLimitAsync(long personnelId, DateTime workDate)
|
||
{
|
||
try
|
||
{
|
||
var workLimits = await _personnelWorkLimitService.GetByPersonnelIdAsync(personnelId);
|
||
if (workLimits == null || !workLimits.Any())
|
||
{
|
||
return new WorkLimitResult
|
||
{
|
||
IsCompliant = true,
|
||
Score = 100,
|
||
Reason = "无工作限制配置",
|
||
Severity = "Info"
|
||
};
|
||
}
|
||
|
||
// 选择第一个工作限制记录
|
||
var workLimit = workLimits.First();
|
||
|
||
var violations = new List<string>();
|
||
var scores = new List<double>();
|
||
|
||
// 检查连续工作天数限制
|
||
var continuousWorkDays = await CalculateContinuousWorkDaysAsync(personnelId, workDate);
|
||
if (workLimit.MaxContinuousWorkDays.HasValue &&
|
||
continuousWorkDays >= workLimit.MaxContinuousWorkDays.Value)
|
||
{
|
||
violations.Add($"连续工作天数({continuousWorkDays}) 超过限制({workLimit.MaxContinuousWorkDays.Value})");
|
||
scores.Add(0);
|
||
}
|
||
else
|
||
{
|
||
var continuousScore = workLimit.MaxContinuousWorkDays.HasValue
|
||
? Math.Max(0, 100 - (continuousWorkDays / (double)workLimit.MaxContinuousWorkDays.Value * 100))
|
||
: 100;
|
||
scores.Add(continuousScore);
|
||
}
|
||
|
||
// 检查周班次数限制
|
||
var weekShiftCount = await CalculateWeekShiftCountAsync(personnelId, workDate);
|
||
if (workLimit.MaxShiftsPerWeek.HasValue && weekShiftCount >= workLimit.MaxShiftsPerWeek.Value)
|
||
{
|
||
violations.Add($"周班次数({weekShiftCount})超过限制({workLimit.MaxShiftsPerWeek.Value})");
|
||
scores.Add(0);
|
||
}
|
||
else
|
||
{
|
||
var weekScore = workLimit.MaxShiftsPerWeek.HasValue
|
||
? Math.Max(0, 100 - (weekShiftCount / (double)workLimit.MaxShiftsPerWeek.Value * 100))
|
||
: 100;
|
||
scores.Add(weekScore);
|
||
}
|
||
|
||
var averageScore = scores.Any() ? scores.Average() : 100;
|
||
var isCompliant = !violations.Any();
|
||
|
||
return new WorkLimitResult
|
||
{
|
||
IsCompliant = isCompliant,
|
||
Score = averageScore,
|
||
Reason = violations.Any() ? string.Join("; ", violations) : "符合所有工作限制",
|
||
Severity = violations.Any() ? "Warning" : "Info"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查工作限制异常,人员ID:{PersonnelId}", personnelId);
|
||
return new WorkLimitResult
|
||
{
|
||
IsCompliant = false,
|
||
Score = 0,
|
||
Reason = $"工作限制检查异常:{ex.Message}",
|
||
Severity = "Error"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查班次规则
|
||
/// </summary>
|
||
private async Task<ShiftRuleResult> CheckShiftRulesAsync(long personnelId, DateTime workDate, long shiftId)
|
||
{
|
||
try
|
||
{
|
||
// 获取班次规则配置
|
||
var shiftRules = await _shiftService.GetShiftRulesAsync(shiftId);
|
||
if (shiftRules == null || !shiftRules.Any())
|
||
{
|
||
return new ShiftRuleResult
|
||
{
|
||
IsCompliant = true,
|
||
Score = 100,
|
||
Reason = "无班次规则配置",
|
||
IsCritical = false
|
||
};
|
||
}
|
||
|
||
var violations = new List<string>();
|
||
var scores = new List<double>();
|
||
var criticalViolations = new List<string>();
|
||
|
||
foreach (var rule in shiftRules.Where(r => r.IsEnabled))
|
||
{
|
||
var ruleResult = await ValidateIndividualShiftRuleAsync(rule, personnelId, workDate, shiftId);
|
||
if (!ruleResult.IsValid)
|
||
{
|
||
violations.Add($"规则{rule.RuleName}:{ruleResult.ViolationMessage}");
|
||
if (ruleResult.IsCritical)
|
||
{
|
||
criticalViolations.Add(ruleResult.ViolationMessage);
|
||
}
|
||
}
|
||
|
||
scores.Add(ruleResult.ComplianceScore);
|
||
}
|
||
|
||
var averageScore = scores.Any() ? scores.Average() : 100;
|
||
var hasCriticalViolation = criticalViolations.Any();
|
||
|
||
return new ShiftRuleResult
|
||
{
|
||
IsCompliant = !violations.Any() || (!hasCriticalViolation && averageScore >= 50),
|
||
Score = averageScore,
|
||
Reason = violations.Any() ? string.Join("; ", violations) : "符合所有班次规则",
|
||
IsCritical = hasCriticalViolation
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查班次规则异常,人员ID:{PersonnelId}", personnelId);
|
||
return new ShiftRuleResult
|
||
{
|
||
IsCompliant = false,
|
||
Score = 0,
|
||
Reason = $"班次规则检查异常:{ex.Message}",
|
||
IsCritical = true
|
||
};
|
||
}
|
||
}
|
||
|
||
#region ?????
|
||
|
||
/// <summary>
|
||
/// 验证人员资质
|
||
/// </summary>
|
||
private async Task<NPP.SmartSchedue.Api.Contracts.Services.Integration.Output.ValidationResult> ValidatePersonnelQualificationAsync(long workOrderId, long personnelId)
|
||
{
|
||
try
|
||
{
|
||
var workOrder = await _workOrderRepository.GetAsync(workOrderId);
|
||
if (workOrder?.ProcessEntity?.QualificationRequirements == null)
|
||
{
|
||
return new ValidationResult
|
||
{
|
||
IsValid = true,
|
||
Violations = new List<ValidationViolation>()
|
||
};
|
||
}
|
||
|
||
var requiredQualifications = workOrder.ProcessEntity.QualificationRequirements.Split(',');
|
||
var qualificationMatch = await CheckQualificationMatchAsync(personnelId, requiredQualifications);
|
||
|
||
if (qualificationMatch.IsMatched)
|
||
{
|
||
return new ValidationResult
|
||
{
|
||
IsValid = true,
|
||
Violations = new List<ValidationViolation>()
|
||
};
|
||
}
|
||
|
||
return new ValidationResult
|
||
{
|
||
IsValid = false,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.QualificationMismatch.ToString(),
|
||
Severity = ViolationSeverity.Error,
|
||
Description = $"人员资质不匹配,缺失:{string.Join(",", qualificationMatch.MissingQualifications)}"
|
||
}
|
||
}
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "验证人员资质异常,任务:{WorkOrderId},人员:{PersonnelId}", workOrderId, personnelId);
|
||
return new ValidationResult
|
||
{
|
||
IsValid = false,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.QualificationMismatch.ToString(),
|
||
Severity = ViolationSeverity.Critical,
|
||
Description = $"资质验证异常:{ex.Message}"
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证时间冲突
|
||
/// </summary>
|
||
private async Task<NPP.SmartSchedue.Api.Contracts.Services.Integration.Output.ValidationResult> ValidateTimeConflictAsync(long personnelId, DateTime assignmentDate,
|
||
long shiftId)
|
||
{
|
||
try
|
||
{
|
||
var timeAvailability = await CheckTimeAvailabilityAsync(personnelId, assignmentDate, shiftId);
|
||
|
||
if (timeAvailability.IsAvailable)
|
||
{
|
||
return new ValidationResult
|
||
{
|
||
IsValid = true,
|
||
Violations = new List<ValidationViolation>()
|
||
};
|
||
}
|
||
|
||
return new ValidationResult
|
||
{
|
||
IsValid = false,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.TimeConflict.ToString(),
|
||
Severity = ViolationSeverity.Error,
|
||
Description = timeAvailability.Reason
|
||
}
|
||
}
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "验证时间冲突异常,人员:{PersonnelId}", personnelId);
|
||
return new ValidationResult
|
||
{
|
||
IsValid = false,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.TimeConflict.ToString(),
|
||
Severity = ViolationSeverity.Critical,
|
||
Description = $"时间冲突验证异常:{ex.Message}"
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证工作限制
|
||
/// </summary>
|
||
private async Task<NPP.SmartSchedue.Api.Contracts.Services.Integration.Output.ValidationResult> ValidateWorkLimitAsync(long personnelId, DateTime assignmentDate)
|
||
{
|
||
try
|
||
{
|
||
var workLimitResult = await CheckWorkLimitAsync(personnelId, assignmentDate);
|
||
|
||
if (workLimitResult.IsCompliant)
|
||
{
|
||
return new ValidationResult
|
||
{
|
||
IsValid = true,
|
||
Violations = new List<ValidationViolation>()
|
||
};
|
||
}
|
||
|
||
var severity = workLimitResult.Severity switch
|
||
{
|
||
"Critical" => ViolationSeverity.Critical,
|
||
"Error" => ViolationSeverity.Error,
|
||
"Warning" => ViolationSeverity.Warning,
|
||
_ => ViolationSeverity.Info
|
||
};
|
||
|
||
return new ValidationResult
|
||
{
|
||
IsValid = severity < ViolationSeverity.Error,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.WorkloadExceeded.ToString(),
|
||
Severity = severity,
|
||
Description = workLimitResult.Reason
|
||
}
|
||
}
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "验证工作限制异常,人员:{PersonnelId}", personnelId);
|
||
return new ValidationResult
|
||
{
|
||
IsValid = false,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.WorkloadExceeded.ToString(),
|
||
Severity = ViolationSeverity.Critical,
|
||
Description = $"工作限制验证异常:{ex.Message}"
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证班次规则
|
||
/// </summary>
|
||
private async Task<NPP.SmartSchedue.Api.Contracts.Services.Integration.Output.ValidationResult> ValidateShiftRuleAsync(long personnelId, DateTime assignmentDate,
|
||
long shiftId)
|
||
{
|
||
try
|
||
{
|
||
var shiftRuleResult = await CheckShiftRulesAsync(personnelId, assignmentDate, shiftId);
|
||
|
||
if (shiftRuleResult.IsCompliant)
|
||
{
|
||
return new ValidationResult
|
||
{
|
||
IsValid = true,
|
||
Violations = new List<ValidationViolation>()
|
||
};
|
||
}
|
||
|
||
var severity = shiftRuleResult.IsCritical ? ViolationSeverity.Critical : ViolationSeverity.Warning;
|
||
|
||
return new ValidationResult
|
||
{
|
||
IsValid = !shiftRuleResult.IsCritical,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.ShiftRuleViolation.ToString(),
|
||
Severity = severity,
|
||
Description = shiftRuleResult.Reason
|
||
}
|
||
}
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "验证班次规则异常,人员:{PersonnelId}", personnelId);
|
||
return new ValidationResult
|
||
{
|
||
IsValid = false,
|
||
Violations = new List<ValidationViolation>
|
||
{
|
||
new ValidationViolation
|
||
{
|
||
ViolationType = ViolationType.ShiftRuleViolation.ToString(),
|
||
Severity = ViolationSeverity.Critical,
|
||
Description = $"班次规则验证异常:{ex.Message}"
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
|
||
#region 检查班次规则实现
|
||
|
||
/// <summary>
|
||
/// 验证个人班次规则
|
||
/// 深度业务思考:基于规则类型、参数和时间有效性进行全面验证
|
||
/// </summary>
|
||
private async Task<RuleValidationResult> ValidateIndividualShiftRuleAsync(ShiftRuleEntity rule,
|
||
long personnelId, DateTime workDate, long shiftId)
|
||
{
|
||
try
|
||
{
|
||
if (!rule.IsEnabled)
|
||
{
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true, // 规则不生效,视为通过
|
||
ComplianceScore = 100.0,
|
||
IsCritical = false,
|
||
ViolationMessage = $"规则'{rule.RuleName}'在此时间段不生效"
|
||
};
|
||
}
|
||
|
||
// 根据规则类型进行详细验证
|
||
var result = rule.RuleType switch
|
||
{
|
||
"1" => await ValidateAssignedPersonnelPriorityRuleAsync(personnelId, workDate), // 指定人员优先
|
||
"2" => await ValidateWeeklyTaskLimitRuleAsync(personnelId, workDate),
|
||
"3" => await ValidateShiftContinuityRuleAsync(personnelId, workDate, shiftId, 3), // 1->2班
|
||
"4" => await ValidateShiftContinuityRuleAsync(personnelId, workDate, shiftId, 4), // 2->3班
|
||
"5" => await ValidateCrossWeekendContinuityRuleAsync(personnelId, workDate, shiftId, 5), // 不能这周日/下周六连
|
||
"6" => await ValidateCrossWeekendContinuityRuleAsync(personnelId, workDate, shiftId, 6), // 不能本周六/本周日连
|
||
"7" => await ValidateContinuousWorkDaysRuleAsync(personnelId, workDate, new Dictionary<string, object>()), // 连续工作天数
|
||
"8" => await ValidateNextDayRestRuleAsync(personnelId, workDate, shiftId, 8), // 三班后一天不排班
|
||
"9" => await ValidateNextDayRestRuleAsync(personnelId, workDate, shiftId, 9), // 二班后一天不排班
|
||
"10" => await ValidateProjectFLPriorityRuleAsync(personnelId, workDate, shiftId, rule), // 优先分配本项目FL
|
||
|
||
_ => await ValidateDefaultRuleAsync(personnelId, workDate, rule, new Dictionary<string, object>()) // 默认规则验证
|
||
};
|
||
|
||
// 记录规则验证详情
|
||
_logger.LogDebug("规则验证完成 - 规则:{RuleName}({RuleType}),人员:{PersonnelId},结果:{IsValid},评分:{Score}",
|
||
rule.RuleName, rule.RuleType, personnelId, result.IsValid, result.ComplianceScore);
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "验证班次规则异常,规则:{RuleName}({RuleType}),人员:{PersonnelId}",
|
||
rule.RuleName, rule.RuleType, personnelId);
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = false,
|
||
ComplianceScore = 0,
|
||
IsCritical = true,
|
||
ViolationMessage = $"规则'{rule.RuleName}'验证异常:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
#region 规则验证方法
|
||
/// <summary>
|
||
/// 验证指定人员优先规则
|
||
/// </summary>
|
||
private async Task<RuleValidationResult> ValidateAssignedPersonnelPriorityRuleAsync(long personnelId,
|
||
DateTime workDate)
|
||
{
|
||
// 检查是否存在指定人员分配
|
||
var hasAssignedTask = await _workOrderRepository.Select
|
||
.AnyAsync(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate.Date == workDate.Date);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true, // 这个规则通常不会阻断,只是影响评分
|
||
ComplianceScore = hasAssignedTask ? 100.0 : 80.0,
|
||
IsCritical = false,
|
||
ViolationMessage = hasAssignedTask ? "" : "非指定人员,优先级稍低"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证周任务限制规则
|
||
/// </summary>
|
||
private async Task<RuleValidationResult> ValidateWeeklyTaskLimitRuleAsync(long personnelId, DateTime workDate)
|
||
{
|
||
var maxWeeklyShifts = 6;
|
||
var weekShiftCount = await CalculateWeekShiftCountAsync(personnelId, workDate);
|
||
|
||
var isValid = weekShiftCount <= maxWeeklyShifts;
|
||
var complianceScore = isValid ? Math.Max(0, 100 - (weekShiftCount / (double)maxWeeklyShifts * 100)) : 0;
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = isValid,
|
||
ComplianceScore = complianceScore,
|
||
IsCritical = !isValid,
|
||
ViolationMessage = isValid ? "" : $"周班次数({weekShiftCount})超过限制({maxWeeklyShifts})"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证同一天内班次连续性规则
|
||
/// 深度业务思考:检查同一天内是否存在不合理的班次连续安排
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="workDate">当前工作日期</param>
|
||
/// <param name="shiftId">当前班次ID</param>
|
||
/// <param name="ruleType">规则类型(3:同天早中班连续性,4:同天中晚班连续性)</param>
|
||
/// <returns>规则验证结果,包含合规状态、评分和详细说明</returns>
|
||
private async Task<RuleValidationResult> ValidateShiftContinuityRuleAsync(long personnelId, DateTime workDate,
|
||
long shiftId, int ruleType)
|
||
{
|
||
try
|
||
{
|
||
// 第一步:检查当天是否已有相同班次的任务分配(防重复分配)
|
||
var hasDuplicateShift = await CheckDuplicateShiftOnSameDateAsync(personnelId, workDate, shiftId);
|
||
if (hasDuplicateShift)
|
||
{
|
||
_logger.LogWarning("班次重复分配违规 - 人员ID:{PersonnelId}, 日期:{Date}, 班次ID:{ShiftId}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = false,
|
||
ComplianceScore = 0,
|
||
IsCritical = true,
|
||
ViolationMessage = "该人员当天已有相同班次的任务分配,不能重复分配"
|
||
};
|
||
}
|
||
|
||
// 第二步:获取当前要分配班次的编号
|
||
var currentShiftNumber = await GetShiftNumberByIdAsync(shiftId);
|
||
if (!currentShiftNumber.HasValue)
|
||
{
|
||
_logger.LogWarning("无法获取班次编号 - 班次ID:{ShiftId}", shiftId);
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = false,
|
||
ComplianceScore = 0,
|
||
IsCritical = true,
|
||
ViolationMessage = $"无法获取班次ID {shiftId} 对应的班次编号"
|
||
};
|
||
}
|
||
|
||
// 第三步:获取当天已分配的所有班次编号
|
||
var todayShiftNumbers = await GetPersonnelAllShiftNumbersOnDateAsync(personnelId, workDate);
|
||
|
||
// 如果当天无其他班次记录,直接通过验证
|
||
if (todayShiftNumbers == null || !todayShiftNumbers.Any())
|
||
{
|
||
_logger.LogDebug("同天班次连续性验证:当天无其他班次记录,通过验证 - 人员ID:{PersonnelId}, 当前班次编号:{ShiftNumber}",
|
||
personnelId, currentShiftNumber.Value);
|
||
return CreateValidResult("当天无其他班次记录,通过连续性验证", 100);
|
||
}
|
||
|
||
// 第四步:根据规则类型检查同一天内班次连续性违规情况
|
||
bool hasViolation = ruleType switch
|
||
{
|
||
3 => todayShiftNumbers.Contains(1) && currentShiftNumber == 2, // 同一天禁止早班(1)和中班(2)同时存在
|
||
4 => todayShiftNumbers.Contains(2) && currentShiftNumber == 3, // 同一天禁止中班(2)和晚班(3)同时存在
|
||
_ => false // 未定义的规则类型默认不违规
|
||
};
|
||
|
||
// 第五步:生成详细的验证结果
|
||
if (hasViolation)
|
||
{
|
||
var existingShift = todayShiftNumbers.First(s => (ruleType == 3 && s == 1) || (ruleType == 4 && s == 2));
|
||
var violationDetail = GenerateSameDayViolationDetail(ruleType, existingShift, currentShiftNumber.Value);
|
||
_logger.LogWarning("同天班次连续性规则违规 - 人员ID:{PersonnelId}, {ViolationDetail}",
|
||
personnelId, violationDetail);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = false,
|
||
ComplianceScore = 0,
|
||
IsCritical = true, // 同天连续班次违规是关键风险
|
||
ViolationMessage = violationDetail
|
||
};
|
||
}
|
||
else
|
||
{
|
||
var successMessage = GenerateSameDaySuccessMessage(ruleType, currentShiftNumber.Value);
|
||
_logger.LogDebug("同天班次连续性验证通过 - 人员ID:{PersonnelId}, {SuccessMessage}",
|
||
personnelId, successMessage);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true,
|
||
ComplianceScore = 100,
|
||
IsCritical = false,
|
||
ViolationMessage = ""
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 记录异常并返回保守结果
|
||
_logger.LogError(ex,
|
||
"验证班次连续性规则异常 - 人员ID:{PersonnelId}, 工作日期:{WorkDate}, 班次ID:{ShiftId}, 规则类型:{RuleType}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId, ruleType);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true, // 异常情况下采用保守策略,不阻断分配
|
||
ComplianceScore = 60, // 给予中等评分
|
||
IsCritical = false,
|
||
ViolationMessage = $"验证异常,建议人工审核:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证连续工作天数规则
|
||
/// </summary>
|
||
private async Task<RuleValidationResult> ValidateContinuousWorkDaysRuleAsync(long personnelId,
|
||
DateTime workDate, Dictionary<string, object> ruleParams)
|
||
{
|
||
var maxContinuousDays = 7;
|
||
var continuousDays = await CalculateContinuousWorkDaysAsync(personnelId, workDate);
|
||
|
||
var isValid = continuousDays <= maxContinuousDays;
|
||
var complianceScore = isValid ? Math.Max(0, 100 - (continuousDays / (double)maxContinuousDays * 100)) : 0;
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = isValid,
|
||
ComplianceScore = complianceScore,
|
||
IsCritical = !isValid,
|
||
ViolationMessage = isValid ? "" : $"连续工作天数({continuousDays})超过限制({maxContinuousDays})"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证跨周末班次连续性规则
|
||
/// 深度业务思考:检查跨周末时期是否存在违反休息间隔的班次连续安排
|
||
/// 规则5:不能这周日/下周六连续工作
|
||
/// 规则6:不能本周六/本周日连续工作
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="workDate">当前工作日期</param>
|
||
/// <param name="shiftId">当前班次ID</param>
|
||
/// <param name="ruleType">规则类型(5:周日/下周六禁止连续,6:周六/周日禁止连续)</param>
|
||
/// <returns>规则验证结果,包含合规状态、评分和详细说明</returns>
|
||
private async Task<RuleValidationResult> ValidateCrossWeekendContinuityRuleAsync(long personnelId, DateTime workDate,
|
||
long shiftId, int ruleType)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始验证跨周末班次连续性规则 - 人员ID:{PersonnelId}, 工作日期:{WorkDate}, 规则类型:{RuleType}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), ruleType);
|
||
|
||
// 第一步:计算相关的日期
|
||
var (firstDate, secondDate) = CalculateWeekendDates(workDate, ruleType);
|
||
|
||
_logger.LogDebug("跨周末日期计算完成 - 第一日期:{FirstDate}, 第二日期:{SecondDate}",
|
||
firstDate.ToString("yyyy-MM-dd"), secondDate.ToString("yyyy-MM-dd"));
|
||
|
||
// 第二步:检查当前工作日期是否匹配需要验证的日期
|
||
bool isTargetDate = workDate.Date == firstDate.Date || workDate.Date == secondDate.Date;
|
||
if (!isTargetDate)
|
||
{
|
||
// 如果当前日期不是目标验证日期,直接通过验证
|
||
_logger.LogDebug("当前日期不在验证范围内,跳过验证 - 当前日期:{CurrentDate}", workDate.ToString("yyyy-MM-dd"));
|
||
return CreateValidResult("非目标验证日期,通过验证", 100);
|
||
}
|
||
|
||
// 第三步:获取两个日期的班次分配情况
|
||
var firstDateShifts = await GetPersonnelAllShiftNumbersOnDateAsync(personnelId, firstDate);
|
||
var secondDateShifts = await GetPersonnelAllShiftNumbersOnDateAsync(personnelId, secondDate);
|
||
|
||
// 如果当前要分配的是第二个日期,需要将当前班次加入到检查中
|
||
if (workDate.Date == secondDate.Date)
|
||
{
|
||
var currentShiftNumber = await GetShiftNumberByIdAsync(shiftId);
|
||
if (currentShiftNumber.HasValue && !secondDateShifts.Contains(currentShiftNumber.Value))
|
||
{
|
||
secondDateShifts.Add(currentShiftNumber.Value);
|
||
}
|
||
}
|
||
// 如果当前要分配的是第一个日期,需要将当前班次加入到检查中
|
||
else if (workDate.Date == firstDate.Date)
|
||
{
|
||
var currentShiftNumber = await GetShiftNumberByIdAsync(shiftId);
|
||
if (currentShiftNumber.HasValue && !firstDateShifts.Contains(currentShiftNumber.Value))
|
||
{
|
||
firstDateShifts.Add(currentShiftNumber.Value);
|
||
}
|
||
}
|
||
|
||
// 第四步:检查是否存在跨周末连续性违规
|
||
bool hasViolation = firstDateShifts.Any() && secondDateShifts.Any();
|
||
|
||
// 第五步:生成验证结果
|
||
if (hasViolation)
|
||
{
|
||
var violationDetail = GenerateCrossWeekendViolationDetail(ruleType, firstDate, secondDate,
|
||
firstDateShifts, secondDateShifts);
|
||
|
||
_logger.LogWarning("跨周末班次连续性规则违规 - 人员ID:{PersonnelId}, {ViolationDetail}",
|
||
personnelId, violationDetail);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = false,
|
||
ComplianceScore = 0,
|
||
IsCritical = true, // 跨周末连续工作是关键风险
|
||
ViolationMessage = violationDetail
|
||
};
|
||
}
|
||
else
|
||
{
|
||
var successMessage = GenerateCrossWeekendSuccessMessage(ruleType, firstDate, secondDate);
|
||
_logger.LogDebug("跨周末班次连续性验证通过 - 人员ID:{PersonnelId}, {SuccessMessage}",
|
||
personnelId, successMessage);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true,
|
||
ComplianceScore = 100,
|
||
IsCritical = false,
|
||
ViolationMessage = ""
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 记录异常并返回保守结果
|
||
_logger.LogError(ex,
|
||
"验证跨周末班次连续性规则异常 - 人员ID:{PersonnelId}, 工作日期:{WorkDate}, 班次ID:{ShiftId}, 规则类型:{RuleType}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId, ruleType);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true, // 异常情况下采用保守策略,不阻断分配
|
||
ComplianceScore = 60, // 给予中等评分
|
||
IsCritical = false,
|
||
ViolationMessage = $"验证异常,建议人工审核:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证项目FL优先分配规则
|
||
/// 【深度业务思考】:确保项目专业性和工作效率的关键规则
|
||
/// 业务背景:
|
||
/// - FL(Front Line,一线人员)是项目的核心执行者
|
||
/// - 本项目FL具有专业知识和操作经验优势
|
||
/// - 优先分配本项目FL有助于提高工作质量和效率
|
||
/// - 降低项目间人员切换的学习成本和沟通成本
|
||
///
|
||
/// 规则10业务逻辑:
|
||
/// - 检查人员是否为当前工作任务所属项目的FL成员
|
||
/// - 本项目FL获得优先分配机会(高评分)
|
||
/// - 非本项目FL仍可分配但评分较低
|
||
/// - 确保项目专业性与人员灵活调配的平衡
|
||
///
|
||
/// 【边界情况深度分析】:
|
||
/// 1. 跨项目FL:人员同时是多个项目的FL,按关联度评分
|
||
/// 2. 项目FL不足:当本项目FL都不可用时,允许跨项目分配
|
||
/// 3. 新项目场景:项目初期可能还没有指定FL,采用宽松策略
|
||
/// 4. FL资质变更:人员FL资格变更后的历史数据处理
|
||
/// 5. 项目暂停/结束:项目状态变更对FL优先级的影响
|
||
/// 6. 紧急任务:紧急情况下可适当放宽FL限制
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="workDate">工作日期</param>
|
||
/// <param name="shiftId">班次ID</param>
|
||
/// <param name="rule">规则实体(包含规则参数配置)</param>
|
||
/// <returns>规则验证结果,包含优先级评分和详细说明</returns>
|
||
private async Task<RuleValidationResult> ValidateProjectFLPriorityRuleAsync(long personnelId, DateTime workDate,
|
||
long shiftId, ShiftRuleEntity rule)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始验证项目FL优先分配规则 - 人员ID:{PersonnelId}, 工作日期:{WorkDate}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"));
|
||
|
||
// 第一步:获取当前分配上下文中的工作任务信息
|
||
// 【重要说明】:由于此方法是规则验证,我们需要从上下文中获取当前要分配的任务信息
|
||
var currentWorkOrder = await GetCurrentWorkOrderFromContextAsync(personnelId, workDate, shiftId);
|
||
if (currentWorkOrder == null)
|
||
{
|
||
_logger.LogWarning("无法获取当前工作任务上下文 - 人员ID:{PersonnelId}, 日期:{WorkDate}, 班次:{ShiftId}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId);
|
||
return CreateValidResult("无法确定工作任务上下文,采用中性评分", 85);
|
||
}
|
||
|
||
// 第二步:检查人员是否为当前项目的FL成员
|
||
var isProjectFL = await CheckPersonnelIsProjectFLAsync(personnelId, currentWorkOrder.ProjectNumber);
|
||
|
||
// 第三步:查询人员的其他项目FL关联情况(用于综合评分)
|
||
var allProjectFLs = await GetPersonnelAllProjectFLsAsync(personnelId);
|
||
|
||
// 第四步:根据FL关联情况进行综合评分
|
||
var scoringResult = CalculateProjectFLPriorityScore(personnelId, currentWorkOrder.ProjectNumber,
|
||
isProjectFL, allProjectFLs, rule);
|
||
|
||
// 第五步:生成详细的验证结果
|
||
var resultMessage = GenerateProjectFLValidationMessage(personnelId, currentWorkOrder.ProjectNumber,
|
||
isProjectFL, scoringResult.Score, scoringResult.Reason);
|
||
|
||
_logger.LogDebug("项目FL优先规则验证完成 - 人员ID:{PersonnelId}, 项目:{ProjectNumber}, " +
|
||
"是否本项目FL:{IsProjectFL}, 评分:{Score}",
|
||
personnelId, currentWorkOrder.ProjectNumber, isProjectFL, scoringResult.Score);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true, // 此规则主要影响优先级,不阻断分配
|
||
ComplianceScore = scoringResult.Score,
|
||
IsCritical = false,
|
||
ViolationMessage = resultMessage
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 【异常处理策略】:记录详细异常信息并返回保守结果
|
||
_logger.LogError(ex,
|
||
"验证项目FL优先规则异常 - 人员ID:{PersonnelId}, 工作日期:{WorkDate}, 班次ID:{ShiftId}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId);
|
||
|
||
// 【保守策略】:异常情况下给予中等评分,不阻断分配流程
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true,
|
||
ComplianceScore = 80, // 给予中等偏上评分
|
||
IsCritical = false,
|
||
ViolationMessage = $"项目FL优先规则验证异常,建议人工审核:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 默认规则验证
|
||
/// </summary>
|
||
private async Task<RuleValidationResult> ValidateDefaultRuleAsync(long personnelId, DateTime workDate,
|
||
ShiftRuleEntity rule, Dictionary<string, object> ruleParams)
|
||
{
|
||
// 对于未识别的规则类型,进行基础验证
|
||
_logger.LogWarning("未识别的规则类型:{RuleType},规则名称:{RuleName}", rule.RuleType, rule.RuleName);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true,
|
||
ComplianceScore = 90, // 给予较高分,但不是满分
|
||
IsCritical = false,
|
||
ViolationMessage = $"未识别的规则类型({rule.RuleType}),采用默认验证"
|
||
};
|
||
}
|
||
|
||
#region 十一种规则辅助方法
|
||
|
||
/// <summary>
|
||
/// 检查指定人员在同一天是否已有相同班次的任务分配
|
||
/// 业务逻辑:防止同一人员在同一天被分配多个相同班次的任务
|
||
/// 深度思考:考虑任务状态、班次类型、数据一致性等因素
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="workDate">工作日期</param>
|
||
/// <param name="shiftId">班次ID</param>
|
||
/// <returns>true表示存在重复班次分配,false表示无重复</returns>
|
||
private async Task<bool> CheckDuplicateShiftOnSameDateAsync(long personnelId, DateTime workDate, long shiftId)
|
||
{
|
||
try
|
||
{
|
||
// 查询该人员在指定日期是否已有相同班次的任务分配
|
||
// 只考虑非取消和非拒绝状态的任务
|
||
var existingAssignment = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate.Date == workDate.Date &&
|
||
w.ShiftId == shiftId &&
|
||
w.Status > (int)WorkOrderStatusEnum.PendingReview)
|
||
.FirstAsync();
|
||
|
||
var hasDuplicate = existingAssignment != null;
|
||
|
||
if (hasDuplicate)
|
||
{
|
||
_logger.LogWarning("发现重复班次分配 - 人员ID:{PersonnelId}, 日期:{Date}, 班次ID:{ShiftId}, 已存在任务ID:{ExistingTaskId}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId, existingAssignment.Id);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("无重复班次分配 - 人员ID:{PersonnelId}, 日期:{Date}, 班次ID:{ShiftId}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId);
|
||
}
|
||
|
||
return hasDuplicate;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查重复班次分配异常 - 人员ID:{PersonnelId}, 日期:{Date}, 班次ID:{ShiftId}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId);
|
||
// 异常情况下采用保守策略,假设无重复
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 线程安全的班次编号缓存锁 - 与GlobalPersonnelAllocationService共享
|
||
/// 【并发安全】:防止多个服务同时访问ShiftService导致DbContext冲突
|
||
/// </summary>
|
||
private static readonly SemaphoreSlim _shiftCacheLock = new SemaphoreSlim(1, 1);
|
||
|
||
/// <summary>
|
||
/// 班次编号缓存 - 与GlobalPersonnelAllocationService共享缓存
|
||
/// 【性能+安全】:跨服务共享班次缓存,避免重复查询和并发冲突
|
||
/// </summary>
|
||
private static readonly Dictionary<long, int?> _shiftNumberCache = new Dictionary<long, int?>();
|
||
|
||
/// <summary>
|
||
/// 根据班次ID获取班次编号 - 线程安全版本
|
||
/// 业务逻辑:将班次ID转换为班次编号,用于班次连续性规则计算
|
||
/// 【关键修复】:使用SemaphoreSlim确保线程安全,避免FreeSql DbContext并发冲突
|
||
/// 深度思考:处理班次不存在、数据异常、并发访问等所有边界情况
|
||
/// </summary>
|
||
/// <param name="shiftId">班次ID</param>
|
||
/// <returns>班次编号(1,2,3等),null表示班次不存在或异常</returns>
|
||
private async Task<int?> GetShiftNumberByIdAsync(long shiftId)
|
||
{
|
||
// 【性能优化】:首先检查缓存,避免不必要的锁等待
|
||
if (_shiftNumberCache.TryGetValue(shiftId, out var cachedNumber))
|
||
{
|
||
_logger.LogTrace("【班次缓存命中】班次ID:{ShiftId} -> 班次编号:{ShiftNumber}", shiftId, cachedNumber);
|
||
return cachedNumber;
|
||
}
|
||
|
||
// 【并发安全】:使用信号量确保同一时间只有一个线程访问数据库
|
||
await _shiftCacheLock.WaitAsync();
|
||
try
|
||
{
|
||
// 【二次检查】:在获取锁后再次检查缓存,可能其他线程已经加载
|
||
if (_shiftNumberCache.TryGetValue(shiftId, out var doubleCheckedNumber))
|
||
{
|
||
_logger.LogTrace("【班次缓存二次命中】班次ID:{ShiftId} -> 班次编号:{ShiftNumber}", shiftId, doubleCheckedNumber);
|
||
return doubleCheckedNumber;
|
||
}
|
||
|
||
// 【数据库查询】:在锁保护下进行数据库访问
|
||
_logger.LogDebug("【线程安全班次查询】开始查询班次ID:{ShiftId}的编号信息", shiftId);
|
||
var shift = await _shiftService.GetAsync(shiftId);
|
||
|
||
int? shiftNumber = null;
|
||
if (shift != null)
|
||
{
|
||
shiftNumber = shift.ShiftNumber;
|
||
_logger.LogDebug("获取班次编号成功 - 班次ID:{ShiftId}, 班次编号:{ShiftNumber}",
|
||
shiftId, shift.ShiftNumber);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("班次不存在 - 班次ID:{ShiftId}", shiftId);
|
||
}
|
||
|
||
// 【缓存更新】:将结果存入缓存供后续使用(包括null值)
|
||
_shiftNumberCache[shiftId] = shiftNumber;
|
||
|
||
return shiftNumber;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "【并发安全修复】获取班次编号异常 - 班次ID:{ShiftId},使用线程安全机制重试", shiftId);
|
||
|
||
// 【错误恢复】:异常情况下缓存null值,避免重复失败查询
|
||
_shiftNumberCache[shiftId] = null;
|
||
return null;
|
||
}
|
||
finally
|
||
{
|
||
// 【资源释放】:确保信号量被正确释放
|
||
_shiftCacheLock.Release();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取人员在指定日期的所有班次编号
|
||
/// 业务逻辑:查询人员在特定日期的所有已分配班次编号,用于班次冲突检查
|
||
/// 深度思考:处理多班次分配、任务状态过滤、数据完整性等复杂场景
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="date">查询日期</param>
|
||
/// <returns>班次编号列表,空列表表示该日期无班次分配</returns>
|
||
private async Task<List<int>> GetPersonnelAllShiftNumbersOnDateAsync(long personnelId, DateTime date)
|
||
{
|
||
try
|
||
{
|
||
// 查询该人员在指定日期的所有工作任务及其班次信息
|
||
var workOrdersWithShifts = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate.Date == date.Date &&
|
||
w.Status > (int)WorkOrderStatusEnum.PendingReview)
|
||
.Include(w => w.ShiftEntity) // 包含班次实体
|
||
.ToListAsync();
|
||
|
||
// 提取班次编号并去重
|
||
var shiftNumbers = workOrdersWithShifts
|
||
.Where(w => w.ShiftEntity != null)
|
||
.Select(w => w.ShiftEntity.ShiftNumber)
|
||
.Distinct()
|
||
.ToList();
|
||
|
||
_logger.LogDebug("获取人员当日班次编号成功 - 人员ID:{PersonnelId}, 日期:{Date}, 班次编号:{ShiftNumbers}",
|
||
personnelId, date.ToString("yyyy-MM-dd"), string.Join(",", shiftNumbers));
|
||
|
||
return shiftNumbers;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "获取人员当日班次编号异常 - 人员ID:{PersonnelId}, 日期:{Date}",
|
||
personnelId, date.ToString("yyyy-MM-dd"));
|
||
// 异常情况下返回空列表,避免阻断验证流程
|
||
return new List<int>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建验证通过结果的辅助方法
|
||
/// </summary>
|
||
private RuleValidationResult CreateValidResult(string reason, double score = 100)
|
||
{
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true,
|
||
ComplianceScore = score,
|
||
IsCritical = false,
|
||
ViolationMessage = ""
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成同天班次违规详细信息
|
||
/// 业务逻辑:为同天班次连续性违规生成详细的错误说明
|
||
/// </summary>
|
||
/// <param name="ruleType">规则类型</param>
|
||
/// <param name="existingShift">已存在的班次编号</param>
|
||
/// <param name="currentShift">当前要分配的班次编号</param>
|
||
/// <returns>详细的违规说明</returns>
|
||
private string GenerateSameDayViolationDetail(int ruleType, int existingShift, int currentShift)
|
||
{
|
||
return ruleType switch
|
||
{
|
||
3 => $"同天早中班连续性违规:违反早中班同天禁止规则",
|
||
4 => $"同天中夜班连续性违规:违反中夜班同天禁止规则",
|
||
_ => $"同天班次连续性违规:规则类型{ruleType},已有班次{existingShift}与当前班次{currentShift}冲突"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成同天班次验证成功信息
|
||
/// 业务逻辑:为同天班次连续性验证通过生成详细的成功说明
|
||
/// </summary>
|
||
/// <param name="ruleType">规则类型</param>
|
||
/// <param name="currentShift">当前班次编号</param>
|
||
/// <returns>成功验证的详细说明</returns>
|
||
private string GenerateSameDaySuccessMessage(int ruleType, int currentShift)
|
||
{
|
||
return ruleType switch
|
||
{
|
||
3 => $"同天早中班连续性验证通过:符合早中班同天禁止规则",
|
||
4 => $"同天中夜班连续性验证通过:符合中夜班同天禁止规则",
|
||
_ => $"同天班次连续性验证通过:规则类型{ruleType},班次{currentShift}符合要求"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算跨周末规则验证的相关日期
|
||
/// 业务逻辑:根据规则类型和当前工作日期,计算需要检查的两个关键日期
|
||
/// 深度思考:考虑周历边界、时区、月份跨越等复杂场景
|
||
/// </summary>
|
||
/// <param name="workDate">当前工作日期</param>
|
||
/// <param name="ruleType">规则类型(5:周日/下周六,6:周六/周日)</param>
|
||
/// <returns>需要检查的两个日期(第一日期,第二日期)</returns>
|
||
private (DateTime firstDate, DateTime secondDate) CalculateWeekendDates(DateTime workDate, int ruleType)
|
||
{
|
||
try
|
||
{
|
||
var currentDate = workDate.Date;
|
||
|
||
if (ruleType == 5) // 不能这周日/下周六连续
|
||
{
|
||
// 计算当前日期所在周的周日(本周日)
|
||
var daysFromSunday = (int)currentDate.DayOfWeek; // Sunday = 0, Monday = 1, ..., Saturday = 6
|
||
var thisWeekSunday = currentDate.AddDays(-daysFromSunday);
|
||
|
||
// 计算下周六(下一周的周六)
|
||
var nextWeekSaturday = thisWeekSunday.AddDays(13); // 下周六 = 本周日 + 13天
|
||
|
||
_logger.LogDebug("规则5日期计算 - 本周日:{ThisSunday}, 下周六:{NextSaturday}",
|
||
thisWeekSunday.ToString("yyyy-MM-dd"), nextWeekSaturday.ToString("yyyy-MM-dd"));
|
||
|
||
return (thisWeekSunday, nextWeekSaturday);
|
||
}
|
||
else if (ruleType == 6) // 不能本周六/本周日连续
|
||
{
|
||
// 计算当前日期所在周的周六和周日
|
||
var daysFromSunday = (int)currentDate.DayOfWeek;
|
||
var thisWeekSunday = currentDate.AddDays(-daysFromSunday);
|
||
var thisWeekSaturday = thisWeekSunday.AddDays(-1); // 本周六 = 本周日 - 1天
|
||
|
||
_logger.LogDebug("规则6日期计算 - 本周六:{ThisSaturday}, 本周日:{ThisSunday}",
|
||
thisWeekSaturday.ToString("yyyy-MM-dd"), thisWeekSunday.ToString("yyyy-MM-dd"));
|
||
|
||
return (thisWeekSaturday, thisWeekSunday);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("未支持的跨周末规则类型:{RuleType}", ruleType);
|
||
return (currentDate, currentDate); // 返回相同日期,这样不会产生违规
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算跨周末日期异常 - 工作日期:{WorkDate}, 规则类型:{RuleType}",
|
||
workDate.ToString("yyyy-MM-dd"), ruleType);
|
||
return (workDate, workDate); // 异常情况返回相同日期
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成跨周末班次违规详细信息
|
||
/// 业务逻辑:为跨周末班次连续性违规生成详细的错误说明
|
||
/// </summary>
|
||
/// <param name="ruleType">规则类型</param>
|
||
/// <param name="firstDate">第一个日期</param>
|
||
/// <param name="secondDate">第二个日期</param>
|
||
/// <param name="firstDateShifts">第一个日期的班次编号列表</param>
|
||
/// <param name="secondDateShifts">第二个日期的班次编号列表</param>
|
||
/// <returns>详细的违规说明</returns>
|
||
private string GenerateCrossWeekendViolationDetail(int ruleType, DateTime firstDate, DateTime secondDate,
|
||
List<int> firstDateShifts, List<int> secondDateShifts)
|
||
{
|
||
var firstDateName = GetDateDisplayName(firstDate, ruleType, true);
|
||
var secondDateName = GetDateDisplayName(secondDate, ruleType, false);
|
||
|
||
var firstShiftsStr = string.Join(",", firstDateShifts);
|
||
var secondShiftsStr = string.Join(",", secondDateShifts);
|
||
|
||
return ruleType switch
|
||
{
|
||
5 => $"跨周末连续性违规:违反周日/下周六禁止连续工作规则,{firstDateName}有班次{firstShiftsStr},{secondDateName}有班次{secondShiftsStr}",
|
||
6 => $"周末连续性违规:违反周六/周日禁止连续工作规则,{firstDateName}有班次{firstShiftsStr},{secondDateName}有班次{secondShiftsStr}",
|
||
_ => $"跨周末班次连续性违规:规则类型{ruleType},{firstDateName}({firstDate:yyyy-MM-dd})和{secondDateName}({secondDate:yyyy-MM-dd})存在班次冲突"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成跨周末班次验证成功信息
|
||
/// 业务逻辑:为跨周末班次连续性验证通过生成详细的成功说明
|
||
/// </summary>
|
||
/// <param name="ruleType">规则类型</param>
|
||
/// <param name="firstDate">第一个日期</param>
|
||
/// <param name="secondDate">第二个日期</param>
|
||
/// <returns>成功验证的详细说明</returns>
|
||
private string GenerateCrossWeekendSuccessMessage(int ruleType, DateTime firstDate, DateTime secondDate)
|
||
{
|
||
var firstDateName = GetDateDisplayName(firstDate, ruleType, true);
|
||
var secondDateName = GetDateDisplayName(secondDate, ruleType, false);
|
||
|
||
return ruleType switch
|
||
{
|
||
5 => $"跨周末连续性验证通过:符合周日/下周六禁止连续工作规则",
|
||
6 => $"周末连续性验证通过:符合周六/周日禁止连续工作规则",
|
||
_ => $"跨周末班次连续性验证通过:规则类型{ruleType},{firstDateName}和{secondDateName}符合要求"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证次日休息规则
|
||
/// 【深度业务思考】:该规则确保员工在高强度班次(二班/三班)工作后获得充分的休息时间
|
||
/// 业务背景:
|
||
/// - 二班(中班)通常是下午14:00-22:00,工作强度较大
|
||
/// - 三班(夜班)通常是晚上22:00-次日6:00,对身体负担最重
|
||
/// - 劳动保护要求:夜班工作后必须有24小时的休息时间
|
||
/// - 生产效率考虑:疲劳工作会导致操作失误和安全隐患
|
||
///
|
||
/// 规则8:三班后一天不排班 - 检查前一天是否有三班(班次编号3),如有则当天不能排班
|
||
/// 规则9:二班后一天不排班 - 检查前一天是否有二班(班次编号2),如有则当天不能排班
|
||
///
|
||
/// 【边界情况深度分析】:
|
||
/// 1. 跨周末场景:周六三班→周日必须休息
|
||
/// 2. 跨月场景:月末最后一天三班→次月第一天必须休息
|
||
/// 3. 节假日场景:节假日前的二班/三班→节假日当天必须休息
|
||
/// 4. 紧急任务场景:即使有紧急任务,也不能违反休息规则
|
||
/// 5. 多班次冲突:前一天有多个班次时,只要包含目标班次就触发规则
|
||
/// 6. 任务状态过滤:只考虑已确认和执行中的任务,草稿状态不算
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="workDate">当前工作日期(即将分配班次的日期)</param>
|
||
/// <param name="shiftId">当前班次ID(即将分配的班次)</param>
|
||
/// <param name="ruleType">规则类型(8:三班后次日休息,9:二班后次日休息)</param>
|
||
/// <returns>规则验证结果,包含合规状态、评分和详细说明</returns>
|
||
private async Task<RuleValidationResult> ValidateNextDayRestRuleAsync(long personnelId, DateTime workDate,
|
||
long shiftId, int ruleType)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogDebug("开始验证次日休息规则 - 人员ID:{PersonnelId}, 工作日期:{WorkDate}, 规则类型:{RuleType}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), ruleType);
|
||
|
||
// 第一步:确定需要检查的特定班次编号
|
||
// 【业务逻辑】:根据规则类型确定需要检查的前置班次
|
||
int targetShiftNumber = ruleType switch
|
||
{
|
||
8 => 3, // 规则8:检查前一天是否有三班(夜班)
|
||
9 => 2, // 规则9:检查前一天是否有二班(中班)
|
||
_ => 0 // 未知规则类型,返回0表示无效
|
||
};
|
||
|
||
if (targetShiftNumber == 0)
|
||
{
|
||
_logger.LogWarning("未支持的次日休息规则类型:{RuleType},跳过验证", ruleType);
|
||
return CreateValidResult("未支持的规则类型,跳过验证", 90);
|
||
}
|
||
|
||
// 第二步:计算前一天日期并进行边界检查
|
||
var previousDate = workDate.AddDays(-1);
|
||
|
||
// 【边界情况处理】:检查日期跨度的合理性
|
||
var daysDifference = (workDate.Date - previousDate.Date).TotalDays;
|
||
if (Math.Abs(daysDifference - 1) > 0.01) // 允许微小的浮点误差
|
||
{
|
||
_logger.LogWarning("日期计算异常 - 工作日期:{WorkDate}, 前一天:{PreviousDate}, 天数差:{Days}",
|
||
workDate.ToString("yyyy-MM-dd"), previousDate.ToString("yyyy-MM-dd"), daysDifference);
|
||
return CreateValidResult("日期计算异常,建议人工审核", 70);
|
||
}
|
||
|
||
_logger.LogDebug("次日休息规则验证 - 检查前一天:{PreviousDate}是否有{TargetShift}班",
|
||
previousDate.ToString("yyyy-MM-dd"), GetShiftDisplayName(targetShiftNumber));
|
||
|
||
// 第三步:获取前一天的所有班次编号(包含状态过滤)
|
||
// 【业务逻辑】:只检查已确认的工作任务,避免草稿状态的干扰
|
||
var previousDayShifts = await GetPersonnelAllShiftNumbersOnDateAsync(personnelId, previousDate);
|
||
|
||
// 【数据完整性检查】:验证查询结果的有效性
|
||
if (previousDayShifts == null)
|
||
{
|
||
_logger.LogWarning("获取前一天班次信息失败 - 人员ID:{PersonnelId}, 日期:{PreviousDate}",
|
||
personnelId, previousDate.ToString("yyyy-MM-dd"));
|
||
return CreateValidResult("数据查询异常,建议人工审核", 70);
|
||
}
|
||
|
||
// 第四步:检查前一天是否有目标班次
|
||
bool hadTargetShiftPreviousDay = previousDayShifts.Contains(targetShiftNumber);
|
||
|
||
// 【详细日志记录】:记录查询到的班次信息,便于调试
|
||
_logger.LogDebug("前一天班次查询结果 - 人员ID:{PersonnelId}, 日期:{PreviousDate}, " +
|
||
"所有班次:{AllShifts}, 目标班次:{TargetShift}, 是否存在目标班次:{HasTarget}",
|
||
personnelId, previousDate.ToString("yyyy-MM-dd"),
|
||
string.Join(",", previousDayShifts), targetShiftNumber, hadTargetShiftPreviousDay);
|
||
|
||
if (!hadTargetShiftPreviousDay)
|
||
{
|
||
// 【正常场景】:前一天没有目标班次,规则不适用,验证通过
|
||
var successMessage = GenerateNextDayRestSuccessMessage(ruleType, previousDate, targetShiftNumber, false);
|
||
_logger.LogDebug("次日休息规则验证通过 - 人员ID:{PersonnelId}, {SuccessMessage}",
|
||
personnelId, successMessage);
|
||
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true,
|
||
ComplianceScore = 100,
|
||
IsCritical = false,
|
||
ViolationMessage = ""
|
||
};
|
||
}
|
||
|
||
// 第五步:【关键业务逻辑】前一天有目标班次,检查当天是否安排了班次(违规检查)
|
||
// 【重要说明】:当前方法是在分配前进行验证,如果执行到这里说明:
|
||
// 1. 前一天确实有目标班次(二班或三班)
|
||
// 2. 当天正在尝试分配新的班次
|
||
// 3. 这违反了"次日必须休息"的规则
|
||
|
||
// 【获取当前即将分配的班次信息】:用于生成更详细的违规说明
|
||
var currentShiftNumber = await GetShiftNumberByIdAsync(shiftId);
|
||
var currentShiftName = currentShiftNumber.HasValue ?
|
||
GetShiftDisplayName(currentShiftNumber.Value) : "未知班次";
|
||
|
||
var violationDetail = GenerateNextDayRestViolationDetail(ruleType, previousDate, workDate,
|
||
targetShiftNumber, previousDayShifts, currentShiftName);
|
||
|
||
_logger.LogWarning("次日休息规则违规 - 人员ID:{PersonnelId}, {ViolationDetail}",
|
||
personnelId, violationDetail);
|
||
|
||
// 【关键决策】:这是强制性的安全规则,必须严格执行
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = false,
|
||
ComplianceScore = 0,
|
||
IsCritical = true, // 违反休息规则是关键风险,涉及员工健康和生产安全
|
||
ViolationMessage = violationDetail
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 【异常处理策略】:记录详细异常信息并返回保守结果
|
||
_logger.LogError(ex,
|
||
"验证次日休息规则异常 - 人员ID:{PersonnelId}, 工作日期:{WorkDate}, 班次ID:{ShiftId}, 规则类型:{RuleType}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId, ruleType);
|
||
|
||
// 【保守策略说明】:在系统异常情况下,倾向于不阻断业务流程,但要求人工审核
|
||
// 这样既避免了系统故障导致的业务停滞,又保证了潜在违规得到人工关注
|
||
return new RuleValidationResult
|
||
{
|
||
IsValid = true, // 异常情况下采用保守策略,不阻断分配
|
||
ComplianceScore = 60, // 给予中等评分,表示存在不确定性
|
||
IsCritical = false, // 标记为非关键,但需要人工审核
|
||
ViolationMessage = $"系统验证异常,强烈建议人工审核此次班次分配的合规性:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成次日休息规则违规详细信息
|
||
/// 【业务逻辑深度思考】:生成详细的违规说明,帮助管理人员理解违规原因和影响
|
||
/// 错误信息设计原则:
|
||
/// 1. 明确指出违规的具体规则
|
||
/// 2. 详细说明前一天的班次情况
|
||
/// 3. 明确当前试图分配的班次信息
|
||
/// 4. 提供易于理解的业务背景说明
|
||
/// </summary>
|
||
/// <param name="ruleType">规则类型(8:三班后休息,9:二班后休息)</param>
|
||
/// <param name="previousDate">前一天日期</param>
|
||
/// <param name="currentDate">当前日期(即将分配班次的日期)</param>
|
||
/// <param name="targetShiftNumber">触发规则的班次编号(2或3)</param>
|
||
/// <param name="previousDayShifts">前一天的所有班次编号列表</param>
|
||
/// <param name="currentShiftName">当前试图分配的班次名称</param>
|
||
/// <returns>详细的违规说明,包含业务上下文和建议</returns>
|
||
private string GenerateNextDayRestViolationDetail(int ruleType, DateTime previousDate, DateTime currentDate,
|
||
int targetShiftNumber, List<int> previousDayShifts, string currentShiftName = "未知班次")
|
||
{
|
||
var targetShiftDisplayName = GetShiftDisplayName(targetShiftNumber);
|
||
var previousShiftsStr = string.Join(",", previousDayShifts);
|
||
var dayOfWeekPrevious = previousDate.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
|
||
var dayOfWeekCurrent = currentDate.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
|
||
|
||
// 【生成详细的违规说明】:包含时间、班次、规则背景等完整信息
|
||
return ruleType switch
|
||
{
|
||
8 => $"【三班后次日强制休息违规】" + Environment.NewLine +
|
||
$"违规详情:" + Environment.NewLine +
|
||
$"• 前一天:{previousDate:yyyy-MM-dd}({dayOfWeekPrevious}) 已安排{targetShiftDisplayName}(班次编号{targetShiftNumber})" + Environment.NewLine +
|
||
$"• 前一天所有班次:{previousShiftsStr}" + Environment.NewLine +
|
||
$"• 当前日期:{currentDate:yyyy-MM-dd}({dayOfWeekCurrent}) 试图分配{currentShiftName}" + Environment.NewLine +
|
||
$"• 违规原因:夜班工作强度大,员工需要24小时恢复时间" + Environment.NewLine +
|
||
$"• 建议方案:将该班次分配给其他人员,或调整至{currentDate.AddDays(1):yyyy-MM-dd}后",
|
||
|
||
9 => $"【二班后次日强制休息违规】" + Environment.NewLine +
|
||
$"违规详情:" + Environment.NewLine +
|
||
$"• 前一天:{previousDate:yyyy-MM-dd}({dayOfWeekPrevious}) 已安排{targetShiftDisplayName}(班次编号{targetShiftNumber})" + Environment.NewLine +
|
||
$"• 前一天所有班次:{previousShiftsStr}" + Environment.NewLine +
|
||
$"• 当前日期:{currentDate:yyyy-MM-dd}({dayOfWeekCurrent}) 试图分配{currentShiftName}" + Environment.NewLine +
|
||
$"• 违规原因:中班工作时间长,员工需要充分休息时间" + Environment.NewLine +
|
||
$"• 建议方案:将该班次分配给其他人员,或调整至{currentDate.AddDays(1):yyyy-MM-dd}后",
|
||
|
||
_ => $"【次日休息规则违规】规则类型{ruleType}" + Environment.NewLine +
|
||
$"前一天({previousDate:yyyy-MM-dd})有{targetShiftDisplayName}(班次{previousShiftsStr})," +
|
||
$"当天({currentDate:yyyy-MM-dd})试图分配{currentShiftName},违反休息规则"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成次日休息规则验证成功信息
|
||
/// 【业务逻辑思考】:为次日休息规则验证通过生成详细的成功说明
|
||
/// 成功信息的价值:
|
||
/// 1. 确认规则检查已正确执行
|
||
/// 2. 记录验证的具体条件和结果
|
||
/// 3. 为系统审计提供完整的决策轨迹
|
||
/// </summary>
|
||
/// <param name="ruleType">规则类型(8:三班后休息,9:二班后休息)</param>
|
||
/// <param name="previousDate">前一天日期</param>
|
||
/// <param name="targetShiftNumber">目标班次编号(2或3)</param>
|
||
/// <param name="hadTargetShift">前一天是否有目标班次(当前总是false,为扩展保留)</param>
|
||
/// <returns>成功验证的详细说明,包含验证条件和结论</returns>
|
||
private string GenerateNextDayRestSuccessMessage(int ruleType, DateTime previousDate, int targetShiftNumber, bool hadTargetShift)
|
||
{
|
||
var shiftDisplayName = GetShiftDisplayName(targetShiftNumber);
|
||
var dayOfWeek = previousDate.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
|
||
|
||
// 【生成详细的成功验证信息】:清楚说明验证通过的原因
|
||
return ruleType switch
|
||
{
|
||
8 => $"三班后次日休息规则验证通过:前一天{previousDate:yyyy-MM-dd}({dayOfWeek})无{shiftDisplayName}安排," +
|
||
$"员工无需强制休息,可正常分配班次。验证条件:检查前日是否有夜班(编号3)作业",
|
||
|
||
9 => $"二班后次日休息规则验证通过:前一天{previousDate:yyyy-MM-dd}({dayOfWeek})无{shiftDisplayName}安排," +
|
||
$"员工无需强制休息,可正常分配班次。验证条件:检查前日是否有中班(编号2)作业",
|
||
|
||
_ => $"次日休息规则验证通过:规则类型{ruleType},前一天{previousDate:yyyy-MM-dd}({dayOfWeek})无{shiftDisplayName},符合规则要求"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取班次显示名称
|
||
/// 业务逻辑:将班次编号转换为易读的显示名称
|
||
/// </summary>
|
||
/// <param name="shiftNumber">班次编号</param>
|
||
/// <returns>班次显示名称</returns>
|
||
private string GetShiftDisplayName(int shiftNumber)
|
||
{
|
||
return shiftNumber switch
|
||
{
|
||
1 => "早班",
|
||
2 => "中班",
|
||
3 => "晚班",
|
||
_ => $"{shiftNumber}班"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取日期的显示名称
|
||
/// 业务逻辑:根据规则类型和日期位置生成易读的日期描述
|
||
/// </summary>
|
||
/// <param name="date">日期</param>
|
||
/// <param name="ruleType">规则类型</param>
|
||
/// <param name="isFirst">是否为第一个日期</param>
|
||
/// <returns>日期显示名称</returns>
|
||
private string GetDateDisplayName(DateTime date, int ruleType, bool isFirst)
|
||
{
|
||
return ruleType switch
|
||
{
|
||
5 => isFirst ? "本周日" : "下周六",
|
||
6 => isFirst ? "本周六" : "本周日",
|
||
_ => $"{(isFirst ? "第一日期" : "第二日期")}({date:yyyy-MM-dd})"
|
||
};
|
||
}
|
||
#endregion
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
|
||
|
||
#region 执行第三层:优化决策处理
|
||
|
||
/// <summary>
|
||
/// 计算优化评分
|
||
/// </summary>
|
||
private async Task<OptimizationScoreResult> CalculateOptimizationScoreAsync(PersonnelCandidate candidate,
|
||
WorkOrderEntity workOrder, AllocationContext context)
|
||
{
|
||
try
|
||
{
|
||
var result = new OptimizationScoreResult
|
||
{
|
||
Candidate = candidate,
|
||
DimensionScores = new Dictionary<string, double>(),
|
||
RiskAssessment = new RiskAssessment()
|
||
};
|
||
|
||
// 指定人员优先评分
|
||
var assignedScore =
|
||
workOrder.AssignedPersonnelId.HasValue &&
|
||
workOrder.AssignedPersonnelId.Value == candidate.PersonnelId
|
||
? 100.0
|
||
: 0.0;
|
||
|
||
result.DimensionScores["AssignedPersonnel"] = assignedScore;
|
||
|
||
// 项目FL人员优先评分
|
||
var projectFLScore = await CalculateProjectFLScoreAsync(candidate.PersonnelId, workOrder.ProjectNumber);
|
||
result.DimensionScores["ProjectFL"] = projectFLScore;
|
||
|
||
// 负载均衡评分
|
||
var workloadScore = await CalculateWorkloadBalanceScoreAsync(candidate.PersonnelId);
|
||
result.DimensionScores["WorkloadBalance"] = workloadScore;
|
||
|
||
// 班次连续性评分
|
||
var continuityScore = await CalculateShiftContinuityScoreAsync(candidate.PersonnelId, workOrder);
|
||
result.DimensionScores["ShiftContinuity"] = continuityScore;
|
||
|
||
// 技能匹配评分
|
||
var skillScore = candidate.QualificationScore.MatchScore;
|
||
result.DimensionScores["SkillMatch"] = skillScore;
|
||
|
||
// 协作历史评分
|
||
var collaborationScore =
|
||
await CalculateCollaborationScoreAsync(candidate.PersonnelId, workOrder.ProjectNumber);
|
||
result.DimensionScores["CollaborationHistory"] = collaborationScore;
|
||
|
||
// 计算加权总分
|
||
var weightedScore = 0.0;
|
||
foreach (var kvp in result.DimensionScores)
|
||
{
|
||
var weight = context.WeightConfiguration.GetValueOrDefault(kvp.Key, 0.0);
|
||
weightedScore += kvp.Value * weight;
|
||
}
|
||
|
||
result.OptimizationScore = Math.Min(100, weightedScore);
|
||
result.Confidence =
|
||
CalculateConfidence(result.DimensionScores, candidate.ConstraintScore.ComplianceScore);
|
||
result.RecommendationLevel = DetermineRecommendationLevel(result.OptimizationScore, result.Confidence);
|
||
result.OptimizationReason =
|
||
GenerateOptimizationReason(result.DimensionScores, context.WeightConfiguration);
|
||
|
||
// 风险评估
|
||
result.RiskAssessment = await CalculateRiskAssessmentAsync(candidate, workOrder, result);
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算优化评分异常,候选人:{PersonnelId}", candidate.PersonnelId);
|
||
return new OptimizationScoreResult
|
||
{
|
||
Candidate = candidate,
|
||
OptimizationScore = 0,
|
||
Confidence = 0,
|
||
RecommendationLevel = RecommendationLevel.NotFeasible,
|
||
OptimizationReason = $"评分计算异常:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算项目FL评分
|
||
/// </summary>
|
||
private async Task<double> CalculateProjectFLScoreAsync(long personnelId, string projectNumber)
|
||
{
|
||
try
|
||
{
|
||
// 查询该人员是否为项目内FL人员
|
||
var isProjectFL = await _workOrderRepository.Select
|
||
.AnyAsync(w => w.ProjectNumber == projectNumber &&
|
||
w.WorkOrderFLPersonnels.Any(fp => fp.FLPersonnelId == personnelId));
|
||
|
||
return isProjectFL ? 100.0 : 0.0;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算项目FL评分异常,人员ID:{PersonnelId},项目:{ProjectNumber}", personnelId, projectNumber);
|
||
return 0.0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算工作负载均衡评分
|
||
/// </summary>
|
||
private async Task<double> CalculateWorkloadBalanceScoreAsync(long personnelId)
|
||
{
|
||
try
|
||
{
|
||
var dateRange = new DateRange
|
||
{
|
||
StartDate = DateTime.Today,
|
||
EndDate = DateTime.Today.AddDays(30)
|
||
};
|
||
|
||
var workloadAnalysis = await AnalyzePersonnelWorkloadAsync(new List<long> { personnelId }, dateRange);
|
||
var personalWorkload = workloadAnalysis.FirstOrDefault();
|
||
|
||
if (personalWorkload == null)
|
||
{
|
||
return 100.0; // 无工作负载,最高分
|
||
}
|
||
|
||
// 基于工作负载百分比计算平衡评分(负载越低,评分越高)
|
||
return personalWorkload.WorkloadPercentage switch
|
||
{
|
||
< 30 => 100.0,
|
||
< 50 => 90.0,
|
||
< 70 => 80.0,
|
||
< 90 => 60.0,
|
||
< 100 => 40.0,
|
||
_ => 20.0
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算工作负载均衡评分异常,人员ID:{PersonnelId}", personnelId);
|
||
return 50.0; // 异常时返回中等评分
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算班次连续性评分
|
||
/// </summary>
|
||
private async Task<double> CalculateShiftContinuityScoreAsync(long personnelId, WorkOrderEntity workOrder)
|
||
{
|
||
try
|
||
{
|
||
// 获取前一天的班次
|
||
var previousDay = workOrder.WorkOrderDate.AddDays(-1);
|
||
var previousShift = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId && w.WorkOrderDate.Date == previousDay.Date)
|
||
.FirstAsync();
|
||
|
||
if (previousShift == null)
|
||
{
|
||
return 80.0; // 无前一天班次,给予较高分
|
||
}
|
||
|
||
// 获取后一天的班次
|
||
var nextDay = workOrder.WorkOrderDate.AddDays(1);
|
||
var nextShift = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId && w.WorkOrderDate.Date == nextDay.Date)
|
||
.FirstAsync();
|
||
|
||
// 评估班次连续性(相同班次的连续性更好)
|
||
var continuityScore = 60.0; // 基础分
|
||
|
||
if (previousShift?.ShiftId == workOrder.ShiftId)
|
||
{
|
||
continuityScore += 20.0;
|
||
}
|
||
|
||
if (nextShift?.ShiftId == workOrder.ShiftId)
|
||
{
|
||
continuityScore += 20.0;
|
||
}
|
||
|
||
return Math.Min(100.0, continuityScore);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算班次连续性评分异常,人员ID:{PersonnelId}", personnelId);
|
||
return 60.0; // 异常时返回中等评分
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算协作历史评分
|
||
/// </summary>
|
||
private async Task<double> CalculateCollaborationScoreAsync(long personnelId, string projectNumber)
|
||
{
|
||
try
|
||
{
|
||
// 查询历史协作次数
|
||
var collaborationCount = await _workOrderRepository.Select
|
||
.Where(w => w.ProjectNumber == projectNumber &&
|
||
w.AssignedPersonnelId == personnelId &&
|
||
w.Status == (int)WorkOrderStatusEnum.Completed)
|
||
.CountAsync();
|
||
|
||
// 基于协作次数计算评分
|
||
return collaborationCount switch
|
||
{
|
||
0 => 50.0, // 无历史协作
|
||
1 => 60.0, // 少量协作
|
||
<= 3 => 75.0, // 适度协作
|
||
<= 5 => 85.0, // 良好协作
|
||
_ => 95.0 // 丰富协作经验
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算协作历史评分异常,人员ID:{PersonnelId},项目:{ProjectNumber}", personnelId, projectNumber);
|
||
return 50.0;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 计算和统计方法
|
||
|
||
/// <summary>
|
||
/// 计算综合总分
|
||
/// </summary>
|
||
private double CalculateTotalScore(PersonnelCandidate candidate, Dictionary<string, double> weights)
|
||
{
|
||
var scores = new Dictionary<string, double>
|
||
{
|
||
["Qualification"] = candidate.QualificationScore.MatchScore,
|
||
["Constraint"] = candidate.ConstraintScore.ComplianceScore,
|
||
["Optimization"] = candidate.OptimizationScore?.TotalOptimizationScore ?? 0
|
||
};
|
||
|
||
var weightedSum = 0.0;
|
||
var totalWeight = 0.0;
|
||
|
||
// 资格和约束是必须项,权重固定
|
||
weightedSum += scores["Qualification"] * 0.3; // 资格权重30%
|
||
weightedSum += scores["Constraint"] * 0.4; // 约束权重40%
|
||
weightedSum += scores["Optimization"] * 0.3; // 优化权重30%
|
||
|
||
return Math.Min(100, weightedSum);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算连续工作天数
|
||
/// </summary>
|
||
private int CalculateContinuousWorkDays(List<DateTime> workDates)
|
||
{
|
||
if (workDates == null || !workDates.Any())
|
||
return 0;
|
||
|
||
var sortedDates = workDates.OrderBy(d => d.Date).ToList();
|
||
var maxContinuous = 1;
|
||
var currentContinuous = 1;
|
||
|
||
for (int i = 1; i < sortedDates.Count; i++)
|
||
{
|
||
if ((sortedDates[i].Date - sortedDates[i - 1].Date).Days == 1)
|
||
{
|
||
currentContinuous++;
|
||
maxContinuous = Math.Max(maxContinuous, currentContinuous);
|
||
}
|
||
else
|
||
{
|
||
currentContinuous = 1;
|
||
}
|
||
}
|
||
|
||
return maxContinuous;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步计算连续工作天数
|
||
/// </summary>
|
||
private async Task<int> CalculateContinuousWorkDaysAsync(long personnelId, DateTime targetDate)
|
||
{
|
||
try
|
||
{
|
||
// 获取目标日期前后两周的工作安排
|
||
var startDate = targetDate.AddDays(-14);
|
||
var endDate = targetDate.AddDays(14);
|
||
|
||
var workDates = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate >= startDate &&
|
||
w.WorkOrderDate <= endDate &&
|
||
w.Status > (int)WorkOrderStatusEnum.PendingReview)
|
||
.ToListAsync(w => w.WorkOrderDate);
|
||
|
||
// 包含目标日期
|
||
workDates.Add(targetDate);
|
||
|
||
return CalculateContinuousWorkDays(workDates);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算连续工作天数异常,人员ID:{PersonnelId}", personnelId);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算周班次数
|
||
/// </summary>
|
||
private async Task<int> CalculateWeekShiftCountAsync(long personnelId, DateTime targetDate)
|
||
{
|
||
try
|
||
{
|
||
// 获取目标日期所在周的开始和结束时间
|
||
var weekStart = targetDate.AddDays(-(int)targetDate.DayOfWeek);
|
||
var weekEnd = weekStart.AddDays(6);
|
||
|
||
var shiftCount = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate >= weekStart &&
|
||
w.WorkOrderDate <= weekEnd &&
|
||
w.Status > (int)WorkOrderStatusEnum.PendingReview)
|
||
.CountAsync();
|
||
|
||
return (int)shiftCount + 1; // 包含目标日期
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算周班次数异常,人员ID:{PersonnelId}", personnelId);
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算月工作小时数
|
||
/// </summary>
|
||
private async Task<decimal> CalculateMonthlyWorkHoursAsync(long personnelId, DateTime targetDate)
|
||
{
|
||
try
|
||
{
|
||
var monthStart = new DateTime(targetDate.Year, targetDate.Month, 1);
|
||
var monthEnd = monthStart.AddMonths(1).AddDays(-1);
|
||
|
||
var totalHours = await _workOrderRepository.Select
|
||
.Where(w => w.AssignedPersonnelId == personnelId &&
|
||
w.WorkOrderDate >= monthStart &&
|
||
w.WorkOrderDate <= monthEnd &&
|
||
w.Status > (int)WorkOrderStatusEnum.Assigned)
|
||
.SumAsync(w => w.EstimatedHours ?? 8.0m);
|
||
|
||
return totalHours + 8.0m; // 包含目标日期的估算工时
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算月工作小时数异常,人员ID:{PersonnelId}", personnelId);
|
||
return 8.0m;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助计算方法
|
||
|
||
/// <summary>
|
||
/// 计算置信度
|
||
/// </summary>
|
||
private double CalculateConfidence(Dictionary<string, double> dimensionScores, double constraintScore)
|
||
{
|
||
var scores = dimensionScores.Values.ToList();
|
||
scores.Add(constraintScore);
|
||
|
||
if (!scores.Any()) return 0;
|
||
|
||
var average = scores.Average();
|
||
var variance = scores.Sum(s => Math.Pow(s - average, 2)) / scores.Count;
|
||
var standardDeviation = Math.Sqrt(variance);
|
||
|
||
// 置信度与平均分正相关,与标准差负相关
|
||
var confidence = average * (1 - standardDeviation / 100);
|
||
return Math.Max(0, Math.Min(100, confidence));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确定推荐等级
|
||
/// </summary>
|
||
private RecommendationLevel DetermineRecommendationLevel(double optimizationScore, double confidence)
|
||
{
|
||
var combinedScore = (optimizationScore + confidence) / 2;
|
||
|
||
return combinedScore switch
|
||
{
|
||
>= 90 => RecommendationLevel.HighlyRecommended,
|
||
>= 75 => RecommendationLevel.Recommended,
|
||
>= 60 => RecommendationLevel.Acceptable,
|
||
>= 40 => RecommendationLevel.NotRecommended,
|
||
_ => RecommendationLevel.NotFeasible
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成优化理由
|
||
/// </summary>
|
||
private string GenerateOptimizationReason(Dictionary<string, double> dimensionScores,
|
||
Dictionary<string, double> weights)
|
||
{
|
||
var reasons = new List<string>();
|
||
|
||
foreach (var kvp in dimensionScores.OrderByDescending(x => x.Value * weights.GetValueOrDefault(x.Key, 0)))
|
||
{
|
||
var weight = weights.GetValueOrDefault(kvp.Key, 0);
|
||
var weightedScore = kvp.Value * weight;
|
||
|
||
if (weightedScore > 50) // 只显示较好的评分
|
||
{
|
||
var dimensionName = kvp.Key switch
|
||
{
|
||
"AssignedPersonnel" => "指定人员匹配",
|
||
"ProjectFL" => "项目FL人员",
|
||
"WorkloadBalance" => "负载均衡",
|
||
"ShiftContinuity" => "班次连续性",
|
||
"SkillMatch" => "技能匹配",
|
||
"CollaborationHistory" => "协作历史",
|
||
_ => kvp.Key
|
||
};
|
||
|
||
reasons.Add($"{dimensionName}({kvp.Value:F1}分,权重{weight:P0})");
|
||
}
|
||
}
|
||
|
||
return reasons.Any() ? $"优势:{string.Join(";", reasons)}" : "综合评分一般";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算风险评估
|
||
/// </summary>
|
||
private async Task<RiskAssessment> CalculateRiskAssessmentAsync(PersonnelCandidate candidate,
|
||
WorkOrderEntity workOrder, OptimizationScoreResult scoreResult)
|
||
{
|
||
var riskAssessment = new RiskAssessment
|
||
{
|
||
RiskFactors = new List<RiskFactor>()
|
||
};
|
||
|
||
try
|
||
{
|
||
// 约束风险
|
||
if (candidate.ConstraintScore.ComplianceScore < 80)
|
||
{
|
||
riskAssessment.RiskFactors.Add(new RiskFactor
|
||
{
|
||
RiskType = "约束合规风险",
|
||
Description = $"约束符合度较低({candidate.ConstraintScore.ComplianceScore:F1}分)",
|
||
Level = candidate.ConstraintScore.ComplianceScore < 60 ? RiskLevel.High : RiskLevel.Medium,
|
||
Probability = 100 - candidate.ConstraintScore.ComplianceScore,
|
||
Impact = 70
|
||
});
|
||
}
|
||
|
||
// 资质风险
|
||
if (candidate.QualificationScore.MatchScore < 90)
|
||
{
|
||
riskAssessment.RiskFactors.Add(new NPP.SmartSchedue.Api.Contracts.Services.Integration.Models.RiskFactor
|
||
{
|
||
RiskType = "资质匹配风险",
|
||
Description = $"资质匹配度不完全({candidate.QualificationScore.MatchScore:F1}分)",
|
||
Level = candidate.QualificationScore.MatchScore < 60 ? RiskLevel.High : RiskLevel.Medium,
|
||
Probability = 100 - candidate.QualificationScore.MatchScore,
|
||
Impact = 60
|
||
});
|
||
}
|
||
|
||
// 工作负载风险
|
||
var workloadScore = scoreResult.DimensionScores.GetValueOrDefault("WorkloadBalance", 100);
|
||
if (workloadScore < 50)
|
||
{
|
||
riskAssessment.RiskFactors.Add(new NPP.SmartSchedue.Api.Contracts.Services.Integration.Models.RiskFactor
|
||
{
|
||
RiskType = "工作负载风险",
|
||
Description = "人员工作负载较重,可能影响工作质量",
|
||
Level = workloadScore < 30 ? RiskLevel.High : RiskLevel.Medium,
|
||
Probability = 100 - workloadScore,
|
||
Impact = 50
|
||
});
|
||
}
|
||
|
||
// 计算总体风险
|
||
if (riskAssessment.RiskFactors.Any())
|
||
{
|
||
var avgProbability = riskAssessment.RiskFactors.Average(rf => rf.Probability);
|
||
var avgImpact = riskAssessment.RiskFactors.Average(rf => rf.Impact);
|
||
riskAssessment.RiskScore = (avgProbability + avgImpact) / 2;
|
||
|
||
riskAssessment.OverallRiskLevel = riskAssessment.RiskScore switch
|
||
{
|
||
< 30 => RiskLevel.Low,
|
||
< 60 => RiskLevel.Medium,
|
||
< 80 => RiskLevel.High,
|
||
_ => RiskLevel.Critical
|
||
};
|
||
}
|
||
else
|
||
{
|
||
riskAssessment.OverallRiskLevel = RiskLevel.Low;
|
||
riskAssessment.RiskScore = 10;
|
||
}
|
||
|
||
riskAssessment.RiskDescription = GenerateRiskDescription(riskAssessment);
|
||
riskAssessment.MitigationSuggestions = GenerateMitigationSuggestions(riskAssessment.RiskFactors);
|
||
|
||
return riskAssessment;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算风险评估异常,候选人:{PersonnelId}", candidate.PersonnelId);
|
||
return new RiskAssessment
|
||
{
|
||
OverallRiskLevel = RiskLevel.Medium,
|
||
RiskScore = 50,
|
||
RiskDescription = $"风险评估异常:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成风险描述
|
||
/// </summary>
|
||
private string GenerateRiskDescription(RiskAssessment riskAssessment)
|
||
{
|
||
if (!riskAssessment.RiskFactors.Any())
|
||
{
|
||
return "风险较低,可以安全执行";
|
||
}
|
||
|
||
var highRiskFactors = riskAssessment.RiskFactors.Where(rf => rf.Level >= RiskLevel.High).ToList();
|
||
if (highRiskFactors.Any())
|
||
{
|
||
return
|
||
$"存在{highRiskFactors.Count}个高风险因子,需要特别关注:{string.Join(";", highRiskFactors.Select(rf => rf.RiskType))}";
|
||
}
|
||
|
||
return $"存在{riskAssessment.RiskFactors.Count}个中低风险因子,建议采取预防措施";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成缓解建议
|
||
/// </summary>
|
||
private List<string> GenerateMitigationSuggestions(List<RiskFactor> riskFactors)
|
||
{
|
||
var suggestions = new List<string>();
|
||
|
||
foreach (var riskFactor in riskFactors)
|
||
{
|
||
var suggestion = riskFactor.RiskType switch
|
||
{
|
||
"约束合规风险" => "建议调整任务时间或寻找替代人员",
|
||
"资质匹配风险" => "建议提供必要的培训或技术支持",
|
||
"工作负载风险" => "建议减少该人员的其他任务或增加协助人员",
|
||
_ => "建议进一步评估和监控"
|
||
};
|
||
|
||
suggestions.Add(suggestion);
|
||
}
|
||
|
||
return suggestions.Distinct().ToList();
|
||
}
|
||
|
||
#endregion
|
||
|
||
|
||
#region 结果计算和生成方法
|
||
|
||
/// <summary>
|
||
/// 计算验证评分
|
||
/// </summary>
|
||
private double CalculateValidationScore(List<ValidationViolation> violations,
|
||
List<NPP.SmartSchedue.Api.Contracts.Services.Integration.Models.ValidationWarning> warnings,
|
||
int totalAssignments)
|
||
{
|
||
if (totalAssignments == 0) return 100;
|
||
|
||
var criticalCount = violations.Count(v => v.Severity == ViolationSeverity.Critical);
|
||
var errorCount = violations.Count(v => v.Severity == ViolationSeverity.Error);
|
||
var warningCount = violations.Count(v => v.Severity == ViolationSeverity.Warning) + warnings.Count;
|
||
|
||
// 计算扣分
|
||
var deductions = criticalCount * 30 + errorCount * 20 + warningCount * 5;
|
||
var baseScore = Math.Max(0, 100 - (deductions / (double)totalAssignments));
|
||
|
||
return Math.Round(baseScore, 1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成验证报告
|
||
/// </summary>
|
||
private string GenerateValidationReport(PersonnelAllocationValidationResult result)
|
||
{
|
||
var report =
|
||
$"验证完成:{result.PassedAssignmentCount}个通过,{result.FailedAssignmentCount}个失败,评分:{result.ValidationScore}";
|
||
|
||
if (result.Violations.Any())
|
||
{
|
||
var groupedViolations = result.Violations.GroupBy(v => v.Severity);
|
||
var violationSummary = string.Join(";", groupedViolations.Select(g => $"{g.Key}级别{g.Count()}个"));
|
||
report += $";违规分布:{violationSummary}";
|
||
}
|
||
|
||
return report;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据策略排序候选人员
|
||
/// </summary>
|
||
private List<PersonnelCandidate> SortCandidatesByStrategy(List<PersonnelCandidate> candidates,
|
||
PersonnelAllocationStrategy strategy)
|
||
{
|
||
return strategy switch
|
||
{
|
||
_ => candidates.OrderByDescending(c => c.TotalScore).ToList()
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算推荐置信度
|
||
/// </summary>
|
||
private double CalculateRecommendationConfidence(IEnumerable<PersonnelCandidate> optimalAssignments)
|
||
{
|
||
var candidates = optimalAssignments.ToList();
|
||
if (!candidates.Any()) return 0;
|
||
|
||
var averageScore = candidates.Average(c => c.TotalScore);
|
||
var minScore = candidates.Min(c => c.TotalScore);
|
||
|
||
// 置信度基于平均分和最低分
|
||
var confidence = (averageScore + minScore) / 2;
|
||
return Math.Round(confidence, 1);
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
|
||
#region 其他辅助方法
|
||
|
||
|
||
/// <summary>
|
||
/// 创建失败结果
|
||
/// </summary>
|
||
private PersonnelAllocationResult CreateFailureResult(string errorMessage)
|
||
{
|
||
return new PersonnelAllocationResult
|
||
{
|
||
SuccessfulMatches = new List<TaskPersonnelMatch>(),
|
||
FailedAllocations = new List<FailedPersonnelAllocation>(),
|
||
WorkloadAnalysis = new List<PersonnelWorkloadAnalysis>(),
|
||
FairnessScore = 0,
|
||
AllocationSummary = $"分配失败:{errorMessage}",
|
||
ProcessingDetails = errorMessage,
|
||
AllocationStrategy = "Failed",
|
||
ValidationResult = new
|
||
{
|
||
OverallScore = 0,
|
||
ProcessingMetrics = new Dictionary<string, object>(),
|
||
ValidationPassed = false,
|
||
ErrorMessage = errorMessage
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建部分失败结果
|
||
/// </summary>
|
||
private PersonnelAllocationResult CreatePartialFailureResult(AllocationContext context, string errorMessage)
|
||
{
|
||
var result = new PersonnelAllocationResult
|
||
{
|
||
SuccessfulMatches = new List<TaskPersonnelMatch>(),
|
||
FailedAllocations = new List<FailedPersonnelAllocation>(),
|
||
WorkloadAnalysis = new List<PersonnelWorkloadAnalysis>(),
|
||
FairnessScore = 0,
|
||
AllocationSummary = $"部分失败:{errorMessage}",
|
||
ProcessingDetails = errorMessage,
|
||
AllocationStrategy = context.Strategy.ToString(),
|
||
ValidationResult = new
|
||
{
|
||
OverallScore = 0,
|
||
ProcessingMetrics = context.Metrics,
|
||
ValidationPassed = false,
|
||
ErrorMessage = errorMessage
|
||
}
|
||
};
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建空结果
|
||
/// </summary>
|
||
private PersonnelAllocationResult CreateEmptyResult()
|
||
{
|
||
return new PersonnelAllocationResult
|
||
{
|
||
SuccessfulMatches = new List<TaskPersonnelMatch>(),
|
||
FailedAllocations = new List<FailedPersonnelAllocation>(),
|
||
WorkloadAnalysis = new List<PersonnelWorkloadAnalysis>(),
|
||
FairnessScore = 100, // 没有分配时公平性为满分
|
||
AllocationSummary = "无任务需要分配",
|
||
ProcessingDetails = "输入的任务列表为空",
|
||
AllocationStrategy = "Empty",
|
||
ValidationResult = new
|
||
{
|
||
OverallScore = 100,
|
||
ProcessingMetrics = new Dictionary<string, object>(),
|
||
ValidationPassed = true,
|
||
ErrorMessage = ""
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算整体质量评分
|
||
/// </summary>
|
||
private double CalculateOverallQualityScore(AllocationGenerationResult result)
|
||
{
|
||
if (!result.SuccessfulAllocations.Any()) return 0;
|
||
|
||
var averageScore = result.SuccessfulAllocations.Values.Average(c => c.TotalScore);
|
||
var successRate = (double)result.SuccessfulAllocations.Count /
|
||
(result.SuccessfulAllocations.Count + result.FailedAllocations.Count) * 100;
|
||
|
||
return Math.Round((averageScore + successRate) / 2, 1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成分配摘要
|
||
/// </summary>
|
||
private string GenerateAllocationSummary(AllocationGenerationResult result, AllocationContext context)
|
||
{
|
||
var totalTasks = context.WorkOrders.Count;
|
||
var successCount = result.SuccessfulAllocations.Count;
|
||
var failureCount = result.FailedAllocations.Count;
|
||
|
||
return $"分配完成:{totalTasks}个任务,成功{successCount}个,失败{failureCount}个,质量评分{result.OverallQualityScore:F1}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成工作负载分析
|
||
/// </summary>
|
||
private async Task<List<PersonnelWorkloadAnalysis>> GenerateWorkloadAnalysisAsync(
|
||
Dictionary<long, PersonnelCandidate> successfulAllocations)
|
||
{
|
||
var workloadAnalysis = new List<PersonnelWorkloadAnalysis>();
|
||
var personnelGroups = successfulAllocations.Values.GroupBy(c => c.PersonnelId);
|
||
|
||
foreach (var group in personnelGroups)
|
||
{
|
||
var personnel = group.First();
|
||
workloadAnalysis.Add(new PersonnelWorkloadAnalysis
|
||
{
|
||
PersonnelId = personnel.PersonnelId,
|
||
PersonnelName = personnel.PersonnelName,
|
||
AssignedTaskCount = group.Count(),
|
||
WorkloadPercentage = Math.Min(100, group.Count() * 25), // 假设每个任务25%工作负载
|
||
WorkloadStatus = group.Count() switch
|
||
{
|
||
1 => "轻松",
|
||
2 => "适中",
|
||
3 => "繁重",
|
||
_ => "超负荷"
|
||
}
|
||
});
|
||
}
|
||
|
||
return workloadAnalysis.OrderByDescending(w => w.WorkloadPercentage).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算公平性评分
|
||
/// </summary>
|
||
private int CalculateFairnessScore(List<PersonnelWorkloadAnalysis> workloadAnalysis)
|
||
{
|
||
if (!workloadAnalysis.Any()) return 100;
|
||
|
||
var workloads = workloadAnalysis.Select(w => w.WorkloadPercentage).ToList();
|
||
var average = workloads.Average();
|
||
var variance = workloads.Sum(w => Math.Pow(w - average, 2)) / workloads.Count;
|
||
var standardDeviation = Math.Sqrt(variance);
|
||
|
||
// 公平性评分:标准差越小,公平性越高
|
||
var fairnessScore = Math.Max(0, 100 - standardDeviation);
|
||
return (int)Math.Round(fairnessScore);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 计算预估效率
|
||
/// </summary>
|
||
private decimal CalculateEstimatedEfficiency(PersonnelCandidate candidate)
|
||
{
|
||
// 基于各项评分计算预估效率
|
||
var qualificationFactor = candidate.QualificationScore.MatchScore / 100;
|
||
var constraintFactor = candidate.ConstraintScore.ComplianceScore / 100;
|
||
var optimizationFactor = candidate.OptimizationScore?.TotalOptimizationScore / 100 ?? 0.8;
|
||
|
||
var efficiency = (qualificationFactor + constraintFactor + optimizationFactor) / 3;
|
||
return Math.Round((decimal)efficiency, 2);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助数据结构
|
||
|
||
/// <summary>
|
||
/// 人员状态检查结果
|
||
/// </summary>
|
||
private class PersonnelStatusCheck
|
||
{
|
||
public bool IsActive { get; set; }
|
||
public string Reason { get; set; }
|
||
public string StatusCode { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 项目授权检查结果
|
||
/// </summary>
|
||
private class ProjectAuthorizationCheck
|
||
{
|
||
public bool IsAuthorized { get; set; }
|
||
public double AuthorizationScore { get; set; }
|
||
public string Reason { get; set; }
|
||
public string AuthorizationLevel { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设备可用性检查结果
|
||
/// </summary>
|
||
private class EquipmentAvailabilityCheck
|
||
{
|
||
public bool IsAvailable { get; set; }
|
||
public double AvailabilityScore { get; set; }
|
||
public string Reason { get; set; }
|
||
public int ConflictCount { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 地理位置约束检查结果
|
||
/// </summary>
|
||
private class LocationConstraintCheck
|
||
{
|
||
public bool IsAccessible { get; set; }
|
||
public double AccessibilityScore { get; set; }
|
||
public string Reason { get; set; }
|
||
public TimeSpan EstimatedTravelTime { get; set; }
|
||
public decimal TravelCost { get; set; }
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 规则验证结果
|
||
/// </summary>
|
||
private class RuleValidationResult
|
||
{
|
||
public bool IsValid { get; set; }
|
||
public double ComplianceScore { get; set; }
|
||
public bool IsCritical { get; set; }
|
||
public string ViolationMessage { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 时间可用性结果
|
||
/// </summary>
|
||
private class TimeAvailabilityResult
|
||
{
|
||
public bool IsAvailable { get; set; }
|
||
public double Score { get; set; }
|
||
public string Reason { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 工作限制结果
|
||
/// </summary>
|
||
private class WorkLimitResult
|
||
{
|
||
public bool IsCompliant { get; set; }
|
||
public double Score { get; set; }
|
||
public string Reason { get; set; }
|
||
public string Severity { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 班次规则结果
|
||
/// </summary>
|
||
private class ShiftRuleResult
|
||
{
|
||
public bool IsCompliant { get; set; }
|
||
public double Score { get; set; }
|
||
public string Reason { get; set; }
|
||
public bool IsCritical { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 班次时间冲突结果
|
||
/// </summary>
|
||
private class ShiftTimeConflictResult
|
||
{
|
||
public bool HasConflict { get; set; }
|
||
public string ConflictDescription { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 资质匹配结果
|
||
/// </summary>
|
||
private class QualificationMatchResult
|
||
{
|
||
public bool IsMatched { get; set; }
|
||
public bool IsValid { get; set; }
|
||
public double MatchScore { get; set; }
|
||
public List<string> MatchedQualifications { get; set; } = new();
|
||
public List<string> MissingQualifications { get; set; } = new();
|
||
public string Details { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 输入验证结果
|
||
/// </summary>
|
||
private class InputValidationResult
|
||
{
|
||
public bool IsValid { get; set; }
|
||
public string ErrorMessage { get; set; }
|
||
public List<WorkOrderEntity> WorkOrders { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 人员基础资格检查结果
|
||
/// </summary>
|
||
private class PersonnelEligibilityResult
|
||
{
|
||
public long PersonnelId { get; set; }
|
||
public bool IsEligible { get; set; }
|
||
public List<string> EligibilityReasons { get; set; } = new();
|
||
}
|
||
|
||
#region 规则10:项目FL优先分配相关辅助方法
|
||
|
||
/// <summary>
|
||
/// 从当前分配上下文中获取工作任务信息
|
||
/// 【业务逻辑】:在规则验证过程中获取当前正在分配的工作任务
|
||
/// 深度思考:考虑多种获取任务信息的方式,确保数据准确性
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="workDate">工作日期</param>
|
||
/// <param name="shiftId">班次ID</param>
|
||
/// <returns>当前工作任务实体,如果获取失败返回null</returns>
|
||
private async Task<WorkOrderEntity> GetCurrentWorkOrderFromContextAsync(long personnelId, DateTime workDate, long shiftId)
|
||
{
|
||
try
|
||
{
|
||
// 方法一:从当前分配上下文获取(如果有的话)
|
||
// 这里需要根据实际的分配上下文实现
|
||
|
||
// 方法二:查询最近创建的、尚未分配FL的工作任务
|
||
var recentWorkOrders = await _workOrderRepository.Select
|
||
.Where(w => w.WorkOrderDate.Date == workDate.Date &&
|
||
w.ShiftId == shiftId &&
|
||
w.Status == (int)WorkOrderStatusEnum.PendingAssignment)
|
||
.OrderByDescending(w => w.CreatedTime)
|
||
.Take(10)
|
||
.ToListAsync();
|
||
|
||
if (recentWorkOrders.Any())
|
||
{
|
||
// 优先返回项目号相关的任务(如果人员已有项目关联)
|
||
var personnelProjects = await GetPersonnelAllProjectFLsAsync(personnelId);
|
||
if (personnelProjects.Any())
|
||
{
|
||
var matchedOrder = recentWorkOrders.FirstOrDefault(w =>
|
||
personnelProjects.Any(p => p.ProjectNumber == w.ProjectNumber));
|
||
if (matchedOrder != null)
|
||
{
|
||
return matchedOrder;
|
||
}
|
||
}
|
||
|
||
// 否则返回第一个符合条件的任务
|
||
return recentWorkOrders.First();
|
||
}
|
||
|
||
// 方法三:如果没有待分配的任务,查找相似的已分配任务作为参考
|
||
var similarWorkOrders = await _workOrderRepository.Select
|
||
.Where(w => w.WorkOrderDate.Date == workDate.Date && w.ShiftId == shiftId)
|
||
.OrderByDescending(w => w.CreatedTime)
|
||
.Take(1)
|
||
.ToListAsync();
|
||
|
||
return similarWorkOrders.FirstOrDefault();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "获取当前工作任务上下文异常 - 人员ID:{PersonnelId}, 日期:{WorkDate}, 班次:{ShiftId}",
|
||
personnelId, workDate.ToString("yyyy-MM-dd"), shiftId);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查人员是否为指定项目的FL成员
|
||
/// 【业务逻辑】:查询WorkOrderFLPersonnelEntity表,确定人员与项目的FL关联关系
|
||
/// 深度思考:考虑FL资格的时效性、状态有效性等因素
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="projectNumber">项目号</param>
|
||
/// <returns>true表示是该项目的FL,false表示不是</returns>
|
||
private async Task<bool> CheckPersonnelIsProjectFLAsync(long personnelId, string projectNumber)
|
||
{
|
||
try
|
||
{
|
||
// 查询该人员在指定项目中的FL记录
|
||
var flRecords = await _workOrderRepository.Orm.Select<WorkOrderFLPersonnelEntity>()
|
||
.From<WorkOrderEntity>()
|
||
.Where((flp, wo) => flp.FLPersonnelId == personnelId &&
|
||
wo.ProjectNumber == projectNumber &&
|
||
flp.WorkOrderId == wo.Id)
|
||
.ToListAsync();
|
||
|
||
bool isProjectFL = flRecords.Any();
|
||
|
||
_logger.LogDebug("检查人员项目FL关联 - 人员ID:{PersonnelId}, 项目:{ProjectNumber}, 是否FL:{IsFL}, FL记录数:{Count}",
|
||
personnelId, projectNumber, isProjectFL, flRecords.Count);
|
||
|
||
return isProjectFL;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查人员项目FL关联异常 - 人员ID:{PersonnelId}, 项目:{ProjectNumber}",
|
||
personnelId, projectNumber);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取人员所有项目FL关联情况
|
||
/// 【业务逻辑】:查询人员参与的所有项目FL记录,用于综合评分
|
||
/// 深度思考:考虑项目活跃度、历史记录权重等因素
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <returns>人员所有项目FL关联列表</returns>
|
||
private async Task<List<ProjectFLInfo>> GetPersonnelAllProjectFLsAsync(long personnelId)
|
||
{
|
||
try
|
||
{
|
||
// 查询该人员的所有项目FL记录,并统计每个项目的任务数量
|
||
var projectFLRecords = await _workOrderRepository.Orm.Select<WorkOrderFLPersonnelEntity>()
|
||
.From<WorkOrderEntity>()
|
||
.Where((flp, wo) => flp.FLPersonnelId == personnelId && flp.WorkOrderId == wo.Id)
|
||
.ToListAsync((flp, wo) => new { flp.FLPersonnelId, wo.ProjectNumber, wo.CreatedTime });
|
||
|
||
var projectFLStats = projectFLRecords
|
||
.GroupBy(x => x.ProjectNumber)
|
||
.Select(g => new ProjectFLInfo
|
||
{
|
||
ProjectNumber = g.Key,
|
||
TaskCount = g.Count(),
|
||
LatestAssignmentDate = g.Max(x => x.CreatedTime) ?? DateTime.MinValue
|
||
})
|
||
.ToList();
|
||
|
||
_logger.LogDebug("获取人员所有项目FL关联 - 人员ID:{PersonnelId}, 项目数:{ProjectCount}",
|
||
personnelId, projectFLStats.Count);
|
||
|
||
return projectFLStats;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "获取人员所有项目FL关联异常 - 人员ID:{PersonnelId}", personnelId);
|
||
return new List<ProjectFLInfo>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算项目FL优先级评分
|
||
/// 【业务逻辑】:基于FL关联情况进行多维度评分
|
||
/// 评分策略:
|
||
/// - 本项目FL:100分(最高优先级)
|
||
/// - 有其他项目FL经验:85分(有经验,但需要适应)
|
||
/// - 无FL经验:70分(可以培养,但优先级较低)
|
||
/// - 多项目FL:95分(经验丰富,适应性强)
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="currentProjectNumber">当前项目号</param>
|
||
/// <param name="isProjectFL">是否为当前项目FL</param>
|
||
/// <param name="allProjectFLs">所有项目FL关联情况</param>
|
||
/// <param name="rule">规则实体(可能包含评分权重配置)</param>
|
||
/// <returns>评分结果,包含分数和详细原因</returns>
|
||
private ProjectFLScoringResult CalculateProjectFLPriorityScore(long personnelId, string currentProjectNumber,
|
||
bool isProjectFL, List<ProjectFLInfo> allProjectFLs, ShiftRuleEntity rule)
|
||
{
|
||
try
|
||
{
|
||
// 基础评分逻辑
|
||
if (isProjectFL)
|
||
{
|
||
// 本项目FL获得最高优先级
|
||
return new ProjectFLScoringResult
|
||
{
|
||
Score = 100,
|
||
Reason = $"本项目FL成员,具有专业经验和项目知识,优先分配"
|
||
};
|
||
}
|
||
|
||
// 非本项目FL的评分策略
|
||
if (allProjectFLs.Any())
|
||
{
|
||
// 有其他项目FL经验
|
||
int projectCount = allProjectFLs.Count;
|
||
DateTime latestAssignment = allProjectFLs.Max(p => p.LatestAssignmentDate);
|
||
int daysSinceLastAssignment = (DateTime.Now - latestAssignment).Days;
|
||
|
||
double score = 85; // 基础分
|
||
|
||
// 多项目经验加分
|
||
if (projectCount >= 3)
|
||
{
|
||
score += 10; // 多项目经验丰富
|
||
}
|
||
else if (projectCount == 2)
|
||
{
|
||
score += 5; // 有跨项目经验
|
||
}
|
||
|
||
// 最近活跃度调整
|
||
if (daysSinceLastAssignment <= 30)
|
||
{
|
||
score += 5; // 最近活跃
|
||
}
|
||
else if (daysSinceLastAssignment > 180)
|
||
{
|
||
score -= 10; // 长期未参与FL工作
|
||
}
|
||
|
||
// 确保评分在合理范围内
|
||
score = Math.Min(95, Math.Max(70, score));
|
||
|
||
return new ProjectFLScoringResult
|
||
{
|
||
Score = score,
|
||
Reason = $"有{projectCount}个项目FL经验,最近参与时间:{latestAssignment:yyyy-MM-dd},具有一定项目经验"
|
||
};
|
||
}
|
||
else
|
||
{
|
||
// 无FL经验,但仍可培养
|
||
return new ProjectFLScoringResult
|
||
{
|
||
Score = 70,
|
||
Reason = "暂无项目FL经验,可作为培养对象适当分配"
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "计算项目FL优先级评分异常 - 人员ID:{PersonnelId}, 项目:{ProjectNumber}",
|
||
personnelId, currentProjectNumber);
|
||
return new ProjectFLScoringResult
|
||
{
|
||
Score = 75,
|
||
Reason = $"评分计算异常,采用默认中等评分:{ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成项目FL验证信息
|
||
/// 【业务逻辑】:生成详细的验证结果说明,帮助理解分配决策
|
||
/// </summary>
|
||
/// <param name="personnelId">人员ID</param>
|
||
/// <param name="projectNumber">项目号</param>
|
||
/// <param name="isProjectFL">是否为项目FL</param>
|
||
/// <param name="score">评分</param>
|
||
/// <param name="reason">评分原因</param>
|
||
/// <returns>详细的验证信息</returns>
|
||
private string GenerateProjectFLValidationMessage(long personnelId, string projectNumber,
|
||
bool isProjectFL, double score, string reason)
|
||
{
|
||
string priorityLevel = score switch
|
||
{
|
||
>= 95 => "最高优先级",
|
||
>= 85 => "高优先级",
|
||
>= 75 => "中等优先级",
|
||
_ => "低优先级"
|
||
};
|
||
|
||
return $"【项目FL优先分配验证】" + Environment.NewLine +
|
||
$"• 人员ID:{personnelId}" + Environment.NewLine +
|
||
$"• 目标项目:{projectNumber}" + Environment.NewLine +
|
||
$"• 是否本项目FL:{(isProjectFL ? "是" : "否")}" + Environment.NewLine +
|
||
$"• 优先级评分:{score:F1}分({priorityLevel})" + Environment.NewLine +
|
||
$"• 评分依据:{reason}" + Environment.NewLine +
|
||
$"• 分配建议:{GetAllocationRecommendation(score, isProjectFL)}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取分配建议
|
||
/// 【业务逻辑】:基于评分提供分配建议
|
||
/// </summary>
|
||
private string GetAllocationRecommendation(double score, bool isProjectFL)
|
||
{
|
||
if (isProjectFL)
|
||
{
|
||
return "强烈推荐分配,本项目FL具有最佳匹配度";
|
||
}
|
||
else if (score >= 85)
|
||
{
|
||
return "推荐分配,有相关项目经验,适应性较强";
|
||
}
|
||
else if (score >= 75)
|
||
{
|
||
return "可以分配,需要适当培训和指导";
|
||
}
|
||
else
|
||
{
|
||
return "谨慎分配,建议优先考虑其他候选人";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 项目FL信息
|
||
/// </summary>
|
||
private class ProjectFLInfo
|
||
{
|
||
public string ProjectNumber { get; set; }
|
||
public int TaskCount { get; set; }
|
||
public DateTime LatestAssignmentDate { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 项目FL评分结果
|
||
/// </summary>
|
||
private class ProjectFLScoringResult
|
||
{
|
||
public double Score { get; set; }
|
||
public string Reason { get; set; }
|
||
}
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
}
|
||
} |