教学大纲新增导出模板和导入按钮

This commit is contained in:
2026-09-17 10:03:29 +08:00
parent 9af6de6ff3
commit f7299c36e8
7 changed files with 602 additions and 41 deletions
@@ -65,8 +65,16 @@ public interface ZYJXJHBService {
*/
PageResult<ZYJXJHB> pageByZydhAndTy(PageQuery query, String zydh, Integer ty);
/** 从 Excel(.xls/.xlsx) 导入专业教学计划 */
int importZYJXJHBFromExcel(org.springframework.web.multipart.MultipartFile file) throws Exception;
/**
* 从 Excel(.xls/.xlsx) 导入专业教学计划。
* <p>按表头名匹配列;必填列:专业代号、课编号、学期第次、课类型、学时;
* "专业代号+课编号+学期第次"已存在(含文件内重复)的行跳过,校验失败的行记入失败明细。</p>
*/
com.roomroot.jwgl.vo.syllabus.SyllabusImportResultVO importExcel(
org.springframework.web.multipart.MultipartFile file) throws Exception;
/** 生成教学大纲导入模板(数据 sheet + 填写说明 sheet) */
void downloadTemplate(jakarta.servlet.http.HttpServletResponse response) throws Exception;
/**
* 导出专业教学计划到 Excel(.xlsx)。
@@ -24,6 +24,15 @@ public class ZYJXJHBServiceImpl implements ZYJXJHBService {
@Resource
private ZYJXJHBMapper zyjxjhbMapper;
@Resource
private com.roomroot.jwgl.mapper.ZYBMapper zybMapper;
@Resource
private com.roomroot.jwgl.mapper.KBMapper kbMapper;
@Resource
private com.roomroot.jwgl.mapper.JYSBMapper jysbMapper;
@Override
public void add(ZYJXJHB zyjxjhb) {
zyjxjhb.setBh(UuidUtil.getUUID());
@@ -85,22 +94,211 @@ public class ZYJXJHBServiceImpl implements ZYJXJHBService {
@Override
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public int importZYJXJHBFromExcel(org.springframework.web.multipart.MultipartFile file) throws Exception {
public com.roomroot.jwgl.vo.syllabus.SyllabusImportResultVO importExcel(
org.springframework.web.multipart.MultipartFile file) throws Exception {
com.roomroot.jwgl.vo.syllabus.SyllabusImportResultVO result =
new com.roomroot.jwgl.vo.syllabus.SyllabusImportResultVO();
if (file == null || file.isEmpty()) {
throw new RuntimeException("导入文件不能为空");
}
List<ZYJXJHB> list = com.roomroot.jwgl.utils.ExcelParseUtil.parseZYJXJHBExcel(
file.getInputStream(), file.getOriginalFilename());
if (list.isEmpty()) {
java.util.Map<Integer, ZYJXJHB> rows;
try {
rows = com.roomroot.jwgl.utils.ExcelParseUtil.parseZYJXJHBExcel(
file.getInputStream(), file.getOriginalFilename());
} catch (IllegalArgumentException e) {
throw new RuntimeException(e.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException("Excel解析失败: " + e.getMessage(), e);
}
if (rows.isEmpty()) {
throw new RuntimeException("Excel中无有效数据");
}
for (ZYJXJHB entity : list) {
if (entity.getTy() == null) {
entity.setTy(0);
}
zyjxjhbMapper.insert(entity);
// 预载引用数据与已有计划键,逐行校验
java.util.Set<String> zydhSet = new java.util.HashSet<>();
java.util.Set<String> kbhSet = new java.util.HashSet<>();
java.util.Set<String> jysdhSet = new java.util.HashSet<>();
for (ZYJXJHB e : rows.values()) {
if (e.getZydh() != null && !e.getZydh().isEmpty()) zydhSet.add(e.getZydh());
if (e.getKbh() != null && !e.getKbh().isEmpty()) kbhSet.add(e.getKbh());
if (e.getJysdh() != null && !e.getJysdh().isEmpty()) jysdhSet.add(e.getJysdh());
}
java.util.Set<String> validZydh = new java.util.HashSet<>();
if (!zydhSet.isEmpty()) {
for (com.roomroot.jwgl.entity.ZYB zy : zybMapper.selectList(
new LambdaQueryWrapper<com.roomroot.jwgl.entity.ZYB>()
.in(com.roomroot.jwgl.entity.ZYB::getZydh, zydhSet))) {
validZydh.add(zy.getZydh());
}
}
java.util.Set<String> validKbh = new java.util.HashSet<>();
if (!kbhSet.isEmpty()) {
for (com.roomroot.jwgl.entity.KB kb : kbMapper.selectBatchIds(kbhSet)) {
validKbh.add(kb.getKbh());
}
}
java.util.Set<String> validJysdh = new java.util.HashSet<>();
if (!jysdhSet.isEmpty()) {
for (com.roomroot.jwgl.entity.JYSB jys : jysbMapper.selectBatchIds(jysdhSet)) {
validJysdh.add(jys.getJysdh());
}
}
java.util.Set<String> existingKeys = new java.util.HashSet<>();
if (!zydhSet.isEmpty()) {
for (ZYJXJHB exist : zyjxjhbMapper.selectList(
new LambdaQueryWrapper<ZYJXJHB>()
.select(ZYJXJHB::getZydh, ZYJXJHB::getKbh, ZYJXJHB::getXqdc)
.in(ZYJXJHB::getZydh, zydhSet))) {
existingKeys.add(planKey(exist.getZydh(), exist.getKbh(), exist.getXqdc()));
}
}
java.util.Set<String> fileKeys = new java.util.HashSet<>();
for (java.util.Map.Entry<Integer, ZYJXJHB> entry : rows.entrySet()) {
int rowNo = entry.getKey();
ZYJXJHB entity = entry.getValue();
String failReason = validateImportRow(entity, validZydh, validKbh, validJysdh);
if (failReason != null) {
result.addFail("第" + rowNo + "行:" + failReason);
continue;
}
String key = planKey(entity.getZydh(), entity.getKbh(), entity.getXqdc());
if (fileKeys.contains(key) || existingKeys.contains(key)) {
result.addSkip("第" + rowNo + "行:专业代号[" + entity.getZydh() + "] 课编号["
+ entity.getKbh() + "] 学期第次[" + entity.getXqdc() + "] 已存在,跳过");
continue;
}
if (entity.getTy() == null) entity.setTy(0);
if (entity.getBjrxypjf() == null) entity.setBjrxypjf(0);
if (entity.getKsksbxs() == null) entity.setKsksbxs(0);
if (entity.getDgkc() == null) entity.setDgkc(0);
entity.setBh(UuidUtil.getUUID());
entity.setQysj(LocalDateTime.now());
zyjxjhbMapper.insert(entity);
fileKeys.add(key);
existingKeys.add(key);
result.setInsertCount(result.getInsertCount() + 1);
}
result.setMessage(String.format("共导入 %d 条,跳过 %d 条,失败 %d 条",
result.getInsertCount(), result.getSkipCount(), result.getFailCount()));
return result;
}
private static String planKey(String zydh, String kbh, Integer xqdc) {
return zydh + "|" + kbh + "|" + xqdc;
}
/**
* 校验导入行:必填项 + 引用数据存在性。
*
* @return 失败原因,通过返回 null
*/
private static String validateImportRow(ZYJXJHB e,
java.util.Set<String> validZydh,
java.util.Set<String> validKbh,
java.util.Set<String> validJysdh) {
StringBuilder sb = new StringBuilder();
if (e.getZydh() == null || e.getZydh().isEmpty()) sb.append("专业代号为必填项;");
if (e.getKbh() == null || e.getKbh().isEmpty()) sb.append("课编号为必填项;");
if (e.getXqdc() == null) sb.append("学期第次为必填项且需为数字;");
if (e.getKlx() == null || e.getKlx().isEmpty()) sb.append("课类型为必填项;");
if (e.getXs() == null) sb.append("学时为必填项且需为数字;");
if (sb.length() > 0) {
return sb.toString();
}
if (!validZydh.contains(e.getZydh())) sb.append("专业代号[").append(e.getZydh()).append("]不存在;");
if (!validKbh.contains(e.getKbh())) sb.append("课编号[").append(e.getKbh()).append("]不存在;");
if (e.getJysdh() != null && !e.getJysdh().isEmpty() && !validJysdh.contains(e.getJysdh())) {
sb.append("教研室代号[").append(e.getJysdh()).append("]不存在;");
}
return sb.length() > 0 ? sb.toString() : null;
}
@Override
public void downloadTemplate(jakarta.servlet.http.HttpServletResponse response) throws Exception {
org.apache.poi.ss.usermodel.Workbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
try {
org.apache.poi.ss.usermodel.CellStyle headerStyle = workbook.createCellStyle();
org.apache.poi.ss.usermodel.Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
headerStyle.setAlignment(org.apache.poi.ss.usermodel.HorizontalAlignment.CENTER);
org.apache.poi.ss.usermodel.CellStyle requiredStyle = workbook.createCellStyle();
org.apache.poi.ss.usermodel.Font requiredFont = workbook.createFont();
requiredFont.setBold(true);
requiredFont.setColor(org.apache.poi.ss.usermodel.IndexedColors.RED.getIndex());
requiredStyle.setFont(requiredFont);
requiredStyle.setAlignment(org.apache.poi.ss.usermodel.HorizontalAlignment.CENTER);
// 数据 sheet:列与新增页字段一致,必填列标 *
String[] headers = {"专业代号*", "课编号*", "学期第次*", "课类型*", "学时*", "停用",
"简称", "学分", "课程定位", "模块", "成绩分制", "教研室代号",
"考试课时", "理论学时", "实践学时", "周课时", "不计入平均分", "考试课时不显示", "大纲课程"};
org.apache.poi.ss.usermodel.Sheet dataSheet = workbook.createSheet("专业教学计划");
org.apache.poi.ss.usermodel.Row headerRow = dataSheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
org.apache.poi.ss.usermodel.Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headers[i].endsWith("*") ? requiredStyle : headerStyle);
dataSheet.setColumnWidth(i, 14 * 256);
}
// 填写说明 sheet
String[][] instructions = {
{"专业代号", "必填", "专业表中的专业代号(非专业名称),导入时下拉选择项对应的代号"},
{"课编号", "必填", "课表中的课编号(非课程名称)"},
{"学期第次", "必填", "数字,1-20,表示该课程安排在第几个学期"},
{"课类型", "必填", "课类型字典值,如:必修、选修等(以系统字典 course_type 为准)"},
{"学时", "必填", "数字,总学时"},
{"停用", "选填", "是/否,默认否(导入后立即停用才填是)"},
{"简称", "选填", "课程简称"},
{"学分", "选填", "数字,可带一位小数"},
{"课程定位", "选填", "文本"},
{"模块", "选填", "文本"},
{"成绩分制", "选填", "文本,如:百分制、五级制"},
{"教研室代号", "选填", "教研室表中的教研室代号,填了必须存在"},
{"考试课时", "选填", "数字"},
{"理论学时", "选填", "数字"},
{"实践学时", "选填", "数字"},
{"周课时", "选填", "数字"},
{"不计入平均分", "选填", "是/否,默认否"},
{"考试课时不显示", "选填", "是/否,默认否"},
{"大纲课程", "选填", "是/否,默认否"}
};
org.apache.poi.ss.usermodel.Sheet helpSheet = workbook.createSheet("填写说明");
org.apache.poi.ss.usermodel.Row titleRow = helpSheet.createRow(0);
org.apache.poi.ss.usermodel.Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue("教学大纲导入填写说明(红色表头列为必填;专业代号+课编号+学期第次 相同的行视为重复,将跳过不导入)");
titleCell.setCellStyle(headerStyle);
String[] helpHeaders = {"列名", "是否必填", "填写说明"};
org.apache.poi.ss.usermodel.Row helpHeaderRow = helpSheet.createRow(1);
for (int i = 0; i < helpHeaders.length; i++) {
org.apache.poi.ss.usermodel.Cell cell = helpHeaderRow.createCell(i);
cell.setCellValue(helpHeaders[i]);
cell.setCellStyle(headerStyle);
}
for (int i = 0; i < instructions.length; i++) {
org.apache.poi.ss.usermodel.Row row = helpSheet.createRow(i + 2);
for (int j = 0; j < 3; j++) {
row.createCell(j).setCellValue(instructions[i][j]);
}
if ("必填".equals(instructions[i][1])) {
row.getCell(1).setCellStyle(requiredStyle);
}
}
helpSheet.setColumnWidth(0, 18 * 256);
helpSheet.setColumnWidth(1, 10 * 256);
helpSheet.setColumnWidth(2, 70 * 256);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition",
"attachment; filename=" + java.net.URLEncoder.encode(
"教学大纲导入模板.xlsx", java.nio.charset.StandardCharsets.UTF_8));
workbook.write(response.getOutputStream());
response.getOutputStream().flush();
} finally {
workbook.close();
}
return list.size();
}
@Override