Timefold能否解决多项目多阶段人员调度场景?求可行方案
用Timefold解决项目集阶段调度与工程师分配问题
结论
完全可以用Timefold解决这个场景——它本质是带资源约束的项目调度优化问题,正好匹配Timefold擅长的约束满足与组合优化能力,能同时处理项目依赖排序、技能匹配、时间可用性等多维度约束。
可行方案核心思路
1. 规划建模
核心实体
- 项目阶段任务:将每个项目的Plan/Design/Execute/Deploy拆分为独立任务实体,包含属性:
- 基础信息:项目ID、阶段类型、依赖的前置任务、所需技能集、总工作量(小时)
- 规划变量:任务开始时间(决定执行顺序)、分配的工程师列表(多值变量,满足技能与时间要求)
- 工程师资源:作为调度资源,包含属性:ID、技能集、每日可用小时、休假日期集合
约束定义
分硬约束(必须满足,违反则方案无效)和软约束(优化目标,提升方案质量):
- 硬约束:
- 依赖约束:前置阶段任务未完成时,后续阶段不能启动
- 技能覆盖:任务所需技能集必须被分配的工程师组合完全覆盖
- 工程师时间合规:任务执行期间,工程师不能处于休假状态,且单日工作时长不超过可用上限
- 周期约束:所有任务必须在2023-01-01至2023-06-30内完成
- 软约束:
- 工程师负载平衡:尽量让各工程师总工作量差异最小
- 项目集总工期最短:在满足硬约束的前提下,尽量提前完成所有项目
- 技能精准匹配:优先分配技能完全覆盖任务需求的工程师
简化代码示例
实体类定义(Java)
import org.optaplanner.core.api.domain.entity.PlanningEntity; import org.optaplanner.core.api.domain.variable.PlanningVariable; import java.time.LocalDateTime; import java.util.List; import java.util.Set; // 项目阶段任务实体 @PlanningEntity public class ProjectPhaseTask { private Long taskId; private String projectId; private PhaseType phase; private Set<String> requiredSkills; private int totalWorkHours; private ProjectPhaseTask predecessorTask; // 规划变量:任务开始时间 @PlanningVariable(valueRangeProviderRefs = "timeRange") private LocalDateTime startTime; // 规划变量:分配的工程师列表 @PlanningVariable(valueRangeProviderRefs = "engineerRange", nullable = false) private List<Engineer> assignedEngineers; // 计算任务结束时间(按工程师总可用产能折算) public LocalDateTime getEndTime() { if (startTime == null || assignedEngineers.isEmpty()) return null; int totalDailyCapacity = assignedEngineers.stream() .mapToInt(Engineer::getDailyAvailableHours) .sum(); int daysNeeded = (int) Math.ceil((double) totalWorkHours / totalDailyCapacity); return startTime.plusDays(daysNeeded); } // Getter、Setter省略 } // 工程师资源类 public class Engineer { private Long engineerId; private Set<String> skills; private int dailyAvailableHours; private Set<LocalDate> leaveDates; // 判断工程师某日期是否可用 public boolean isAvailableOn(LocalDate date) { return !leaveDates.contains(date); } // Getter、Setter省略 } // 规划解决方案类 import org.optaplanner.core.api.domain.solution.PlanningSolution; import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty; import org.optaplanner.core.api.domain.value.ValueRangeProvider; import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; import org.optaplanner.core.api.domain.score.PlanningScore; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Stream; @PlanningSolution public class ProjectPortfolioSolution { // 时间范围:2023-01-01至2023-06-30,按天粒度 @ValueRangeProvider(id = "timeRange") public List<LocalDateTime> getTimeRange() { return Stream.iterate(LocalDateTime.of(2023, 1, 1, 9, 0), date -> date.plusDays(1)) .limit(182) .toList(); } // 工程师资源范围 @ValueRangeProvider(id = "engineerRange") public List<Engineer> getEngineers() { return engineers; } @PlanningEntityCollectionProperty private List<ProjectPhaseTask> tasks; private List<Engineer> engineers; @PlanningScore private HardSoftScore score; // Getter、Setter省略 } // 阶段枚举 enum PhaseType { Plan, Design, Execute, Deploy }
约束规则定义
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; import org.optaplanner.core.api.score.stream.Constraint; import org.optaplanner.core.api.score.stream.ConstraintFactory; import org.optaplanner.core.api.score.stream.ConstraintProvider; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; public class ProjectPortfolioConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory cf) { return new Constraint[] { // 硬约束:前置任务未完成,当前任务不能启动 predecessorDependency(cf), // 硬约束:任务技能必须被完全覆盖 skillCoverage(cf), // 硬约束:工程师任务期间不能休假 engineerLeaveConstraint(cf), // 硬约束:所有任务必须在计划周期内完成 withinPlanPeriod(cf), // 软约束:平衡工程师负载 balanceEngineerLoad(cf), // 软约束:缩短项目集总工期 minimizeTotalDuration(cf) }; } private Constraint predecessorDependency(ConstraintFactory cf) { return cf.forEach(ProjectPhaseTask.class) .filter(task -> task.getPredecessorTask() != null) .filter(task -> task.getStartTime().isBefore(task.getPredecessorTask().getEndTime())) .penalize("Predecessor not completed", HardSoftScore.ONE_HARD); } private Constraint skillCoverage(ConstraintFactory cf) { return cf.forEach(ProjectPhaseTask.class) .filter(task -> { Set<String> assignedSkills = task.getAssignedEngineers().stream() .flatMap(e -> e.getSkills().stream()) .collect(Collectors.toSet()); return !task.getRequiredSkills().stream().allMatch(assignedSkills::contains); }) .penalize("Skill not covered", HardSoftScore.ONE_HARD); } private Constraint engineerLeaveConstraint(ConstraintFactory cf) { return cf.forEach(ProjectPhaseTask.class) .join(Engineer.class, Joiners.equal(ProjectPhaseTask::getAssignedEngineers, engineer -> Collections.singletonList(engineer))) .filter((task, engineer) -> { LocalDate startDate = task.getStartTime().toLocalDate(); LocalDate endDate = task.getEndTime().toLocalDate(); return startDate.datesUntil(endDate.plusDays(1)) .anyMatch(date -> !engineer.isAvailableOn(date)); }) .penalize("Engineer on leave during task", HardSoftScore.ONE_HARD); } private Constraint withinPlanPeriod(ConstraintFactory cf) { LocalDateTime planEnd = LocalDateTime.of(2023, 6, 30, 17, 0); return cf.forEach(ProjectPhaseTask.class) .filter(task -> task.getEndTime().isAfter(planEnd)) .penalize("Task exceeds plan period", HardSoftScore.ONE_HARD); } private Constraint balanceEngineerLoad(ConstraintFactory cf) { return cf.forEach(Engineer.class) .groupBy(engineer -> engineer, ConstraintCollectors.sum(task -> calculateEngineerTaskHours(task, engineer))) .join(Engineer.class) .filter((e1, totalHours1, e2, totalHours2) -> totalHours1 > totalHours2 + 8) .penalize("Engineer load imbalance", HardSoftScore.ONE_SOFT); } private int calculateEngineerTaskHours(ProjectPhaseTask task, Engineer engineer) { if (!task.getAssignedEngineers().contains(engineer)) return 0; int totalDailyCapacity = task.getAssignedEngineers().stream() .mapToInt(Engineer::getDailyAvailableHours) .sum(); int daysNeeded = (int) Math.ceil((double) task.getTotalWorkHours() / totalDailyCapacity); return daysNeeded * engineer.getDailyAvailableHours(); } private Constraint minimizeTotalDuration(ConstraintFactory cf) { return cf.forEach(ProjectPhaseTask.class) .groupBy(ConstraintCollectors.max(ProjectPhaseTask::getEndTime)) .penalize("Long total duration", HardSoftScore.ONE_SOFT, maxEndTime -> maxEndTime.getDayOfYear() - LocalDateTime.of(2023,1,1).getDayOfYear()); } }
求解执行代码
import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; import java.time.LocalDate; import java.util.List; import java.util.Set; public class ProjectPortfolioOptimizer { public static void main(String[] args) { // 初始化模拟数据 List<Engineer> engineers = initEngineers(); List<ProjectPhaseTask> tasks = initProjectPhaseTasks(); // 构建初始解决方案 ProjectPortfolioSolution initialSolution = new ProjectPortfolioSolution(); initialSolution.setEngineers(engineers); initialSolution.setTasks(tasks); // 配置并启动求解器 SolverFactory<ProjectPortfolioSolution> solverFactory = SolverFactory.create( SolverConfig.createFromXmlResource("projectPortfolioSolverConfig.xml") ); Solver<ProjectPortfolioSolution> solver = solverFactory.buildSolver(); ProjectPortfolioSolution bestSolution = solver.solve(initialSolution); // 输出最优方案结果 bestSolution.getTasks().forEach(task -> { System.out.printf("项目%s-%s:开始时间[%s],结束时间[%s],分配工程师ID:%s%n", task.getProjectId(), task.getPhase(), task.getStartTime(), task.getEndTime(), task.getAssignedEngineers().stream().map(Engineer::getEngineerId).toList()); }); } // 模拟初始化工程师数据 private static List<Engineer> initEngineers() { Engineer e1 = new Engineer(); e1.setEngineerId(1L); e1.setSkills(Set.of("Planning", "Design")); e1.setDailyAvailableHours(8); e1.setLeaveDates(Set.of(LocalDate.of(2023, 2, 10))); Engineer e2 = new Engineer(); e2.setEngineerId(2L); e2.setSkills(Set.of("Execute", "Deploy")); e2.setDailyAvailableHours(8); e2.setLeaveDates(Set.of()); return List.of(e1, e2); } // 模拟初始化项目阶段任务(含依赖) private static List<ProjectPhaseTask> initProjectPhaseTasks() { ProjectPhaseTask p1Plan = new ProjectPhaseTask(); p1Plan.setTaskId(1L); p1Plan.setProjectId("P1"); p1Plan.setPhase(PhaseType.Plan); p1Plan.setRequiredSkills(Set.of("Planning")); p1Plan.setTotalWorkHours(40); ProjectPhaseTask p1Design = new ProjectPhaseTask(); p1Design.setTaskId(2L); p1Design.setProjectId("P1"); p1Design.setPhase(PhaseType.Design); p1Design.setRequiredSkills(Set.of("Design")); p1Design.setTotalWorkHours(80); p1Design.setPredecessorTask(p1Plan); // 可继续添加其他项目任务及依赖关系 return List.of(p1Plan, p1Design); } }
内容的提问来源于stack exchange,提问作者dskumar101
相关产品推荐
相关产品推荐

