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
相关产品推荐
相关产品推荐

