forked from liweijie/education
教学大纲新增导出模板和导入按钮
This commit is contained in:
+15
-5
@@ -92,13 +92,23 @@ public class ZYJXJHBController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 专业教学计划表数据文件导入(.xls/.xlsx)。
|
* 专业教学计划表数据文件导入(.xls/.xlsx)。
|
||||||
* <p>表头顺序:专业代号, 课编号, 学期第次, 课类型, 学时, 学分, 周课时, 课程定位,
|
* <p>按表头名匹配列(列位置不限),推荐使用 /template 下载的模板填写。
|
||||||
* 模块, 成绩分制, 理论学时, 实践学时, 考试课时, 教研室代号, 简称。编号由系统自动生成。</p>
|
* 必填列:专业代号、课编号、学期第次、课类型、学时;
|
||||||
|
* "专业代号+课编号+学期第次"已存在的行跳过,校验失败行返回行级明细。</p>
|
||||||
*/
|
*/
|
||||||
@PostMapping("/import")
|
@PostMapping("/import")
|
||||||
public Result<Integer> importData(@RequestParam("file") MultipartFile file) throws Exception {
|
public Result<com.roomroot.jwgl.vo.syllabus.SyllabusImportResultVO> importData(
|
||||||
int count = zyjxjhbService.importZYJXJHBFromExcel(file);
|
@RequestParam("file") MultipartFile file) throws Exception {
|
||||||
return Result.success("导入成功,共" + count + "条", count);
|
com.roomroot.jwgl.vo.syllabus.SyllabusImportResultVO result = zyjxjhbService.importExcel(file);
|
||||||
|
return Result.success(result.getMessage(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载教学大纲导入模板(数据 sheet + 填写说明 sheet)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/template")
|
||||||
|
public void downloadTemplate(HttpServletResponse response) throws Exception {
|
||||||
|
zyjxjhbService.downloadTemplate(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,8 +65,16 @@ public interface ZYJXJHBService {
|
|||||||
*/
|
*/
|
||||||
PageResult<ZYJXJHB> pageByZydhAndTy(PageQuery query, String zydh, Integer ty);
|
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)。
|
* 导出专业教学计划到 Excel(.xlsx)。
|
||||||
|
|||||||
+208
-10
@@ -24,6 +24,15 @@ public class ZYJXJHBServiceImpl implements ZYJXJHBService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ZYJXJHBMapper zyjxjhbMapper;
|
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
|
@Override
|
||||||
public void add(ZYJXJHB zyjxjhb) {
|
public void add(ZYJXJHB zyjxjhb) {
|
||||||
zyjxjhb.setBh(UuidUtil.getUUID());
|
zyjxjhb.setBh(UuidUtil.getUUID());
|
||||||
@@ -85,22 +94,211 @@ public class ZYJXJHBServiceImpl implements ZYJXJHBService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
|
@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()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new RuntimeException("导入文件不能为空");
|
throw new RuntimeException("导入文件不能为空");
|
||||||
}
|
}
|
||||||
List<ZYJXJHB> list = com.roomroot.jwgl.utils.ExcelParseUtil.parseZYJXJHBExcel(
|
java.util.Map<Integer, ZYJXJHB> rows;
|
||||||
file.getInputStream(), file.getOriginalFilename());
|
try {
|
||||||
if (list.isEmpty()) {
|
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中无有效数据");
|
throw new RuntimeException("Excel中无有效数据");
|
||||||
}
|
}
|
||||||
for (ZYJXJHB entity : list) {
|
|
||||||
if (entity.getTy() == null) {
|
// 预载引用数据与已有计划键,逐行校验
|
||||||
entity.setTy(0);
|
java.util.Set<String> zydhSet = new java.util.HashSet<>();
|
||||||
}
|
java.util.Set<String> kbhSet = new java.util.HashSet<>();
|
||||||
zyjxjhbMapper.insert(entity);
|
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
|
@Override
|
||||||
|
|||||||
@@ -659,37 +659,86 @@ public class ExcelParseUtil {
|
|||||||
* @return 专业教学计划实体列表
|
* @return 专业教学计划实体列表
|
||||||
* @throws Exception 解析异常
|
* @throws Exception 解析异常
|
||||||
*/
|
*/
|
||||||
public static List<ZYJXJHB> parseZYJXJHBExcel(InputStream inputStream, String fileName) throws Exception {
|
/**
|
||||||
List<ZYJXJHB> result = new ArrayList<>();
|
* 教学大纲导入模板支持的表头名 → 实体字段标识。
|
||||||
|
* 兼容导出文件与新增页字段两种叫法。
|
||||||
|
*/
|
||||||
|
private static final Map<String, String> ZYJXJHB_HEADER_FIELDS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Map<String, String> map = new LinkedHashMap<>();
|
||||||
|
map.put("专业代号", "zydh");
|
||||||
|
map.put("专业", "zydh");
|
||||||
|
map.put("课编号", "kbh");
|
||||||
|
map.put("课程", "kbh");
|
||||||
|
map.put("学期第次", "xqdc");
|
||||||
|
map.put("课类型", "klx");
|
||||||
|
map.put("学时", "xs");
|
||||||
|
map.put("停用", "ty");
|
||||||
|
map.put("简称", "jc");
|
||||||
|
map.put("学分", "xf");
|
||||||
|
map.put("课程定位", "kcdw");
|
||||||
|
map.put("模块", "mk");
|
||||||
|
map.put("成绩分制", "cjfz");
|
||||||
|
map.put("教研室代号", "jysdh");
|
||||||
|
map.put("教研室", "jysdh");
|
||||||
|
map.put("考试课时", "ksks");
|
||||||
|
map.put("理论学时", "llxs");
|
||||||
|
map.put("实践学时", "sjxs");
|
||||||
|
map.put("周课时", "zks");
|
||||||
|
map.put("不计入平均分", "bjrxypjf");
|
||||||
|
map.put("不计入学员平均分", "bjrxypjf");
|
||||||
|
map.put("考试课时不显示", "ksksbxs");
|
||||||
|
map.put("大纲课程", "dgkc");
|
||||||
|
ZYJXJHB_HEADER_FIELDS = java.util.Collections.unmodifiableMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按表头名称解析教学大纲导入 Excel(第一个 sheet)。
|
||||||
|
* <p>列位置不固定,按表头名匹配;必填列「专业代号」「课编号」缺失时报错。
|
||||||
|
* 是否类列(停用/不计入平均分/考试课时不显示/大纲课程)支持 是/否/1/0。</p>
|
||||||
|
*
|
||||||
|
* @return Excel 行号(1-based)→ 实体,跳过整行空白行
|
||||||
|
*/
|
||||||
|
public static Map<Integer, ZYJXJHB> parseZYJXJHBExcel(InputStream inputStream, String fileName) throws Exception {
|
||||||
|
Map<Integer, ZYJXJHB> result = new LinkedHashMap<>();
|
||||||
Workbook workbook = createWorkbook(inputStream, fileName);
|
Workbook workbook = createWorkbook(inputStream, fileName);
|
||||||
try {
|
try {
|
||||||
Sheet sheet = workbook.getSheetAt(0);
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
int lastRowNum = sheet.getLastRowNum();
|
Row headerRow = sheet.getRow(0);
|
||||||
for (int rowNum = 1; rowNum <= lastRowNum; rowNum++) {
|
if (headerRow == null) {
|
||||||
|
throw new IllegalArgumentException("Excel缺少表头行,请下载导入模板填写");
|
||||||
|
}
|
||||||
|
Map<Integer, String> colField = new LinkedHashMap<>();
|
||||||
|
for (int c = 0; c < headerRow.getLastCellNum(); c++) {
|
||||||
|
String header = getCellStringValue(headerRow.getCell(c));
|
||||||
|
if (header == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String field = ZYJXJHB_HEADER_FIELDS.get(header.replace("*", "").replace(" ", "").trim());
|
||||||
|
if (field != null) {
|
||||||
|
colField.put(c, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!colField.containsValue("zydh") || !colField.containsValue("kbh")) {
|
||||||
|
throw new IllegalArgumentException("表头缺少必需列「专业代号」「课编号」,请下载导入模板填写");
|
||||||
|
}
|
||||||
|
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
||||||
Row row = sheet.getRow(rowNum);
|
Row row = sheet.getRow(rowNum);
|
||||||
if (row == null) {
|
if (row == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ZYJXJHB entity = new ZYJXJHB();
|
ZYJXJHB entity = new ZYJXJHB();
|
||||||
entity.setZydh(getCellStringValue(row.getCell(0)));
|
boolean hasValue = false;
|
||||||
entity.setKbh(getCellStringValue(row.getCell(1)));
|
for (Map.Entry<Integer, String> entry : colField.entrySet()) {
|
||||||
entity.setXqdc(parseIntCell(row.getCell(2)));
|
Cell cell = row.getCell(entry.getKey());
|
||||||
entity.setKlx(getCellStringValue(row.getCell(3)));
|
if (getCellStringValue(cell) != null) {
|
||||||
entity.setXs(parseIntCell(row.getCell(4)));
|
hasValue = true;
|
||||||
entity.setXf(parseFloatCell(row.getCell(5)));
|
}
|
||||||
entity.setZks(parseIntCell(row.getCell(6)));
|
applyZYJXJHBField(entity, entry.getValue(), cell);
|
||||||
entity.setKcdw(getCellStringValue(row.getCell(7)));
|
}
|
||||||
entity.setMk(getCellStringValue(row.getCell(8)));
|
if (hasValue) {
|
||||||
entity.setCjfz(getCellStringValue(row.getCell(9)));
|
result.put(rowNum + 1, entity);
|
||||||
entity.setLlxs(parseIntCell(row.getCell(10)));
|
|
||||||
entity.setSjxs(parseIntCell(row.getCell(11)));
|
|
||||||
entity.setKsks(parseIntCell(row.getCell(12)));
|
|
||||||
entity.setJysdh(getCellStringValue(row.getCell(13)));
|
|
||||||
entity.setJc(getCellStringValue(row.getCell(14)));
|
|
||||||
// 至少要有专业代号或课编号才视为有效行
|
|
||||||
if (entity.getZydh() != null && !entity.getZydh().isEmpty()
|
|
||||||
|| entity.getKbh() != null && !entity.getKbh().isEmpty()) {
|
|
||||||
result.add(entity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -698,6 +747,49 @@ public class ExcelParseUtil {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applyZYJXJHBField(ZYJXJHB entity, String field, Cell cell) {
|
||||||
|
switch (field) {
|
||||||
|
case "zydh": entity.setZydh(getCellStringValue(cell)); break;
|
||||||
|
case "kbh": entity.setKbh(getCellStringValue(cell)); break;
|
||||||
|
case "xqdc": entity.setXqdc(parseIntCell(cell)); break;
|
||||||
|
case "klx": entity.setKlx(getCellStringValue(cell)); break;
|
||||||
|
case "xs": entity.setXs(parseIntCell(cell)); break;
|
||||||
|
case "ty": entity.setTy(parseYesNoCell(cell)); break;
|
||||||
|
case "jc": entity.setJc(getCellStringValue(cell)); break;
|
||||||
|
case "xf": entity.setXf(parseFloatCell(cell)); break;
|
||||||
|
case "kcdw": entity.setKcdw(getCellStringValue(cell)); break;
|
||||||
|
case "mk": entity.setMk(getCellStringValue(cell)); break;
|
||||||
|
case "cjfz": entity.setCjfz(getCellStringValue(cell)); break;
|
||||||
|
case "jysdh": entity.setJysdh(getCellStringValue(cell)); break;
|
||||||
|
case "ksks": entity.setKsks(parseIntCell(cell)); break;
|
||||||
|
case "llxs": entity.setLlxs(parseIntCell(cell)); break;
|
||||||
|
case "sjxs": entity.setSjxs(parseIntCell(cell)); break;
|
||||||
|
case "zks": entity.setZks(parseIntCell(cell)); break;
|
||||||
|
case "bjrxypjf": entity.setBjrxypjf(parseYesNoCell(cell)); break;
|
||||||
|
case "ksksbxs": entity.setKsksbxs(parseYesNoCell(cell)); break;
|
||||||
|
case "dgkc": entity.setDgkc(parseYesNoCell(cell)); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析是否类单元格:是/Y/1/true → 1,否/N/0/false → 0,空或无法识别 → null。
|
||||||
|
*/
|
||||||
|
private static Integer parseYesNoCell(Cell cell) {
|
||||||
|
String value = getCellStringValue(cell);
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String v = value.trim();
|
||||||
|
if ("是".equals(v) || "1".equals(v) || "Y".equalsIgnoreCase(v) || "true".equalsIgnoreCase(v)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ("否".equals(v) || "0".equals(v) || "N".equalsIgnoreCase(v) || "false".equalsIgnoreCase(v)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static Workbook createWorkbook(InputStream inputStream, String fileName) throws Exception {
|
private static Workbook createWorkbook(InputStream inputStream, String fileName) throws Exception {
|
||||||
String name = fileName == null ? "" : fileName.toLowerCase();
|
String name = fileName == null ? "" : fileName.toLowerCase();
|
||||||
if (!name.endsWith(".xlsx") && !name.endsWith(".xls")) {
|
if (!name.endsWith(".xlsx") && !name.endsWith(".xls")) {
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package com.roomroot.jwgl.vo.syllabus;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学大纲(专业教学计划表)导入结果。
|
||||||
|
* <p>
|
||||||
|
* 返回导入操作的执行结果:新增数、跳过数(重复数据)、失败数与行级明细。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SyllabusImportResultVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际新增的教学大纲数。
|
||||||
|
*/
|
||||||
|
private Integer insertCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 因"专业代号+课编号+学期第次"已存在而跳过的条数。
|
||||||
|
*/
|
||||||
|
private Integer skipCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验失败(必填缺失、引用不存在、格式错误等)而未导入的条数。
|
||||||
|
*/
|
||||||
|
private Integer failCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳过明细。
|
||||||
|
*/
|
||||||
|
private List<String> skipList = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败明细(第 N 行:失败原因)。
|
||||||
|
*/
|
||||||
|
private List<String> failList = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果提示信息。
|
||||||
|
*/
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public void addSkip(String detail) {
|
||||||
|
this.skipList.add(detail);
|
||||||
|
this.skipCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFail(String detail) {
|
||||||
|
this.failList.add(detail);
|
||||||
|
this.failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,3 +66,25 @@ export function exportSyllabus(query) {
|
|||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载教学大纲导入模板(数据 sheet + 填写说明 sheet)
|
||||||
|
export function downloadSyllabusTemplate() {
|
||||||
|
return request({
|
||||||
|
url: '/zyjxjhb/template',
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入教学大纲 Excel,返回 { insertCount, skipCount, failCount, skipList, failList, message }
|
||||||
|
export function importSyllabus(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request({
|
||||||
|
url: '/zyjxjhb/import',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
timeout: 60000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
@click="handleBatchDelete"
|
@click="handleBatchDelete"
|
||||||
>停用所选</el-button>
|
>停用所选</el-button>
|
||||||
<el-button icon="el-icon-download" @click="handleDownload">下载</el-button>
|
<el-button icon="el-icon-download" @click="handleDownload">下载</el-button>
|
||||||
|
<el-button icon="el-icon-download" @click="handleDownloadTemplate">下载模板</el-button>
|
||||||
|
<el-button icon="el-icon-upload2" @click="openImportDialog">导入</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-loading="loading" :data="pagedData" border stripe highlight-current-row
|
<el-table v-loading="loading" :data="pagedData" border stripe highlight-current-row
|
||||||
@@ -287,6 +289,52 @@
|
|||||||
<el-button type="primary" :loading="dialog.submitting" @click="submitDialog">确 定</el-button>
|
<el-button type="primary" :loading="dialog.submitting" @click="submitDialog">确 定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 模板导入弹窗 -->
|
||||||
|
<el-dialog title="教学大纲导入" :visible.sync="importDialog.visible" width="660px" append-to-body
|
||||||
|
:close-on-click-modal="false">
|
||||||
|
<el-alert type="info" :closable="false" show-icon class="import-tip"
|
||||||
|
title="请先下载导入模板,按「专业教学计划」sheet 列填写后再上传;「专业代号+课编号+学期第次」已存在的行将跳过。" />
|
||||||
|
<el-form label-width="110px" class="import-form">
|
||||||
|
<el-form-item label="导入模板">
|
||||||
|
<el-button icon="el-icon-download" :loading="importDialog.downloading" @click="handleDownloadTemplate">
|
||||||
|
下载导入模板
|
||||||
|
</el-button>
|
||||||
|
<span class="tip-text">红色表头列为必填,详见模板「填写说明」sheet</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数据文件">
|
||||||
|
<div class="file-input">
|
||||||
|
<input ref="importFileInput" type="file" accept=".xls,.xlsx" style="display: none"
|
||||||
|
@change="handleImportFileChange" />
|
||||||
|
<el-button icon="el-icon-folder-opened" @click="handleChooseFile">选择文件</el-button>
|
||||||
|
<span class="file-name">{{ importDialog.fileName || '未选择任何文件' }}</span>
|
||||||
|
<el-button v-if="importDialog.file" type="text" class="danger-text-btn" @click="clearImportFile">
|
||||||
|
清除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="导入结果" v-if="importDialog.result">
|
||||||
|
<div class="import-result">
|
||||||
|
共导入 <span class="num">{{ importDialog.result.insertCount }}</span> 条<template
|
||||||
|
v-if="importDialog.result.skipCount > 0">,跳过 <span
|
||||||
|
class="num warn">{{ importDialog.result.skipCount }}</span> 条(数据已存在)</template><template
|
||||||
|
v-if="importDialog.result.failCount > 0">,失败 <span
|
||||||
|
class="num warn">{{ importDialog.result.failCount }}</span> 条</template>。
|
||||||
|
<div v-if="importDialog.result.failList.length" class="skip-detail">
|
||||||
|
<div v-for="(item, idx) in importDialog.result.failList" :key="'f' + idx">{{ item }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="importDialog.result.skipList.length" class="skip-detail">
|
||||||
|
<div v-for="(item, idx) in importDialog.result.skipList" :key="'s' + idx">{{ item }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="importDialog.visible = false">关 闭</el-button>
|
||||||
|
<el-button type="primary" :loading="importDialog.importing" :disabled="!importDialog.file"
|
||||||
|
@click="handleImport">开始导入</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -297,7 +345,9 @@ import {
|
|||||||
batchDeleteSyllabus,
|
batchDeleteSyllabus,
|
||||||
updateSyllabus,
|
updateSyllabus,
|
||||||
listByZydhAndTy,
|
listByZydhAndTy,
|
||||||
exportSyllabus
|
exportSyllabus,
|
||||||
|
downloadSyllabusTemplate,
|
||||||
|
importSyllabus
|
||||||
} from '@/api/teachBusiness/syllabus'
|
} from '@/api/teachBusiness/syllabus'
|
||||||
import { listMajor } from '@/api/subjectMajor/major'
|
import { listMajor } from '@/api/subjectMajor/major'
|
||||||
import { listKb } from '@/api/teachOffice/kb'
|
import { listKb } from '@/api/teachOffice/kb'
|
||||||
@@ -334,6 +384,14 @@ export default {
|
|||||||
submitting: false,
|
submitting: false,
|
||||||
form: this.createEmptyForm()
|
form: this.createEmptyForm()
|
||||||
},
|
},
|
||||||
|
importDialog: {
|
||||||
|
visible: false,
|
||||||
|
downloading: false,
|
||||||
|
importing: false,
|
||||||
|
file: null,
|
||||||
|
fileName: '',
|
||||||
|
result: null
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
zydh: [{ required: true, message: '请选择专业', trigger: 'change' }],
|
zydh: [{ required: true, message: '请选择专业', trigger: 'change' }],
|
||||||
kbh: [{ required: true, message: '请选择课程', trigger: 'change' }],
|
kbh: [{ required: true, message: '请选择课程', trigger: 'change' }],
|
||||||
@@ -524,6 +582,76 @@ export default {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* ---------- 模板导入 ---------- */
|
||||||
|
openImportDialog() {
|
||||||
|
this.importDialog.file = null
|
||||||
|
this.importDialog.fileName = ''
|
||||||
|
this.importDialog.result = null
|
||||||
|
this.importDialog.visible = true
|
||||||
|
if (this.$refs.importFileInput) this.$refs.importFileInput.value = ''
|
||||||
|
},
|
||||||
|
handleDownloadTemplate() {
|
||||||
|
this.importDialog.downloading = true
|
||||||
|
downloadSyllabusTemplate().then(blob => {
|
||||||
|
saveAs(blob, '教学大纲导入模板.xlsx')
|
||||||
|
this.$message.success('模板下载成功')
|
||||||
|
}).catch(() => {}).finally(() => {
|
||||||
|
this.importDialog.downloading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleChooseFile() {
|
||||||
|
if (this.$refs.importFileInput) this.$refs.importFileInput.click()
|
||||||
|
},
|
||||||
|
handleImportFileChange(e) {
|
||||||
|
const input = e.target
|
||||||
|
this.importDialog.result = null
|
||||||
|
if (!input.files || input.files.length === 0) {
|
||||||
|
this.importDialog.file = null
|
||||||
|
this.importDialog.fileName = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const file = input.files[0]
|
||||||
|
const name = (file.name || '').toLowerCase()
|
||||||
|
if (!name.endsWith('.xls') && !name.endsWith('.xlsx')) {
|
||||||
|
this.$message.warning('仅支持 .xls / .xlsx 格式的 Excel 文件')
|
||||||
|
this.clearImportFile()
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.importDialog.file = file
|
||||||
|
this.importDialog.fileName = file.name
|
||||||
|
},
|
||||||
|
clearImportFile() {
|
||||||
|
this.importDialog.file = null
|
||||||
|
this.importDialog.fileName = ''
|
||||||
|
this.importDialog.result = null
|
||||||
|
if (this.$refs.importFileInput) this.$refs.importFileInput.value = ''
|
||||||
|
},
|
||||||
|
handleImport() {
|
||||||
|
if (!this.importDialog.file) {
|
||||||
|
this.$message.warning('请先选择文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.importDialog.importing = true
|
||||||
|
importSyllabus(this.importDialog.file).then(response => {
|
||||||
|
const data = (response && response.data) || {}
|
||||||
|
this.importDialog.file = null
|
||||||
|
this.importDialog.fileName = ''
|
||||||
|
if (this.$refs.importFileInput) this.$refs.importFileInput.value = ''
|
||||||
|
this.importDialog.result = {
|
||||||
|
insertCount: data.insertCount || 0,
|
||||||
|
skipCount: data.skipCount || 0,
|
||||||
|
failCount: data.failCount || 0,
|
||||||
|
skipList: data.skipList || [],
|
||||||
|
failList: data.failList || []
|
||||||
|
}
|
||||||
|
this.$message.success(data.message || '导入完成')
|
||||||
|
this.fetchList()
|
||||||
|
}).catch(() => {}).finally(() => {
|
||||||
|
this.importDialog.importing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/* ---------- 分页 ---------- */
|
/* ---------- 分页 ---------- */
|
||||||
handleSizeChange(size) {
|
handleSizeChange(size) {
|
||||||
this.pageSize = size
|
this.pageSize = size
|
||||||
@@ -629,4 +757,51 @@ export default {
|
|||||||
color: #f78989;
|
color: #f78989;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import-tip {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-result {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
|
.num {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num.warn {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-detail {
|
||||||
|
margin-top: 8px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user