You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 00:24:51