From abf616a8ee083ab0f67a186bfb349ce164b04e32 Mon Sep 17 00:00:00 2001 From: liweijie Date: Wed, 16 Sep 2026 17:13:57 +0800 Subject: [PATCH] =?UTF-8?q?=E7=8F=AD=E5=8E=86=E5=8A=9F=E8=83=BD=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwgl/ClassEventCalendarController.java | 20 +++ .../jwgl/ClassSemesterCalendarController.java | 2 +- .../calendar/ClassCalendarBatchSaveDTO.java | 22 +++ .../dto/calendar/ClassCalendarEventDTO.java | 5 + .../java/com/roomroot/jwgl/entity/JCSJB.java | 10 +- .../com/roomroot/jwgl/mapper/JCSJBMapper.java | 9 + .../service/ClassEventCalendarService.java | 16 +- .../jwgl/service/ClassPeriodService.java | 21 +++ .../impl/ClassEventCalendarServiceImpl.java | 166 +++++++++++++++++- .../service/impl/ClassPeriodServiceImpl.java | 60 +++++++ .../impl/SchedulingWindowServiceImpl.java | 29 +-- .../impl/SemesterCalendarServiceImpl.java | 14 +- .../impl/TimetableConflictServiceImpl.java | 48 +++-- .../roomroot/jwgl/utils/OperationLogUtil.java | 4 + .../com/roomroot/jwgl/utils/PeriodUtil.java | 52 ++++++ .../vo/calendar/ClassCalendarResyncVO.java | 28 +++ .../src/api/teachOffice/classCalendarEvent.js | 18 ++ .../components/SchoolCalendarEditor.vue | 41 ++++- .../ClassEventCalendarDialog.vue | 34 +++- 19 files changed, 526 insertions(+), 73 deletions(-) create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarBatchSaveDTO.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/JCSJBMapper.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassPeriodService.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassPeriodServiceImpl.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/PeriodUtil.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/calendar/ClassCalendarResyncVO.java diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java index bdf28ce7..ecb8db72 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java @@ -1,9 +1,11 @@ package com.roomroot.web.controller.jwgl; import com.roomroot.jwgl.dto.calendar.ClassCalendarApplyDTO; +import com.roomroot.jwgl.dto.calendar.ClassCalendarBatchSaveDTO; import com.roomroot.jwgl.dto.calendar.ClassCalendarEventDTO; import com.roomroot.jwgl.service.ClassEventCalendarService; import com.roomroot.jwgl.unit.Result; +import com.roomroot.jwgl.vo.calendar.ClassCalendarResyncVO; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -35,6 +37,24 @@ public class ClassEventCalendarController { return Result.success(); } + /** + * 批量保存班历时间格(拖选区域一次提交,单事务 upsert;action=clear 恢复默认) + */ + @PostMapping("/batchSave") + public Result batchSave(@RequestBody ClassCalendarBatchSaveDTO body) { + int saved = classEventCalendarService.batchSave(body); + return Result.success("已保存 " + saved + " 个时间格", saved); + } + + /** + * 重新同步校历:confirm=false 返回缺失格预览,confirm=true 实际补齐(只补缺失,不覆盖人工改动) + */ + @PostMapping("/resync") + public Result resync(@RequestParam("xydxqbh") String xydxqbh, + @RequestParam(value = "confirm", defaultValue = "false") boolean confirm) { + return Result.success(classEventCalendarService.resync(xydxqbh, confirm)); + } + @PostMapping("/apply") public Result apply(@RequestBody ClassCalendarApplyDTO body) { return Result.success(classEventCalendarService.apply( diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassSemesterCalendarController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassSemesterCalendarController.java index 6fa62d72..4b595bb5 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassSemesterCalendarController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassSemesterCalendarController.java @@ -13,7 +13,7 @@ import java.util.Map; * 学员队任务表控制器 */ @RestController -@RequestMapping("/classCalendar") +@RequestMapping({"/classCalendar", "/course-task"}) public class ClassSemesterCalendarController { @Resource diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarBatchSaveDTO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarBatchSaveDTO.java new file mode 100644 index 00000000..974685e0 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarBatchSaveDTO.java @@ -0,0 +1,22 @@ +package com.roomroot.jwgl.dto.calendar; + +import lombok.Data; + +import java.util.List; + +/** + * 班历批量保存入参。一次拖选产生的所有时间格在单事务内 upsert。 + */ +@Data +public class ClassCalendarBatchSaveDTO { + + /** + * 学员队学期编号(班次学期) + */ + private String xydxqbh; + + /** + * 时间格列表。cell.action = "clear" 表示恢复默认(清名称、可排课)。 + */ + private List cells; +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java index ead54af0..144563a1 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java @@ -21,4 +21,9 @@ public class ClassCalendarEventDTO { private Boolean zdpk; private Boolean zk; private String courseClass; + + /** + * 批量保存动作:"clear" 表示恢复默认(清名称、可排课);空或其它值按事件内容写入。 + */ + private String action; } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JCSJB.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JCSJB.java index 4c565d73..a9d0ab88 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JCSJB.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JCSJB.java @@ -1,10 +1,10 @@ package com.roomroot.jwgl.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; -import lombok.EqualsAndHashCode; /** * 节次时间表 @@ -22,41 +22,49 @@ public class JCSJB { /** * 双节次 */ + @TableField("双节次") private Boolean sjc; /** * 节次索引 */ + @TableField("节次索引") private String jcsy; /** * 开始时间 */ + @TableField("开始时间") private String kssj; /** * 结束时间 */ + @TableField("结束时间") private String jssj; /** * 备注 */ + @TableField("备注") private String bz; /** * 名称 */ + @TableField("名称") private String mc; /** * 简称 */ + @TableField("简称") private String jc; /** * 时段 */ + @TableField("时段") private String sd; } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/JCSJBMapper.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/JCSJBMapper.java new file mode 100644 index 00000000..ef045a8a --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/JCSJBMapper.java @@ -0,0 +1,9 @@ +package com.roomroot.jwgl.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.roomroot.jwgl.entity.JCSJB; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface JCSJBMapper extends BaseMapper { +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java index 6daa4658..a0444f43 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java @@ -1,6 +1,8 @@ package com.roomroot.jwgl.service; +import com.roomroot.jwgl.dto.calendar.ClassCalendarBatchSaveDTO; import com.roomroot.jwgl.dto.calendar.ClassCalendarEventDTO; +import com.roomroot.jwgl.vo.calendar.ClassCalendarResyncVO; import java.util.List; @@ -15,10 +17,22 @@ public interface ClassEventCalendarService { List listOrInherit(String xydxqbh); /** - * 更新一个时间格。班次日期范围外的格子拒绝保存。 + * 更新一个时间格(upsert:编号不存在时按 学员队学期编号+日期+节次 定位,仍无则插入)。 + * 班次日期范围外的格子拒绝保存。 */ void update(ClassCalendarEventDTO event); + /** + * 批量保存时间格,单事务 upsert;cell.action="clear" 恢复默认。 + */ + int batchSave(ClassCalendarBatchSaveDTO body); + + /** + * 重新同步校历:只补校历有而班历缺失的时间格,不覆盖人工改动。 + * confirm=false 仅返回差异预览;confirm=true 实际写入。 + */ + ClassCalendarResyncVO resync(String xydxqbh, boolean confirm); + /** * 把选定或整个班历覆盖到其它班次的对应时间格。不改课程任务。 */ diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassPeriodService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassPeriodService.java new file mode 100644 index 00000000..e5cf0e34 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassPeriodService.java @@ -0,0 +1,21 @@ +package com.roomroot.jwgl.service; + +import java.util.List; +import java.util.Set; + +/** + * 节次字典。校历 / 班历 / 场地历的节次段统一由「节次时间表」驱动, + * 表内无数据时回退到默认六段(上午三节 + 下午一节 + 晚上两节)。 + */ +public interface ClassPeriodService { + + /** + * 双节次时间格标签(如 1-2、3-4),按起始节次排序。 + */ + List slotLabels(); + + /** + * 上午时段的双节次标签,用于「正课」默认判定。 + */ + Set mainSlotLabels(); +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java index cd600e08..1423fdb0 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java @@ -2,6 +2,7 @@ package com.roomroot.jwgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.roomroot.common.exception.ServiceException; +import com.roomroot.jwgl.dto.calendar.ClassCalendarBatchSaveDTO; import com.roomroot.jwgl.dto.calendar.ClassCalendarEventDTO; import com.roomroot.jwgl.entity.JQB; import com.roomroot.jwgl.entity.XQXLB; @@ -10,9 +11,11 @@ import com.roomroot.jwgl.mapper.ClassSemesterMapper; import com.roomroot.jwgl.mapper.JQBMapper; import com.roomroot.jwgl.mapper.SemesterCalendarMapper; import com.roomroot.jwgl.service.ClassEventCalendarService; +import com.roomroot.jwgl.service.ClassPeriodService; import com.roomroot.jwgl.service.TeachingTaskWriteGuard; import com.roomroot.jwgl.utils.SemesterCodeUtil; import com.roomroot.jwgl.utils.UuidUtil; +import com.roomroot.jwgl.vo.calendar.ClassCalendarResyncVO; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -21,7 +24,6 @@ import jakarta.annotation.Resource; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -38,11 +40,14 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter COMPACT = DateTimeFormatter.ofPattern("yyyyMMdd"); - private static final List PERIODS = Arrays.asList("1-2", "3-4", "5-6", "7-8", "9-10", "11-12"); + private static final String ACTION_CLEAR = "clear"; @Resource private JQBMapper jqbMapper; + @Resource + private ClassPeriodService classPeriodService; + @Resource private ClassSemesterMapper classSemesterMapper; @@ -69,18 +74,158 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService } @Override + @Transactional(rollbackFor = Exception.class) public void update(ClassCalendarEventDTO event) { - if (event == null || event.getBh() == null || event.getBh().isEmpty()) { - throw new ServiceException("班历编号不能为空", BAD_REQUEST); + if (event == null) { + throw new ServiceException("班历时间格不能为空", BAD_REQUEST); } - JQB existing = jqbMapper.selectById(event.getBh()); + JQB existing = findCell(event); if (existing == null) { - throw new ServiceException("班历时间格不存在", BAD_REQUEST); + // upsert:编号查不到时按「学员队学期编号+日期+节次」再定位,仍无则插入新格, + // 使班次日期范围扩大(或校历新增的节次段)后出现的格子也能写入。 + if (event.getXydxqbh() == null || event.getXydxqbh().isEmpty()) { + throw new ServiceException("班历时间格不存在", BAD_REQUEST); + } + teachingTaskWriteGuard.assertCourseTaskWritable(event.getXydxqbh()); + XYDNDXQJBXXB semester = requireSemester(event.getXydxqbh()); + LocalDate day = parseDay(event.getJqsj()); + assertInRange(semester, day); + if (event.getCourseClass() == null || event.getCourseClass().isEmpty()) { + throw new ServiceException("班历节次不能为空", BAD_REQUEST); + } + applyCell(semester, null, day, event); + return; } teachingTaskWriteGuard.assertCourseTaskWritable(existing.getXydxqbh()); XYDNDXQJBXXB semester = requireSemester(existing.getXydxqbh()); LocalDate day = parseDay(event.getJqsj() == null ? formatDay(existing.getJqsj()) : event.getJqsj()); assertInRange(semester, day); + applyCell(semester, existing, day, event); + } + + /** + * 批量保存:一次拖选的所有时间格在单事务内 upsert,action=clear 恢复默认。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int batchSave(ClassCalendarBatchSaveDTO body) { + if (body == null || body.getXydxqbh() == null || body.getXydxqbh().isEmpty()) { + throw new ServiceException("请选择班次学期", BAD_REQUEST); + } + List cells = body.getCells(); + if (CollectionUtils.isEmpty(cells)) { + return 0; + } + teachingTaskWriteGuard.assertCourseTaskWritable(body.getXydxqbh()); + XYDNDXQJBXXB semester = requireSemester(body.getXydxqbh()); + // 一次性拉出该班次学期全部已有格,按 (日期|节次) 建索引避免逐格查询 + Map existingByKey = new LinkedHashMap<>(); + for (JQB row : listRows(body.getXydxqbh())) { + existingByKey.putIfAbsent(cellKey(row.getJqsj(), row.getCourseClass()), row); + } + int count = 0; + for (ClassCalendarEventDTO cell : cells) { + if (cell == null || cell.getCourseClass() == null || cell.getCourseClass().isEmpty()) { + continue; + } + LocalDate day = parseDay(cell.getJqsj()); + assertInRange(semester, day); + JQB existing = existingByKey.get(cellKey(Integer.valueOf(day.format(COMPACT)), cell.getCourseClass())); + applyCell(semester, existing, day, cell); + count++; + } + return count; + } + + /** + * 重新同步校历(口径A:只补缺失格,不覆盖人工改动)。 + * confirm=false 返回差异预览;confirm=true 实际写入。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ClassCalendarResyncVO resync(String xydxqbh, boolean confirm) { + XYDNDXQJBXXB semester = requireSemester(xydxqbh); + LocalDate start = semester.getKxrq(); + LocalDate end = semester.getJsrq(); + if (start == null || end == null) { + throw new ServiceException("班次学期缺少开学或结束日期,不能同步校历", BAD_REQUEST); + } + Set existingKeys = new HashSet<>(); + for (JQB row : listRows(xydxqbh)) { + existingKeys.add(cellKey(row.getJqsj(), row.getCourseClass())); + } + List missing = new ArrayList<>(); + for (XQXLB school : mergeByDayAndPeriod(loadSchoolRows(semester, start, end), start, end)) { + LocalDate day = parseDay(school.getJqsj()); + if (!existingKeys.contains(cellKey(Integer.valueOf(day.format(COMPACT)), school.getCourseClass()))) { + ClassCalendarEventDTO dto = new ClassCalendarEventDTO(); + dto.setJqsj(day.format(DAY)); + dto.setCourseClass(school.getCourseClass()); + dto.setJqmc(school.getJqmc()); + dto.setJc(Boolean.TRUE.equals(school.getJc())); + dto.setKpk(Boolean.TRUE.equals(school.getKpk())); + dto.setBzxs(Boolean.TRUE.equals(school.getBzxs())); + dto.setZdpk(Boolean.TRUE.equals(school.getZdpk())); + dto.setZk(Boolean.TRUE.equals(school.getZk())); + dto.setBz(school.getBz()); + missing.add(dto); + } + } + ClassCalendarResyncVO vo = new ClassCalendarResyncVO(); + vo.setItems(missing); + vo.setAddCount(missing.size()); + if (confirm && !missing.isEmpty()) { + teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh); + for (ClassCalendarEventDTO dto : missing) { + insertCell(semester, parseDay(dto.getJqsj()), dto.getCourseClass(), dto.getJqmc(), + flag(dto.getJc()), flag(dto.getKpk()), flag(dto.getBzxs()), + flag(dto.getZdpk()), flag(dto.getZk()), dto.getBz()); + } + vo.setExecuted(true); + } + return vo; + } + + /** + * 按 编号 / (学员队学期编号+日期+节次) 定位已有格。 + */ + private JQB findCell(ClassCalendarEventDTO event) { + if (event.getBh() != null && !event.getBh().isEmpty()) { + JQB row = jqbMapper.selectById(event.getBh()); + if (row != null) { + return row; + } + } + if (event.getXydxqbh() == null || event.getXydxqbh().isEmpty() + || event.getJqsj() == null || event.getCourseClass() == null) { + return null; + } + List matched = jqbMapper.selectList(new LambdaQueryWrapper() + .eq(JQB::getXydxqbh, event.getXydxqbh()) + .eq(JQB::getJqsj, Integer.valueOf(parseDay(event.getJqsj()).format(COMPACT))) + .eq(JQB::getCourseClass, event.getCourseClass())); + return matched.isEmpty() ? null : matched.get(0); + } + + /** + * 把一格事件写入 existing(null 时新建)。action=clear 恢复默认:清名称、可排课。 + */ + private void applyCell(XYDNDXQJBXXB semester, JQB existing, LocalDate day, ClassCalendarEventDTO event) { + if (ACTION_CLEAR.equals(event.getAction())) { + event.setJqmc(""); + event.setBz(null); + event.setJc(Boolean.FALSE); + event.setKpk(Boolean.TRUE); + event.setBzxs(Boolean.FALSE); + event.setZdpk(Boolean.FALSE); + event.setZk(Boolean.FALSE); + } + if (existing == null) { + insertCell(semester, day, event.getCourseClass(), event.getJqmc(), + flag(event.getJc()), flag(event.getKpk()), flag(event.getBzxs()), + flag(event.getZdpk()), flag(event.getZk()), event.getBz()); + return; + } existing.setJqmc(event.getJqmc() == null ? "" : event.getJqmc()); existing.setBz(event.getBz()); existing.setJc(flag(event.getJc())); @@ -95,6 +240,10 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService jqbMapper.updateById(existing); } + private String cellKey(Integer jqsj, String courseClass) { + return jqsj + "|" + courseClass; + } + @Override @Transactional(rollbackFor = Exception.class) public int apply(String sourceXydxqbh, List targetBhList, List eventBhList) { @@ -219,10 +368,11 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService } private void generateEmptyGrid(XYDNDXQJBXXB semester, LocalDate start, LocalDate end) { + Set mainSlots = classPeriodService.mainSlotLabels(); for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) { boolean weekdayMain = day.getDayOfWeek().getValue() <= 5; - for (String period : PERIODS) { - boolean mainSlot = weekdayMain && ("1-2".equals(period) || "3-4".equals(period) || "5-6".equals(period)); + for (String period : classPeriodService.slotLabels()) { + boolean mainSlot = weekdayMain && mainSlots.contains(period); insertCell(semester, day, period, "", 0, mainSlot ? 1 : 0, 0, mainSlot ? 1 : 0, mainSlot ? 1 : 0, null); } } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassPeriodServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassPeriodServiceImpl.java new file mode 100644 index 00000000..8d7be1f6 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassPeriodServiceImpl.java @@ -0,0 +1,60 @@ +package com.roomroot.jwgl.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.roomroot.jwgl.entity.JCSJB; +import com.roomroot.jwgl.mapper.JCSJBMapper; +import com.roomroot.jwgl.service.ClassPeriodService; +import com.roomroot.jwgl.utils.PeriodUtil; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class ClassPeriodServiceImpl implements ClassPeriodService { + + private static final List DEFAULT_SLOTS = + Arrays.asList("1-2", "3-4", "5-6", "7-8", "9-10", "11-12"); + private static final Set DEFAULT_MAIN_SLOTS = + new LinkedHashSet<>(Arrays.asList("1-2", "3-4", "5-6")); + private static final String MAIN_PERIOD_OF_DAY = "上午"; + + @Resource + private JCSJBMapper jcsjbMapper; + + @Override + public List slotLabels() { + List slots = loadSlots(); + if (slots.isEmpty()) { + return DEFAULT_SLOTS; + } + return slots.stream().map(JCSJB::getJcsy) + .filter(s -> s != null && !s.isEmpty()).collect(Collectors.toList()); + } + + @Override + public Set mainSlotLabels() { + List slots = loadSlots(); + if (slots.isEmpty()) { + return DEFAULT_MAIN_SLOTS; + } + Set main = slots.stream() + .filter(s -> MAIN_PERIOD_OF_DAY.equals(s.getSd())) + .map(JCSJB::getJcsy) + .filter(s -> s != null && !s.isEmpty()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return main.isEmpty() ? DEFAULT_MAIN_SLOTS : main; + } + + private List loadSlots() { + List rows = jcsjbMapper.selectList(new LambdaQueryWrapper() + .eq(JCSJB::getSjc, true)); + rows.sort(Comparator.comparingInt(r -> PeriodUtil.firstPeriod(r.getJcsy()))); + return rows; + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java index 3072edb0..6a6be44e 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java @@ -37,6 +37,7 @@ import com.roomroot.jwgl.mapper.SemesterCalendarMapper; import com.roomroot.jwgl.service.SchedulingWindowService; import com.roomroot.jwgl.utils.JwglRoleHelper; import com.roomroot.jwgl.utils.SemesterCodeUtil; +import com.roomroot.jwgl.utils.PeriodUtil; import com.roomroot.jwgl.utils.UuidUtil; import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO; import jakarta.annotation.Resource; @@ -187,7 +188,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { .eq(XQXLB::getNd, ctx.ndCode)); for (XQXLB row : schoolRows) { if (row.getJqsj() == null) continue; - for (Integer jc : parsePeriods(row.getCourseClass())) { + for (Integer jc : PeriodUtil.parsePeriods(row.getCourseClass())) { String key = row.getJqsj() + "#" + jc; if (Boolean.FALSE.equals(row.getKpk())) schoolHard.add(key); else if (Boolean.FALSE.equals(row.getZk())) schoolSoft.add(key); @@ -200,7 +201,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { for (JQB row : classRows) { if (row.getJqsj() == null) continue; String date = intDate(row.getJqsj()); - for (Integer jc : parsePeriods(row.getCourseClass())) { + for (Integer jc : PeriodUtil.parsePeriods(row.getCourseClass())) { String key = date + "#" + jc; if (row.getKpk() != null && row.getKpk() == 0) classHard.add(key); else if (row.getZk() != null && row.getZk() == 0) classSoft.add(key); @@ -694,7 +695,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { for (XQXLB row : semesterCalendarMapper.selectList(new LambdaQueryWrapper() .eq(XQXLB::getNd, ctx.ndCode).eq(XQXLB::getKpk, false))) { if (row.getJqsj() == null) continue; - for (Integer jc : parsePeriods(row.getCourseClass())) keys.add(row.getJqsj() + "#" + jc); + for (Integer jc : PeriodUtil.parsePeriods(row.getCourseClass())) keys.add(row.getJqsj() + "#" + jc); } } else { for (JQB row : jqbMapper.selectList(new LambdaQueryWrapper() @@ -702,7 +703,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { .eq(JQB::getKpk, 0))) { if (row.getJqsj() == null) continue; String date = intDate(row.getJqsj()); - for (Integer jc : parsePeriods(row.getCourseClass())) keys.add(date + "#" + jc); + for (Integer jc : PeriodUtil.parsePeriods(row.getCourseClass())) keys.add(date + "#" + jc); } } return keys; @@ -781,26 +782,6 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { return s.length() == 8 ? s.substring(0, 4) + "-" + s.substring(4, 6) + "-" + s.substring(6, 8) : s; } - private List parsePeriods(String label) { - List out = new ArrayList<>(); - if (label == null) return out; - for (String token : label.split("[,,]")) { - String t = token.trim(); - if (t.contains("-")) { - String[] parts = t.split("-"); - try { - int a = Integer.parseInt(parts[0].trim()); - int b = Integer.parseInt(parts[1].trim()); - for (int i = Math.min(a, b); i <= Math.max(a, b); i++) out.add(i); - } catch (Exception ignored) { } - } else { - try { - out.add(Integer.parseInt(t)); - } catch (Exception ignored) { } - } - } - return out; - } private static String safe(String s) { return s == null || s.isEmpty() ? "-" : s; diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterCalendarServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterCalendarServiceImpl.java index 75142422..3e21427f 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterCalendarServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterCalendarServiceImpl.java @@ -33,15 +33,12 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService { private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - /** - * 正课时段:与班历空网格 {@code generateEmptyGrid} 的规则保持一致 - * (周一至周五的 1-2 / 3-4 / 5-6 节)。 - */ - private static final List MAIN_PERIODS = Arrays.asList("1-2", "3-4", "5-6"); - @Resource private SemesterCalendarMapper xqxlbMapper; + @Resource + private com.roomroot.jwgl.service.ClassPeriodService classPeriodService; + @Resource private XQMapper xqMapper; @@ -135,8 +132,11 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService { /** * 正课判定:周一至周五的 1-2 / 3-4 / 5-6 节。 */ + /** + * 正课判定:周一至周五且节次段属于「节次时间表」的上午时段。 + */ private boolean isMainCourse(LocalDate date, String courseClass) { - return date.getDayOfWeek().getValue() <= 5 && MAIN_PERIODS.contains(courseClass); + return date.getDayOfWeek().getValue() <= 5 && classPeriodService.mainSlotLabels().contains(courseClass); } @Override diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java index ff9a1296..8a8e01ab 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java @@ -9,8 +9,10 @@ import com.roomroot.jwgl.entity.SSKCB; import com.roomroot.jwgl.entity.SSKCXYD; import com.roomroot.jwgl.entity.TimetableConflictResult; import com.roomroot.jwgl.entity.XYDB; +import com.roomroot.jwgl.entity.XYDNDXQJBXXB; import com.roomroot.jwgl.entity.XQXLB; import com.roomroot.jwgl.mapper.ClassRoomCalendarMapper; +import com.roomroot.jwgl.mapper.ClassSemesterMapper; import com.roomroot.jwgl.mapper.JQBMapper; import com.roomroot.jwgl.mapper.KBMapper; import com.roomroot.jwgl.mapper.SSKCBFZJYMapper; @@ -23,6 +25,8 @@ import com.roomroot.jwgl.mapper.SemesterCalendarMapper; import com.roomroot.jwgl.mapper.TimetableConflictResultMapper; import com.roomroot.jwgl.mapper.XYDBMapper; import com.roomroot.jwgl.service.TimetableConflictService; +import com.roomroot.jwgl.utils.PeriodUtil; +import com.roomroot.jwgl.utils.SemesterCodeUtil; import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.unit.conflict.TimetableConflictDimension; import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictCardVO; @@ -86,6 +90,9 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { @Resource private JQBMapper jqbMapper; + @Resource + private ClassSemesterMapper classSemesterMapper; + @Resource private ClassRoomCalendarMapper classRoomCalendarMapper; @@ -516,7 +523,7 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { .eq(XQXLB::getNd, nd) .eq(XQXLB::getKpk, false)); for (XQXLB grid : schoolUnavailable) { - for (Integer jc : parsePeriods(grid.getCourseClass())) { + for (Integer jc : PeriodUtil.parsePeriods(grid.getCourseClass())) { List hits = byGrid.get(grid.getJqsj() + "#" + jc); if (hits != null && !hits.isEmpty()) { result.add(buildEventDetail("SCHOOL|" + grid.getJqsj() + "#" + jc, "校历不可排课格", @@ -527,9 +534,19 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { } // ---- 班历:对某学员队可排课=0 的格 ---- - List classUnavailable = jqbMapper.selectList(new LambdaQueryWrapper() - .eq(JQB::getNd, nd) - .eq(JQB::getKpk, 0)); + // 按班次学期(学员队学期编号)限定:同一年度可能存在多个学期, + // 仅按 年度 过滤会把其它学期的不可排格混进来。 + List semesterRows = classSemesterMapper.selectList(new LambdaQueryWrapper() + .eq(XYDNDXQJBXXB::getDelFlag, 0)); + Set semesterBhs = semesterRows.stream() + .filter(s -> Objects.equals(nd, SemesterCodeUtil.resolve(s.getNd(), s.getXqdc())) + || Objects.equals(nd, s.getNd())) + .map(XYDNDXQJBXXB::getBh) + .collect(Collectors.toSet()); + List classUnavailable = semesterBhs.isEmpty() ? Collections.emptyList() + : jqbMapper.selectList(new LambdaQueryWrapper() + .in(JQB::getXydxqbh, semesterBhs) + .eq(JQB::getKpk, 0)); if (!classUnavailable.isEmpty()) { Map lessonByBh = lessons.stream() .collect(Collectors.toMap(SSKCB::getBh, l -> l, (a, b) -> a)); @@ -544,7 +561,7 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { for (JQB grid : classUnavailable) { if (grid.getJqsj() == null || grid.getCourseClass() == null) continue; String dateStr = intDateToString(grid.getJqsj()); - for (Integer jc : parsePeriods(grid.getCourseClass())) { + for (Integer jc : PeriodUtil.parsePeriods(grid.getCourseClass())) { for (Map.Entry> e : teamsOfLesson.entrySet()) { if (!e.getValue().contains(grid.getXydbh())) continue; SSKCB lesson = lessonByBh.get(e.getKey()); @@ -646,27 +663,6 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { return names; } - /** 解析历表「节次」标签为节次号集合:"1-2"→[1,2],"3"→[3],无法解析返回空 */ - private List parsePeriods(String label) { - List out = new ArrayList<>(); - if (label == null) return out; - for (String token : label.split("[,,]")) { - String t = token.trim(); - if (t.contains("-")) { - String[] parts = t.split("-"); - try { - int a = Integer.parseInt(parts[0].trim()); - int b = Integer.parseInt(parts[1].trim()); - for (int i = Math.min(a, b); i <= Math.max(a, b); i++) out.add(i); - } catch (Exception ignored) { } - } else { - try { - out.add(Integer.parseInt(t)); - } catch (Exception ignored) { } - } - } - return out; - } private static String intDateToString(Integer d) { if (d == null) return null; diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/OperationLogUtil.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/OperationLogUtil.java index b1754f31..aec6c97a 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/OperationLogUtil.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/OperationLogUtil.java @@ -104,6 +104,7 @@ public final class OperationLogUtil { "教学场地历管理", "教室输出课程表管理", "学期校历管理", + "班历管理", "教材管理", "教材库存管理", "其他")); @@ -1178,6 +1179,9 @@ public final class OperationLogUtil { if (normalizedUrl.contains("/xqxlb/")) { return "学期校历管理"; } + if (normalizedUrl.contains("/class-calendar/")) { + return "班历管理"; + } if (normalizedUrl.contains("/teachingmaterial/")) { return "教材管理"; } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/PeriodUtil.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/PeriodUtil.java new file mode 100644 index 00000000..f58c6531 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/PeriodUtil.java @@ -0,0 +1,52 @@ +package com.roomroot.jwgl.utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * 历表「节次」标签解析。校历 / 班历 / 场地历的节次段(如 "1-2"、"3"、"1,3-4") + * 统一在这里展开为单节次号,供排课窗、冲突检测、历表读写共用。 + */ +public final class PeriodUtil { + + private PeriodUtil() { + } + + /** + * 解析节次标签为节次号集合:"1-2"→[1,2],"3"→[3],"1,3-4"→[1,3,4];无法解析返回空。 + */ + public static List parsePeriods(String label) { + List out = new ArrayList<>(); + if (label == null) { + return out; + } + for (String token : label.split("[,,]")) { + String t = token.trim(); + if (t.contains("-")) { + String[] parts = t.split("-"); + try { + int a = Integer.parseInt(parts[0].trim()); + int b = Integer.parseInt(parts[1].trim()); + for (int i = Math.min(a, b); i <= Math.max(a, b); i++) { + out.add(i); + } + } catch (Exception ignored) { + } + } else { + try { + out.add(Integer.parseInt(t)); + } catch (Exception ignored) { + } + } + } + return out; + } + + /** + * 节次段的起始节次号(用于排序),无法解析返回 Integer.MAX_VALUE。 + */ + public static int firstPeriod(String label) { + List parsed = parsePeriods(label); + return parsed.isEmpty() ? Integer.MAX_VALUE : parsed.get(0); + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/calendar/ClassCalendarResyncVO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/calendar/ClassCalendarResyncVO.java new file mode 100644 index 00000000..621d79f9 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/calendar/ClassCalendarResyncVO.java @@ -0,0 +1,28 @@ +package com.roomroot.jwgl.vo.calendar; + +import com.roomroot.jwgl.dto.calendar.ClassCalendarEventDTO; +import lombok.Data; + +import java.util.List; + +/** + * 班历重新同步校历结果。confirm=false 时为差异预览,confirm=true 时为实际补齐结果。 + */ +@Data +public class ClassCalendarResyncVO { + + /** + * 校历中有、班历缺失的时间格数量 + */ + private int addCount; + + /** + * 待补齐的时间格明细(按校历口径) + */ + private List items; + + /** + * 是否已实际写入(false = 仅预览) + */ + private boolean executed; +} diff --git a/frontend/src/api/teachOffice/classCalendarEvent.js b/frontend/src/api/teachOffice/classCalendarEvent.js index 8083d284..ba42f5e9 100644 --- a/frontend/src/api/teachOffice/classCalendarEvent.js +++ b/frontend/src/api/teachOffice/classCalendarEvent.js @@ -16,6 +16,24 @@ export function updateClassCalendarEvent(data) { }) } +// 批量保存班历时间格(拖选区域一次提交;cell.action='clear' 恢复默认) +export function batchSaveClassCalendarEvent(xydxqbh, cells) { + return request({ + url: '/class-calendar/batchSave', + method: 'post', + data: { xydxqbh, cells } + }) +} + +// 重新同步校历:confirm=false 返回缺失格预览,confirm=true 实际补齐 +export function resyncClassCalendar(xydxqbh, confirm) { + return request({ + url: '/class-calendar/resync', + method: 'post', + params: { xydxqbh, confirm } + }) +} + export function applyClassCalendarEvent(data) { return request({ url: '/class-calendar/apply', diff --git a/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue b/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue index c4ee430b..05acb916 100644 --- a/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue +++ b/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue @@ -140,7 +140,7 @@