package cn.iocoder.yudao.module.mes.service.cal.team; import cn.iocoder.yudao.module.mes.controller.admin.cal.team.vo.shift.MesCalTeamShiftListReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.cal.holiday.MesCalHolidayDO; import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO; import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanShiftDO; import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanTeamDO; import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamShiftDO; import cn.iocoder.yudao.module.mes.dal.mysql.cal.team.MesCalTeamShiftMapper; import cn.iocoder.yudao.module.mes.enums.cal.MesCalHolidayTypeEnum; import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftMethodEnum; import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftTypeEnum; import cn.iocoder.yudao.module.mes.service.cal.holiday.MesCalHolidayService; import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService; import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanShiftService; import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanTeamService; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_TEAM_NOT_ENOUGH; /** * MES 班组排班 Service 实现类 * * @author 芋道源码 */ @Service @Validated public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService { @Resource private MesCalTeamShiftMapper teamShiftMapper; @Resource @Lazy private MesCalPlanService planService; @Resource @Lazy private MesCalPlanShiftService planShiftService; @Resource @Lazy private MesCalPlanTeamService planTeamService; @Resource @Lazy private MesCalHolidayService holidayService; @Transactional(rollbackFor = Exception.class) @Override public void generateTeamShiftRecords(Long planId) { // 1.1 查询排班计划 MesCalPlanDO plan = planService.getPlan(planId); // 1.2 查询计划中的班次列表 List shifts = planShiftService.getPlanShiftListByPlanId(planId); // 1.3 查询计划中的班组列表 List teams = planTeamService.getPlanTeamListByPlanId(planId); // 1.4 校验班组和班次数量是否满足班型要求 MesCalShiftTypeEnum shiftTypeEnum = MesCalShiftTypeEnum.valueOf(plan.getShiftType()); int requiredCount = shiftTypeEnum.getRequiredTeamCount(); if (teams.size() < requiredCount) { throw exception(CAL_TEAM_SHIFT_GENERATE_TEAM_NOT_ENOUGH); } if (shifts.size() < requiredCount) { throw exception(CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH); } // 1.5 加载日期区间内的假日数据,构建 Map<日期, 类型> Map holidayMap = holidayService.getHolidayList(plan.getStartDate(), plan.getEndDate()) .stream().collect(Collectors.toMap(h -> h.getDay().toLocalDate(), MesCalHolidayDO::getType)); // 2. 计算日期跨度,逐天判断工作日,生成排班记录 LocalDate startDate = plan.getStartDate().toLocalDate(); LocalDate endDate = plan.getEndDate().toLocalDate(); long days = ChronoUnit.DAYS.between(startDate, endDate) + 1; boolean excludeWeekend = plan.getExcludeWeekend() != null && plan.getExcludeWeekend(); List allRecords = new ArrayList<>(); int shiftIndex = 0; int workdayOffset = 0; // 仅工作日递增,用于倒班轮换计数 for (int i = 0; i < days; i++) { LocalDate currentDate = startDate.plusDays(i); // 2.1 跳过非工作日 if (!isWorkday(currentDate, holidayMap, excludeWeekend)) { continue; } LocalDateTime currentDay = currentDate.atStartOfDay(); // 2.2.1 根据倒班方式计算 shiftIndex(以工作日偏移为准) shiftIndex = calculateShiftIndex(shiftIndex, workdayOffset, currentDate, startDate, plan); // 2.2.2 根据轮班方式生成排班记录,收集到列表 buildRecordsForDay(allRecords, planId, currentDay, shiftIndex, plan.getShiftType(), teams, shifts); workdayOffset++; } // 3. 批量插入排班记录 teamShiftMapper.insertBatch(allRecords); } /** * 判断指定日期是否为工作日 * * 优先级:holiday 表 > 周末排除开关 > 默认工作日 */ private boolean isWorkday(LocalDate date, Map holidayMap, boolean excludeWeekend) { // 1. holiday 表优先:显式标记的日期以标记为准 Integer holidayType = holidayMap.get(date); if (holidayType != null) { return MesCalHolidayTypeEnum.WORKDAY.getType().equals(holidayType); } // 2. 若开启了排除周末,周六日默认为休息日 if (excludeWeekend) { DayOfWeek dow = date.getDayOfWeek(); if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) { return false; } } // 3. 默认工作日 return true; } /** * 计算当天的 shiftIndex(根据倒班方式判断是否需要递增) */ private int calculateShiftIndex(int shiftIndex, int dayOffset, LocalDate currentDate, LocalDate startDate, MesCalPlanDO plan) { if (dayOffset == 0) { return shiftIndex; // 第一天不递增 } // 单白班不需要倒班,shiftMethod 为 null,直接返回 if (plan.getShiftMethod() == null) { return shiftIndex; } if (MesCalShiftMethodEnum.QUARTER.getMethod().equals(plan.getShiftMethod())) { // 按季度轮班:到了新季度的第一天 LocalDate quarterStart = LocalDateTimeUtils.getQuarterStart(currentDate); LocalDate planQuarterStart = LocalDateTimeUtils.getQuarterStart(startDate); if (currentDate.equals(quarterStart) && !quarterStart.equals(planQuarterStart)) { return shiftIndex + 1; } } else if (MesCalShiftMethodEnum.MONTH.getMethod().equals(plan.getShiftMethod())) { // 按月轮班:到了月初 LocalDate monthStart = currentDate.withDayOfMonth(1); LocalDate planMonthStart = startDate.withDayOfMonth(1); if (currentDate.equals(monthStart) && !monthStart.equals(planMonthStart)) { return shiftIndex + 1; } } else if (MesCalShiftMethodEnum.WEEK.getMethod().equals(plan.getShiftMethod())) { // 按周轮班:到了周一 LocalDate weekStart = LocalDateTimeUtils.getWeekStart(currentDate); LocalDate planWeekStart = LocalDateTimeUtils.getWeekStart(startDate); if (currentDate.equals(weekStart) && !weekStart.equals(planWeekStart)) { return shiftIndex + 1; } } else if (MesCalShiftMethodEnum.DAY.getMethod().equals(plan.getShiftMethod())) { // 按天轮班:到了指定轮班天数的倍数 if (dayOffset % plan.getShiftCount() == 0) { return shiftIndex + 1; } } else { throw new IllegalArgumentException("不支持的倒班方式: " + plan.getShiftMethod()); } return shiftIndex; } /** * 根据班型生成当天的排班记录,添加到 allRecords 列表 */ private void buildRecordsForDay(List allRecords, Long planId, LocalDateTime currentDay, int shiftIndex, Integer shiftType, List teams, List shifts) { if (MesCalShiftTypeEnum.SINGLE.getType().equals(shiftType)) { // 单白班:不需要倒班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId) .day(currentDay) .teamId(teams.get(0).getTeamId()) .shiftId(shifts.get(0).getId()) .sort(1).build()); } else if (MesCalShiftTypeEnum.TWO.getType().equals(shiftType)) { // 两班倒 if (shiftIndex % 2 == 0) { // A 组上白班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(0).getTeamId()) .shiftId(shifts.get(0).getId()) .sort(1).build()); // B 组上夜班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(1).getTeamId()) .shiftId(shifts.get(1).getId()) .sort(2).build()); } else { // A 组上夜班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(0).getTeamId()) .shiftId(shifts.get(1).getId()) .sort(2).build()); // B 组上白班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(1).getTeamId()) .shiftId(shifts.get(0).getId()) .sort(1).build()); } } else if (MesCalShiftTypeEnum.THREE.getType().equals(shiftType)) { // 三班倒 if (shiftIndex % 3 == 0) { // A 组上白班、B 组上中班、C 组上夜班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(0).getTeamId()) .shiftId(shifts.get(0).getId()) .sort(1).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(1).getTeamId()) .shiftId(shifts.get(1).getId()) .sort(2).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(2).getTeamId()) .shiftId(shifts.get(2).getId()) .sort(3).build()); } else if (shiftIndex % 3 == 1) { // A 组上中班、B 组上夜班、C 组上白班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(0).getTeamId()) .shiftId(shifts.get(1).getId()) .sort(2).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(1).getTeamId()) .shiftId(shifts.get(2).getId()) .sort(3).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(2).getTeamId()) .shiftId(shifts.get(0).getId()) .sort(1).build()); } else { // A 组上夜班、B 组上白班、C 组上中班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(0).getTeamId()) .shiftId(shifts.get(2).getId()) .sort(3).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(1).getTeamId()) .shiftId(shifts.get(0).getId()) .sort(1).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(2).getTeamId()) .shiftId(shifts.get(1).getId()) .sort(2).build()); } } } @Override public List getTeamShiftList(MesCalTeamShiftListReqVO reqVO) { return teamShiftMapper.selectList(reqVO); } @Override public void deleteByPlanId(Long planId) { teamShiftMapper.deleteByPlanId(planId); } @Override public void deleteByTeamId(Long teamId) { teamShiftMapper.deleteByTeamId(teamId); } }