摘要:近期一个老客户过来咨询,在人员排班这里,原来是通过人工方式来进行的,但是由于人力资源有限,想要通过软件实现自动排班。排班自动化,一直是个难题,作者君连夜请教了AI助手,寻找有无快速可用的代码生成或者思路。
近期一个老客户过来咨询,在人员排班这里,原来是通过人工方式来进行的,但是由于人力资源有限,想要通过软件实现自动排班。排班自动化,一直是个难题,作者君连夜请教了AI助手,寻找有无快速可用的代码生成或者思路。
先上结果:本次使用的是腾讯元宝,元宝给出的代码运行报错,最后是以元宝的方案为基础,自行编写代码实现算法。
// 员工类(包含所属岗位)public class Employee {private final String id;private final String name;private final String position; // 所属岗位public Employee(String id, String name, String position) {this.id = id;this.name = name;this.position = position;}// Getter方法public String getId { return id; }public String getName { return name; }public String getPosition { return position; }@Overridepublic String toString {return "Employee{" +"id='" + id + '\'' +", name='" + name + '\'' +", position='" + position + '\'' +'}';}}public class ScheduleDateResult {private String group;private LocalDate date;private ShiftType shiftType;public ScheduleDateResult(String group, LocalDate date, ShiftType shiftType) {this.group = group;this.date = date;this.shiftType = shiftType;}public String getGroup {return group;}public void setGroup(String group) {this.group = group;}public LocalDate getDate {return date;}public void setDate(LocalDate date) {this.date = date;}public ShiftType getShiftType {return shiftType;}public void setShiftType(ShiftType shiftType) {this.shiftType = shiftType;}@Overridepublic String toString {return group +","+date+","+shiftType;}}public class ScheduleGenerator {public List generate(ScheduleConfig config) {validateConfig(config);List allEntries = new ArrayList;LocalDate startDate = config.getStartDate;int daysInMonth = startDate.lengthOfMonth;// 生成休息日Map> restDaysMap = generateRestDays(config.getEmployees, daysInMonth, startDate);// 初始化员工排班表(标记休息日,工作日为null)Map> employeeSchedules = initializeSchedules(config.getEmployees, restDaysMap, daysInMonth, startDate);// 按日分配班次for (int day = 1; day > generateRestDays(List employees, int daysInMonth, LocalDate startDate) {Map> restDaysMap = new HashMap;Random random = new Random;for (Employee emp : employees) {Set restDays = new HashSet;int cycleOffset = random.nextInt(15); // 每个员工不同的周期偏移for (int day = 1; day = 13 && cyclePos > initializeSchedules(List employees, Map> restDaysMap,int daysInMonth, LocalDate startDate) {Map> schedules = new HashMap;for (Employee emp : employees) {Map empSchedule = new HashMap;for (int day = 1; day > restDaysMap,Map> employeeSchedules,List allEntries) {Map> positionGroups = config.getEmployees.stream.collect(Collectors.groupingBy(Employee::getPosition));for (String position : config.getMinDay.keySet) {List positionEmps = positionGroups.getOrDefault(position, Collections.emptyList);if (positionEmps.isEmpty) continue;// 当日工作的员工(非休息)List workingEmps = positionEmps.stream.filter(emp -> !restDaysMap.get(emp).contains(date)).collect(Collectors.toList);// 前一天各员工的班次(用于连续校验)Map prevDayShifts = new HashMap;LocalDate prevDate = date.minusDays(1);for (Employee emp : workingEmps) {if (prevDate.getMonthValue == date.getMonthValue && prevDate.getYear == date.getYear) {// 前一天在当月内prevDayShifts.put(emp, employeeSchedules.get(emp).get(prevDate));} else {// 前一天跨月,视为休息prevDayShifts.put(emp, ShiftType.REST);}}// 分配白班和夜班assignShiftsForPosition(date, position, workingEmps, prevDayShifts,config.getMinDay.get(position), config.getMinNight.get(position),employeeSchedules, allEntries, config);}}private void assignShiftsForPosition(LocalDate date, String position,List workingEmps,Map prevDayShifts,int minDay, int minNight,Map> employeeSchedules,List allEntries, ScheduleConfig config) {// 分离不能上白班的员工(前一天白班)List cannotDay = workingEmps.stream.filter(emp -> prevDayShifts.get(emp) == ShiftType.DAY).collect(Collectors.toList);// 分离不能上夜班的员工(前一天夜班且不允许连续)List cannotNight = workingEmps.stream.filter(emp -> prevDayShifts.get(emp) == ShiftType.NIGHT&& config.getMaxConsecutiveSameShift == 0).collect(Collectors.toList);// 可用员工池List availableDay = new ArrayList(workingEmps);availableDay.removeAll(cannotDay);List availableNight = new ArrayList(workingEmps);availableNight.removeAll(cannotNight);// 分配白班(优先满足最小人数)if (availableDay.size workingEmps, LocalDate date,Map> employeeSchedules,Map prevDayShifts,List allEntries) {List unassigned = new ArrayList;for (Employee emp : workingEmps) {if (employeeSchedules.get(emp).get(date) == null) {unassigned.add(emp);}}// 按前一天班次排序,优先分配相反班次unassigned.sort(Comparator.comparing(emp -> prevDayShifts.getOrDefault(emp, ShiftType.REST)));int half = unassigned.size / 2;for (int i = 0; i positions = config.getEmployees.stream.map(Employee::getPosition).collect(Collectors.toSet);if (!positions.equals(config.getMinDay.keySet) ||!positions.equals(config.getMinNight.keySet)) {throw new IllegalArgumentException("岗位配置不一致");}for (String pos : positions) {long totalEmps = config.getEmployees.stream.filter(emp -> emp.getPosition.equals(pos)).count;int required = config.getMinDay.get(pos) + config.getMinNight.get(pos);if (required > totalEmps) {throw new IllegalArgumentException(String.format("岗位[%s]白班+夜班人数超过总员工数(需要%d,实际%d)", pos, required, totalEmps));}}}private void validateFinalSchedule(List entries, ScheduleConfig config, int daysInMonth) {// 按日期和岗位统计班次人数Map>> dailyStats = entries.stream.collect(Collectors.groupingBy(ScheduleEntry::getDate,Collectors.groupingBy(ScheduleEntry::getPosition,Collectors.groupingBy(ScheduleEntry::getShift, Collectors.counting))));// 检查每日人数是否达标for (LocalDate date : dailyStats.keySet) {Map> posStats = dailyStats.get(date);for (String pos : posStats.keySet) {Map shifts = posStats.get(pos);long dayCount = shifts.getOrDefault(ShiftType.DAY, 0L);long nightCount = shifts.getOrDefault(ShiftType.NIGHT, 0L);if (dayCount > empEntries = entries.stream.collect(Collectors.groupingBy(ScheduleEntry::getEmployeeId));for (List empSchedule : empEntries.values) {empSchedule.sort(Comparator.comparing(ScheduleEntry::getDate));for (int i = 1; i public class MyCstGenerator {// 排班计划 总天数 14天// 休息 3天// 排班策略: 固定周期+休息日缓冲 6,1,5,2 3,1,4,1,4,1// 5,1,6,2/*** 逻辑:* 1、按照班次 白班、夜班、休息 三个排班状态,准备相同的组* 2、每组最小人数需要满足班次排班的最少人数要求* 3、休息日安排,排班周期内,保证休息日数量足够且不超出* 4、排班策略: 固定周期+休息日缓冲* 5、通过休息之后,主动切换排班方式,让其他组进入休息状态* 6、通过先满足至少每天每个班次都有人员安排的情况下后,将多余的休息日转化为排班* 7、将排班结果复制两个周期* 8、截取想要输出的月份排班数据**/public void init{//// 员工数据(1个岗位,3名员工)String position = "客服";List employees = new ArrayList;employees.add(new Employee("101", "张三", position));employees.add(new Employee("102", "李四", position));employees.add(new Employee("103", "王五", position));int totalEmployeeCount = employees.size;int shiftMinEmployeeCount = 1;// 两个班次List shiftTypeList = new ArrayList;shiftTypeList.add(ShiftType.DAY);shiftTypeList.add(ShiftType.NIGHT);int shiftTypeCount = shiftTypeList.size;if (totalEmployeeCount scheduleRuleMap = new HashMap;scheduleRuleMap.put(1, new ScheduleRule(1, ShiftType.DAY, 0));scheduleRuleMap.put(2, new ScheduleRule(2, ShiftType.REST, 0));scheduleRuleMap.put(3, new ScheduleRule(3, ShiftType.NIGHT, 0));scheduleRuleMap.put(4, new ScheduleRule(4, ShiftType.REST, 0));// 初始化排班策略 班次1 + 休息1 + 班次2 + 休息2 + 班次3 + 休息3List scheduleRuleList = new ArrayList;for (int i = 0; i item.getShiftType!=ShiftType.REST).mapToInt(ScheduleRule::getDays).sum;restDays = scheduleRangeDays / 2 - workDays;}// 处理scheduleRuleMap.get(i * 2 + 1).setDays(workDays);scheduleRuleMap.get(i * 2 + 2).setDays(restDays);scheduleRuleList.add(scheduleRuleMap.get(i * 2 + 1));scheduleRuleList.add(scheduleRuleMap.get(i * 2 + 2));}// 排班计划结果scheduleRuleList.forEach(item -> System.out.println(item.toString));// 人员分组 最好是事先完成初始化Map employeeGroupMap = new HashMap;employeeGroupMap.put(1, new EmployeeGroup(1));employeeGroupMap.put(2, new EmployeeGroup(2));employeeGroupMap.put(3, new EmployeeGroup(3));Map employeeMap = new HashMap;employees.stream.filter(p-> p.getPosition.equals(position)).forEach(employee -> employeeMap.put((employeeMap.size + 1), employee));employeeMap.keySet.forEach(index -> {int t = index % employeeGroupMap.size + 1;employeeGroupMap.get(t).getEmployeeInGroup.add(employeeMap.get(index));});employeeGroupMap.forEach((integer, employeeGroup) -> employeeGroup.getEmployeeInGroup.forEach(item -> System.out.println(employeeGroup.getGroup + " : " +item.toString)));// 排班验证LocalDate startDate = LocalDate.of(2025,1, 1);int fullScheduleDays = scheduleRangeDays * shiftTypeCount * (shiftTypeCount + 1);// 获取模板LocalDate tmpStartDate = startDate;// 生成排班模版 14*6 = 84天 一个周期LocalDate currentDate = LocalDate.now;while (tmpStartDate.isBefore(currentDate)) {tmpStartDate = tmpStartDate.plusDays(fullScheduleDays);}tmpStartDate = tmpStartDate.plusDays(-fullScheduleDays);// 模拟排班规则int totalDays = 0;int tmp = 0;String group = null;ShiftType shiftType = ShiftType.DAY;List scheduleTmpResultList = new ArrayList;while (totalDays preGroupShiftTypeMap = preScheduleTmpResult.getGroupShiftTypeMap;// 找到上一个REST 对应的 group nameSet groupNameSet = new HashSet(preGroupShiftTypeMap.keySet);String preRestGroupName = null;for (String groupName: groupNameSet) {if (preGroupShiftTypeMap.get(groupName) == ShiftType.REST) {preRestGroupName = groupName;}}// 移出groupNameSet.remove(preRestGroupName);group = preRestGroupName;// 找到再上一个对应的班次ScheduleTmpResult prePreScheduleTmpResult = null;try {prePreScheduleTmpResult = scheduleTmpResultList.get(scheduleTmpResultList.size - 2);} catch (Exception e) {}ShiftType nextShiftType = ShiftType.DAY;if (null != prePreScheduleTmpResult) {Map groupShiftTypeMap = prePreScheduleTmpResult.getGroupShiftTypeMap;ShiftType shiftType1 = groupShiftTypeMap.get(preRestGroupName);nextShiftType = shiftType1.getReverse;}Map groupShiftTypeMap = scheduleTmpResult.getGroupShiftTypeMap;groupShiftTypeMap.put(preRestGroupName, nextShiftType);// 基于nextShiftType 寻找上一个 group ,让其进行休息String preGroupNameB = null;for (String groupName: groupNameSet) {if (preGroupShiftTypeMap.get(groupName) == nextShiftType) {preGroupNameB = groupName;}}groupNameSet.remove(preGroupNameB);groupShiftTypeMap.put(preGroupNameB, ShiftType.REST);// 剩下的一组 不动String otherGroupName = groupNameSet.iterator.next;groupShiftTypeMap.put(otherGroupName, preGroupShiftTypeMap.get(otherGroupName));scheduleTmpResult.setGroupShiftTypeMap(groupShiftTypeMap);} else {Map groupShiftTypeMap = scheduleTmpResult.getGroupShiftTypeMap;EmployeeGroup employeeGroupA = employeeGroupMap.get(1);groupShiftTypeMap.put(employeeGroupA.getGroup, shiftType);EmployeeGroup employeeGroupB = employeeGroupMap.get(2);groupShiftTypeMap.put(employeeGroupB.getGroup, shiftType.getNext);EmployeeGroup employeeGroupC = employeeGroupMap.get(3);groupShiftTypeMap.put(employeeGroupC.getGroup, shiftType.getNext.getNext);scheduleTmpResult.setGroupShiftTypeMap(groupShiftTypeMap);group = employeeGroupC.getGroup;}scheduleTmpResultList.add(scheduleTmpResult);totalDays = totalDays + scheduleRule.getDays;}scheduleTmpResultList.forEach(System.out::println);// 将List分成各个组Map> scheduleResultMap = new HashMap;int index = 0;for (ScheduleTmpResult item : scheduleTmpResultList) {index = index + 1;for (Map.Entry entry : item.getGroupShiftTypeMap.entrySet) {String gn = entry.getKey;ShiftType st = entry.getValue;List orDefault = scheduleResultMap.getOrDefault(gn, new ArrayList);ScheduleResult scheduleResult = new ScheduleResult(index, item.getDays, st);orDefault.add(scheduleResult);scheduleResultMap.put(gn, orDefault);}}// 依次处理REST超过 3 的部分int maxRestDays = restDaysInRange;int totalMaxRestDays = restDaysInRange * shiftTypeCount * (shiftTypeCount + 1);Set keySet = scheduleResultMap.keySet;while (maxRestDays > 1) {for (String gn : keySet) {List scheduleResults = scheduleResultMap.get(gn);int nowTotalRestDays = scheduleResults.stream.filter(p -> p.getShiftType == ShiftType.REST).mapToInt(ScheduleResult::getDays).sum;for (int i = 0; i maxRestDays) {// 减少的数量int needRemoveDays = scheduleResult.getDays - maxRestDays;if (nowTotalRestDays - needRemoveDays scheduleResults = scheduleResultMap.get(gn);int nowTotalRestDays = scheduleResults.stream.filter(p -> p.getShiftType == ShiftType.REST).mapToInt(ScheduleResult::getDays).sum;System.out.println(gn + " , " + nowTotalRestDays);for (ScheduleResult sr : scheduleResults) {System.out.println(sr);}}// 将结果输出为一个连续周期内的日期结果List scheduleDateResults = new ArrayList;for (String gn : keySet) {LocalDate tmpDate = tmpStartDate;List scheduleResults = scheduleResultMap.get(gn);// 输出两个full周期的内容,确保可以获取当前月或者下个月的内容for (ScheduleResult sr : scheduleResults) {int days = sr.getDays;for (int i = 0; i newScheduleDateResults = scheduleDateResults.stream.filter(new Predicate {@Overridepublic boolean test(ScheduleDateResult scheduleDateResult) {return scheduleDateResult.getDate.isBefore(end) && scheduleDateResult.getDate.isAfter(start);}}).collect(Collectors.toList);// 最后结果输出for (ScheduleDateResult scheduleDateResult : newScheduleDateResults) {System.out.println(scheduleDateResult);}}public static void main(String args) {new MyCstGenerator.init;}}C组,2025-09-01,RESTC组,2025-09-02,RESTC组,2025-09-03,NIGHTC组,2025-09-04,NIGHTC组,2025-09-05,NIGHTC组,2025-09-06,NIGHTC组,2025-09-07,NIGHTC组,2025-09-08,NIGHTC组,2025-09-09,NIGHTC组,2025-09-10,RESTC组,2025-09-11,RESTC组,2025-09-12,RESTC组,2025-09-13,DAYC组,2025-09-14,DAYC组,2025-09-15,DAYC组,2025-09-16,DAYC组,2025-09-17,DAYC组,2025-09-18,DAYC组,2025-09-19,DAYC组,2025-09-20,DAYC组,2025-09-21,DAYC组,2025-09-22,DAYC组,2025-09-23,RESTC组,2025-09-24,NIGHTC组,2025-09-25,NIGHTC组,2025-09-26,NIGHTC组,2025-09-27,NIGHTC组,2025-09-28,NIGHTC组,2025-09-29,NIGHTC组,2025-09-30,NIGHTB组,2025-09-01,DAYB组,2025-09-02,DAYB组,2025-09-03,DAYB组,2025-09-04,DAYB组,2025-09-05,DAYB组,2025-09-06,DAYB组,2025-09-07,DAYB组,2025-09-08,DAYB组,2025-09-09,RESTB组,2025-09-10,NIGHTB组,2025-09-11,NIGHTB组,2025-09-12,NIGHTB组,2025-09-13,NIGHTB组,2025-09-14,NIGHTB组,2025-09-15,NIGHTB组,2025-09-16,NIGHTB组,2025-09-17,RESTB组,2025-09-18,RESTB组,2025-09-19,RESTB组,2025-09-20,DAYB组,2025-09-21,DAYB组,2025-09-22,DAYB组,2025-09-23,DAYB组,2025-09-24,DAYB组,2025-09-25,DAYB组,2025-09-26,DAYB组,2025-09-27,DAYB组,2025-09-28,DAYB组,2025-09-29,RESTB组,2025-09-30,RESTA组,2025-09-01,NIGHTA组,2025-09-02,NIGHTA组,2025-09-03,RESTA组,2025-09-04,RESTA组,2025-09-05,RESTA组,2025-09-06,DAYA组,2025-09-07,DAYA组,2025-09-08,DAYA组,2025-09-09,DAYA组,2025-09-10,DAYA组,2025-09-11,DAYA组,2025-09-12,DAYA组,2025-09-13,DAYA组,2025-09-14,DAYA组,2025-09-15,RESTA组,2025-09-16,RESTA组,2025-09-17,NIGHTA组,2025-09-18,NIGHTA组,2025-09-19,NIGHTA组,2025-09-20,NIGHTA组,2025-09-21,NIGHTA组,2025-09-22,NIGHTA组,2025-09-23,NIGHTA组,2025-09-24,RESTA组,2025-09-25,RESTA组,2025-09-26,RESTA组,2025-09-27,DAYA组,2025-09-28,DAYA组,2025-09-29,DAYA组,2025-09-30,DAY转Excel显示
关键约束分析
排班策略设计
方案验证
虽然,元宝给出的代码无法直接执行,但是也是给出了可用的设计方案思路,最终帮助作者君解决了问题。
如果你也有类似的自动排班想法,欢迎评论区留言或者私信沟通~
来源:大山
