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

如何在OptaPlanner中指定固定员工轮班模式?

员工固定轮班模式实现指导(OptaWeb)

问题核心

需实现3白班→3夜班→3休息的固定轮班循环,现有约束因未处理日期顺序、未明确轮班序列逻辑,导致排班结果随机,且缺少休息规则的强制约束。

现有约束的问题

  1. 无日期排序逻辑:join关联的Slot未按日期排序,校验的并非连续日期的班次,逻辑完全失效。
  2. 仅限制班次数量,未强制序列:只检查了同班次的连续数量,未约束白班之后必须转夜班、夜班之后必须转休息的顺序。
  3. 缺失休息规则:未实现“连续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);
    }
}

关键注意事项

  1. 硬约束设置:将上述约束的惩罚值设为足够高(如100以上),确保OptaPlanner优先满足这些规则,避免排班随机。
  2. 日期字段正确性:确保SlotDAO的日期字段准确,排序逻辑能正确识别连续日期。
  3. 测试验证:先验证单个员工的排班是否符合3白→3夜→3休的循环,再扩展到多员工场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 10:54:51