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

Spring Boot+Timefold动态约束实现及代码示例问询

在Spring Boot + Timefold中实现动态约束(开关+权重)

核心思路

ConstraintProvider的defineConstraints方法仅在Timefold初始化时执行一次,无法通过修改Provider实例动态调整约束。正确的方式是利用ConstraintConfiguration动态控制约束的启用状态和权重,同时在约束定义中加入基于配置的过滤逻辑。

1. ConstraintConfiguration的作用

它是Timefold提供的用于动态配置约束的工具类,核心能力:

  • 为每个约束定义可配置的权重值(替代固定的ONE_HARD等硬编码值)
  • 控制每个约束是否启用
  • 支持每个求解请求携带独立配置,实现请求级别的动态约束调整

2. 定义ConstraintConfiguration实体

创建对应所有约束的配置类,包含启用状态和权重:

import org.optaplanner.core.api.score.constraint.ConstraintConfiguration;
import org.optaplanner.core.api.score.constraint.ConstraintWeight;
import org.optaplanner.core.api.score.hardmediumsoft.HardMediumSoftScore;

@ConstraintConfiguration(constraintPackage = "com.yourpackage.planning") // 需与ConstraintProvider所在包一致
public class TaskConstraintConfiguration {

    // NO_MISSING_SKILL约束配置
    private boolean noMissingSkillEnabled = true;
    @ConstraintWeight("NO_MISSING_SKILL")
    private HardMediumSoftScore noMissingSkillWeight = HardMediumSoftScore.ONE_HARD;

    // LOCATION约束配置
    private boolean locationEnabled = true;
    @ConstraintWeight("LOCATION")
    private HardMediumSoftScore locationWeight = HardMediumSoftScore.ONE_HARD;

    // EMPLOYEE_TASK_OVERLAP约束配置
    private boolean employeeTaskOverlapEnabled = true;
    @ConstraintWeight("EMPLOYEE_TASK_OVERLAP")
    private HardMediumSoftScore employeeTaskOverlapWeight = HardMediumSoftScore.ONE_MEDIUM;

    // Getter & Setter
    public boolean isNoMissingSkillEnabled() { return noMissingSkillEnabled; }
    public void setNoMissingSkillEnabled(boolean noMissingSkillEnabled) { this.noMissingSkillEnabled = noMissingSkillEnabled; }
    public HardMediumSoftScore getNoMissingSkillWeight() { return noMissingSkillWeight; }
    public void setNoMissingSkillWeight(HardMediumSoftScore noMissingSkillWeight) { this.noMissingSkillWeight = noMissingSkillWeight; }

    public boolean isLocationEnabled() { return locationEnabled; }
    public void setLocationEnabled(boolean locationEnabled) { this.locationEnabled = locationEnabled; }
    public HardMediumSoftScore getLocationWeight() { return locationWeight; }
    public void setLocationWeight(HardMediumSoftScore locationWeight) { this.locationWeight = locationWeight; }

    public boolean isEmployeeTaskOverlapEnabled() { return employeeTaskOverlapEnabled; }
    public void setEmployeeTaskOverlapEnabled(boolean employeeTaskOverlapEnabled) { this.employeeTaskOverlapEnabled = employeeTaskOverlapEnabled; }
    public HardMediumSoftScore getEmployeeTaskOverlapWeight() { return employeeTaskOverlapWeight; }
    public void setEmployeeTaskOverlapWeight(HardMediumSoftScore employeeTaskOverlapWeight) { this.employeeTaskOverlapWeight = employeeTaskOverlapWeight; }
}

3. 改造ConstraintProvider

使用penalizeConfigurable替代固定权重,并加入基于配置的启用判断:

import org.optaplanner.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore;
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 org.springframework.stereotype.Component;

@Component
public class PlanningConstraintsProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[]{
                noMissingSkills(constraintFactory),
                employeeTaskOverlapConstraint(constraintFactory),
                locationConstraint(constraintFactory)
        };
    }

    private Constraint noMissingSkills(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Task.class)
                // 仅当约束启用且任务存在技能缺失时生效
                .filter(task -> task.getConstraintConfiguration().isNoMissingSkillEnabled() 
                        && task.getMissingSkillCount() > 0)
                // 从配置中读取权重
                .penalizeConfigurable(HardMediumSoftScore.class, Task::getMissingSkillCount)
                .asConstraint("NO_MISSING_SKILL");
    }

    private Constraint locationConstraint(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Task.class)
                // 仅当约束启用时生效
                .filter(task -> task.getConstraintConfiguration().isLocationEnabled())
                .penalizeConfigurable(HardMediumSoftScore.class,
                        task -> calculateWeightLocation(task.getAssignedEmployee().getLocation(), task.getLocation()))
                .asConstraint("LOCATION");
    }

    private Constraint employeeTaskOverlapConstraint(ConstraintFactory constraintFactory) {
        return constraintFactory.forEachUniquePair(Task.class, 
                        Joiners.equal(Task::getAssignedEmployee),
                        Joiners.overlapping(Task::getStartTime, Task::getEndTime))
                .filter((task1, task2) -> task1.getConstraintConfiguration().isEmployeeTaskOverlapEnabled())
                .penalizeConfigurable(HardMediumSoftScore.class)
                .asConstraint("EMPLOYEE_TASK_OVERLAP");
    }

    private int calculateWeightLocation(String employeeLocation, String taskLocation) {
        return employeeLocation.equals(taskLocation) ? 0 : 1;
    }
}

4. 修改PlanningSolution类

将ConstraintConfiguration作为Solution的一部分,确保每个求解请求的配置能被约束读取:

import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore;

import java.util.List;

@PlanningSolution
public class TaskAssignmentSolution {

    private TaskConstraintConfiguration constraintConfiguration;

    @ProblemFactCollectionProperty
    private List<Employee> employeeList;

    @PlanningEntityCollectionProperty
    private List<Task> taskList;

    @PlanningScore
    private HardMediumSoftScore score;

    // Getter & Setter
    public TaskConstraintConfiguration getConstraintConfiguration() { return constraintConfiguration; }
    public void setConstraintConfiguration(TaskConstraintConfiguration constraintConfiguration) { 
        this.constraintConfiguration = constraintConfiguration; 
        // 将配置传递给每个Task,确保约束能读取到
        taskList.forEach(task -> task.setConstraintConfiguration(constraintConfiguration));
    }

    // 其他Getter/Setter省略
}

5. Spring Boot控制器处理API请求

接收前端传入的约束配置,绑定到Solution后调用SolveManager求解:

import org.optaplanner.core.api.solver.SolverManager;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TaskAssignmentController {

    private final SolverManager<TaskAssignmentSolution, Long> solverManager;

    public TaskAssignmentController(SolverManager<TaskAssignmentSolution, Long> solverManager) {
        this.solverManager = solverManager;
    }

    @PostMapping("/solve")
    public TaskAssignmentSolution solve(@RequestBody SolveRequest request) {
        // 1. 构建基础求解模型(从数据库/请求参数中获取员工、任务数据)
        TaskAssignmentSolution solution = buildBaseSolution(request.getEmployeeIds(), request.getTaskIds());

        // 2. 绑定前端传入的约束配置
        TaskConstraintConfiguration config = new TaskConstraintConfiguration();
        config.setNoMissingSkillEnabled(request.isNoMissingSkillEnabled());
        config.setNoMissingSkillWeight(HardMediumSoftScore.ofHard(request.getNoMissingSkillWeight()));
        config.setLocationEnabled(request.isLocationEnabled());
        config.setLocationWeight(HardMediumSoftScore.ofHard(request.getLocationWeight()));
        config.setEmployeeTaskOverlapEnabled(request.isEmployeeTaskOverlapEnabled());
        config.setEmployeeTaskOverlapWeight(HardMediumSoftScore.ofMedium(request.getEmployeeTaskOverlapWeight()));

        solution.setConstraintConfiguration(config);

        // 3. 执行求解(同步示例,也可使用异步solveAndListen)
        return solverManager.solve(1L, solution);
    }

    // 构建基础求解模型的辅助方法,根据业务实现
    private TaskAssignmentSolution buildBaseSolution(List<Long> employeeIds, List<Long> taskIds) {
        TaskAssignmentSolution solution = new TaskAssignmentSolution();
        // 从数据库查询并设置员工、任务列表
        // solution.setEmployeeList(...);
        // solution.setTaskList(...);
        return solution;
    }

    // 请求DTO,接收前端参数
    public static class SolveRequest {
        private List<Long> employeeIds;
        private List<Long> taskIds;
        // 约束配置参数
        private boolean noMissingSkillEnabled;
        private int noMissingSkillWeight;
        private boolean locationEnabled;
        private int locationWeight;
        private boolean employeeTaskOverlapEnabled;
        private int employeeTaskOverlapWeight;

        // Getter & Setter省略
    }
}

关键注意事项

  • ConstraintConfiguration的constraintPackage必须与ConstraintProvider所在包一致,否则Timefold无法匹配约束与配置。
  • 每个PlanningEntity(如Task)需持有ConstraintConfiguration引用,确保约束过滤逻辑能读取到配置。
  • 不要尝试修改ConstraintProvider实例,它是单例且初始化后不会重新执行defineConstraints。

内容的提问来源于stack exchange,提问作者MZ7

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 19:14:54