diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TrainingProgramController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TrainingProgramController.java index f25f906e..bd5f6047 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TrainingProgramController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TrainingProgramController.java @@ -121,10 +121,18 @@ public class TrainingProgramController { return Result.success(pageResult); } + /** + * 培养方案目录导入模板(仅表头,含学年制、学期数等必填列)。 + */ + @GetMapping("/template") + public void downloadTemplate(HttpServletResponse response) throws Exception { + trainingProgramService.exportZYBTemplate(response); + } + /** * 培养方案(专业)数据文件导入(.xls/.xlsx)。 - *

表头顺序:专业代号, 专业名称, 专业代码, 专业版本, 培训类型, 培训层次, 学年制, - * 学期数, 专业方向, 专业备注, 学员类别, 简称, 培训类型2。专业代号不填自动生成。

+ *

按列名匹配:专业代号, 专业名称, 专业代码, 专业版本, 培训类型, 培训层次, 学年制, + * 学期数, 专业方向, 专业备注, 学员类别, 简称, 培训类型2。专业代号不填自动生成;学年制、学期数必填。

*/ @PostMapping("/import") public Result importData(@RequestParam("file") MultipartFile file) throws Exception { diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/ZYB.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/ZYB.java index f6af3eef..a868b27e 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/ZYB.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/ZYB.java @@ -1,5 +1,6 @@ package com.roomroot.jwgl.entity; +import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; @@ -33,12 +34,12 @@ public class ZYB { @TableField("专业方向") private String zyfx; - /** 学年制 */ - @TableField("学年制") + /** 学年制(达梦非空;空串会被当成 NULL,插入时必须有值) */ + @TableField(value = "学年制", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS) private String xnz; /** 学期数 */ - @TableField("学期数") + @TableField(value = "学期数", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS) private Integer xqs; /** 专业备注 */ @@ -109,26 +110,26 @@ public class ZYB { @TableField("简称") private String jc; - /** 节次类别 */ - @TableField("节次类别") - private String jclb; - - /** 系统模式 */ - @TableField("系统模式") - private String xtms; - /** JSONZD NVARCHAR(4000) */ @TableField("JSONZD") private String jsonzd; /** 主干专业 */ - @TableField("主干专业") + @TableField(value = "主干专业", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS) private Boolean zgzy; - /** 规范名称 */ - @TableField("规范名称") + /** 规范名称(达梦非空,缺省用专业名称) */ + @TableField(value = "规范名称", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS) private String gfmc; + /** 节次类别 */ + @TableField(value = "节次类别", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS) + private String jclb; + + /** 系统模式 */ + @TableField(value = "系统模式", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS) + private String xtms; + /** 教学大纲编号 */ @TableField("教学大纲编号") private String jxdgbh; diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/handler/ZybInsertFillHandler.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/handler/ZybInsertFillHandler.java new file mode 100644 index 00000000..dd8b0980 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/handler/ZybInsertFillHandler.java @@ -0,0 +1,111 @@ +package com.roomroot.jwgl.handler; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.roomroot.jwgl.entity.ZYB; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +/** + * 专业表插入时补齐达梦非空列。达梦将空串视为 NULL,缺省值必须是非空文本或数字。 + */ +@Component +public class ZybInsertFillHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + Object origin = metaObject.getOriginalObject(); + if (!(origin instanceof ZYB)) { + return; + } + ZYB entity = (ZYB) origin; + fillNotNullColumns(entity); + strictInsertFill(metaObject, "xnz", String.class, entity.getXnz()); + strictInsertFill(metaObject, "xqs", Integer.class, entity.getXqs()); + strictInsertFill(metaObject, "gfmc", String.class, entity.getGfmc()); + strictInsertFill(metaObject, "jclb", String.class, entity.getJclb()); + strictInsertFill(metaObject, "xtms", String.class, entity.getXtms()); + strictInsertFill(metaObject, "zgzy", Boolean.class, entity.getZgzy()); + } + + @Override + public void updateFill(MetaObject metaObject) { + // 仅插入补齐 + } + + /** @deprecated 使用 {@link #fillNotNullColumns(ZYB)} */ + public static void fillAcademicYears(ZYB entity) { + fillNotNullColumns(entity); + } + + public static void fillNotNullColumns(ZYB entity) { + if (entity == null) { + return; + } + String name = blankToNull(entity.getZymc()); + String xnz = blankToNull(entity.getXnz()); + if (xnz == null) { + if (entity.getXqs() != null && entity.getXqs() > 0) { + xnz = String.valueOf(Math.max(1, (entity.getXqs() + 1) / 2)); + } else { + xnz = "4"; + } + } + entity.setXnz(xnz); + if (entity.getXqs() == null) { + int years = 4; + try { + years = (int) Math.round(Double.parseDouble(xnz)); + } catch (NumberFormatException ignored) { + years = 4; + } + entity.setXqs(Math.max(1, years) * 2); + } + if (blankToNull(entity.getGfmc()) == null) { + entity.setGfmc(name != null ? name : "-"); + } + if (blankToNull(entity.getZybsh()) == null && blankToNull(entity.getZydh()) != null) { + entity.setZybsh(entity.getZydh()); + } + if (blankToNull(entity.getJc()) == null) { + entity.setJc(name != null ? name : "-"); + } + if (blankToNull(entity.getZyfx()) == null) { + entity.setZyfx("-"); + } + if (blankToNull(entity.getZybb()) == null) { + entity.setZybb("-"); + } + if (blankToNull(entity.getPxlx2()) == null) { + entity.setPxlx2("其他"); + } + if (blankToNull(entity.getXylb()) == null) { + entity.setXylb("其他"); + } + if (blankToNull(entity.getJclb()) == null) { + entity.setJclb("双节次"); + } + if (blankToNull(entity.getXtms()) == null) { + entity.setXtms("军校模式"); + } + if (blankToNull(entity.getZdyfl()) == null) { + entity.setZdyfl("-"); + } + if (blankToNull(entity.getZygf()) == null) { + entity.setZygf("-"); + } + if (entity.getZgzy() == null) { + entity.setZgzy(false); + } + if (entity.getTy() == null) { + entity.setTy(false); + } + } + + private static String blankToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TrainingProgramService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TrainingProgramService.java index e0d90749..133b2ea4 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TrainingProgramService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TrainingProgramService.java @@ -63,6 +63,9 @@ public interface TrainingProgramService { /** 导出培养方案(专业)到 Excel(.xlsx) */ void exportZYBExcel(jakarta.servlet.http.HttpServletResponse response) throws Exception; + /** 下载培养方案目录导入模板(仅表头) */ + void exportZYBTemplate(jakarta.servlet.http.HttpServletResponse response) throws Exception; + /** * 查询未停用的教学管理机构下拉选项。 */ diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/DisciplineServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/DisciplineServiceImpl.java index eddc165e..14254810 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/DisciplineServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/DisciplineServiceImpl.java @@ -104,6 +104,7 @@ public class DisciplineServiceImpl implements DisciplineService { } assertMajorCodeUnique(entity.getZydh()); fillMajorDefaults(entity); + com.roomroot.jwgl.handler.ZybInsertFillHandler.fillNotNullColumns(entity); zybMapper.insert(entity); } @@ -284,6 +285,7 @@ public class DisciplineServiceImpl implements DisciplineService { if (StringUtils.isEmpty(entity.getGfmc())) { entity.setGfmc(entity.getZymc()); } + com.roomroot.jwgl.handler.ZybInsertFillHandler.fillNotNullColumns(entity); } /** diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TrainingProgramServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TrainingProgramServiceImpl.java index 5bfd70b9..5dc6edac 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TrainingProgramServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TrainingProgramServiceImpl.java @@ -44,6 +44,10 @@ public class TrainingProgramServiceImpl implements TrainingProgramService { } entity.setTy(false); entity.setQysj(LocalDateTime.now()); + com.roomroot.jwgl.handler.ZybInsertFillHandler.fillNotNullColumns(entity); + if (findExistingMajor(entity) != null) { + throw new RuntimeException("专业名称已存在,请勿重复新增"); + } zybMapper.insert(entity); } @@ -90,24 +94,84 @@ public class TrainingProgramServiceImpl implements TrainingProgramService { if (list.isEmpty()) { throw new RuntimeException("Excel中无有效数据"); } + int inserted = 0; + int updated = 0; + int rowNo = 1; for (ZYB entity : list) { - if (entity.getZydh() == null || entity.getZydh().isEmpty()) { - entity.setZydh(com.roomroot.jwgl.utils.UuidUtil.getUUID()); + rowNo++; + if (entity.getZymc() == null || entity.getZymc().trim().isEmpty()) { + throw new RuntimeException("第" + rowNo + "行:专业名称不能为空"); } - if (entity.getTy() == null) { + entity.setZymc(entity.getZymc().trim()); + if (entity.getZydh() != null) { + entity.setZydh(entity.getZydh().trim()); + if (entity.getZydh().isEmpty()) { + entity.setZydh(null); + } + } + com.roomroot.jwgl.handler.ZybInsertFillHandler.fillNotNullColumns(entity); + + ZYB existing = findExistingMajor(entity); + if (existing != null) { + entity.setZydh(existing.getZydh()); + if (entity.getZybsh() == null || entity.getZybsh().isEmpty()) { + entity.setZybsh(existing.getZybsh() != null ? existing.getZybsh() : existing.getZydh()); + } entity.setTy(false); + zybMapper.updateById(entity); + updated++; + } else { + if (entity.getZydh() == null || entity.getZydh().isEmpty()) { + entity.setZydh(com.roomroot.jwgl.utils.UuidUtil.getUUID()); + } + if (entity.getZybsh() == null || entity.getZybsh().isEmpty()) { + entity.setZybsh(entity.getZydh()); + } + entity.setTy(false); + entity.setQysj(LocalDateTime.now()); + zybMapper.insert(entity); + inserted++; } - zybMapper.insert(entity); } - return list.size(); + return inserted + updated; + } + + /** + * 按主键或专业名称命中已有记录,避免触发 PK_专业名称表。 + */ + private ZYB findExistingMajor(ZYB entity) { + if (entity.getZydh() != null && !entity.getZydh().isEmpty()) { + ZYB byId = zybMapper.selectById(entity.getZydh()); + if (byId != null) { + return byId; + } + } + java.util.List sameName = zybMapper.selectList(new LambdaQueryWrapper() + .eq(ZYB::getZymc, entity.getZymc())); + if (sameName == null || sameName.isEmpty()) { + return null; + } + for (ZYB row : sameName) { + boolean fxSame = java.util.Objects.equals( + nullToEmpty(row.getZyfx()), nullToEmpty(entity.getZyfx())); + boolean lxSame = java.util.Objects.equals( + nullToEmpty(row.getPxlx()), nullToEmpty(entity.getPxlx())); + if (fxSame && lxSame) { + return row; + } + } + return sameName.get(0); + } + + private static String nullToEmpty(String value) { + return value == null ? "" : value.trim(); } @Override public void exportZYBExcel(jakarta.servlet.http.HttpServletResponse response) throws Exception { java.util.List list = zybMapper.selectList( new LambdaQueryWrapper().orderByAsc(ZYB::getZydh)); - String[] headers = {"专业代号", "专业名称", "专业代码", "专业版本", "培训类型", "培训层次", - "学年制", "学期数", "专业方向", "专业备注", "学员类别", "简称", "培训类型2"}; + String[] headers = com.roomroot.jwgl.utils.ExcelParseUtil.TRAINING_PROGRAM_IMPORT_HEADERS; byte[] bytes = com.roomroot.jwgl.utils.ExcelExportUtil.export("培养方案", headers, list, ZYB::getZydh, ZYB::getZymc, ZYB::getZydm, ZYB::getZybb, ZYB::getPxlx, ZYB::getPxcc, ZYB::getXnz, ZYB::getXqs, ZYB::getZyfx, ZYB::getZybz, @@ -120,6 +184,19 @@ public class TrainingProgramServiceImpl implements TrainingProgramService { response.getOutputStream().flush(); } + @Override + public void exportZYBTemplate(jakarta.servlet.http.HttpServletResponse response) throws Exception { + String[] headers = com.roomroot.jwgl.utils.ExcelParseUtil.TRAINING_PROGRAM_IMPORT_HEADERS; + byte[] bytes = com.roomroot.jwgl.utils.ExcelExportUtil.export("培养方案", headers, + java.util.Collections.emptyList()); + 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)); + response.getOutputStream().write(bytes); + response.getOutputStream().flush(); + } + @Override public java.util.List listTeachingOrganizations() { java.util.List list = jxgljgMapper.selectEnabledOptions(); diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/ExcelParseUtil.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/ExcelParseUtil.java index 0ec8bdbf..162e3cd2 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/ExcelParseUtil.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/ExcelParseUtil.java @@ -450,11 +450,17 @@ public class ExcelParseUtil { return true; } + /** + * 人才培养方案(专业表)导入模板列头。下载模板与解析共用,按列名匹配,顺序可调整。 + */ + public static final String[] TRAINING_PROGRAM_IMPORT_HEADERS = { + "专业代号", "专业名称", "专业代码", "专业版本", "培训类型", "培训层次", + "学年制", "学期数", "专业方向", "专业备注", "学员类别", "简称", "培训类型2" + }; + /** * 解析培养方案(专业表)导入 Excel 文件(支持 .xls / .xlsx)。 - *

表头顺序:

- * 专业代号, 专业名称, 专业代码, 专业版本, 培训类型, 培训层次, 学年制, 学期数, - * 专业方向, 专业备注, 学员类别, 简称, 培训类型2 + *

按 {@link #TRAINING_PROGRAM_IMPORT_HEADERS} 列名匹配;未识别表头则拒绝导入。

* * @return 专业实体列表 * @throws Exception 解析异常 @@ -464,29 +470,53 @@ public class ExcelParseUtil { Workbook workbook = createWorkbook(inputStream, fileName); try { Sheet sheet = workbook.getSheetAt(0); - int lastRowNum = sheet.getLastRowNum(); - for (int rowNum = 1; rowNum <= lastRowNum; rowNum++) { + int firstRowNum = sheet.getFirstRowNum(); + Row headerRow = sheet.getRow(firstRowNum); + if (headerRow == null) { + throw new IllegalArgumentException("导入文件缺少表头行,请先下载「人才培养方案目录模板」填写"); + } + + Map headerIndex = new LinkedHashMap<>(); + for (int col = headerRow.getFirstCellNum(); col < headerRow.getLastCellNum(); col++) { + String title = normalizeHeader(getCellStringValue(headerRow.getCell(col))); + if (!title.isEmpty() && !headerIndex.containsKey(title)) { + headerIndex.put(title, col); + } + } + int matched = 0; + for (String header : TRAINING_PROGRAM_IMPORT_HEADERS) { + if (headerIndex.containsKey(header)) { + matched++; + } + } + if (matched == 0) { + throw new IllegalArgumentException( + "未识别到人才培养方案目录模板表头,请下载「人才培养方案目录模板」并按表头填写(须含学年制、学期数等列)"); + } + if (!headerIndex.containsKey("学年制") && headerIndex.containsKey("学制")) { + headerIndex.put("学年制", headerIndex.get("学制")); + } + + for (int rowNum = firstRowNum + 1; rowNum <= sheet.getLastRowNum(); rowNum++) { Row row = sheet.getRow(rowNum); if (row == null) { continue; } ZYB entity = new ZYB(); - entity.setZydh(getCellStringValue(row.getCell(0))); - entity.setZymc(getCellStringValue(row.getCell(1))); - entity.setZydm(getCellStringValue(row.getCell(2))); - entity.setZybb(getCellStringValue(row.getCell(3))); - entity.setPxlx(getCellStringValue(row.getCell(4))); - entity.setPxcc(getCellStringValue(row.getCell(5))); - entity.setXnz(getCellStringValue(row.getCell(6))); - entity.setXqs(parseIntCell(row.getCell(7))); - entity.setZyfx(getCellStringValue(row.getCell(8))); - entity.setZybz(getCellStringValue(row.getCell(9))); - entity.setXylb(getCellStringValue(row.getCell(10))); - entity.setJc(getCellStringValue(row.getCell(11))); - entity.setPxlx2(getCellStringValue(row.getCell(12))); - // 至少要有专业代号或名称才视为有效行 - if (entity.getZydh() != null && !entity.getZydh().isEmpty() - || entity.getZymc() != null && !entity.getZymc().isEmpty()) { + entity.setZydh(cellText(row, headerIndex, "专业代号")); + entity.setZymc(cellText(row, headerIndex, "专业名称")); + entity.setZydm(cellText(row, headerIndex, "专业代码")); + entity.setZybb(cellText(row, headerIndex, "专业版本")); + entity.setPxlx(cellText(row, headerIndex, "培训类型")); + entity.setPxcc(cellText(row, headerIndex, "培训层次")); + entity.setXnz(cellText(row, headerIndex, "学年制")); + entity.setXqs(cellInt(row, headerIndex, "学期数")); + entity.setZyfx(cellText(row, headerIndex, "专业方向")); + entity.setZybz(cellText(row, headerIndex, "专业备注")); + entity.setXylb(cellText(row, headerIndex, "学员类别")); + entity.setJc(cellText(row, headerIndex, "简称")); + entity.setPxlx2(cellText(row, headerIndex, "培训类型2")); + if (isNotBlank(entity.getZydh()) || isNotBlank(entity.getZymc())) { result.add(entity); } } diff --git a/frontend/src/api/teachBusiness/training.js b/frontend/src/api/teachBusiness/training.js index 3bf5be02..5e46238d 100644 --- a/frontend/src/api/teachBusiness/training.js +++ b/frontend/src/api/teachBusiness/training.js @@ -83,12 +83,12 @@ export function exportTrainingProgram() { } /** - * 下载培养方案导入模板(返回 Blob) - * GET /download/training-program + * 下载培养方案目录导入模板(返回 Blob) + * GET /training/template */ export function downloadTrainingProgramTemplate() { return request({ - url: '/download/training-program', + url: '/training/template', method: 'get', responseType: 'blob' }) diff --git a/frontend/src/views/teachBusiness/trainingPlan/index.vue b/frontend/src/views/teachBusiness/trainingPlan/index.vue index 22b7b695..0bb43c6f 100644 --- a/frontend/src/views/teachBusiness/trainingPlan/index.vue +++ b/frontend/src/views/teachBusiness/trainingPlan/index.vue @@ -631,7 +631,7 @@ export default { }, handleTemplateDownload() { downloadTrainingProgramTemplate().then(blob => { - saveAs(blob, '人才培养方案课程数据文件模板.xls') + saveAs(blob, '人才培养方案目录模板.xlsx') this.$message.success('模板下载成功') }).catch(() => {}) },