1课表模板管理;

2、课表生成;
3、课表的网格视图;
4、导出与打印
This commit is contained in:
2026-09-18 09:02:41 +08:00
parent 971ea46da5
commit 911dfe911b
16 changed files with 1864 additions and 0 deletions
@@ -0,0 +1,30 @@
package com.roomroot.jwgl.dto.kcb;
import lombok.Data;
/**
* 课表网格查询参数(手册一第12章 课表生成)。
* <p>dim 维度:semester 班次学期 / team 学员队 / teacher 教员 / room 教室。
* id 为对应维度主键(xydxqbh / xydbh / jybh / jsbh)。</p>
*/
@Data
public class TimetableGridQuery {
/** 维度:semester | team | teacher | room */
private String dim;
/** 维度目标编号 */
private String id;
/** 年度(校历/备注取数用;semester 维度自动从班次学期带出) */
private Integer nd;
/** 起始日期 yyyy-MM-dd;空时 semester 取学期范围,其它维度取本周 */
private String start;
/** 结束日期 yyyy-MM-dd */
private String end;
/** 课表模板编号(可选;套用模板显示开关:78节/晚上/夜间/选修/天数) */
private String mbbh;
}
@@ -1,5 +1,7 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.entity.JCSJB;
import java.util.List;
import java.util.Set;
@@ -18,4 +20,10 @@ public interface ClassPeriodService {
* 上午时段的双节次标签,用于「正课」默认判定。
*/
Set<String> mainSlotLabels();
/**
* 单节次时间行(双节次=0),按节次序号升序;表内无数据时返回空,
* 调用方回退默认 1–12 节。
*/
List<JCSJB> singleRows();
}
@@ -0,0 +1,29 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.entity.KCBMBB;
import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult;
import java.util.List;
/**
* 课表模版管理(手册一第12章:只做 CRUD + 套用,不建新表)。
*/
public interface KCBMBService {
PageResult<KCBMBB> page(PageQuery query, String mbmc);
KCBMBB get(String bh);
void add(KCBMBB tpl);
void update(KCBMBB tpl);
void delete(String bh);
/** 启用/停用(模版可用开关) */
void toggle(String bh, boolean enabled);
/** 可用模板下拉(只回编号+名称) */
List<KCBMBB> options();
}
@@ -0,0 +1,21 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.dto.kcb.TimetableGridQuery;
import com.roomroot.jwgl.vo.kcb.TimetableGridVO;
/**
* 课表网格生成(手册一第12章)。
* <p>按 班次学期/学员队/教员/教室 四个维度生成「周次 × 星期 × 节次」横版网格。</p>
*/
public interface TimetableGridService {
/**
* 生成课表网格数据。
*/
TimetableGridVO grid(TimetableGridQuery query);
/**
* 导出网格课表为矩阵 Excel(每周一个 sheet)。
*/
byte[] exportGrid(TimetableGridQuery query);
}
@@ -51,6 +51,14 @@ public class ClassPeriodServiceImpl implements ClassPeriodService {
return main.isEmpty() ? DEFAULT_MAIN_SLOTS : main;
}
@Override
public List<JCSJB> singleRows() {
List<JCSJB> rows = jcsjbMapper.selectList(new LambdaQueryWrapper<JCSJB>()
.eq(JCSJB::getSjc, false));
rows.sort(Comparator.comparingInt(r -> PeriodUtil.firstPeriod(r.getJcsy())));
return rows;
}
private List<JCSJB> loadSlots() {
List<JCSJB> rows = jcsjbMapper.selectList(new LambdaQueryWrapper<JCSJB>()
.eq(JCSJB::getSjc, true));
@@ -0,0 +1,94 @@
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.KCBMBB;
import com.roomroot.jwgl.mapper.KCBMBBMapper;
import com.roomroot.jwgl.service.KCBMBService;
import com.roomroot.jwgl.unit.BusinessException;
import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.utils.UuidUtil;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class KCBMBServiceImpl implements KCBMBService {
@Resource
private KCBMBBMapper kcbmbbMapper;
@Override
public PageResult<KCBMBB> page(PageQuery query, String mbmc) {
Page<KCBMBB> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<KCBMBB> qw = new LambdaQueryWrapper<KCBMBB>()
.like(mbmc != null && !mbmc.isEmpty(), KCBMBB::getMBMC, mbmc)
.orderByDesc(KCBMBB::getGXSJ);
Page<KCBMBB> result = kcbmbbMapper.selectPage(page, qw);
return new PageResult<>(result.getRecords(), result.getTotal(), query.getPageNum(), query.getPageSize());
}
@Override
public KCBMBB get(String bh) {
return kcbmbbMapper.selectById(bh);
}
@Override
public void add(KCBMBB tpl) {
if (tpl.getMBMC() == null || tpl.getMBMC().isEmpty()) {
throw new BusinessException("模版名称不能为空");
}
tpl.setBH(UuidUtil.getUUID());
tpl.setGXSJ(LocalDateTime.now());
if (tpl.getMBKY() == null) {
tpl.setMBKY(true);
}
tpl.setMBCZ(tpl.getMB() != null && !tpl.getMB().isEmpty());
kcbmbbMapper.insert(tpl);
}
@Override
public void update(KCBMBB tpl) {
if (tpl.getBH() == null || tpl.getBH().isEmpty()) {
throw new BusinessException("模版编号不能为空");
}
tpl.setGXSJ(LocalDateTime.now());
tpl.setMBCZ(tpl.getMB() != null && !tpl.getMB().isEmpty());
kcbmbbMapper.updateById(tpl);
}
@Override
public void delete(String bh) {
kcbmbbMapper.deleteById(bh);
}
@Override
public void toggle(String bh, boolean enabled) {
KCBMBB tpl = kcbmbbMapper.selectById(bh);
if (tpl == null) {
throw new BusinessException("模版不存在:" + bh);
}
tpl.setMBKY(enabled);
tpl.setGXSJ(LocalDateTime.now());
kcbmbbMapper.updateById(tpl);
}
@Override
public List<KCBMBB> options() {
return kcbmbbMapper.selectList(new LambdaQueryWrapper<KCBMBB>()
.eq(KCBMBB::getMBKY, true)
.orderByDesc(KCBMBB::getGXSJ))
.stream()
.map(t -> {
KCBMBB o = new KCBMBB();
o.setBH(t.getBH());
o.setMBMC(t.getMBMC());
return o;
})
.collect(Collectors.toList());
}
}
@@ -0,0 +1,617 @@
package com.roomroot.jwgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.roomroot.jwgl.dto.kcb.TimetableGridQuery;
import com.roomroot.jwgl.entity.JCSJB;
import com.roomroot.jwgl.entity.JQB;
import com.roomroot.jwgl.entity.JSB;
import com.roomroot.jwgl.entity.JYB;
import com.roomroot.jwgl.entity.KB;
import com.roomroot.jwgl.entity.KCBMBB;
import com.roomroot.jwgl.entity.SSKC;
import com.roomroot.jwgl.entity.SSKCB;
import com.roomroot.jwgl.entity.SSKCBJS;
import com.roomroot.jwgl.entity.SSKCBFZJY;
import com.roomroot.jwgl.entity.SSKCBXYD;
import com.roomroot.jwgl.entity.XQXLB;
import com.roomroot.jwgl.entity.XYDB;
import com.roomroot.jwgl.entity.XYDNDXQJBXXB;
import com.roomroot.jwgl.mapper.ClassRoomMapper;
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
import com.roomroot.jwgl.mapper.JQBMapper;
import com.roomroot.jwgl.mapper.JYBMapper;
import com.roomroot.jwgl.mapper.KBMapper;
import com.roomroot.jwgl.mapper.KCBMBBMapper;
import com.roomroot.jwgl.mapper.SSKCBJSMapper;
import com.roomroot.jwgl.mapper.SSKCBFZJYMapper;
import com.roomroot.jwgl.mapper.SSKCBMapper;
import com.roomroot.jwgl.mapper.SSKCBXYDMapper;
import com.roomroot.jwgl.mapper.SSKCMapper;
import com.roomroot.jwgl.mapper.SemesterCalendarMapper;
import com.roomroot.jwgl.mapper.XYDBMapper;
import com.roomroot.jwgl.service.ClassPeriodService;
import com.roomroot.jwgl.service.TimetableGridService;
import com.roomroot.jwgl.unit.BusinessException;
import com.roomroot.jwgl.utils.PeriodUtil;
import com.roomroot.jwgl.vo.kcb.TimetableGridVO;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 课表网格生成实现。
* <p>课次一律从 实施_课程表(SSKCB) 按日期范围取;维度过滤经三张关联子表
* (学员队 / 教室 / 辅助教员)在内存归并,避免 N+1 与达梦 GUID 双形态问题。</p>
*/
@Service
public class TimetableGridServiceImpl implements TimetableGridService {
private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter COMPACT = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final String[] WEEKDAY_NAMES = {"", "", "", "", "", "", "", ""};
@Resource
private SSKCBMapper sskcbMapper;
@Resource
private SSKCBXYDMapper sskcbxydMapper;
@Resource
private SSKCBJSMapper sskcbjsMapper;
@Resource
private SSKCBFZJYMapper sskcbfzjyMapper;
@Resource
private SSKCMapper sskcMapper;
@Resource
private KBMapper kbMapper;
@Resource
private XYDBMapper xydbMapper;
@Resource
private JYBMapper jybMapper;
@Resource
private ClassRoomMapper classRoomMapper;
@Resource
private ClassSemesterMapper classSemesterMapper;
@Resource
private SemesterCalendarMapper semesterCalendarMapper;
@Resource
private JQBMapper jqbMapper;
@Resource
private KCBMBBMapper kcbmbbMapper;
@Resource
private ClassPeriodService classPeriodService;
@Override
public TimetableGridVO grid(TimetableGridQuery query) {
if (query == null || query.getDim() == null || query.getId() == null || query.getId().isEmpty()) {
throw new BusinessException("课表网格需要维度(dim)与目标编号(id)");
}
// ---- 维度目标与日期范围 ----
String dim = query.getDim();
String targetName = query.getId();
String teamFilter = null; // 学员队编号(semester/team 维度)
String teacherFilter = null;
String roomFilter = null;
String semesterBh = null; // 班历取数用(semester 维度)
LocalDate start = parseDay(query.getStart());
LocalDate end = parseDay(query.getEnd());
Integer nd = query.getNd();
switch (dim) {
case "semester": {
XYDNDXQJBXXB sem = classSemesterMapper.selectById(query.getId());
if (sem == null) {
throw new BusinessException("班次学期不存在:" + query.getId());
}
semesterBh = sem.getBh();
teamFilter = sem.getXydbh();
nd = sem.getNd();
if (start == null) start = sem.getKxrq();
if (end == null) end = sem.getJsrq();
String xydmc = resolveTeamName(teamFilter);
targetName = xydmc + "" + sem.getNd() + "年度 第" + sem.getXqdc() + "学期)";
break;
}
case "team": {
XYDB team = xydbMapper.selectOne(new LambdaQueryWrapper<XYDB>().eq(XYDB::getXydbh, query.getId()));
if (team == null) {
throw new BusinessException("学员队不存在:" + query.getId());
}
teamFilter = team.getXydbh();
targetName = team.getXydmc() != null ? team.getXydmc() : team.getXydbh();
break;
}
case "teacher": {
JYB teacher = jybMapper.selectOne(new LambdaQueryWrapper<JYB>().eq(JYB::getJybh, query.getId()));
if (teacher == null) {
throw new BusinessException("教员不存在:" + query.getId());
}
teacherFilter = teacher.getJybh();
targetName = (teacher.getJyxm() != null ? teacher.getJyxm() : teacher.getJybh());
break;
}
case "room": {
JSB room = classRoomMapper.selectOne(new LambdaQueryWrapper<JSB>()
.eq(JSB::getJsbh, query.getId()).eq(JSB::getDelFlag, 0));
if (room == null) {
throw new BusinessException("教室不存在:" + query.getId());
}
roomFilter = room.getJsbh();
targetName = room.getJsmc() != null ? room.getJsmc() : room.getJsbh();
break;
}
default:
throw new BusinessException("不支持的课表维度:" + dim + "(可选 semester/team/teacher/room");
}
if (start == null || end == null) {
LocalDate monday = LocalDate.now().with(DayOfWeek.MONDAY);
if (start == null) start = monday;
if (end == null) end = monday.plusDays(6);
}
if (end.isBefore(start)) {
throw new BusinessException("结束日期不能早于开始日期");
}
if (nd == null) {
nd = start.getYear();
}
// ---- 模板显示开关 ----
KCBMBB tpl = null;
if (query.getMbbh() != null && !query.getMbbh().isEmpty()) {
tpl = kcbmbbMapper.selectById(query.getMbbh());
}
// ---- 课次与关联 ----
List<SSKCB> lessons = sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
.ge(SSKCB::getRq, start.atStartOfDay())
.lt(SSKCB::getRq, end.plusDays(1).atStartOfDay())
.eq(SSKCB::getSczt, 0)
.isNotNull(SSKCB::getRq));
List<String> lessonBhs = lessons.stream().map(SSKCB::getBh).filter(Objects::nonNull).distinct().collect(Collectors.toList());
List<String> courseBhs = lessons.stream().map(SSKCB::getSskcbh).filter(Objects::nonNull).distinct().collect(Collectors.toList());
Map<String, List<String>> teamsOfLesson = new HashMap<>();
Map<String, String> teamShortName = new HashMap<>(); // sskcbbh -> 简称(SSKCBXYD.jc
Map<String, List<String>> roomsOfLesson = new HashMap<>();
Map<String, List<String>> teachersOfLesson = new HashMap<>();
Map<String, String> mainTeacherOfLesson = new HashMap<>();
if (!lessonBhs.isEmpty()) {
for (SSKCBXYD l : sskcbxydMapper.selectList(new LambdaQueryWrapper<SSKCBXYD>()
.in(SSKCBXYD::getSskcbbh, lessonBhs))) {
teamsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getXydbh());
if (l.getJc() != null && !l.getJc().isEmpty()) {
teamShortName.putIfAbsent(l.getSskcbbh(), l.getJc());
}
}
for (SSKCBJS l : sskcbjsMapper.selectList(new LambdaQueryWrapper<SSKCBJS>()
.in(SSKCBJS::getSskcbbh, lessonBhs))) {
roomsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getJsbh());
}
for (SSKCBFZJY l : sskcbfzjyMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
.in(SSKCBFZJY::getSskcbbh, lessonBhs))) {
if (l.getFzjybh() == null) continue;
teachersOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getFzjybh());
if (l.getZjy() != null && l.getZjy() == 1) {
mainTeacherOfLesson.put(l.getSskcbbh(), l.getFzjybh());
} else {
mainTeacherOfLesson.putIfAbsent(l.getSskcbbh(), l.getFzjybh());
}
}
}
Map<String, SSKC> courses = courseBhs.isEmpty() ? Map.of()
: sskcMapper.selectByBhs(courseBhs).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(SSKC::getBh, c -> c, (a, b) -> a));
Set<String> kbhs = courses.values().stream().map(SSKC::getKmbh).filter(Objects::nonNull).collect(Collectors.toSet());
Map<String, KB> kbByKbh = kbhs.isEmpty() ? Map.of()
: kbMapper.selectBatchIds(kbhs).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(KB::getKbh, k -> k, (a, b) -> a));
Set<String> jybhSet = new HashSet<>();
teachersOfLesson.values().forEach(jybhSet::addAll);
courses.values().forEach(c -> { if (c.getJybh() != null) jybhSet.add(c.getJybh()); });
Map<String, String> teacherNames = jybhSet.isEmpty() ? Map.of()
: jybMapper.selectList(new LambdaQueryWrapper<JYB>().in(JYB::getJybh, jybhSet)).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(JYB::getJybh, j -> j.getJyxm() == null ? j.getJybh() : j.getJyxm(), (a, b) -> a));
Set<String> jsbhSet = new HashSet<>();
roomsOfLesson.values().forEach(jsbhSet::addAll);
Map<String, String> roomNames = jsbhSet.isEmpty() ? Map.of()
: classRoomMapper.selectList(new LambdaQueryWrapper<JSB>()
.in(JSB::getJsbh, jsbhSet).eq(JSB::getDelFlag, 0)).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(JSB::getJsbh, j -> j.getJsmc() == null ? j.getJsbh() : j.getJsmc(), (a, b) -> a));
Set<String> xydbhSet = new HashSet<>();
teamsOfLesson.values().forEach(xydbhSet::addAll);
Map<String, String> teamNames = xydbhSet.isEmpty() ? Map.of()
: xydbMapper.selectList(new LambdaQueryWrapper<XYDB>().in(XYDB::getXydbh, xydbhSet)).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(XYDB::getXydbh, t -> t.getXydmc() == null ? t.getXydbh() : t.getXydmc(), (a, b) -> a));
// ---- 节次行 ----
List<TimetableGridVO.Period> periods = buildPeriods(tpl);
Set<Integer> periodJcs = periods.stream().map(TimetableGridVO.Period::getJc).collect(Collectors.toSet());
Map<Integer, String> periodSd = periods.stream()
.collect(Collectors.toMap(TimetableGridVO.Period::getJc, TimetableGridVO.Period::getSd, (a, b) -> a));
// ---- 历表:不可排格 + 备注栏 ----
// 备注栏规则:晚上/夜间/周末 且 备注显示=1 的事件写入备注而非格子。
Map<String, String> blockByCell = new HashMap<>(); // date#jc -> 事件名
List<TimetableGridVO.Remark> remarks = new ArrayList<>();
if (nd != null) {
for (XQXLB g : semesterCalendarMapper.selectList(new LambdaQueryWrapper<XQXLB>().eq(XQXLB::getNd, nd))) {
applyCalendarRow(g.getJqsj(), g.getCourseClass(), g.getKpk() != null && g.getKpk(),
Boolean.TRUE.equals(g.getBzxs()), joinName(g.getJqmc(), g.getBz()),
start, end, periodSd, blockByCell, remarks, "校历");
}
}
if (semesterBh != null) {
for (JQB g : jqbMapper.selectList(new LambdaQueryWrapper<JQB>().eq(JQB::getXydxqbh, semesterBh))) {
String dateStr = g.getJqsj() == null ? null : intToDay(g.getJqsj());
applyCalendarRow(dateStr, g.getCourseClass(), g.getKpk() != null && g.getKpk() == 1,
g.getBzxs() != null && g.getBzxs() == 1, joinName(g.getJqmc(), g.getBz()),
start, end, periodSd, blockByCell, remarks, "班历");
}
}
// ---- 组装网格 ----
TimetableGridVO vo = new TimetableGridVO();
vo.setDim(dim);
vo.setTargetName(targetName);
vo.setNd(nd);
vo.setStartDate(start.format(DAY));
vo.setEndDate(end.format(DAY));
vo.setPeriods(periods);
vo.setRemarks(remarks);
// 课次按 date#jc 归并(先按维度过滤)
Map<String, List<SSKCB>> lessonByCell = new HashMap<>();
for (SSKCB l : lessons) {
if (!matchDimension(l, dim, teamFilter, teacherFilter, roomFilter, teamsOfLesson, teachersOfLesson, roomsOfLesson, courses)) {
continue;
}
SSKC course = courses.get(l.getSskcbh());
KB kb = course == null ? null : kbByKbh.get(course.getKmbh());
if (tpl != null && Boolean.FALSE.equals(tpl.getKBBHXX())
&& kb != null && kb.getKclx() != null && kb.getKclx().contains("选修")) {
continue;
}
if (l.getJc() == null || !periodJcs.contains(l.getJc())) {
continue;
}
lessonByCell.computeIfAbsent(l.getRq().toLocalDate().format(DAY) + "#" + l.getJc(), k -> new ArrayList<>()).add(l);
}
LocalDate weekStart = start.with(DayOfWeek.MONDAY);
int weekNo = 1;
while (!weekStart.isAfter(end)) {
TimetableGridVO.Week week = new TimetableGridVO.Week();
week.setWeekNo(weekNo++);
week.setStartDate(weekStart.format(DAY));
week.setEndDate(weekStart.plusDays(6).format(DAY));
for (int wd = 1; wd <= 7; wd++) {
LocalDate date = weekStart.plusDays(wd - 1);
TimetableGridVO.Day day = new TimetableGridVO.Day();
day.setDate(date.format(DAY));
day.setWeekday(wd);
if (tpl != null && tpl.getZXXSTS() != null && tpl.getZXXSTS() <= 5 && wd > tpl.getZXXSTS()) {
// 模板「纵向显示天数」截断周末列
continue;
}
if (date.isBefore(start) || date.isAfter(end)) {
day.setBlocked(true);
day.setBlockReason("范围外");
week.getDays().add(day);
continue;
}
boolean dayBlocked = false;
String reason = null;
for (Integer jc : periodJcs) {
String key = date.format(DAY) + "#" + jc;
String block = blockByCell.get(key);
if (block != null) {
dayBlocked = true;
reason = block;
}
List<SSKCB> cellLessons = lessonByCell.get(key);
if (cellLessons == null) continue;
for (SSKCB l : cellLessons) {
day.getCells().computeIfAbsent(String.valueOf(jc), k -> new ArrayList<>())
.add(buildCell(l, dim, courses, kbByKbh, teamShortName, teacherNames,
roomNames, teamNames, mainTeacherOfLesson, roomsOfLesson, teamsOfLesson));
}
}
day.setBlocked(dayBlocked);
day.setBlockReason(reason);
week.getDays().add(day);
}
vo.getWeeks().add(week);
}
return vo;
}
@Override
public byte[] exportGrid(TimetableGridQuery query) {
TimetableGridVO vo = grid(query);
try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
CellStyle head = wb.createCellStyle();
Font hf = wb.createFont();
hf.setBold(true);
head.setFont(hf);
head.setAlignment(HorizontalAlignment.CENTER);
head.setVerticalAlignment(VerticalAlignment.CENTER);
CellStyle center = wb.createCellStyle();
center.setAlignment(HorizontalAlignment.CENTER);
center.setVerticalAlignment(VerticalAlignment.CENTER);
center.setWrapText(true);
CellStyle title = wb.createCellStyle();
Font tf = wb.createFont();
tf.setBold(true);
tf.setFontHeightInPoints((short) 14);
title.setFont(tf);
title.setAlignment(HorizontalAlignment.CENTER);
for (TimetableGridVO.Week week : vo.getWeeks()) {
org.apache.poi.ss.usermodel.Sheet sheet = wb.createSheet("" + week.getWeekNo() + "");
int r = 0;
org.apache.poi.ss.usermodel.Row titleRow = sheet.createRow(r++);
org.apache.poi.ss.usermodel.Cell tc = titleRow.createCell(0);
tc.setCellValue(vo.getTargetName() + " 课表(" + week.getStartDate() + " ~ " + week.getEndDate() + "");
tc.setCellStyle(title);
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(0, 0, 0, 7));
org.apache.poi.ss.usermodel.Row headRow = sheet.createRow(r++);
String[] heads = {"节次", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"};
for (int i = 0; i < heads.length; i++) {
org.apache.poi.ss.usermodel.Cell c = headRow.createCell(i);
c.setCellValue(heads[i]);
c.setCellStyle(head);
}
for (TimetableGridVO.Period p : vo.getPeriods()) {
org.apache.poi.ss.usermodel.Row row = sheet.createRow(r++);
org.apache.poi.ss.usermodel.Cell pc = row.createCell(0);
pc.setCellValue(p.getMc() + (p.getKssj() != null ? "\n" + p.getKssj() + "-" + p.getJssj() : ""));
pc.setCellStyle(head);
for (TimetableGridVO.Day day : week.getDays()) {
org.apache.poi.ss.usermodel.Cell c = row.createCell(day.getWeekday());
List<TimetableGridVO.Cell> cells = day.getCells().get(String.valueOf(p.getJc()));
if (cells != null && !cells.isEmpty()) {
c.setCellValue(cells.stream().map(this::cellText).collect(Collectors.joining("\n")));
}
c.setCellStyle(center);
}
}
if (!vo.getRemarks().isEmpty()) {
r++;
org.apache.poi.ss.usermodel.Row rh = sheet.createRow(r++);
org.apache.poi.ss.usermodel.Cell rc = rh.createCell(0);
rc.setCellValue("备注栏");
rc.setCellStyle(head);
for (TimetableGridVO.Remark rm : vo.getRemarks()) {
org.apache.poi.ss.usermodel.Row rr = sheet.createRow(r++);
org.apache.poi.ss.usermodel.Cell cc = rr.createCell(0);
cc.setCellValue(rm.getDate() + " " + rm.getScope() + "" + rm.getText());
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(rr.getRowNum(), rr.getRowNum(), 0, 7));
}
}
sheet.setColumnWidth(0, 14 * 256);
for (int i = 1; i <= 7; i++) {
sheet.setColumnWidth(i, 22 * 256);
}
}
if (vo.getWeeks().isEmpty()) {
wb.createSheet("课表").createRow(0).createCell(0).setCellValue("范围内无课表数据");
}
wb.write(out);
return out.toByteArray();
} catch (Exception e) {
throw new BusinessException("课表导出失败:" + e.getMessage());
}
}
// ==================== 内部方法 ====================
private String cellText(TimetableGridVO.Cell c) {
StringBuilder sb = new StringBuilder(c.getKcjc() != null && !c.getKcjc().isEmpty() ? c.getKcjc() : c.getKcmc());
List<String> extra = new ArrayList<>();
if (c.getJyxm() != null && !c.getJyxm().isEmpty()) extra.add(c.getJyxm());
if (c.getJsmc() != null && !c.getJsmc().isEmpty()) extra.add(c.getJsmc());
if (c.getXydmc() != null && !c.getXydmc().isEmpty()) extra.add(c.getXydmc());
if (!extra.isEmpty()) sb.append("\n(").append(String.join(" ", extra)).append(")");
return sb.toString();
}
private TimetableGridVO.Cell buildCell(SSKCB l, String dim,
Map<String, SSKC> courses, Map<String, KB> kbByKbh,
Map<String, String> teamShortName,
Map<String, String> teacherNames, Map<String, String> roomNames,
Map<String, String> teamNames,
Map<String, String> mainTeacherOfLesson,
Map<String, List<String>> roomsOfLesson,
Map<String, List<String>> teamsOfLesson) {
TimetableGridVO.Cell cell = new TimetableGridVO.Cell();
cell.setSskcbbh(l.getBh());
cell.setSskcbh(l.getSskcbh());
SSKC course = courses.get(l.getSskcbh());
KB kb = course == null ? null : kbByKbh.get(course.getKmbh());
cell.setKcmc(kb != null && kb.getKmc() != null ? kb.getKmc() : l.getJxnr());
cell.setKclx(kb == null ? null : kb.getKclx());
// 简称优先:课次-学员队关联上的简称 > 课表.简称 > 课程名称
String shortName = teamShortName.get(l.getBh());
if (shortName == null || shortName.isEmpty()) {
shortName = kb != null && kb.getJc() != null && !kb.getJc().isEmpty() ? kb.getJc() : cell.getKcmc();
}
cell.setKcjc(shortName);
String mainJybh = mainTeacherOfLesson.get(l.getBh());
if (mainJybh == null && course != null) {
mainJybh = course.getJybh();
}
cell.setJyxm(mainJybh == null ? null : teacherNames.getOrDefault(mainJybh, mainJybh));
cell.setJsmc(roomsOfLesson.getOrDefault(l.getBh(), List.of()).stream()
.map(b -> roomNames.getOrDefault(b, b)).distinct().collect(Collectors.joining("")));
cell.setXydmc(teamsOfLesson.getOrDefault(l.getBh(), List.of()).stream()
.map(b -> teamNames.getOrDefault(b, b)).distinct().collect(Collectors.joining("")));
return cell;
}
private boolean matchDimension(SSKCB l, String dim, String teamFilter, String teacherFilter, String roomFilter,
Map<String, List<String>> teamsOfLesson,
Map<String, List<String>> teachersOfLesson,
Map<String, List<String>> roomsOfLesson,
Map<String, SSKC> courses) {
switch (dim) {
case "semester":
case "team":
return teamsOfLesson.getOrDefault(l.getBh(), List.of()).contains(teamFilter);
case "teacher": {
if (teachersOfLesson.getOrDefault(l.getBh(), List.of()).contains(teacherFilter)) {
return true;
}
SSKC c = courses.get(l.getSskcbh());
return c != null && (teacherFilter.equals(c.getJybh()) || teacherFilter.equals(c.getZrjybh()));
}
case "room":
return roomsOfLesson.getOrDefault(l.getBh(), List.of()).contains(roomFilter);
default:
return false;
}
}
/**
* 历表一行 → 不可排格标记或备注栏条目。
* 备注栏规则:晚上/夜间时段或周末 且 备注显示 → remarks;可排课=0 → blockByCell。
*/
private void applyCalendarRow(String dateStr, String courseClass, boolean schedulable, boolean bzxs,
String text, LocalDate start, LocalDate end,
Map<Integer, String> periodSd,
Map<String, String> blockByCell,
List<TimetableGridVO.Remark> remarks, String scope) {
if (dateStr == null) {
return;
}
LocalDate day;
try {
day = LocalDate.parse(dateStr.substring(0, 10), DAY);
} catch (Exception e) {
return;
}
if (day.isBefore(start) || day.isAfter(end)) {
return;
}
boolean weekend = day.getDayOfWeek() == DayOfWeek.SATURDAY || day.getDayOfWeek() == DayOfWeek.SUNDAY;
List<Integer> jcs = PeriodUtil.parsePeriods(courseClass);
boolean evening = jcs.stream().anyMatch(jc -> {
String sd = periodSd.getOrDefault(jc, "");
return "晚上".equals(sd) || "夜间".equals(sd);
});
if (bzxs && (weekend || evening) && text != null && !text.isEmpty()) {
TimetableGridVO.Remark rm = new TimetableGridVO.Remark();
rm.setDate(day.format(DAY));
rm.setScope(scope);
rm.setText(text);
remarks.add(rm);
return;
}
if (!schedulable) {
for (Integer jc : jcs) {
blockByCell.put(day.format(DAY) + "#" + jc, text == null ? "不可排课" : text);
}
}
}
/** 节次行:节次时间表单节次行优先,空表回退 1-12;再套模板显示开关 */
private List<TimetableGridVO.Period> buildPeriods(KCBMBB tpl) {
List<TimetableGridVO.Period> periods = new ArrayList<>();
List<JCSJB> rows = classPeriodService.singleRows();
if (rows.isEmpty()) {
for (int jc = 1; jc <= 12; jc++) {
TimetableGridVO.Period p = new TimetableGridVO.Period();
p.setJc(jc);
p.setMc("" + jc + "");
p.setSd(defaultSd(jc));
periods.add(p);
}
} else {
for (JCSJB row : rows) {
for (Integer jc : PeriodUtil.parsePeriods(row.getJcsy())) {
TimetableGridVO.Period p = new TimetableGridVO.Period();
p.setJc(jc);
p.setMc(row.getMc() != null ? row.getMc() : "" + jc + "");
p.setSd(row.getSd() != null ? row.getSd() : defaultSd(jc));
p.setKssj(trimTime(row.getKssj()));
p.setJssj(trimTime(row.getJssj()));
periods.add(p);
}
}
periods.sort(Comparator.comparingInt(TimetableGridVO.Period::getJc));
}
if (tpl != null) {
periods = periods.stream().filter(p -> {
if (Boolean.FALSE.equals(tpl.getZXXS78J()) && (p.getJc() == 7 || p.getJc() == 8)) return false;
if (Boolean.FALSE.equals(tpl.getZXXSWS()) && "晚上".equals(p.getSd())) return false;
if (Boolean.FALSE.equals(tpl.getXSYJ()) && "夜间".equals(p.getSd())) return false;
return true;
}).collect(Collectors.toList());
}
return periods;
}
private static String defaultSd(int jc) {
if (jc <= 4) return "上午";
if (jc <= 8) return "下午";
if (jc <= 10) return "晚上";
return "夜间";
}
private static String trimTime(String t) {
if (t == null) return null;
String s = t.trim();
return s.length() > 5 ? s.substring(0, 5) : s;
}
private static String joinName(String mc, String bz) {
if (mc == null || mc.isEmpty()) return bz;
if (bz == null || bz.isEmpty()) return mc;
return mc + "" + bz + "";
}
private static String intToDay(Integer yyyymmdd) {
String s = String.valueOf(yyyymmdd);
if (s.length() != 8) return null;
return s.substring(0, 4) + "-" + s.substring(4, 6) + "-" + s.substring(6);
}
private static LocalDate parseDay(String s) {
if (s == null || s.trim().isEmpty()) return null;
try {
return LocalDate.parse(s.trim().substring(0, 10), DAY);
} catch (Exception e) {
return null;
}
}
private String resolveTeamName(String xydbh) {
if (xydbh == null) return "";
XYDB team = xydbMapper.selectOne(new LambdaQueryWrapper<XYDB>().eq(XYDB::getXydbh, xydbh));
return team != null && team.getXydmc() != null ? team.getXydmc() : xydbh;
}
}
@@ -0,0 +1,101 @@
package com.roomroot.jwgl.vo.kcb;
import lombok.Data;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 周次横版课表网格。
* <p>weeks 按周纵向排列;每周内 行=节次、列=星期一~日;
* 晚上/夜间/周末且「备注显示」的历表事件进 remarks,不占格子。</p>
*/
@Data
public class TimetableGridVO {
/** 维度 */
private String dim;
/** 维度目标显示名(标题用) */
private String targetName;
/** 年度 */
private Integer nd;
private String startDate;
private String endDate;
/** 节次行定义(升序) */
private List<Period> periods = new ArrayList<>();
/** 周块 */
private List<Week> weeks = new ArrayList<>();
/** 备注栏条目(历表「备注显示」且落在晚上/夜间/周末的事件) */
private List<Remark> remarks = new ArrayList<>();
@Data
public static class Period {
/** 节次序号 */
private Integer jc;
/** 显示名(如 第1节 / 第1-2节) */
private String mc;
/** 时段:上午/下午/晚上/夜间 */
private String sd;
/** 开始时间 HH:mm */
private String kssj;
/** 结束时间 HH:mm */
private String jssj;
}
@Data
public static class Week {
private Integer weekNo;
private String startDate;
private String endDate;
/** 固定 7 天(周一~周日) */
private List<Day> days = new ArrayList<>();
}
@Data
public static class Day {
private String date;
/** 1=周一 … 7=周日 */
private Integer weekday;
/** 校历/班历标记不可排课(该日所有节次或部分节次不可排时为 true) */
private Boolean blocked = false;
/** 不可排说明(如假期名称) */
private String blockReason;
/** key=节次序号字符串 */
private Map<String, List<Cell>> cells = new LinkedHashMap<>();
}
@Data
public static class Cell {
private String sskcbbh;
private String sskcbh;
/** 课程名称 */
private String kcmc;
/** 课程简称(格子主显示) */
private String kcjc;
/** 主讲教员姓名 */
private String jyxm;
/** 场地名称 */
private String jsmc;
/** 班次/学员队名(教员、教室维度下展示) */
private String xydmc;
/** 课程类型(选修过滤用) */
private String kclx;
}
@Data
public static class Remark {
private String date;
/** 来源:校历 / 班历 */
private String scope;
/** 备注文本(事件名 + 备注) */
private String text;
}
}