班历功能添加

This commit is contained in:
2026-09-16 17:13:57 +08:00
parent 2e151ea228
commit abf616a8ee
19 changed files with 526 additions and 73 deletions
@@ -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<ClassCalendarEventDTO> cells;
}
@@ -21,4 +21,9 @@ public class ClassCalendarEventDTO {
private Boolean zdpk;
private Boolean zk;
private String courseClass;
/**
* 批量保存动作:"clear" 表示恢复默认(清名称、可排课);空或其它值按事件内容写入。
*/
private String action;
}
@@ -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;
}
@@ -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<JCSJB> {
}
@@ -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<ClassCalendarEventDTO> 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);
/**
* 把选定或整个班历覆盖到其它班次的对应时间格。不改课程任务。
*/
@@ -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<String> slotLabels();
/**
* 上午时段的双节次标签,用于「正课」默认判定。
*/
Set<String> mainSlotLabels();
}
@@ -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<String> 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<ClassCalendarEventDTO> cells = body.getCells();
if (CollectionUtils.isEmpty(cells)) {
return 0;
}
teachingTaskWriteGuard.assertCourseTaskWritable(body.getXydxqbh());
XYDNDXQJBXXB semester = requireSemester(body.getXydxqbh());
// 一次性拉出该班次学期全部已有格,按 (日期|节次) 建索引避免逐格查询
Map<String, JQB> 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<String> existingKeys = new HashSet<>();
for (JQB row : listRows(xydxqbh)) {
existingKeys.add(cellKey(row.getJqsj(), row.getCourseClass()));
}
List<ClassCalendarEventDTO> 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<JQB> matched = jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
.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<String> targetBhList, List<String> eventBhList) {
@@ -219,10 +368,11 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService
}
private void generateEmptyGrid(XYDNDXQJBXXB semester, LocalDate start, LocalDate end) {
Set<String> 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);
}
}
@@ -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<String> DEFAULT_SLOTS =
Arrays.asList("1-2", "3-4", "5-6", "7-8", "9-10", "11-12");
private static final Set<String> 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<String> slotLabels() {
List<JCSJB> 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<String> mainSlotLabels() {
List<JCSJB> slots = loadSlots();
if (slots.isEmpty()) {
return DEFAULT_MAIN_SLOTS;
}
Set<String> 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<JCSJB> loadSlots() {
List<JCSJB> rows = jcsjbMapper.selectList(new LambdaQueryWrapper<JCSJB>()
.eq(JCSJB::getSjc, true));
rows.sort(Comparator.comparingInt(r -> PeriodUtil.firstPeriod(r.getJcsy())));
return rows;
}
}
@@ -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<XQXLB>()
.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<JQB>()
@@ -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<Integer> parsePeriods(String label) {
List<Integer> 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;
@@ -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<String> 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
@@ -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<SSKCB> 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<JQB> classUnavailable = jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
.eq(JQB::getNd, nd)
.eq(JQB::getKpk, 0));
// 按班次学期(学员队学期编号)限定:同一年度可能存在多个学期,
// 仅按 年度 过滤会把其它学期的不可排格混进来。
List<XYDNDXQJBXXB> semesterRows = classSemesterMapper.selectList(new LambdaQueryWrapper<XYDNDXQJBXXB>()
.eq(XYDNDXQJBXXB::getDelFlag, 0));
Set<String> 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<JQB> classUnavailable = semesterBhs.isEmpty() ? Collections.emptyList()
: jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
.in(JQB::getXydxqbh, semesterBhs)
.eq(JQB::getKpk, 0));
if (!classUnavailable.isEmpty()) {
Map<String, SSKCB> 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<String, List<String>> 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<Integer> parsePeriods(String label) {
List<Integer> 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;
@@ -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 "教材管理";
}
@@ -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<Integer> parsePeriods(String label) {
List<Integer> 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<Integer> parsed = parsePeriods(label);
return parsed.isEmpty() ? Integer.MAX_VALUE : parsed.get(0);
}
}
@@ -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<ClassCalendarEventDTO> items;
/**
* 是否已实际写入(false = 仅预览)
*/
private boolean executed;
}