如何在OptaPlanner中指定固定员工轮班模式?
员工固定轮班模式实现指导(OptaWeb)
问题核心
需实现3白班→3夜班→3休息的固定轮班循环,现有约束因未处理日期顺序、未明确轮班序列逻辑,导致排班结果随机,且缺少休息规则的强制约束。
现有约束的问题
- 无日期排序逻辑:join关联的Slot未按日期排序,校验的并非连续日期的班次,逻辑完全失效。
- 仅限制班次数量,未强制序列:只检查了同班次的连续数量,未约束白班之后必须转夜班、夜班之后必须转休息的顺序。
- 缺失休息规则:未实现“连续3天休息”的强制约束,也未限制休息后的轮班类型。
解决方案思路
1. 引入Shadow Variable跟踪轮班阶段
用Shadow Variable记录员工当前处于轮班循环的哪个阶段(如第2天白班、第3天夜班等),避免重复排序和关联Slot,提升约束效率。
步骤1:定义轮班阶段枚举
public enum ShiftCyclePhase { DAY_SHIFT_1, DAY_SHIFT_2, DAY_SHIFT_3, NIGHT_SHIFT_1, NIGHT_SHIFT_2, NIGHT_SHIFT_3, REST_1, REST_2, REST_3 }
步骤2:给Employee添加Shadow Variable
@PlanningEntity public class Employee { // 原有字段... @ShadowVariable(sourceVariableName = "provisionalEmployee", variableListenerClass = EmployeeShiftCyclePhaseListener.class) private ShiftCyclePhase currentCyclePhase; // getter、setter方法 }
步骤3:实现VariableListener计算轮班阶段
public class EmployeeShiftCyclePhaseListener implements VariableListener<SlotDAO> { @Override public void afterVariableChanged(ScoreDirector scoreDirector, SlotDAO slot) { Employee employee = slot.getProvisionalEmployee(); if (employee == null) { return; } // 获取该员工已分配的所有Slot,按日期升序排序 List<SlotDAO> employeeSlots = scoreDirector.getWorkingSolution().getSlotList().stream() .filter(s -> employee.equals(s.getProvisionalEmployee())) .sorted(Comparator.comparing(SlotDAO::getDate)) .collect(Collectors.toList()); // 计算当前轮班阶段:9天为一个循环,取模后对应阶段 int cycleIndex = employeeSlots.size() % 9; ShiftCyclePhase phase = switch (cycleIndex) { case 0 -> ShiftCyclePhase.DAY_SHIFT_1; case 1 -> ShiftCyclePhase.DAY_SHIFT_2; case 2 -> ShiftCyclePhase.DAY_SHIFT_3; case 3 -> ShiftCyclePhase.NIGHT_SHIFT_1; case 4 -> ShiftCyclePhase.NIGHT_SHIFT_2; case 5 -> ShiftCyclePhase.NIGHT_SHIFT_3; case 6 -> ShiftCyclePhase.REST_1; case 7 -> ShiftCyclePhase.REST_2; case 8 -> ShiftCyclePhase.REST_3; default -> null; }; // 更新Shadow Variable scoreDirector.beforeVariableChanged(employee, "currentCyclePhase"); employee.setCurrentCyclePhase(phase); scoreDirector.afterVariableChanged(employee, "currentCyclePhase"); } // 以下方法默认空实现即可 @Override public void beforeVariableChanged(ScoreDirector scoreDirector, SlotDAO slot) {} @Override public void afterEntityAdded(ScoreDirector scoreDirector, SlotDAO slot) {} @Override public void afterEntityRemoved(ScoreDirector scoreDirector, SlotDAO slot) {} @Override public void beforeEntityAdded(ScoreDirector scoreDirector, SlotDAO slot) {} @Override public void beforeEntityRemoved(ScoreDirector scoreDirector, SlotDAO slot) {} }
2. 基于Shadow Variable编写约束
通过轮班阶段与实际班次的匹配校验,强制遵循轮班序列:
public class ShiftPatternConstraints { // 约束名称常量 private static final String PHASE_SHIFT_MATCH = "PhaseMatchesShift"; private static final String MAX_CONSECUTIVE_SAME_SHIFT = "MaxConsecutiveSameShift"; private static final String REST_PHASE_ENFORCE = "RestPhaseEnforce"; // 核心约束:轮班阶段必须匹配对应班次类型 public Constraint phaseMatchesShift(ConstraintFactory factory) { return factory.forEach(SlotDAO.class) .filter(slot -> slot.getProvisionalEmployee() != null) .filter(slot -> { ShiftCyclePhase phase = slot.getProvisionalEmployee().getCurrentCyclePhase(); if (phase == null) return false; String shiftId = slot.getClientShiftId(); // 白班阶段必须分配白班(替换为实际白班ID) if (phase.name().startsWith("DAY_SHIFT")) { return !"DAY_SHIFT_ID".equals(shiftId); } // 夜班阶段必须分配夜班(替换为实际夜班ID) if (phase.name().startsWith("NIGHT_SHIFT")) { return !"NIGHT_SHIFT_ID".equals(shiftId); } // 休息阶段不得分配员工(或匹配休息班次ID) return phase.name().startsWith("REST"); }) .penalizeConfigurableLong(PHASE_SHIFT_MATCH, 100); // 高惩罚确保为硬约束 } // 兜底约束:禁止连续4天同类型班次 public Constraint noMoreThan3ConsecutiveSameShifts(ConstraintFactory factory) { return factory.forEach(SlotDAO.class) .join(SlotDAO.class, equal(SlotDAO::getProvisionalEmployee), slot -> slot.getDate().plusDays(1), equal(SlotDAO::getDate)) .join(SlotDAO.class, equal((s1, s2) -> s2.getProvisionalEmployee()), (s1, s2) -> s2.getDate().plusDays(1), equal(SlotDAO::getDate)) .join(SlotDAO.class, equal((s1, s2, s3) -> s3.getProvisionalEmployee()), (s1, s2, s3) -> s3.getDate().plusDays(1), equal(SlotDAO::getDate)) .filter((s1, s2, s3, s4) -> s1.getClientShiftId().equals(s2.getClientShiftId()) && s2.getClientShiftId().equals(s3.getClientShiftId()) && s3.getClientShiftId().equals(s4.getClientShiftId())) .penalizeConfigurableLong(MAX_CONSECUTIVE_SAME_SHIFT, 200); } // 强制休息阶段无排班 public Constraint enforceRestPhase(ConstraintFactory factory) { return factory.forEach(Employee.class) .join(SlotDAO.class, equal(Employee::getId, SlotDAO::getProvisionalEmployeeId)) .filter((emp, slot) -> { ShiftCyclePhase phase = emp.getCurrentCyclePhase(); return phase != null && phase.name().startsWith("REST"); }) .penalizeConfigurableLong(REST_PHASE_ENFORCE, 150); } }
关键注意事项
- 硬约束设置:将上述约束的惩罚值设为足够高(如100以上),确保OptaPlanner优先满足这些规则,避免排班随机。
- 日期字段正确性:确保
SlotDAO的日期字段准确,排序逻辑能正确识别连续日期。 - 测试验证:先验证单个员工的排班是否符合
3白→3夜→3休的循环,再扩展到多员工场景。
内容的提问来源于stack exchange,提问作者Terblanche Daniel
相关产品推荐
相关产品推荐

