diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/DisciplineManagementController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/DisciplineManagementController.java index 225d9ba1..9c298d54 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/DisciplineManagementController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/DisciplineManagementController.java @@ -6,14 +6,19 @@ import com.roomroot.jwgl.service.DisciplineService; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.unit.Result; +import com.roomroot.jwgl.vo.discipline.MajorImportResultVO; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; /** * 学科专业管理控制器 @@ -212,4 +217,36 @@ public class DisciplineManagementController { cond.setTy(ty); return Result.success(disciplineService.pageMajor(new PageQuery(pageNum, pageSize), cond)); } + + /** + * 从 Excel 导入专业(.xls/.xlsx)。 + *
+ * 表头按专业导入模板的列名匹配(列顺序不限)。已存在的专业跳过、不覆盖库中已有数据; + * 校验不通过的行跳过并在结果明细中给出原因,合格行正常导入。 + *
+ * + * @param file 上传的 Excel 文件(form-data 字段名 file) + * @return 导入结果(新增/跳过/失败数量与明细) + */ + @PostMapping("/major/import") + public Result+ * 保留 Excel 中的原始行号,便于把校验失败原因定位到具体行。 + *
+ */ +@Data +public class MajorImportRowDTO { + + /** + * Excel 行号(1 起始,含表头行)。 + */ + private int rowNum; + + /** + * 该行解析出的专业实体。 + */ + private ZYB entity; + + public MajorImportRowDTO() { + } + + public MajorImportRowDTO(int rowNum, ZYB entity) { + this.rowNum = rowNum; + this.entity = entity; + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/DisciplineService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/DisciplineService.java index d9a96cea..df1dc5f6 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/DisciplineService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/DisciplineService.java @@ -4,7 +4,9 @@ import com.roomroot.jwgl.entity.XKZYXX; import com.roomroot.jwgl.entity.ZYB; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.vo.discipline.MajorImportResultVO; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; /** * 学科专业管理服务接口 @@ -89,4 +91,26 @@ public interface DisciplineService { * @return 分页结果 */ PageResult+ * 表头按专业导入模板的列名匹配。已存在的专业(专业代号已存在,或未填代号但 + * 专业名称+专业方向+培训类型已存在)直接跳过,不覆盖库中已有数据; + * 校验不通过的行跳过并记录原因,其余合格行正常导入。 + *
+ * + * @param file 上传的 Excel 文件 + * @return 导入结果(新增/跳过/失败与明细) + * @throws Exception 解析异常 + */ + MajorImportResultVO importMajorExcel(MultipartFile file) throws Exception; + + /** + * 生成专业导入模板(xlsx 字节流),列头与导入解析保持一致。 + * + * @return xlsx 文件字节数组 + * @throws Exception 生成异常 + */ + byte[] buildMajorImportTemplate() 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 b3798ca1..eddc165e 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 @@ -11,14 +11,25 @@ import com.roomroot.jwgl.mapper.ZYBMapper; import com.roomroot.jwgl.service.DisciplineService; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.utils.ExcelExportUtil; +import com.roomroot.jwgl.utils.ExcelParseUtil; import com.roomroot.jwgl.utils.UuidUtil; +import com.roomroot.jwgl.dto.discipline.MajorImportRowDTO; +import com.roomroot.jwgl.vo.discipline.MajorImportResultVO; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; +import java.io.InputStream; import java.text.SimpleDateFormat; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; @@ -167,6 +178,137 @@ public class DisciplineServiceImpl implements DisciplineService { return new PageResult<>(result.getRecords(), result.getTotal(), pageNum, pageSize); } + @Override + @Transactional(rollbackFor = Exception.class) + public MajorImportResultVO importMajorExcel(MultipartFile file) throws Exception { + if (file == null || file.isEmpty()) { + throw new ServiceException("请选择要导入的文件", BAD_REQUEST); + } + + List+ * 只补"库中没有非空缺省值"或需要按业务推导的字段(专业标识号、规范名称), + * 其余空值保持 null,交由数据库缺省值生效(如 节次类别='双节次'、系统模式='军校模式'、 + * 培训类型2/学员类别='其他'),避免写成空串。 + *
+ */ + private void fillMajorDefaultsForImport(ZYB entity) { + entity.setTy(false); + entity.setQysj(LocalDateTime.now()); + if (entity.getZgzy() == null) { + entity.setZgzy(false); + } + if (StringUtils.isEmpty(entity.getZybsh())) { + entity.setZybsh(entity.getZydh()); + } + if (StringUtils.isEmpty(entity.getGfmc())) { + entity.setGfmc(entity.getZymc()); + } + } + + /** + * 业务键:专业名称 + 专业方向 + 培训类型(多版本通过培训类型区分)。 + */ + private String buildMajorKey(String zymc, String zyfx, String pxlx) { + return normalize(zymc) + "|" + normalize(zyfx) + "|" + normalize(pxlx); + } + + private String normalize(String value) { + return value == null ? "" : value.trim(); + } + + private String describeMajor(ZYB entity) { + StringBuilder text = new StringBuilder("专业「"); + text.append(normalize(entity.getZymc())).append("」"); + if (StringUtils.isNotEmpty(entity.getZyfx())) { + text.append("(方向:").append(entity.getZyfx().trim()).append(")"); + } + if (StringUtils.isNotEmpty(entity.getPxlx())) { + text.append("(培训类型:").append(entity.getPxlx().trim()).append(")"); + } + return text.toString(); + } + private void fillMajorDefaults(ZYB entity) { if (entity.getTy() == null) { entity.setTy(false); 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 99cbc355..8eda1646 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 @@ -4,6 +4,7 @@ import com.roomroot.jwgl.dto.courserunning.ClassesImportDTO; import com.roomroot.jwgl.dto.courserunning.SemesterImportDTO; import com.roomroot.jwgl.dto.courserunning.SingleCourseImportDTO; import com.roomroot.jwgl.dto.departmentpersonnel.DepartmentPersonnelCreateDTO; +import com.roomroot.jwgl.dto.discipline.MajorImportRowDTO; import com.roomroot.jwgl.entity.JXSSJH; import com.roomroot.jwgl.entity.JYB; import com.roomroot.jwgl.entity.JYSB; @@ -25,7 +26,9 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class ExcelParseUtil { @@ -479,6 +482,174 @@ public class ExcelParseUtil { return result; } + /** + * 专业管理导入模板的列头定义。 + *下载模板与解析共用同一份列头,避免模板与解析器不一致。解析按列名匹配,列顺序可调整。
+ */ + public static final String[] MAJOR_IMPORT_HEADERS = { + "专业代号", "专业名称", "专业方向", "专业代码", "专业版本", "学年制", "学期数", + "培训层次", "培训类型", "培训类型2", "学员类别", "主干专业", "自定义分类", + "节次类别", "系统模式", "学科专业信息标识号", "教学管理机构编号", "专业标识号", + "简称", "规范名称", "培养目标", "专业备注", "专业规范" + }; + + /** + * 解析专业管理导入 Excel 文件(支持 .xls / .xlsx)。 + *+ * 表头按 {@link #MAJOR_IMPORT_HEADERS} 的列名匹配(顺序不限,可少列、可多列); + * 未识别的列忽略;空白单元格统一归一为 null,由业务层兜底或使用库缺省值。 + * 专业代号与专业名称均为空的行视为空行跳过。 + *
+ * + * @param inputStream Excel 文件输入流 + * @param fileName 文件名(用于判断 .xls/.xlsx) + * @return 导入行列表(含 Excel 原始行号) + * @throws Exception 解析异常 + */ + public static List表头顺序:
diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/discipline/MajorImportResultVO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/discipline/MajorImportResultVO.java new file mode 100644 index 00000000..3f57df46 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/discipline/MajorImportResultVO.java @@ -0,0 +1,75 @@ +package com.roomroot.jwgl.vo.discipline; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 专业管理导入结果。 + *+ * 用于返回导入操作的执行结果:新增数、跳过数(专业已存在)、失败数(校验不通过)与明细列表。 + *
+ */ +@Data +public class MajorImportResultVO { + + /** + * 解析出的有效数据行数(不含空行)。 + */ + private Integer totalCount = 0; + + /** + * 实际新增的专业数。 + */ + private Integer insertCount = 0; + + /** + * 因"已存在"而跳过的专业数(不覆盖库中已有数据)。 + */ + private Integer skipCount = 0; + + /** + * 因校验不通过而失败的专业数。 + */ + private Integer failCount = 0; + + /** + * 跳过明细(专业代号或名称+方向+培训类型)。 + */ + private List1. 请先下载专业导入模板,按表头逐列填写;专业名称、专业代码、学年制、学期数、培训层次、培训类型为必填。
+2. 专业代号已存在(或未填代号但「专业名称 + 专业方向 + 培训类型」已存在)的记录会被跳过,不会覆盖库中已有数据。
+3. 单行校验不通过只跳过该行并给出原因,其余合格行正常导入;未填写的可空字段使用系统缺省值。
+