From 518743528528d50ff8369cd6bc95a1fcf71f3dd6 Mon Sep 17 00:00:00 2001 From: liweijie Date: Mon, 14 Sep 2026 17:24:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E6=9C=9F=E6=96=B0=E5=A2=9E=E7=9A=84?= =?UTF-8?q?=E6=97=B6=E5=80=99=E6=A0=A1=E9=AA=8C=E6=A0=A1=E5=8E=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=82=B9=E6=B7=BB=E5=8A=A0=EF=BC=8C=E5=90=A6=E5=88=99?= =?UTF-8?q?=E4=BC=9A=E6=9C=89=E5=A4=A7=E9=87=8F=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwgl/SemesterCalendarController.java | 8 +- .../jwgl/service/SemesterCalendarService.java | 7 +- .../impl/ClassEventCalendarServiceImpl.java | 62 ++++++- .../impl/ClassSemesterServiceImpl.java | 35 +++- .../impl/SemesterCalendarServiceImpl.java | 168 +++++++++++++----- .../service/impl/SemesterServiceImpl.java | 44 ++++- .../roomroot/jwgl/utils/SemesterCodeUtil.java | 76 ++++++++ frontend/src/api/teachBusiness/xqxlb.js | 8 +- .../components/SchoolCalendarEditor.vue | 31 ++-- 9 files changed, 352 insertions(+), 87 deletions(-) create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SemesterCodeUtil.java diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SemesterCalendarController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SemesterCalendarController.java index 085a12941..c52660684 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SemesterCalendarController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SemesterCalendarController.java @@ -20,11 +20,13 @@ public class SemesterCalendarController { private SemesterCalendarService semesterCalendarService; /** - * 新增学期校历 + * 预生成学期校历(幂等)。nd 为 6 位学期代号,为空时后端按日期在「学期」表反查。 */ @PostMapping("/add") - public Result add(@RequestParam("startDate") String startDate,@RequestParam("endDate") String endDate) { - semesterCalendarService.add(startDate,endDate); + public Result add(@RequestParam("startDate") String startDate, + @RequestParam("endDate") String endDate, + @RequestParam(value = "nd", required = false) Integer nd) { + semesterCalendarService.add(startDate, endDate, nd); return Result.success(); } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SemesterCalendarService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SemesterCalendarService.java index b63850bf4..651fab901 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SemesterCalendarService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SemesterCalendarService.java @@ -10,12 +10,13 @@ import java.util.List; public interface SemesterCalendarService { /** - * 新增学期校历 + * 预生成学期校历(幂等:已存在的「年度 + 日期 + 节次」不会被重复插入,也不会覆盖用户已手工标记的格子) * * @param startDate 学期起始日期 - * @param endDate 学期结束日期 + * @param endDate 学期结束日期 + * @param nd 6 位学期代号(如 202601)。为空时按日期在「学期」表反查 */ - void add(String startDate,String endDate); + void add(String startDate, String endDate, Integer nd); /** * 删除学期校历 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 a316407d2..6414c9090 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 @@ -11,6 +11,7 @@ import com.roomroot.jwgl.mapper.JQBMapper; import com.roomroot.jwgl.mapper.SemesterCalendarMapper; import com.roomroot.jwgl.service.ClassEventCalendarService; import com.roomroot.jwgl.service.TeachingTaskWriteGuard; +import com.roomroot.jwgl.utils.SemesterCodeUtil; import com.roomroot.jwgl.utils.UuidUtil; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,7 +23,9 @@ 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; +import java.util.Map; import java.util.Set; import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; @@ -141,20 +144,63 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService generateEmptyGrid(semester, start, end); return; } - for (XQXLB school : schoolRows) { - LocalDate day = parseDay(school.getJqsj()); - if (day.isBefore(start) || day.isAfter(end)) { - continue; - } - insertCell(semester, day, school.getCourseClass(), school.getJqmc(), + // 校历表同一 (日期, 节次) 可能存在多条重复记录,先归并再落库, + // 否则班历会被按校历的重复倍数整体放大。 + for (XQXLB school : mergeByDayAndPeriod(schoolRows, start, end)) { + insertCell(semester, parseDay(school.getJqsj()), school.getCourseClass(), school.getJqmc(), flag(school.getJc()), flag(school.getKpk()), flag(school.getBzxs()), flag(school.getZdpk()), flag(school.getZk()), school.getBz()); } } + /** + * 按 (日期, 节次) 归并校历行,同一格只保留一条。 + *

同一格存在多条时按「正课 > 可排课 > 带假期名称」的优先级取一条, + * 避免校历中的重复行被逐条继承导致班历成倍膨胀。 + */ + private List mergeByDayAndPeriod(List rows, LocalDate start, LocalDate end) { + Map merged = new LinkedHashMap<>(); + for (XQXLB row : rows) { + String courseClass = row.getCourseClass(); + if (courseClass == null || courseClass.isEmpty()) { + continue; + } + LocalDate day = parseDay(row.getJqsj()); + if (day.isBefore(start) || day.isAfter(end)) { + continue; + } + String key = row.getJqsj() + "|" + courseClass; + XQXLB kept = merged.get(key); + if (kept == null || preferredOver(row, kept)) { + merged.put(key, row); + } + } + return new ArrayList<>(merged.values()); + } + + /** + * 判断 candidate 是否比 current 更值得保留(正课 > 可排课 > 带假期名称)。 + */ + private boolean preferredOver(XQXLB candidate, XQXLB current) { + boolean candidateMain = Boolean.TRUE.equals(candidate.getZk()); + boolean currentMain = Boolean.TRUE.equals(current.getZk()); + if (candidateMain != currentMain) { + return candidateMain; + } + boolean candidateKpk = Boolean.TRUE.equals(candidate.getKpk()); + boolean currentKpk = Boolean.TRUE.equals(current.getKpk()); + if (candidateKpk != currentKpk) { + return candidateKpk; + } + boolean candidateNamed = candidate.getJqmc() != null && !candidate.getJqmc().isEmpty(); + boolean currentNamed = current.getJqmc() != null && !current.getJqmc().isEmpty(); + return candidateNamed && !currentNamed; + } + private List loadSchoolRows(XYDNDXQJBXXB semester, LocalDate start, LocalDate end) { - Integer schoolNd = semester.getNd() == null || semester.getXqdc() == null - ? null : semester.getNd() * 100 + semester.getXqdc(); + // 「年度 + 学期第次」统一解析为 6 位学期代号。 + // 历史数据里 年度 可能直接存的 6 位学期代号,两种口径都要兼容(详见 SemesterCodeUtil)。 + Integer schoolNd = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc()); List rows = querySchool(schoolNd, start, end); if (!rows.isEmpty() || semester.getNd() == null) { return rows; diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java index f7cd1b381..aeef0f058 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java @@ -16,6 +16,7 @@ import com.roomroot.jwgl.service.ClassSemesterService; import com.roomroot.jwgl.service.ElectiveService; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.utils.SemesterCodeUtil; import com.roomroot.jwgl.utils.UuidUtil; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -349,6 +350,8 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { if (rows.size() < 2) { throw new ServiceException("请至少选择两个班次进行预设合班", BAD_REQUEST); } + // 编波是学期级操作:所选班次必须同属一个学期,否则合班结果会跨学期串味 + assertSameSemester(rows); Set waves = new HashSet<>(); for (XYDNDXQJBXXB row : rows) { if (row.getYsbbh() != null && row.getYsbbh() > 0) { @@ -383,19 +386,22 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { } private boolean insertClassSemester(XYDB clazz, int nd, int xqdc, LocalDate kxrq, LocalDate jsrq) { + // 统一落库为「4 位年份 + 学期第次」,避免同一列再次出现 4 位/6 位两套口径 + Integer year = SemesterCodeUtil.toYear(nd); + Integer period = SemesterCodeUtil.toPeriod(nd, xqdc); XYDNDXQJBXXB existing = classSemesterMapper.selectByBcAndNdAndXqdc( - clazz.getXydbh(), String.valueOf(nd), String.valueOf(xqdc)); + clazz.getXydbh(), String.valueOf(year), String.valueOf(period)); if (existing != null) { return false; } XYDNDXQJBXXB row = new XYDNDXQJBXXB(); row.setBh(UuidUtil.getUUID()); row.setXydbh(clazz.getXydbh()); - row.setNd(nd); - row.setXqdc(xqdc); + row.setNd(year); + row.setXqdc(period); row.setKxrq(kxrq); row.setJsrq(jsrq); - row.setZyjsbh(resolveRoom(clazz, xqdc)); + row.setZyjsbh(resolveRoom(clazz, period)); row.setXhbs(0); row.setYsbbh(0); row.setJclb(clazz.getJclb() != null && !clazz.getJclb().isEmpty() ? clazz.getJclb() : "双节次"); @@ -425,7 +431,8 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { } private XQ findSchoolSemester(Integer nd, Integer xqdc) { - String key = String.valueOf(nd * 100 + xqdc); + // 「年度 + 学期第次」统一解析为 6 位学期代号,兼容历史数据里 年度 直接存 6 位的情况 + String key = String.valueOf(SemesterCodeUtil.resolve(nd, xqdc)); XQ school = semesterMapper.selectById(key); if (school != null) { return school; @@ -466,6 +473,24 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { } } + /** + * 校验所选班次同属一个学期。 + * + *

编波(预设合班)是学期级操作。跨学期合班会让同一个「预设编班号」同时落在多个学期上, + * 后续班历生成与课时配当无法判断该把合班结果归到哪个学期。 + */ + private void assertSameSemester(List rows) { + Set semesters = new HashSet<>(); + for (XYDNDXQJBXXB row : rows) { + Integer code = SemesterCodeUtil.resolve(row.getNd(), row.getXqdc()); + semesters.add(code == null ? "未知学期" : String.valueOf(code)); + if (semesters.size() > 1) { + throw new ServiceException("预设合班要求所选班次属于同一学期,当前选中项跨了 " + + semesters.size() + " 个学期,请先按学期筛选后再操作", BAD_REQUEST); + } + } + } + private int parseXqdc(String xqdc) { if (xqdc == null || xqdc.isEmpty()) { throw new ServiceException("请选择学期第次", BAD_REQUEST); 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 bed7fe9e9..75142422f 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 @@ -1,9 +1,10 @@ package com.roomroot.jwgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.roomroot.jwgl.entity.XQ; import com.roomroot.jwgl.entity.XQXLB; import com.roomroot.jwgl.mapper.SemesterCalendarMapper; +import com.roomroot.jwgl.mapper.XQMapper; import com.roomroot.jwgl.service.SemesterCalendarService; import com.roomroot.jwgl.utils.DateCourseUtil; import com.roomroot.jwgl.utils.UuidUtil; @@ -17,9 +18,12 @@ import jakarta.annotation.Resource; import java.io.InputStream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * 学期校历表服务实现类 @@ -27,50 +31,112 @@ import java.util.Map; @Service 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 XQMapper xqMapper; + @Override - public void add(String startDate,String endDateb) { - Map dateWeekAndCourseMap = DateCourseUtil.getDateWeekAndCourseMap(startDate, endDateb); + public void add(String startDate, String endDate, Integer nd) { + Map dateWeekAndCourseMap = DateCourseUtil.getDateWeekAndCourseMap(startDate, endDate); + if (dateWeekAndCourseMap.isEmpty()) { + return; + } + // 学期定义:用于把日期解析成 6 位学期代号,与前端查询、班历继承口径保持一致 + List semesters = xqMapper.selectList(new LambdaQueryWrapper().orderByAsc(XQ::getKxrq)); + // 每个年度已存在的「日期|节次」键,按年度懒加载,避免逐格查库 + Map> existingCache = new HashMap<>(); for (Map.Entry entry : dateWeekAndCourseMap.entrySet()) { String dateStr = entry.getKey(); - Page page = new Page<>(0, 10); - XQXLB query = new XQXLB(); - //将日期分为年度和月日 - LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd")); - int nd = date.getYear(); - String md = String.format("%02d%02d", date.getMonthValue(), date.getDayOfMonth()); - query.setJqsj(md); - query.setNd(nd); - Page result = xqxlbMapper.selectPageList(page, query); - if(result.getTotal() > 0){ + LocalDate date = LocalDate.parse(dateStr, DAY); + Integer year = resolveNd(nd, date, semesters); + Set existing = existingCache.computeIfAbsent( + year, key -> loadExistingKeys(key, startDate, endDate)); + Map map = (Map) entry.getValue(); + Object course = map.get("course"); + if (!(course instanceof List)) { continue; } - Map map = (Map) entry.getValue(); - Object course = map.get("course"); - if (course instanceof ArrayList) { - ArrayList list = (ArrayList) course; - for (int i = 0; i < list.size(); i++) { - String courseClass = list.get(i); - XQXLB xqxlb = new XQXLB(); - //编号,用时间戳 - String bh = UuidUtil.getUUID(); - xqxlb.setBh(bh); - //假期时间 - xqxlb.setJqsj(dateStr); - //年度 - xqxlb.setNd(Integer.valueOf(nd)); - //节次 - xqxlb.setCourseClass(courseClass); - xqxlb.setKpk(true); - xqxlb.setBzxs(true); - xqxlb.setZdpk(true); - xqxlbMapper.insert(xqxlb); + for (Object item : (List) course) { + String courseClass = String.valueOf(item); + // 幂等:同一 (年度, 日期, 节次) 已存在则跳过。 + // 既不重复插入,也不覆盖用户已手工标记的假期/正课等内容。 + if (!existing.add(dateStr + "|" + courseClass)) { + continue; + } + XQXLB xqxlb = new XQXLB(); + xqxlb.setBh(UuidUtil.getUUID()); + // 假期时间:完整日期 yyyy-MM-dd(判重键与落库值必须同格式) + xqxlb.setJqsj(dateStr); + // 年度:6 位学期代号 + xqxlb.setNd(year); + // 节次 + xqxlb.setCourseClass(courseClass); + xqxlb.setKpk(true); + xqxlb.setBzxs(true); + xqxlb.setZdpk(true); + // 正课:周一至周五 1-6 节,与班历空网格规则一致 + xqxlb.setZk(isMainCourse(date, courseClass)); + xqxlbMapper.insert(xqxlb); + } + } + } + + /** + * 解析 6 位学期代号(如 202601)。 + *

优先级:合法的 6 位入参 > 按日期在「学期」表反查 > 兜底用 4 位年份。 + * 4 位年份(如 2026)属历史错误口径,一律走反查纠正,避免同一批日期被贴上两套年度标签。 + */ + private Integer resolveNd(Integer nd, LocalDate date, List semesters) { + if (nd != null && nd >= 100000) { + return nd; + } + for (XQ xq : semesters) { + if (xq.getNd() == null || xq.getKxrq() == null || xq.getJsrq() == null) { + continue; + } + LocalDate kxrq = xq.getKxrq().toLocalDate(); + LocalDate jsrq = xq.getJsrq().toLocalDate(); + if (!date.isBefore(kxrq) && !date.isAfter(jsrq)) { + try { + return Integer.valueOf(xq.getNd().trim()); + } catch (NumberFormatException ignore) { + // 学期代号非数字,继续匹配下一条 } } - } + return date.getYear(); + } + + /** + * 读取该年度在 [startDate, endDate] 区间内已存在的「日期|节次」键集合。 + */ + private Set loadExistingKeys(Integer nd, String startDate, String endDate) { + Set keys = new HashSet<>(); + List rows = xqxlbMapper.selectList(new LambdaQueryWrapper() + .eq(XQXLB::getNd, nd) + .ge(XQXLB::getJqsj, startDate) + .le(XQXLB::getJqsj, endDate)); + for (XQXLB row : rows) { + keys.add(row.getJqsj() + "|" + row.getCourseClass()); + } + return keys; + } + + /** + * 正课判定:周一至周五的 1-2 / 3-4 / 5-6 节。 + */ + private boolean isMainCourse(LocalDate date, String courseClass) { + return date.getDayOfWeek().getValue() <= 5 && MAIN_PERIODS.contains(courseClass); } @Override @@ -124,9 +190,14 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService { // 假期时间 String jqsj = getCellValue(row, 1); xqxlb.setJqsj(jqsj); - Page page = new Page<>(0, 10); - Page result = xqxlbMapper.selectPageList(page, xqxlb); - if(result.getTotal() > 0){ + + // 节次:判重键的一部分,必须在查重前解析 + String courseClass = getCellValue(row, 8); + xqxlb.setCourseClass(courseClass); + + // 按 (年度, 假期时间, 节次) 精确判重。 + // 原实现只按「年度 + 日期模糊匹配」,会导致同一天的其余节次被误判为已存在而整行跳过。 + if (xqxlb.getNd() != null && jqsj != null && !jqsj.isEmpty() && hasSameCell(xqxlb.getNd(), jqsj, courseClass)) { continue; } @@ -152,9 +223,6 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService { String zdpk = getCellValue(row, 7); xqxlb.setZdpk("是".equals(zdpk)); - // 节次 - xqxlb.setCourseClass(getCellValue(row, 8)); - xqxlbMapper.insert(xqxlb); count++; } @@ -164,6 +232,22 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService { } } + /** + * 判断同一 (年度, 假期时间, 节次) 是否已存在。 + */ + private boolean hasSameCell(Integer nd, String jqsj, String courseClass) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(XQXLB::getNd, nd) + .eq(XQXLB::getJqsj, jqsj); + if (courseClass == null || courseClass.isEmpty()) { + wrapper.isNull(XQXLB::getCourseClass); + } else { + wrapper.eq(XQXLB::getCourseClass, courseClass); + } + Long exist = xqxlbMapper.selectCount(wrapper); + return exist != null && exist > 0; + } + /** * 获取单元格字符串值 */ @@ -197,4 +281,4 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService { } } -} \ No newline at end of file +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java index 968a9e35b..4fe29d65e 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java @@ -63,7 +63,23 @@ public class SemesterServiceImpl extends ServiceImpl impleme LocalDateTime jsrq = xq.getJsrq(); String startDate = localDateTimeToDateStr(kxrq); String endDate = localDateTimeToDateStr(jsrq); - semesterCalendarService.add(startDate, endDate); + // 直接把本学期 6 位代号传给校历生成,避免与前端查询口径不一致导致重复落库 + semesterCalendarService.add(startDate, endDate, parseSemesterNd(xq.getNd())); + } + } + + /** + * 把「学期」表的年度主键(6 位学期代号,如 202601)转为 Integer。 + * 非数字时返回 null,由 {@code SemesterCalendarService.add} 按日期反查兜底。 + */ + private static Integer parseSemesterNd(String nd) { + if (nd == null) { + return null; + } + try { + return Integer.valueOf(nd.trim()); + } catch (NumberFormatException ignore) { + return null; } } @@ -142,14 +158,36 @@ public class SemesterServiceImpl extends ServiceImpl impleme } } + /** + * 校验学期时间范围不与其它学期重叠。 + * + *

只拦截「本次修改新引入」的重叠:若某条重叠在修改前就已存在 + * (库中 202501/202502、202601/202602 两对历史数据本身就互相重叠), + * 则放行本次保存。否则这些记录连改个名称、设为「当前学期」都会被挡住, + * 脏数据反而永远修不掉。 + */ private void assertNoOverlap(XQ xq) { - Long overlap = semesterMapper.selectCount(new LambdaQueryWrapper() + List overlaps = semesterMapper.selectList(new LambdaQueryWrapper() .ne(XQ::getNd, xq.getNd()) .lt(XQ::getKxrq, xq.getJsrq()) .gt(XQ::getJsrq, xq.getKxrq())); - if (overlap != null && overlap > 0) { + if (overlaps.isEmpty()) { + return; + } + XQ existing = semesterMapper.selectById(xq.getNd()); + if (existing == null || existing.getKxrq() == null || existing.getJsrq() == null) { throw new ServiceException("学期时间范围与已有学期重叠", CONFLICT); } + for (XQ other : overlaps) { + if (other.getKxrq() == null || other.getJsrq() == null) { + continue; + } + boolean preExisting = other.getKxrq().isBefore(existing.getJsrq()) + && other.getJsrq().isAfter(existing.getKxrq()); + if (!preExisting) { + throw new ServiceException("学期时间范围与已有学期重叠", CONFLICT); + } + } } @Override diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SemesterCodeUtil.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SemesterCodeUtil.java new file mode 100644 index 000000000..98ced8852 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SemesterCodeUtil.java @@ -0,0 +1,76 @@ +package com.roomroot.jwgl.utils; + +/** + * 学期代号工具。 + * + *

系统内学期统一用 6 位学期代号表示,形如 {@code 202601}:前 4 位为年份,后 2 位为学期第次。 + * + *

但「学员队年度学期基本信息表」的 {@code 年度} 列在历史上出现过两种口径: + *

    + *
  • 4 位年份 + 学期第次,例如 年度=2026、学期第次=1 → 2026 * 100 + 1 = 202601
  • + *
  • 直接存 6 位学期代号,例如 年度=202503
  • + *
+ * 把 6 位值再套 {@code nd * 100 + xqdc} 会溢出(202503 * 100 + 1 = 20250301), + * 所以各处都需要先判别口径。本工具把两种口径统一解析为 6 位学期代号, + * 同时提供「写库时统一为 4 位年份」的规范化方法,避免新增数据继续产生双口径。 + */ +public final class SemesterCodeUtil { + + /** 6 位学期代号的下界(4 位年份最大 9999)。 */ + private static final int SEMESTER_CODE_LOWER_BOUND = 100000; + + private SemesterCodeUtil() { + } + + /** + * 解析为 6 位学期代号,兼容 4 位年份与 6 位学期代号两种历史口径。 + * + * @param nd 年度:4 位年份(如 2026)或 6 位学期代号(如 202503) + * @param xqdc 学期第次(1/2/3)。仅当 nd 为 4 位年份时参与计算 + * @return 6 位学期代号;nd 为空时返回 null + */ + public static Integer resolve(Integer nd, Integer xqdc) { + if (nd == null) { + return null; + } + if (isSemesterCode(nd)) { + return nd; + } + if (xqdc == null) { + return nd; + } + return nd * 100 + xqdc; + } + + /** + * 判断给定年度是否已经是 6 位学期代号。 + */ + public static boolean isSemesterCode(Integer nd) { + return nd != null && nd >= SEMESTER_CODE_LOWER_BOUND; + } + + /** + * 取 4 位年份部分。传入 6 位学期代号时拆分,传入 4 位年份时原样返回。 + * + * @return 4 位年份;nd 为空时返回 null + */ + public static Integer toYear(Integer nd) { + if (nd == null) { + return null; + } + return isSemesterCode(nd) ? nd / 100 : nd; + } + + /** + * 取学期第次。传入 6 位学期代号时取末两位,传入 4 位年份时返回传入的兜底值。 + * + * @param xqdcFallback 4 位年份口径下的学期第次兜底值 + * @return 学期第次;nd 为空时返回 null + */ + public static Integer toPeriod(Integer nd, Integer xqdcFallback) { + if (nd == null) { + return null; + } + return isSemesterCode(nd) ? nd % 100 : xqdcFallback; + } +} diff --git a/frontend/src/api/teachBusiness/xqxlb.js b/frontend/src/api/teachBusiness/xqxlb.js index 11447c97e..99c872324 100644 --- a/frontend/src/api/teachBusiness/xqxlb.js +++ b/frontend/src/api/teachBusiness/xqxlb.js @@ -18,12 +18,14 @@ export function getXqxlb(bh) { }) } -// 预生成学期整张校历占位记录(首次进入新学期时调用,接口自带去重) -export function addXqxlb(startDate, endDate) { +// 预生成学期整张校历占位记录(首次进入新学期时调用,接口幂等) +// nd 为 6 位学期代号(如 202601),后端据此落库,必须与 listXqxlb 的查询口径一致, +// 否则后端写入的年度与前端查询的年度对不上,会每次进入都重新生成一遍。 +export function addXqxlb(startDate, endDate, nd) { return request({ url: '/xqxlb/add', method: 'post', - params: { startDate: startDate, endDate: endDate } + params: { startDate: startDate, endDate: endDate, nd: nd } }) } diff --git a/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue b/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue index e8201b74e..7ed548da1 100644 --- a/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue +++ b/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue @@ -304,26 +304,24 @@ export default { this.$message.warning('学期详情获取失败,请确认学期数据') }) }, - // 从后端拉取本学期校历事件,渲染到对应时间格(年度由 6 位学期代号截取前 4 位) - // XQXLB 的 nd 存的是「年份」(如 2026),春/夏/秋三学期共用同一批年度记录; + // 从后端拉取本学期校历事件,渲染到对应时间格。 + // XQXLB 的 nd 存 6 位学期代号(如 202601),与 this.xqxlbNd 一致; // 后端 updateById 只能改已存在的 bh,所以每个格子必须有预生成的记录才能存。 - // 若本学期日期区间 [kxrq, jsrq] 内还没有任何占位记录(首次进入该学期), - // 调用 /xqxlb/add 只按本学期 kxrq/jsrq 预生成这一张网格,再重新拉取, - // 这样后续 /xqxlb/update 的 updateById 才能命中每一格。(区间有覆盖则跳过,幂等) + // 每次进入都调一次 /xqxlb/add 补齐网格:后端已幂等,只会插入缺失的格子, + // 既不重复插入也不覆盖已有标记。这样即便区间只被部分覆盖, + // 也不会出现「格子没有 bh → /xqxlb/update 静默失效」。 loadXqxlbEvents() { const fetchList = () => listXqxlb({ nd: this.xqxlbNd }).then(res => { const data = res.data return Array.isArray(data) ? data : (data && data.records) || [] }) - fetchList().then(list => { - const hasCoverage = list.some(item => this.isInRange(item.jqsj)) - if (!hasCoverage && this.startDate && this.endDate) { - const startStr = fmtDate(this.startDate) - const endStr = fmtDate(this.endDate) - return addXqxlb(startStr, endStr).then(() => fetchList()) + const ensureGrid = () => { + if (!this.startDate || !this.endDate) { + return Promise.resolve() } - return list - }).then(list => { + return addXqxlb(fmtDate(this.startDate), fmtDate(this.endDate), this.xqxlbNd) + } + ensureGrid().then(fetchList).then(list => { this.renderEventList(list) }).catch(() => { this.$message.warning('校历事件加载失败') @@ -362,13 +360,6 @@ export default { }) this.events = loaded }, - // 判断某条记录(jqsj 日期)是否落在本学期 [startDate, endDate] 区间内 - isInRange(jqsj) { - if (!this.startDate || !this.endDate || !jqsj) return false - const d = new Date(String(jqsj).slice(0, 10).replace(/-/g, '/')) - if (isNaN(d.getTime())) return false - return d >= this.startDate && d <= this.endDate - }, // 根据假期记录反推时间格位置(jqsj 日期 + courseClass 节次),与列是否可见无关 locateXqxlb(item) { if (!this.calendarStart || !item.jqsj) return null