forked from liweijie/education
学期新增的时候校验校历功能点添加,否则会有大量的数据重复生成
This commit is contained in:
+5
-3
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除学期校历
|
* 删除学期校历
|
||||||
|
|||||||
+54
-8
@@ -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>同一格存在多条时按「正课 > 可排课 > 带假期名称」的优先级取一条,
|
||||||
|
* 避免校历中的重复行被逐条继承导致班历成倍膨胀。
|
||||||
|
*/
|
||||||
|
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 更值得保留(正课 > 可排课 > 带假期名称)。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|||||||
+30
-5
@@ -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);
|
||||||
|
|||||||
+117
-33
@@ -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();
|
|
||||||
String md = String.format("%02d%02d", date.getMonthValue(), date.getDayOfMonth());
|
|
||||||
query.setJqsj(md);
|
|
||||||
query.setNd(nd);
|
|
||||||
Page<XQXLB> result = xqxlbMapper.selectPageList(page, query);
|
|
||||||
if(result.getTotal() > 0){
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Map<String, Object> map = (Map<String, Object>) entry.getValue();
|
Map<String, Object> map = (Map<String, Object>) entry.getValue();
|
||||||
Object course = map.get("course");
|
Object course = map.get("course");
|
||||||
if (course instanceof ArrayList<?>) {
|
if (!(course instanceof List<?>)) {
|
||||||
ArrayList<String> list = (ArrayList<String>) course;
|
continue;
|
||||||
for (int i = 0; i < list.size(); i++) {
|
}
|
||||||
String courseClass = list.get(i);
|
for (Object item : (List<?>) course) {
|
||||||
|
String courseClass = String.valueOf(item);
|
||||||
|
// 幂等:同一 (年度, 日期, 节次) 已存在则跳过。
|
||||||
|
// 既不重复插入,也不覆盖用户已手工标记的假期/正课等内容。
|
||||||
|
if (!existing.add(dateStr + "|" + courseClass)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
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 位入参 > 按日期在「学期」表反查 > 兜底用 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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取单元格字符串值
|
* 获取单元格字符串值
|
||||||
*/
|
*/
|
||||||
|
|||||||
+41
-3
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user