学期新增的时候校验校历功能点添加,否则会有大量的数据重复生成

This commit is contained in:
2026-09-14 17:24:56 +08:00
parent 52a04dbf60
commit 5187435285
9 changed files with 352 additions and 87 deletions
@@ -20,11 +20,13 @@ public class SemesterCalendarController {
private SemesterCalendarService semesterCalendarService; private SemesterCalendarService semesterCalendarService;
/** /**
* 新增学期校历 * 预生成学期校历(幂等)。nd 为 6 位学期代号,为空时后端按日期在「学期」表反查。
*/ */
@PostMapping("/add") @PostMapping("/add")
public Result<Void> add(@RequestParam("startDate") String startDate,@RequestParam("endDate") String endDate) { public Result<Void> add(@RequestParam("startDate") String startDate,
semesterCalendarService.add(startDate,endDate); @RequestParam("endDate") String endDate,
@RequestParam(value = "nd", required = false) Integer nd) {
semesterCalendarService.add(startDate, endDate, nd);
return Result.success(); return Result.success();
} }
@@ -10,12 +10,13 @@ import java.util.List;
public interface SemesterCalendarService { public interface SemesterCalendarService {
/** /**
* 新增学期校历 * 预生成学期校历(幂等:已存在的「年度 + 日期 + 节次」不会被重复插入,也不会覆盖用户已手工标记的格子)
* *
* @param startDate 学期起始日期 * @param startDate 学期起始日期
* @param endDate 学期结束日期 * @param endDate 学期结束日期
* @param nd 6 位学期代号(如 202601)。为空时按日期在「学期」表反查
*/ */
void add(String startDate,String endDate); void add(String startDate, String endDate, Integer nd);
/** /**
* 删除学期校历 * 删除学期校历
@@ -11,6 +11,7 @@ import com.roomroot.jwgl.mapper.JQBMapper;
import com.roomroot.jwgl.mapper.SemesterCalendarMapper; import com.roomroot.jwgl.mapper.SemesterCalendarMapper;
import com.roomroot.jwgl.service.ClassEventCalendarService; import com.roomroot.jwgl.service.ClassEventCalendarService;
import com.roomroot.jwgl.service.TeachingTaskWriteGuard; import com.roomroot.jwgl.service.TeachingTaskWriteGuard;
import com.roomroot.jwgl.utils.SemesterCodeUtil;
import com.roomroot.jwgl.utils.UuidUtil; import com.roomroot.jwgl.utils.UuidUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -22,7 +23,9 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
@@ -141,20 +144,63 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService
generateEmptyGrid(semester, start, end); generateEmptyGrid(semester, start, end);
return; return;
} }
for (XQXLB school : schoolRows) { // 校历表同一 (日期, 节次) 可能存在多条重复记录,先归并再落库,
LocalDate day = parseDay(school.getJqsj()); // 否则班历会被按校历的重复倍数整体放大。
if (day.isBefore(start) || day.isAfter(end)) { for (XQXLB school : mergeByDayAndPeriod(schoolRows, start, end)) {
continue; insertCell(semester, parseDay(school.getJqsj()), school.getCourseClass(), school.getJqmc(),
}
insertCell(semester, day, school.getCourseClass(), school.getJqmc(),
flag(school.getJc()), flag(school.getKpk()), flag(school.getBzxs()), flag(school.getJc()), flag(school.getKpk()), flag(school.getBzxs()),
flag(school.getZdpk()), flag(school.getZk()), school.getBz()); flag(school.getZdpk()), flag(school.getZk()), school.getBz());
} }
} }
/**
* 按 (日期, 节次) 归并校历行,同一格只保留一条。
* <p>同一格存在多条时按「正课 &gt; 可排课 &gt; 带假期名称」的优先级取一条,
* 避免校历中的重复行被逐条继承导致班历成倍膨胀。
*/
private List<XQXLB> mergeByDayAndPeriod(List<XQXLB> rows, LocalDate start, LocalDate end) {
Map<String, XQXLB> 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 更值得保留(正课 &gt; 可排课 &gt; 带假期名称)。
*/
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<XQXLB> loadSchoolRows(XYDNDXQJBXXB semester, LocalDate start, LocalDate end) { private List<XQXLB> loadSchoolRows(XYDNDXQJBXXB semester, LocalDate start, LocalDate end) {
Integer schoolNd = semester.getNd() == null || semester.getXqdc() == null // 「年度 + 学期第次」统一解析为 6 位学期代号。
? null : semester.getNd() * 100 + semester.getXqdc(); // 历史数据里 年度 可能直接存的 6 位学期代号,两种口径都要兼容(详见 SemesterCodeUtil)。
Integer schoolNd = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
List<XQXLB> rows = querySchool(schoolNd, start, end); List<XQXLB> rows = querySchool(schoolNd, start, end);
if (!rows.isEmpty() || semester.getNd() == null) { if (!rows.isEmpty() || semester.getNd() == null) {
return rows; return rows;
@@ -16,6 +16,7 @@ import com.roomroot.jwgl.service.ClassSemesterService;
import com.roomroot.jwgl.service.ElectiveService; import com.roomroot.jwgl.service.ElectiveService;
import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.utils.SemesterCodeUtil;
import com.roomroot.jwgl.utils.UuidUtil; import com.roomroot.jwgl.utils.UuidUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -349,6 +350,8 @@ public class ClassSemesterServiceImpl implements ClassSemesterService {
if (rows.size() < 2) { if (rows.size() < 2) {
throw new ServiceException("请至少选择两个班次进行预设合班", BAD_REQUEST); throw new ServiceException("请至少选择两个班次进行预设合班", BAD_REQUEST);
} }
// 编波是学期级操作:所选班次必须同属一个学期,否则合班结果会跨学期串味
assertSameSemester(rows);
Set<Integer> waves = new HashSet<>(); Set<Integer> waves = new HashSet<>();
for (XYDNDXQJBXXB row : rows) { for (XYDNDXQJBXXB row : rows) {
if (row.getYsbbh() != null && row.getYsbbh() > 0) { 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) { 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( XYDNDXQJBXXB existing = classSemesterMapper.selectByBcAndNdAndXqdc(
clazz.getXydbh(), String.valueOf(nd), String.valueOf(xqdc)); clazz.getXydbh(), String.valueOf(year), String.valueOf(period));
if (existing != null) { if (existing != null) {
return false; return false;
} }
XYDNDXQJBXXB row = new XYDNDXQJBXXB(); XYDNDXQJBXXB row = new XYDNDXQJBXXB();
row.setBh(UuidUtil.getUUID()); row.setBh(UuidUtil.getUUID());
row.setXydbh(clazz.getXydbh()); row.setXydbh(clazz.getXydbh());
row.setNd(nd); row.setNd(year);
row.setXqdc(xqdc); row.setXqdc(period);
row.setKxrq(kxrq); row.setKxrq(kxrq);
row.setJsrq(jsrq); row.setJsrq(jsrq);
row.setZyjsbh(resolveRoom(clazz, xqdc)); row.setZyjsbh(resolveRoom(clazz, period));
row.setXhbs(0); row.setXhbs(0);
row.setYsbbh(0); row.setYsbbh(0);
row.setJclb(clazz.getJclb() != null && !clazz.getJclb().isEmpty() ? clazz.getJclb() : "双节次"); 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) { 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); XQ school = semesterMapper.selectById(key);
if (school != null) { if (school != null) {
return school; return school;
@@ -466,6 +473,24 @@ public class ClassSemesterServiceImpl implements ClassSemesterService {
} }
} }
/**
* 校验所选班次同属一个学期。
*
* <p>编波(预设合班)是学期级操作。跨学期合班会让同一个「预设编班号」同时落在多个学期上,
* 后续班历生成与课时配当无法判断该把合班结果归到哪个学期。
*/
private void assertSameSemester(List<XYDNDXQJBXXB> rows) {
Set<String> 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) { private int parseXqdc(String xqdc) {
if (xqdc == null || xqdc.isEmpty()) { if (xqdc == null || xqdc.isEmpty()) {
throw new ServiceException("请选择学期第次", BAD_REQUEST); throw new ServiceException("请选择学期第次", BAD_REQUEST);
@@ -1,9 +1,10 @@
package com.roomroot.jwgl.service.impl; package com.roomroot.jwgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.entity.XQXLB;
import com.roomroot.jwgl.mapper.SemesterCalendarMapper; import com.roomroot.jwgl.mapper.SemesterCalendarMapper;
import com.roomroot.jwgl.mapper.XQMapper;
import com.roomroot.jwgl.service.SemesterCalendarService; import com.roomroot.jwgl.service.SemesterCalendarService;
import com.roomroot.jwgl.utils.DateCourseUtil; import com.roomroot.jwgl.utils.DateCourseUtil;
import com.roomroot.jwgl.utils.UuidUtil; import com.roomroot.jwgl.utils.UuidUtil;
@@ -17,9 +18,12 @@ import jakarta.annotation.Resource;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; 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.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* 学期校历表服务实现类 * 学期校历表服务实现类
@@ -27,50 +31,112 @@ import java.util.Map;
@Service @Service
public class SemesterCalendarServiceImpl implements SemesterCalendarService { 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<String> MAIN_PERIODS = Arrays.asList("1-2", "3-4", "5-6");
@Resource @Resource
private SemesterCalendarMapper xqxlbMapper; private SemesterCalendarMapper xqxlbMapper;
@Resource
private XQMapper xqMapper;
@Override @Override
public void add(String startDate,String endDateb) { public void add(String startDate, String endDate, Integer nd) {
Map<String, Object> dateWeekAndCourseMap = DateCourseUtil.getDateWeekAndCourseMap(startDate, endDateb); Map<String, Object> dateWeekAndCourseMap = DateCourseUtil.getDateWeekAndCourseMap(startDate, endDate);
if (dateWeekAndCourseMap.isEmpty()) {
return;
}
// 学期定义:用于把日期解析成 6 位学期代号,与前端查询、班历继承口径保持一致
List<XQ> semesters = xqMapper.selectList(new LambdaQueryWrapper<XQ>().orderByAsc(XQ::getKxrq));
// 每个年度已存在的「日期|节次」键,按年度懒加载,避免逐格查库
Map<Integer, Set<String>> existingCache = new HashMap<>();
for (Map.Entry<String, Object> entry : dateWeekAndCourseMap.entrySet()) { for (Map.Entry<String, Object> entry : dateWeekAndCourseMap.entrySet()) {
String dateStr = entry.getKey(); String dateStr = entry.getKey();
Page<XQXLB> page = new Page<>(0, 10); LocalDate date = LocalDate.parse(dateStr, DAY);
XQXLB query = new XQXLB(); Integer year = resolveNd(nd, date, semesters);
//将日期分为年度和月日 Set<String> existing = existingCache.computeIfAbsent(
LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd")); year, key -> loadExistingKeys(key, startDate, endDate));
int nd = date.getYear(); Map<String, Object> map = (Map<String, Object>) entry.getValue();
String md = String.format("%02d%02d", date.getMonthValue(), date.getDayOfMonth()); Object course = map.get("course");
query.setJqsj(md); if (!(course instanceof List<?>)) {
query.setNd(nd); continue;
Page<XQXLB> result = xqxlbMapper.selectPageList(page, query); }
if(result.getTotal() > 0){ for (Object item : (List<?>) course) {
String courseClass = String.valueOf(item);
// 幂等:同一 (年度, 日期, 节次) 已存在则跳过。
// 既不重复插入,也不覆盖用户已手工标记的假期/正课等内容。
if (!existing.add(dateStr + "|" + courseClass)) {
continue; continue;
} }
Map<String,Object> map = (Map<String, Object>) entry.getValue();
Object course = map.get("course");
if (course instanceof ArrayList<?>) {
ArrayList<String> list = (ArrayList<String>) course;
for (int i = 0; i < list.size(); i++) {
String courseClass = list.get(i);
XQXLB xqxlb = new XQXLB(); XQXLB xqxlb = new XQXLB();
//编号,用时间戳 xqxlb.setBh(UuidUtil.getUUID());
String bh = UuidUtil.getUUID(); // 假期时间:完整日期 yyyy-MM-dd(判重键与落库值必须同格式)
xqxlb.setBh(bh);
//假期时间
xqxlb.setJqsj(dateStr); xqxlb.setJqsj(dateStr);
//年度 // 年度6 位学期代号
xqxlb.setNd(Integer.valueOf(nd)); xqxlb.setNd(year);
//节次 // 节次
xqxlb.setCourseClass(courseClass); xqxlb.setCourseClass(courseClass);
xqxlb.setKpk(true); xqxlb.setKpk(true);
xqxlb.setBzxs(true); xqxlb.setBzxs(true);
xqxlb.setZdpk(true); xqxlb.setZdpk(true);
// 正课:周一至周五 1-6 节,与班历空网格规则一致
xqxlb.setZk(isMainCourse(date, courseClass));
xqxlbMapper.insert(xqxlb); xqxlbMapper.insert(xqxlb);
} }
} }
} }
/**
* 解析 6 位学期代号(如 202601)。
* <p>优先级:合法的 6 位入参 &gt; 按日期在「学期」表反查 &gt; 兜底用 4 位年份。
* 4 位年份(如 2026)属历史错误口径,一律走反查纠正,避免同一批日期被贴上两套年度标签。
*/
private Integer resolveNd(Integer nd, LocalDate date, List<XQ> 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<String> loadExistingKeys(Integer nd, String startDate, String endDate) {
Set<String> keys = new HashSet<>();
List<XQXLB> rows = xqxlbMapper.selectList(new LambdaQueryWrapper<XQXLB>()
.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 @Override
@@ -124,9 +190,14 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService {
// 假期时间 // 假期时间
String jqsj = getCellValue(row, 1); String jqsj = getCellValue(row, 1);
xqxlb.setJqsj(jqsj); xqxlb.setJqsj(jqsj);
Page<XQXLB> page = new Page<>(0, 10);
Page<XQXLB> 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; continue;
} }
@@ -152,9 +223,6 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService {
String zdpk = getCellValue(row, 7); String zdpk = getCellValue(row, 7);
xqxlb.setZdpk("".equals(zdpk)); xqxlb.setZdpk("".equals(zdpk));
// 节次
xqxlb.setCourseClass(getCellValue(row, 8));
xqxlbMapper.insert(xqxlb); xqxlbMapper.insert(xqxlb);
count++; count++;
} }
@@ -164,6 +232,22 @@ public class SemesterCalendarServiceImpl implements SemesterCalendarService {
} }
} }
/**
* 判断同一 (年度, 假期时间, 节次) 是否已存在。
*/
private boolean hasSameCell(Integer nd, String jqsj, String courseClass) {
LambdaQueryWrapper<XQXLB> wrapper = new LambdaQueryWrapper<XQXLB>()
.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;
}
/** /**
* 获取单元格字符串值 * 获取单元格字符串值
*/ */
@@ -63,7 +63,23 @@ public class SemesterServiceImpl extends ServiceImpl<SemesterMapper, XQ> impleme
LocalDateTime jsrq = xq.getJsrq(); LocalDateTime jsrq = xq.getJsrq();
String startDate = localDateTimeToDateStr(kxrq); String startDate = localDateTimeToDateStr(kxrq);
String endDate = localDateTimeToDateStr(jsrq); 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<SemesterMapper, XQ> impleme
} }
} }
/**
* 校验学期时间范围不与其它学期重叠。
*
* <p>只拦截「本次修改新引入」的重叠:若某条重叠在修改前就已存在
* (库中 202501/202502、202601/202602 两对历史数据本身就互相重叠),
* 则放行本次保存。否则这些记录连改个名称、设为「当前学期」都会被挡住,
* 脏数据反而永远修不掉。
*/
private void assertNoOverlap(XQ xq) { private void assertNoOverlap(XQ xq) {
Long overlap = semesterMapper.selectCount(new LambdaQueryWrapper<XQ>() List<XQ> overlaps = semesterMapper.selectList(new LambdaQueryWrapper<XQ>()
.ne(XQ::getNd, xq.getNd()) .ne(XQ::getNd, xq.getNd())
.lt(XQ::getKxrq, xq.getJsrq()) .lt(XQ::getKxrq, xq.getJsrq())
.gt(XQ::getJsrq, xq.getKxrq())); .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); 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 @Override
@@ -0,0 +1,76 @@
package com.roomroot.jwgl.utils;
/**
* 学期代号工具。
*
* <p>系统内学期统一用 6 位学期代号表示,形如 {@code 202601}:前 4 位为年份,后 2 位为学期第次。
*
* <p>但「学员队年度学期基本信息表」的 {@code 年度} 列在历史上出现过两种口径:
* <ul>
* <li>4 位年份 + 学期第次,例如 年度=2026、学期第次=1 → 2026 * 100 + 1 = 202601</li>
* <li>直接存 6 位学期代号,例如 年度=202503</li>
* </ul>
* 把 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;
}
}
+5 -3
View File
@@ -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({ return request({
url: '/xqxlb/add', url: '/xqxlb/add',
method: 'post', method: 'post',
params: { startDate: startDate, endDate: endDate } params: { startDate: startDate, endDate: endDate, nd: nd }
}) })
} }
@@ -304,26 +304,24 @@ export default {
this.$message.warning('学期详情获取失败,请确认学期数据') this.$message.warning('学期详情获取失败,请确认学期数据')
}) })
}, },
// 从后端拉取本学期校历事件,渲染到对应时间格(年度由 6 位学期代号截取前 4 位) // 从后端拉取本学期校历事件,渲染到对应时间格
// XQXLB 的 nd 存的是「年份」(如 2026),春/夏/秋三学期共用同一批年度记录 // XQXLB 的 nd 存 6 位学期代号(如 202601),与 this.xqxlbNd 一致
// 后端 updateById 只能改已存在的 bh,所以每个格子必须有预生成的记录才能存。 // 后端 updateById 只能改已存在的 bh,所以每个格子必须有预生成的记录才能存。
// 若本学期日期区间 [kxrq, jsrq] 内还没有任何占位记录(首次进入该学期) // 每次进入都调一次 /xqxlb/add 补齐网格:后端已幂等,只会插入缺失的格子
// 调用 /xqxlb/add 只按本学期 kxrq/jsrq 预生成这一张网格,再重新拉取 // 既不重复插入也不覆盖已有标记。这样即便区间只被部分覆盖
// 这样后续 /xqxlb/update 的 updateById 才能命中每一格。(区间有覆盖则跳过,幂等) // 也不会出现「格子没有 bh → /xqxlb/update 静默失效」。
loadXqxlbEvents() { loadXqxlbEvents() {
const fetchList = () => listXqxlb({ nd: this.xqxlbNd }).then(res => { const fetchList = () => listXqxlb({ nd: this.xqxlbNd }).then(res => {
const data = res.data const data = res.data
return Array.isArray(data) ? data : (data && data.records) || [] return Array.isArray(data) ? data : (data && data.records) || []
}) })
fetchList().then(list => { const ensureGrid = () => {
const hasCoverage = list.some(item => this.isInRange(item.jqsj)) if (!this.startDate || !this.endDate) {
if (!hasCoverage && this.startDate && this.endDate) { return Promise.resolve()
const startStr = fmtDate(this.startDate)
const endStr = fmtDate(this.endDate)
return addXqxlb(startStr, endStr).then(() => fetchList())
} }
return list return addXqxlb(fmtDate(this.startDate), fmtDate(this.endDate), this.xqxlbNd)
}).then(list => { }
ensureGrid().then(fetchList).then(list => {
this.renderEventList(list) this.renderEventList(list)
}).catch(() => { }).catch(() => {
this.$message.warning('校历事件加载失败') this.$message.warning('校历事件加载失败')
@@ -362,13 +360,6 @@ export default {
}) })
this.events = loaded 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 节次),与列是否可见无关 // 根据假期记录反推时间格位置(jqsj 日期 + courseClass 节次),与列是否可见无关
locateXqxlb(item) { locateXqxlb(item) {
if (!this.calendarStart || !item.jqsj) return null if (!this.calendarStart || !item.jqsj) return null