diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/plan/MesCalPlanMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/plan/MesCalPlanMapper.java index 9478a13..3123514 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/plan/MesCalPlanMapper.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/plan/MesCalPlanMapper.java @@ -8,6 +8,9 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.apache.ibatis.annotations.Mapper; +import java.time.LocalDate; +import java.util.List; + /** * MES 排班计划 Mapper * @@ -32,6 +35,15 @@ public interface MesCalPlanMapper extends BaseMapperX { return selectOne(MesCalPlanDO::getCode, code); } + default List selectListByStatusAndDay(Integer status, LocalDate day) { + return selectList(new LambdaQueryWrapperX() + .eq(MesCalPlanDO::getStatus, status) + // TODO @AI:dayend 获取下?应该有方法,这样可以 le,更统一点; + .lt(MesCalPlanDO::getStartDate, day.plusDays(1).atStartOfDay()) + .ge(MesCalPlanDO::getEndDate, day.atStartOfDay()) + .orderByAsc(MesCalPlanDO::getId)); + } + default void updateWithShiftFieldCleanup(MesCalPlanDO plan, boolean clearShiftMethod, boolean clearShiftCount) { update(plan, Wrappers.lambdaUpdate() .eq(MesCalPlanDO::getId, plan.getId()) diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java index bc19c6d..d9cf299 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java @@ -140,6 +140,9 @@ public interface ErrorCodeConstants { ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH = new ErrorCode(1_040_201_202, "班次数量不满足轮班方式要求"); ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_COUNT_INVALID = new ErrorCode(1_040_201_203, "按天倒班必须填写合法的倒班天数"); ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_METHOD_INVALID = new ErrorCode(1_040_201_204, "不支持的倒班方式:{}"); + ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID = new ErrorCode(1_040_201_205, "班次【{}】的开始或结束时间格式不正确,应为 HH:mm,结束时间可为 24:00"); + ErrorCode CAL_TEAM_SHIFT_GENERATE_REST_INVALID = new ErrorCode(1_040_201_206, + "班组【{}】{} {}与 {} {}的上班时间重叠或首尾相接,没有休息间隔,请调整班次时间或倒班方式"); // ========== MES 日历排班-排班计划(1-040-202-000) ========== ErrorCode CAL_PLAN_NOT_EXISTS = new ErrorCode(1_040_202_000, "排班计划不存在"); @@ -152,6 +155,8 @@ public interface ErrorCodeConstants { // ========== MES 日历排班-假期设置(1-040-203-000) ========== ErrorCode CAL_HOLIDAY_NOT_EXISTS = new ErrorCode(1_040_203_000, "假期设置不存在"); + ErrorCode CAL_HOLIDAY_PLAN_CONFIRMED = new ErrorCode(1_040_203_001, + "{} 在已确认的排班计划【{}】范围内,修改是否节假日会与已生成的排班不一致,不允许修改"); // ========== MES 设备管理-设备类型(1-040-300-000) ========== ErrorCode DV_MACHINERY_TYPE_NOT_EXISTS = new ErrorCode(1_040_300_000, "设备类型不存在"); diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImpl.java index 0502794..3c7695a 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImpl.java @@ -1,16 +1,27 @@ package cn.iocoder.yudao.module.mes.service.cal.holiday; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.mes.controller.admin.cal.holiday.vo.MesCalHolidaySaveReqVO; 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.mysql.cal.holiday.MesCalHolidayMapper; +import cn.iocoder.yudao.module.mes.enums.cal.MesCalHolidayTypeEnum; +import cn.iocoder.yudao.module.mes.enums.cal.MesCalPlanStatusEnum; +import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.List; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_HOLIDAY_PLAN_CONFIRMED; + /** * MES 假期设置 Service 实现类 * @@ -23,10 +34,17 @@ public class MesCalHolidayServiceImpl implements MesCalHolidayService { @Resource private MesCalHolidayMapper holidayMapper; + @Resource + @Lazy + private MesCalPlanService planService; + @Override public Long saveHoliday(MesCalHolidaySaveReqVO saveReqVO) { - // Upsert 逻辑:如果该日期已有记录,则更新 + // 1. 校验是否节假日的变化,不影响已确认的排班计划 MesCalHolidayDO existing = holidayMapper.selectByDay(saveReqVO.getDay()); + validateHolidayChangeForConfirmedPlan(existing, saveReqVO); + + // 2.1 Upsert 逻辑:如果该日期已有记录,则更新 if (existing != null) { MesCalHolidayDO updateObj = BeanUtils.toBean(saveReqVO, MesCalHolidayDO.class); updateObj.setId(existing.getId()); @@ -34,12 +52,33 @@ public class MesCalHolidayServiceImpl implements MesCalHolidayService { return existing.getId(); } - // 插入新记录 + // 2.2 插入新记录 MesCalHolidayDO holiday = BeanUtils.toBean(saveReqVO, MesCalHolidayDO.class); holidayMapper.insert(holiday); return holiday.getId(); } + /** + * 校验是否节假日的变化,不影响已确认的排班计划 + * + * 排班计划确认时按“节假日停班”生成排班记录(按天倒班时还会顺延后续倒班),确认后不会重新生成。 + * 因此已确认计划范围内的日期,不允许在“节假日”与“非节假日”之间切换,避免与已生成的排班不一致 https://t.zsxq.com/XdcRW + */ + private void validateHolidayChangeForConfirmedPlan(MesCalHolidayDO existing, MesCalHolidaySaveReqVO saveReqVO) { + boolean oldHoliday = existing != null && MesCalHolidayTypeEnum.HOLIDAY.getType().equals(existing.getType()); + boolean newHoliday = MesCalHolidayTypeEnum.HOLIDAY.getType().equals(saveReqVO.getType()); + if (oldHoliday == newHoliday) { + return; + } + List plans = planService.getPlanListByStatusAndDay( + MesCalPlanStatusEnum.CONFIRMED.getStatus(), saveReqVO.getDay().toLocalDate()); + if (CollUtil.isEmpty(plans)) { + return; + } + throw exception(CAL_HOLIDAY_PLAN_CONFIRMED, saveReqVO.getDay().toLocalDate(), + StrUtil.join("、", convertList(plans, MesCalPlanDO::getName))); + } + @Override public List getHolidayList(LocalDateTime startDay, LocalDateTime endDay) { return holidayMapper.selectList(startDay, endDay); diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanService.java index 5e141f6..b66e1bf 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanService.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanService.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.mes.controller.admin.cal.plan.vo.MesCalPlanSaveRe import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO; import jakarta.validation.Valid; +import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Map; @@ -90,4 +91,13 @@ public interface MesCalPlanService { */ PageResult getPlanPage(MesCalPlanPageReqVO pageReqVO); + /** + * 获得指定状态、且起止日期包含指定日期的排班计划列表 + * + * @param status 状态 + * @param day 日期 + * @return 排班计划列表 + */ + List getPlanListByStatusAndDay(Integer status, LocalDate day); + } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java index 68631dd..cb59047 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.time.LocalDate; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -143,6 +144,11 @@ public class MesCalPlanServiceImpl implements MesCalPlanService { } @Override + public List getPlanListByStatusAndDay(Integer status, LocalDate day) { + return planMapper.selectListByStatusAndDay(status, day); + } + + @Override public MesCalPlanDO validatePlanPrepare(Long planId) { MesCalPlanDO plan = validatePlanExists(planId); if (ObjUtil.notEqual(MesCalPlanStatusEnum.PREPARE.getStatus(), plan.getStatus())) { @@ -171,11 +177,13 @@ public class MesCalPlanServiceImpl implements MesCalPlanService { /** * 确认排班计划时,校验班组数量是否与轮班方式匹配 + * + * 排班只会使用轮班方式所需数量的班组,多出的班组不会参与轮休或替班,因此要求数量一致,避免被静默忽略 https://t.zsxq.com/XdcRW */ private void validateTeamCountForConfirm(Long planId, Integer shiftType) { Long teamCount = planTeamService.getPlanTeamCountByPlanId(planId); MesCalShiftTypeEnum shiftTypeEnum = MesCalShiftTypeEnum.valueOf(shiftType); - if (teamCount < shiftTypeEnum.getRequiredTeamCount()) { + if (teamCount == null || teamCount != shiftTypeEnum.getRequiredTeamCount()) { throw exception(CAL_PLAN_TEAM_COUNT_NOT_MATCH); } } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImpl.java index 52fa763..b026c03 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImpl.java @@ -1,13 +1,18 @@ package cn.iocoder.yudao.module.mes.service.cal.team; +import cn.hutool.core.collection.CollUtil; 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.MesCalTeamDO; 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; @@ -20,14 +25,25 @@ import org.springframework.validation.annotation.Validated; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_REST_INVALID; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_COUNT_INVALID; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_METHOD_INVALID; 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_SHIFT_TIME_INVALID; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_TEAM_NOT_ENOUGH; /** @@ -39,6 +55,11 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIF @Validated public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService { + /** + * 班次时间格式:H:mm 或 HH:mm + */ + private static final Pattern SHIFT_TIME_PATTERN = Pattern.compile("^(\\d{1,2}):(\\d{2})$"); + @Resource private MesCalTeamShiftMapper teamShiftMapper; @@ -51,6 +72,12 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService { @Resource @Lazy private MesCalPlanTeamService planTeamService; + @Resource + @Lazy + private MesCalHolidayService holidayService; + @Resource + @Lazy + private MesCalTeamService teamService; @Transactional(rollbackFor = Exception.class) @Override @@ -71,25 +98,118 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService { throw exception(CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH); } - // 2. 计算日期跨度,遍历每天生成排班记录,最后统一写入数据库 + // 2.1 计算日期跨度,查询范围内的节假日 LocalDate startDate = plan.getStartDate().toLocalDate(); LocalDate endDate = plan.getEndDate().toLocalDate(); long days = ChronoUnit.DAYS.between(startDate, endDate) + 1; + Set holidayDates = getHolidayDates(startDate, endDate); + // 2.2 遍历每天生成排班记录,最后统一写入数据库 + // 假期停班:节假日不生成排班,按天倒班只累计有效排班日(假期不消耗倒班天数);按周/月/季度仍按日历周期换班 https://t.zsxq.com/XdcRW + boolean dayMethod = MesCalShiftMethodEnum.DAY.getMethod().equals(plan.getShiftMethod()); List allRecords = new ArrayList<>(); int shiftIndex = 0; + int workDayOffset = 0; for (int i = 0; i < days; i++) { LocalDate currentDate = startDate.plusDays(i); LocalDateTime currentDay = currentDate.atStartOfDay(); - // 2.2.1 根据倒班方式计算 shiftIndex 单白班不倒班,跳过 - if (!MesCalShiftTypeEnum.SINGLE.getType().equals(plan.getShiftType())) { - shiftIndex = calculateShiftIndex(shiftIndex, i, currentDate, startDate, plan); + boolean holiday = holidayDates.contains(currentDate); + // 2.2.1 根据倒班方式计算 shiftIndex 单白班不倒班,跳过;周期起点恰逢假期时,也照常换班 + if (!MesCalShiftTypeEnum.SINGLE.getType().equals(plan.getShiftType()) && !(dayMethod && holiday)) { + shiftIndex = calculateShiftIndex(shiftIndex, dayMethod ? workDayOffset : i, currentDate, startDate, plan); + } + if (holiday) { + continue; } + workDayOffset++; // 2.2.2 根据轮班方式生成排班记录,收集到列表 buildRecordsForDay(allRecords, planId, currentDay, shiftIndex, plan.getShiftType(), teams, shifts); } + // 2.3 校验同一班组相邻两次排班之间留有休息间隔 https://t.zsxq.com/XdcRW + validateTeamShiftRest(allRecords, shifts); // 3. 批量插入排班记录 - teamShiftMapper.insertBatch(allRecords); + if (CollUtil.isNotEmpty(allRecords)) { + teamShiftMapper.insertBatch(allRecords); + } + } + + /** + * 获得日期范围内的节假日(日期类型为节假日的,排班时停班) + */ + private Set getHolidayDates(LocalDate startDate, LocalDate endDate) { + List holidays = holidayService.getHolidayList( + startDate.atStartOfDay(), endDate.atTime(23, 59, 59)); + return convertSet(holidays, holiday -> holiday.getDay().toLocalDate(), + holiday -> MesCalHolidayTypeEnum.HOLIDAY.getType().equals(holiday.getType())); + } + + /** + * 校验同一班组相邻两次排班之间留有休息间隔 + * + * 按全部排班的上班开始时刻排序,前一班的结束时刻不早于同一班组下一班的开始时刻, + * 即上班时间重叠或首尾相接(例如夜班次日 08:00 下班后立即接 08:00 的白班),不允许生成 + */ + private void validateTeamShiftRest(List records, List shifts) { + // 1. 解析班次的开始、结束时间 + Map shiftMap = convertMap(shifts, MesCalPlanShiftDO::getId); + Map shiftTimeMap = convertMap(shifts, MesCalPlanShiftDO::getId, + shift -> new LocalTime[]{parseShiftTime(shift, shift.getStartTime(), false), + parseShiftTime(shift, shift.getEndTime(), true)}); + + // 2. 按上班开始时刻排序,逐条与同一班组的上一次排班比较 + List sortedRecords = new ArrayList<>(records); + sortedRecords.sort(Comparator.comparing(record -> buildShiftTimeRange(record, shiftTimeMap)[0])); + Map lastRecordMap = new HashMap<>(); + for (MesCalTeamShiftDO record : sortedRecords) { + MesCalTeamShiftDO lastRecord = lastRecordMap.put(record.getTeamId(), record); + if (lastRecord == null || buildShiftTimeRange(lastRecord, shiftTimeMap)[1] + .isBefore(buildShiftTimeRange(record, shiftTimeMap)[0])) { + continue; + } + MesCalTeamDO team = teamService.getTeam(record.getTeamId()); + throw exception(CAL_TEAM_SHIFT_GENERATE_REST_INVALID, team != null ? team.getName() : record.getTeamId(), + lastRecord.getDay().toLocalDate(), shiftMap.get(lastRecord.getShiftId()).getName(), + record.getDay().toLocalDate(), shiftMap.get(record.getShiftId()).getName()); + } + } + + /** + * 计算排班记录的实际上班时间段 + * + * 开始时间在排班日期当天;结束时间不晚于开始时间(例如 20:00~08:00、16:00~00:00)时,跨午夜到次日结束 + * + * @return [开始时刻, 结束时刻] + */ + private static LocalDateTime[] buildShiftTimeRange(MesCalTeamShiftDO record, Map shiftTimeMap) { + LocalTime[] shiftTime = shiftTimeMap.get(record.getShiftId()); + LocalDate day = record.getDay().toLocalDate(); + LocalDateTime start = day.atTime(shiftTime[0]); + LocalDateTime end = shiftTime[1].isAfter(shiftTime[0]) ? day.atTime(shiftTime[1]) + : day.plusDays(1).atTime(shiftTime[1]); + return new LocalDateTime[]{start, end}; + } + + /** + * 解析班次时间,兼容 H:mm(例如 8:00) + * + * 结束时间允许 24:00,表示当天 24:00,按 00:00 处理后由“结束时间不晚于开始时间时跨午夜到次日”规则得到次日 00:00 https://t.zsxq.com/XdcRW + * + * @param endTime 是否为结束时间 + */ + private static LocalTime parseShiftTime(MesCalPlanShiftDO shift, String time, boolean endTime) { + Matcher matcher = time != null ? SHIFT_TIME_PATTERN.matcher(time.trim()) : null; + if (matcher == null || !matcher.matches()) { + throw exception(CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, shift.getName()); + } + int hour = Integer.parseInt(matcher.group(1)); + int minute = Integer.parseInt(matcher.group(2)); + if (endTime && hour == 24 && minute == 0) { + return LocalTime.MIDNIGHT; + } + if (hour > 23 || minute > 59) { + throw exception(CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, shift.getName()); + } + return LocalTime.of(hour, minute); } /** @@ -180,7 +300,7 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService { .sort(1).build()); } } else if (MesCalShiftTypeEnum.THREE.getType().equals(shiftType)) { - // 三班倒 + // 三班倒:按 白班 → 夜班 → 中班 轮转。原 白班 → 中班 → 夜班 轮转时,换班当天中班 24:00 下班的班组会立即接 00:00 的夜班 https://t.zsxq.com/XdcRW if (shiftIndex % 3 == 0) { // A 组上白班、B 组上中班、C 组上夜班 allRecords.add(MesCalTeamShiftDO.builder() @@ -199,39 +319,39 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService { .shiftId(shifts.get(2).getId()) .sort(3).build()); } else if (shiftIndex % 3 == 1) { - // A 组上中班、B 组上夜班、C 组上白班 + // 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()) + .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()); } else { - // A 组上夜班、B 组上白班、C 组上中班 + // A 组上中班、B 组上夜班、C 组上白班 allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(0).getTeamId()) - .shiftId(shifts.get(2).getId()) - .sort(3).build()); + .shiftId(shifts.get(1).getId()) + .sort(2).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(1).getTeamId()) - .shiftId(shifts.get(0).getId()) - .sort(1).build()); + .shiftId(shifts.get(2).getId()) + .sort(3).build()); allRecords.add(MesCalTeamShiftDO.builder() .planId(planId).day(currentDay) .teamId(teams.get(2).getTeamId()) - .shiftId(shifts.get(1).getId()) - .sort(2).build()); + .shiftId(shifts.get(0).getId()) + .sort(1).build()); } } } diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImplTest.java new file mode 100644 index 0000000..a0d0033 --- /dev/null +++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImplTest.java @@ -0,0 +1,133 @@ +package cn.iocoder.yudao.module.mes.service.cal.holiday; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.mes.controller.admin.cal.holiday.vo.MesCalHolidaySaveReqVO; +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.mysql.cal.holiday.MesCalHolidayMapper; +import cn.iocoder.yudao.module.mes.enums.cal.MesCalHolidayTypeEnum; +import cn.iocoder.yudao.module.mes.enums.cal.MesCalPlanStatusEnum; +import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_HOLIDAY_PLAN_CONFIRMED; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link MesCalHolidayServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +@Import(MesCalHolidayServiceImpl.class) +public class MesCalHolidayServiceImplTest extends BaseDbUnitTest { + + @Resource + private MesCalHolidayServiceImpl holidayService; + + @Resource + private MesCalHolidayMapper holidayMapper; + + @MockitoBean + private MesCalPlanService planService; + + @Test + public void testSaveHoliday_addHolidayInConfirmedPlan() { + // 准备参数 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> { + o.setId(null); // 避免 id 被赋值 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0)); + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType()); + }); + // mock planService 的方法:该日期在已确认的排班计划范围内 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> o.setStatus(MesCalPlanStatusEnum.CONFIRMED.getStatus())); + when(planService.getPlanListByStatusAndDay(eq(MesCalPlanStatusEnum.CONFIRMED.getStatus()), + eq(reqVO.getDay().toLocalDate()))).thenReturn(singletonList(plan)); + + // 调用,并断言异常 + assertServiceException(() -> holidayService.saveHoliday(reqVO), CAL_HOLIDAY_PLAN_CONFIRMED, + reqVO.getDay().toLocalDate(), plan.getName()); + // 断言:未写入假期设置 + assertNull(holidayMapper.selectByDay(reqVO.getDay())); + } + + @Test + public void testSaveHoliday_cancelHolidayInConfirmedPlan() { + // mock 数据:已有节假日 + MesCalHolidayDO dbHoliday = randomPojo(MesCalHolidayDO.class, o -> { + o.setId(null); // 避免 id 被赋值 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0)); + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType()); + }); + holidayMapper.insert(dbHoliday); + // 准备参数:改为工作日 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> { + o.setId(null); // 避免 id 被赋值 + o.setDay(dbHoliday.getDay()); + o.setType(MesCalHolidayTypeEnum.WORKDAY.getType()); + }); + // mock planService 的方法:该日期在已确认的排班计划范围内 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> o.setStatus(MesCalPlanStatusEnum.CONFIRMED.getStatus())); + when(planService.getPlanListByStatusAndDay(eq(MesCalPlanStatusEnum.CONFIRMED.getStatus()), + eq(reqVO.getDay().toLocalDate()))).thenReturn(singletonList(plan)); + + // 调用,并断言异常 + assertServiceException(() -> holidayService.saveHoliday(reqVO), CAL_HOLIDAY_PLAN_CONFIRMED, + reqVO.getDay().toLocalDate(), plan.getName()); + // 断言:假期设置未被修改 + assertPojoEquals(dbHoliday, holidayMapper.selectById(dbHoliday.getId())); + } + + @Test + public void testSaveHoliday_updateRemarkInConfirmedPlan() { + // mock 数据:已有节假日 + MesCalHolidayDO dbHoliday = randomPojo(MesCalHolidayDO.class, o -> { + o.setId(null); // 避免 id 被赋值 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0)); + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType()); + }); + holidayMapper.insert(dbHoliday); + // 准备参数:仍为节假日,只修改备注 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> { + o.setId(null); // 避免 id 被赋值 + o.setDay(dbHoliday.getDay()); + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType()); + }); + + // 调用 + Long id = holidayService.saveHoliday(reqVO); + // 断言:更新已有记录,且是否节假日未变化,无需校验排班计划 + assertEquals(dbHoliday.getId(), id); + assertPojoEquals(reqVO, holidayMapper.selectById(id), "id"); + verify(planService, never()).getPlanListByStatusAndDay(any(), any()); + } + + @Test + public void testSaveHoliday_createWithoutConfirmedPlan() { + // 准备参数 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> { + o.setId(null); // 避免 id 被赋值 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0)); + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType()); + }); + // mock planService 的方法:该日期没有已确认的排班计划 + when(planService.getPlanListByStatusAndDay(eq(MesCalPlanStatusEnum.CONFIRMED.getStatus()), + eq(reqVO.getDay().toLocalDate()))).thenReturn(emptyList()); + + // 调用 + Long id = holidayService.saveHoliday(reqVO); + // 断言 + assertPojoEquals(reqVO, holidayMapper.selectById(id), "id"); + } + +} diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImplTest.java index 9f0016e..83fe34e 100644 --- a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImplTest.java +++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImplTest.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.mes.service.cal.plan; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.mes.controller.admin.cal.plan.vo.MesCalPlanSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO; @@ -14,9 +15,17 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import jakarta.annotation.Resource; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_PLAN_NOT_PREPARE; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_PLAN_TEAM_COUNT_NOT_MATCH; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_REST_INVALID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -88,6 +97,85 @@ public class MesCalPlanServiceImplTest extends BaseDbUnitTest { assertEquals(MesCalPlanStatusEnum.CONFIRMED.getStatus(), updatePlan.getStatus()); // 断言 2:触发生成班组排班记录 verify(teamShiftService).generateTeamShiftRecords(plan.getId()); + // 断言 3:重复确认被拒绝,且不会再次生成班组排班记录 + assertServiceException(() -> planService.confirmPlan(plan.getId()), CAL_PLAN_NOT_PREPARE); + verify(teamShiftService, times(1)).generateTeamShiftRecords(plan.getId()); + } + + @Test + public void testConfirmPlan_generateFailed_rollbackStatus() { + // mock 数据:插入一条草稿状态的两班倒排班计划 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> { + o.setCalendarType(1); + o.setStartDate(LocalDateTime.of(2026, 10, 19, 0, 0)); + o.setEndDate(LocalDateTime.of(2026, 10, 21, 0, 0)); + o.setStatus(MesCalPlanStatusEnum.PREPARE.getStatus()); + o.setShiftType(MesCalShiftTypeEnum.TWO.getType()); + o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod()); + o.setShiftCount(1); + }); + planMapper.insert(plan); + // mock 方法:班组数量满足要求,但生成排班时校验到没有休息间隔 + when(planTeamService.getPlanTeamCountByPlanId(plan.getId())).thenReturn(2L); + doThrow(exception(CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班", + LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班")) + .when(teamShiftService).generateTeamShiftRecords(plan.getId()); + + // 调用,并断言异常 + assertServiceException(() -> planService.confirmPlan(plan.getId()), CAL_TEAM_SHIFT_GENERATE_REST_INVALID, + "乙班", LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班"); + // 断言:事务回滚,计划仍为草稿 + MesCalPlanDO dbPlan = planMapper.selectById(plan.getId()); + assertEquals(MesCalPlanStatusEnum.PREPARE.getStatus(), dbPlan.getStatus()); + } + + @Test + public void testConfirmPlan_teamCountExceed() { + // mock 数据:插入一条草稿状态的两班倒排班计划 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> { + o.setCalendarType(1); + o.setStartDate(LocalDateTime.of(2026, 10, 19, 0, 0)); + o.setEndDate(LocalDateTime.of(2026, 10, 21, 0, 0)); + o.setStatus(MesCalPlanStatusEnum.PREPARE.getStatus()); + o.setShiftType(MesCalShiftTypeEnum.TWO.getType()); + o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod()); + o.setShiftCount(1); + }); + planMapper.insert(plan); + // mock 方法:分配了 3 个班组,多出的班组不会参与排班 + when(planTeamService.getPlanTeamCountByPlanId(plan.getId())).thenReturn(3L); + + // 调用,并断言异常 + assertServiceException(() -> planService.confirmPlan(plan.getId()), CAL_PLAN_TEAM_COUNT_NOT_MATCH); + // 断言:计划仍为草稿,且未生成排班记录 + MesCalPlanDO dbPlan = planMapper.selectById(plan.getId()); + assertEquals(MesCalPlanStatusEnum.PREPARE.getStatus(), dbPlan.getStatus()); + verify(teamShiftService, never()).generateTeamShiftRecords(plan.getId()); + } + + @Test + public void testGetPlanListByStatusAndDay() { + // mock 数据:已确认,结束日期为当天 + MesCalPlanDO plan01 = randomPlan(LocalDateTime.of(2026, 10, 1, 0, 0), + LocalDateTime.of(2026, 10, 19, 0, 0), MesCalPlanStatusEnum.CONFIRMED.getStatus()); + planMapper.insert(plan01); + // mock 数据:已确认,开始日期为当天 + MesCalPlanDO plan02 = randomPlan(LocalDateTime.of(2026, 10, 19, 0, 0), + LocalDateTime.of(2026, 10, 31, 0, 0), MesCalPlanStatusEnum.CONFIRMED.getStatus()); + planMapper.insert(plan02); + // mock 数据:已确认,但不包含当天 + planMapper.insert(randomPlan(LocalDateTime.of(2026, 10, 20, 0, 0), + LocalDateTime.of(2026, 10, 31, 0, 0), MesCalPlanStatusEnum.CONFIRMED.getStatus())); + // mock 数据:包含当天,但为草稿 + planMapper.insert(randomPlan(LocalDateTime.of(2026, 10, 1, 0, 0), + LocalDateTime.of(2026, 10, 31, 0, 0), MesCalPlanStatusEnum.PREPARE.getStatus())); + + // 调用 + List list = planService.getPlanListByStatusAndDay( + MesCalPlanStatusEnum.CONFIRMED.getStatus(), LocalDate.of(2026, 10, 19)); + + // 断言:只返回包含当天的已确认计划(起止日期为当天也算) + assertEquals(SetUtils.asSet(plan01.getId(), plan02.getId()), convertSet(list, MesCalPlanDO::getId)); } @Test @@ -116,4 +204,16 @@ public class MesCalPlanServiceImplTest extends BaseDbUnitTest { verify(teamShiftService, never()).deleteByPlanId(plan.getId()); } + private static MesCalPlanDO randomPlan(LocalDateTime startDate, LocalDateTime endDate, Integer status) { + return randomPojo(MesCalPlanDO.class, o -> { + o.setCalendarType(1); + o.setStartDate(startDate); + o.setEndDate(endDate); + o.setStatus(status); + o.setShiftType(MesCalShiftTypeEnum.TWO.getType()); + o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod()); + o.setShiftCount(1); + }); + } + } diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImplTest.java index cd0d419..0d3749c 100644 --- a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImplTest.java +++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImplTest.java @@ -2,13 +2,17 @@ package cn.iocoder.yudao.module.mes.service.cal.team; import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +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.MesCalTeamDO; 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; @@ -18,10 +22,15 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import jakarta.annotation.Resource; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_REST_INVALID; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -45,6 +54,10 @@ public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest { private MesCalPlanShiftService planShiftService; @MockitoBean private MesCalPlanTeamService planTeamService; + @MockitoBean + private MesCalHolidayService holidayService; + @MockitoBean + private MesCalTeamService teamService; @Test public void testGenerateTeamShiftRecords_threeShift_rotateThreeStates() { @@ -79,22 +92,22 @@ public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest { // 调用 teamShiftService.generateTeamShiftRecords(planId); - // 断言:3 天 × 3 班次 = 9 条排班记录,班组按天轮转 - List records = teamShiftMapper.selectListByPlanId(planId); - records.sort(Comparator.comparing(MesCalTeamShiftDO::getDay).thenComparing(MesCalTeamShiftDO::getSort)); + // 断言:3 天 × 3 班次 = 9 条排班记录,班组按 白班 → 夜班 → 中班 轮转 + // 说明:原先按 白班 → 中班 → 夜班 轮转时,B 组 4/1 中班 24:00 下班后会立即接 4/2 00:00 的夜班,没有休息间隔 + List records = getRecords(planId); assertEquals(9, records.size()); // 断言:第 1 天(shiftIndex=0):A 白班、B 中班、C 夜班 assertRecord(records.get(0), LocalDateTime.of(2026, 4, 1, 0, 0), 1, 101L, 11L); assertRecord(records.get(1), LocalDateTime.of(2026, 4, 1, 0, 0), 2, 102L, 12L); assertRecord(records.get(2), LocalDateTime.of(2026, 4, 1, 0, 0), 3, 103L, 13L); - // 断言:第 2 天(shiftIndex=1):C 白班、A 中班、B 夜班 - assertRecord(records.get(3), LocalDateTime.of(2026, 4, 2, 0, 0), 1, 103L, 11L); - assertRecord(records.get(4), LocalDateTime.of(2026, 4, 2, 0, 0), 2, 101L, 12L); - assertRecord(records.get(5), LocalDateTime.of(2026, 4, 2, 0, 0), 3, 102L, 13L); - // 断言:第 3 天(shiftIndex=2):B 白班、C 中班、A 夜班 - assertRecord(records.get(6), LocalDateTime.of(2026, 4, 3, 0, 0), 1, 102L, 11L); - assertRecord(records.get(7), LocalDateTime.of(2026, 4, 3, 0, 0), 2, 103L, 12L); - assertRecord(records.get(8), LocalDateTime.of(2026, 4, 3, 0, 0), 3, 101L, 13L); + // 断言:第 2 天(shiftIndex=1):B 白班、C 中班、A 夜班(A、B 休息 8 小时,C 休息 32 小时) + assertRecord(records.get(3), LocalDateTime.of(2026, 4, 2, 0, 0), 1, 102L, 11L); + assertRecord(records.get(4), LocalDateTime.of(2026, 4, 2, 0, 0), 2, 103L, 12L); + assertRecord(records.get(5), LocalDateTime.of(2026, 4, 2, 0, 0), 3, 101L, 13L); + // 断言:第 3 天(shiftIndex=2):C 白班、A 中班、B 夜班 + assertRecord(records.get(6), LocalDateTime.of(2026, 4, 3, 0, 0), 1, 103L, 11L); + assertRecord(records.get(7), LocalDateTime.of(2026, 4, 3, 0, 0), 2, 101L, 12L); + assertRecord(records.get(8), LocalDateTime.of(2026, 4, 3, 0, 0), 3, 102L, 13L); } @Test @@ -121,6 +134,392 @@ public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest { assertRecord(records.get(1), start.plusDays(1), 1, 101L, 11L); } + @Test + public void testGenerateTeamShiftRecords_singleCrossMidnight() { + // 准备参数 + Long planId = 3003L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 21); + // mock 方法:单班次 22:00~06:00,跨午夜到次日结束,每天之间休息 16 小时 + mockPlan(planId, MesCalShiftTypeEnum.SINGLE, null, null, startDate, endDate); + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of( + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("夜班") + .startTime("22:00").endTime("06:00").build())); + mockTeams(planId, 101L); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:每天 1 条排班记录 + List records = getRecords(planId); + assertEquals(3, records.size()); + assertRecord(records.get(0), startDate.atStartOfDay(), 1, 101L, 11L); + assertRecord(records.get(1), startDate.plusDays(1).atStartOfDay(), 1, 101L, 11L); + assertRecord(records.get(2), endDate.atStartOfDay(), 1, 101L, 11L); + } + + @Test + public void testGenerateTeamShiftRecords_twoShiftFullDay_rotateWithoutRest() { + // 准备参数 + Long planId = 3004L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 21); + // mock 方法:两班倒、按天倒班 1 天,默认班次 08:00~20:00 / 20:00~08:00 覆盖全天 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate); + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00"); + mockTeams(planId, 101L, 102L); + when(teamService.getTeam(eq(102L))).thenReturn(MesCalTeamDO.builder().id(102L).name("乙班").build()); + + // 调用,并断言异常:B 组 10-19 夜班次日 08:00 下班后,立即接 10-20 08:00 的白班,连续上班 24 小时 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId), + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班", + LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班"); + // 断言:没有写入任何排班记录 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty()); + } + + @Test + public void testGenerateTeamShiftRecords_twoShiftFullDay_longerShiftCountOnlyDelay() { + // 准备参数 + Long planId = 3005L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 23); + // mock 方法:两班倒、按天倒班 3 天,默认班次覆盖全天 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 3, startDate, endDate); + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00"); + mockTeams(planId, 101L, 102L); + when(teamService.getTeam(eq(102L))).thenReturn(MesCalTeamDO.builder().id(102L).name("乙班").build()); + + // 调用,并断言异常:增加倒班天数只是推迟到 10-22 换班,B 组仍然夜班下班后立即接白班 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId), + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班", + LocalDate.of(2026, 10, 21), "夜班", LocalDate.of(2026, 10, 22), "白班"); + // 断言:没有写入任何排班记录 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty()); + } + + @Test + public void testGenerateTeamShiftRecords_twoShiftOverlap() { + // 准备参数 + Long planId = 3006L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 20); + // mock 方法:两班倒、按天倒班 1 天,夜班 20:00~08:30 与次日 08:00 的白班重叠 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate); + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:30"); + mockTeams(planId, 101L, 102L); + when(teamService.getTeam(eq(102L))).thenReturn(MesCalTeamDO.builder().id(102L).name("乙班").build()); + + // 调用,并断言异常 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId), + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班", + LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班"); + // 断言:没有写入任何排班记录 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty()); + } + + @Test + public void testGenerateTeamShiftRecords_twoShiftOneMinuteRest() { + // 准备参数 + Long planId = 3007L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 21); + // mock 方法:两班倒、按天倒班 1 天,夜班 20:00~07:59,与次日 08:00 的白班之间留 1 分钟 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate); + mockTwoShifts(planId, "08:00", "20:00", "20:00", "07:59"); + mockTeams(planId, 101L, 102L); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:3 天 × 2 班次 = 6 条排班记录,每天换班 + List records = getRecords(planId); + assertEquals(6, records.size()); + assertRecord(records.get(0), LocalDateTime.of(2026, 10, 19, 0, 0), 1, 101L, 11L); + assertRecord(records.get(1), LocalDateTime.of(2026, 10, 19, 0, 0), 2, 102L, 12L); + assertRecord(records.get(2), LocalDateTime.of(2026, 10, 20, 0, 0), 1, 102L, 11L); + assertRecord(records.get(3), LocalDateTime.of(2026, 10, 20, 0, 0), 2, 101L, 12L); + assertRecord(records.get(4), LocalDateTime.of(2026, 10, 21, 0, 0), 1, 101L, 11L); + assertRecord(records.get(5), LocalDateTime.of(2026, 10, 21, 0, 0), 2, 102L, 12L); + } + + @Test + public void testGenerateTeamShiftRecords_twoShiftFullDay_singleDay() { + // 准备参数 + Long planId = 3008L; + LocalDate day = LocalDate.of(2026, 10, 19); + // mock 方法:两班倒、默认班次覆盖全天,只排 1 天,不发生换班 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, day, day); + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00"); + mockTeams(planId, 101L, 102L); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:1 天 × 2 班次 = 2 条排班记录 + List records = getRecords(planId); + assertEquals(2, records.size()); + assertRecord(records.get(0), day.atStartOfDay(), 1, 101L, 11L); + assertRecord(records.get(1), day.atStartOfDay(), 2, 102L, 12L); + } + + @Test + public void testGenerateTeamShiftRecords_shiftTimeInvalid() { + // 准备参数 + Long planId = 3009L; + LocalDate day = LocalDate.of(2026, 10, 19); + // mock 方法:白班结束时间不合法 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, day, day); + mockTwoShifts(planId, "08:00", "25:00", "20:00", "08:00"); + mockTeams(planId, 101L, 102L); + + // 调用,并断言异常 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId), + CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, "白班"); + // 断言:没有写入任何排班记录 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty()); + } + + @Test + public void testGenerateTeamShiftRecords_threeShift_hourWithoutZeroAndEnd24() { + // 准备参数 + Long planId = 3016L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 20); + // mock 方法:三班倒、按天倒班 1 天,时间写作 8:00、0:00,中班结束写作 24:00 + mockPlan(planId, MesCalShiftTypeEnum.THREE, MesCalShiftMethodEnum.DAY, 1, startDate, endDate); + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of( + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班").startTime("8:00").endTime("16:00").build(), + MesCalPlanShiftDO.builder().id(12L).planId(planId).sort(2).name("中班").startTime("16:00").endTime("24:00").build(), + MesCalPlanShiftDO.builder().id(13L).planId(planId).sort(3).name("夜班").startTime("0:00").endTime("8:00").build())); + mockTeams(planId, 101L, 102L, 103L); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:与默认班次一致,2 天 × 3 班次 = 6 条排班记录,换班后休息 8 / 8 / 32 小时 + List records = getRecords(planId); + assertEquals(6, records.size()); + assertRecord(records.get(0), startDate.atStartOfDay(), 1, 101L, 11L); + assertRecord(records.get(1), startDate.atStartOfDay(), 2, 102L, 12L); + assertRecord(records.get(2), startDate.atStartOfDay(), 3, 103L, 13L); + assertRecord(records.get(3), endDate.atStartOfDay(), 1, 102L, 11L); + assertRecord(records.get(4), endDate.atStartOfDay(), 2, 103L, 12L); + assertRecord(records.get(5), endDate.atStartOfDay(), 3, 101L, 13L); + } + + @Test + public void testGenerateTeamShiftRecords_singleFullDayEnd24_withoutRest() { + // 准备参数 + Long planId = 3017L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 20); + // mock 方法:单班次 00:00~24:00,结束于次日 00:00,与次日的班次首尾相接 + mockPlan(planId, MesCalShiftTypeEnum.SINGLE, null, null, startDate, endDate); + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of( + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("全天班") + .startTime("00:00").endTime("24:00").build())); + mockTeams(planId, 101L); + when(teamService.getTeam(eq(101L))).thenReturn(MesCalTeamDO.builder().id(101L).name("甲班").build()); + + // 调用,并断言异常:24:00 不放宽连班校验 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId), + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "甲班", startDate, "全天班", endDate, "全天班"); + // 断言:没有写入任何排班记录 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty()); + } + + @Test + public void testGenerateTeamShiftRecords_shiftTimeInvalid_boundary() { + // 准备参数:开始时间不允许 24:00;结束时间只允许 24:00 整;分钟、小时越界;非 H:mm 格式 + String[][] cases = { + {"24:00", "08:00"}, {"08:00", "24:30"}, {"08:60", "16:00"}, + {"08:00", "16:5"}, {"8点", "16:00"}, {"08:00:00", "16:00"} + }; + for (int i = 0; i < cases.length; i++) { + Long planId = 3020L + i; + LocalDate day = LocalDate.of(2026, 10, 19); + // mock 方法:单班次 + mockPlan(planId, MesCalShiftTypeEnum.SINGLE, null, null, day, day); + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of( + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班") + .startTime(cases[i][0]).endTime(cases[i][1]).build())); + mockTeams(planId, 101L); + + // 调用,并断言异常 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId), + CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, "白班"); + // 断言:没有写入任何排班记录 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty()); + } + } + + @Test + public void testGenerateTeamShiftRecords_dayMethod_holidayNotConsumeShiftCount() { + // 准备参数 + Long planId = 3010L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 22); + // mock 方法:两班倒、按天倒班 2 天,班次之间留有空档 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 2, startDate, endDate); + mockTwoShifts(planId, "08:00", "17:00", "20:00", "05:00"); + mockTeams(planId, 101L, 102L); + // mock 方法:10-20 为节假日;10-21 为工作日,照常排班 + mockHolidays(startDate, endDate, buildHoliday(LocalDate.of(2026, 10, 20), MesCalHolidayTypeEnum.HOLIDAY), + buildHoliday(LocalDate.of(2026, 10, 21), MesCalHolidayTypeEnum.WORKDAY)); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:节假日停班,3 个有效排班日 × 2 班次 = 6 条排班记录 + List records = getRecords(planId); + assertEquals(6, records.size()); + // 断言:10-19、10-21 为第 1、2 个有效排班日,A 组白班 + assertRecord(records.get(0), LocalDateTime.of(2026, 10, 19, 0, 0), 1, 101L, 11L); + assertRecord(records.get(1), LocalDateTime.of(2026, 10, 19, 0, 0), 2, 102L, 12L); + assertRecord(records.get(2), LocalDateTime.of(2026, 10, 21, 0, 0), 1, 101L, 11L); + assertRecord(records.get(3), LocalDateTime.of(2026, 10, 21, 0, 0), 2, 102L, 12L); + // 断言:10-22 为第 3 个有效排班日,才换班 + assertRecord(records.get(4), LocalDateTime.of(2026, 10, 22, 0, 0), 1, 102L, 11L); + assertRecord(records.get(5), LocalDateTime.of(2026, 10, 22, 0, 0), 2, 101L, 12L); + } + + @Test + public void testGenerateTeamShiftRecords_dayMethod_firstDayHoliday() { + // 准备参数 + Long planId = 3011L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 21); + // mock 方法:两班倒、按天倒班 1 天,首日 10-19 为节假日 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate); + mockTwoShifts(planId, "08:00", "17:00", "20:00", "05:00"); + mockTeams(planId, 101L, 102L); + mockHolidays(startDate, endDate, buildHoliday(startDate, MesCalHolidayTypeEnum.HOLIDAY)); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:从 10-20 开始排班,10-20 为第 1 个有效排班日,不换班 + List records = getRecords(planId); + assertEquals(4, records.size()); + assertRecord(records.get(0), LocalDateTime.of(2026, 10, 20, 0, 0), 1, 101L, 11L); + assertRecord(records.get(1), LocalDateTime.of(2026, 10, 20, 0, 0), 2, 102L, 12L); + assertRecord(records.get(2), LocalDateTime.of(2026, 10, 21, 0, 0), 1, 102L, 11L); + assertRecord(records.get(3), LocalDateTime.of(2026, 10, 21, 0, 0), 2, 101L, 12L); + } + + @Test + public void testGenerateTeamShiftRecords_allHoliday() { + // 准备参数 + Long planId = 3012L; + LocalDate startDate = LocalDate.of(2026, 10, 19); + LocalDate endDate = LocalDate.of(2026, 10, 20); + // mock 方法:两班倒、计划范围内全部为节假日 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate); + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00"); + mockTeams(planId, 101L, 102L); + mockHolidays(startDate, endDate, buildHoliday(startDate, MesCalHolidayTypeEnum.HOLIDAY), + buildHoliday(endDate, MesCalHolidayTypeEnum.HOLIDAY)); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:不生成排班记录 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty()); + } + + @Test + public void testGenerateTeamShiftRecords_weekMethod_holidayOnWeekStart() { + // 10-18 周日、10-19 周一为节假日、10-20 周二 + assertRotateAcrossHoliday(3013L, MesCalShiftMethodEnum.WEEK, + LocalDate.of(2026, 10, 18), LocalDate.of(2026, 10, 19)); + } + + @Test + public void testGenerateTeamShiftRecords_monthMethod_holidayOnMonthStart() { + // 09-30、10-01 月初为节假日、10-02 + assertRotateAcrossHoliday(3014L, MesCalShiftMethodEnum.MONTH, + LocalDate.of(2026, 9, 30), LocalDate.of(2026, 10, 1)); + } + + @Test + public void testGenerateTeamShiftRecords_quarterMethod_holidayOnQuarterStart() { + // 06-30、07-01 季度初为节假日、07-02 + assertRotateAcrossHoliday(3015L, MesCalShiftMethodEnum.QUARTER, + LocalDate.of(2026, 6, 30), LocalDate.of(2026, 7, 1)); + } + + /** + * 断言按周/月/季度倒班时,周期起点恰逢节假日也照常换班 + * + * 默认两班倒班次覆盖全天,换班前一天夜班的班组,隔着节假日停班一天再上白班,留有 24 小时休息 + */ + private void assertRotateAcrossHoliday(Long planId, MesCalShiftMethodEnum shiftMethod, + LocalDate startDate, LocalDate holidayDate) { + // 准备参数 + LocalDate endDate = holidayDate.plusDays(1); + // mock 方法:两班倒、默认班次覆盖全天,周期起点为节假日 + mockPlan(planId, MesCalShiftTypeEnum.TWO, shiftMethod, null, startDate, endDate); + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00"); + mockTeams(planId, 101L, 102L); + mockHolidays(startDate, endDate, buildHoliday(holidayDate, MesCalHolidayTypeEnum.HOLIDAY)); + + // 调用 + teamShiftService.generateTeamShiftRecords(planId); + + // 断言:节假日停班,2 天 × 2 班次 = 4 条排班记录 + List records = getRecords(planId); + assertEquals(4, records.size()); + // 断言:周期起点前,A 组白班、B 组夜班 + assertRecord(records.get(0), startDate.atStartOfDay(), 1, 101L, 11L); + assertRecord(records.get(1), startDate.atStartOfDay(), 2, 102L, 12L); + // 断言:周期起点虽为节假日,仍然换班:B 组白班、A 组夜班 + assertRecord(records.get(2), endDate.atStartOfDay(), 1, 102L, 11L); + assertRecord(records.get(3), endDate.atStartOfDay(), 2, 101L, 12L); + } + + private void mockPlan(Long planId, MesCalShiftTypeEnum shiftType, MesCalShiftMethodEnum shiftMethod, + Integer shiftCount, LocalDate startDate, LocalDate endDate) { + when(planService.getPlan(planId)).thenReturn(MesCalPlanDO.builder() + .id(planId).code("PLAN-" + planId).name("排班计划" + planId) + .calendarType(1).startDate(startDate.atStartOfDay()).endDate(endDate.atStartOfDay()) + .shiftType(shiftType.getType()) + .shiftMethod(shiftMethod != null ? shiftMethod.getMethod() : null) + .shiftCount(shiftCount).build()); + } + + private void mockTwoShifts(Long planId, String dayStartTime, String dayEndTime, + String nightStartTime, String nightEndTime) { + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of( + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班") + .startTime(dayStartTime).endTime(dayEndTime).build(), + MesCalPlanShiftDO.builder().id(12L).planId(planId).sort(2).name("夜班") + .startTime(nightStartTime).endTime(nightEndTime).build())); + } + + private void mockTeams(Long planId, Long... teamIds) { + List teams = new ArrayList<>(); + for (int i = 0; i < teamIds.length; i++) { + teams.add(MesCalPlanTeamDO.builder().id(21L + i).planId(planId).teamId(teamIds[i]).build()); + } + when(planTeamService.getPlanTeamListByPlanId(planId)).thenReturn(teams); + } + + private void mockHolidays(LocalDate startDate, LocalDate endDate, MesCalHolidayDO... holidays) { + when(holidayService.getHolidayList(startDate.atStartOfDay(), endDate.atTime(23, 59, 59))) + .thenReturn(ListUtil.of(holidays)); + } + + private static MesCalHolidayDO buildHoliday(LocalDate day, MesCalHolidayTypeEnum type) { + return MesCalHolidayDO.builder().day(day.atStartOfDay()).type(type.getType()).build(); + } + + private List getRecords(Long planId) { + List records = teamShiftMapper.selectListByPlanId(planId); + records.sort(Comparator.comparing(MesCalTeamShiftDO::getDay).thenComparing(MesCalTeamShiftDO::getSort)); + return records; + } + private static void assertRecord(MesCalTeamShiftDO actual, LocalDateTime day, Integer sort, Long teamId, Long shiftId) { assertEquals(day, actual.getDay()); diff --git a/yudao-module-mes/src/test/resources/sql/clean.sql b/yudao-module-mes/src/test/resources/sql/clean.sql index 8cb28c3..106ff8d 100644 --- a/yudao-module-mes/src/test/resources/sql/clean.sql +++ b/yudao-module-mes/src/test/resources/sql/clean.sql @@ -2,6 +2,7 @@ DELETE FROM "mes_cal_team_shift"; DELETE FROM "mes_cal_plan_team"; DELETE FROM "mes_cal_plan_shift"; DELETE FROM "mes_cal_plan"; +DELETE FROM "mes_cal_holiday"; DELETE FROM "mes_md_auto_code_rule"; DELETE FROM "mes_md_auto_code_part"; DELETE FROM "mes_md_auto_code_record"; diff --git a/yudao-module-mes/src/test/resources/sql/create_tables.sql b/yudao-module-mes/src/test/resources/sql/create_tables.sql index fd73613..23fe4c2 100644 --- a/yudao-module-mes/src/test/resources/sql/create_tables.sql +++ b/yudao-module-mes/src/test/resources/sql/create_tables.sql @@ -80,6 +80,23 @@ CREATE TABLE IF NOT EXISTS "mes_cal_team_shift" ( ); -- ---------------------------- +-- MES 假期设置 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS "mes_cal_holiday" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "day" timestamp DEFAULT NULL, + "type" tinyint DEFAULT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +-- ---------------------------- -- MES 编码规则表 -- ---------------------------- CREATE TABLE IF NOT EXISTS "mes_md_auto_code_rule" (