From 6e767947f74b1402b88b022f7987f5785df83733 Mon Sep 17 00:00:00 2001 From: liweijie Date: Tue, 15 Sep 2026 17:43:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=93=E4=B8=9A=E6=B7=BB=E5=8A=A0=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=A8=A1=E6=9D=BF=E4=B8=8B=E8=BD=BD=E5=92=8C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwgl/DisciplineManagementController.java | 37 ++++ .../dto/discipline/MajorImportRowDTO.java | 32 +++ .../jwgl/service/DisciplineService.java | 24 +++ .../service/impl/DisciplineServiceImpl.java | 142 ++++++++++++ .../roomroot/jwgl/utils/ExcelParseUtil.java | 171 +++++++++++++++ .../vo/discipline/MajorImportResultVO.java | 75 +++++++ frontend/src/api/subjectMajor/major.js | 17 +- .../src/views/subjectMajor/major/index.vue | 203 +++++++++++++++++- 8 files changed, 691 insertions(+), 10 deletions(-) create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/discipline/MajorImportRowDTO.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/discipline/MajorImportResultVO.java 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 225d9ba16..9c298d544 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 importMajor(@RequestParam("file") MultipartFile file) throws Exception { + MajorImportResultVO result = disciplineService.importMajorExcel(file); + return Result.success(result.getMessage(), result); + } + + /** + * 下载专业导入模板(xlsx),列头与导入解析保持一致。 + * + * @param response 响应对象 + */ + @GetMapping("/major/template") + public void downloadMajorTemplate(HttpServletResponse response) throws Exception { + byte[] bytes = disciplineService.buildMajorImportTemplate(); + String fileName = "专业导入模板.xlsx"; + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment; filename=" + + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); + response.getOutputStream().write(bytes); + response.getOutputStream().flush(); + } } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/discipline/MajorImportRowDTO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/discipline/MajorImportRowDTO.java new file mode 100644 index 000000000..cf894ca7a --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/discipline/MajorImportRowDTO.java @@ -0,0 +1,32 @@ +package com.roomroot.jwgl.dto.discipline; + +import com.roomroot.jwgl.entity.ZYB; +import lombok.Data; + +/** + * 专业(专业表)导入行解析结果。 + *

+ * 保留 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 d9a96cea9..df1dc5f6a 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 pageMajor(PageQuery query, ZYB cond); + + /** + * 从 Excel 导入专业(.xls/.xlsx)。 + *

+ * 表头按专业导入模板的列名匹配。已存在的专业(专业代号已存在,或未填代号但 + * 专业名称+专业方向+培训类型已存在)直接跳过,不覆盖库中已有数据; + * 校验不通过的行跳过并记录原因,其余合格行正常导入。 + *

+ * + * @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 b3798ca10..eddc165e9 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 rows; + try (InputStream inputStream = file.getInputStream()) { + rows = ExcelParseUtil.parseMajorExcel(inputStream, file.getOriginalFilename()); + } catch (IllegalArgumentException ex) { + // 文件格式不支持、表头未识别等:转为业务异常,前端可展示明确提示 + throw new ServiceException(ex.getMessage(), BAD_REQUEST); + } + if (rows.isEmpty()) { + throw new ServiceException("Excel 中未解析到有效数据,请使用「专业导入模板」填写", BAD_REQUEST); + } + + MajorImportResultVO result = new MajorImportResultVO(); + result.setTotalCount(rows.size()); + + // 库中已有专业:代号集合 + (专业名称|专业方向|培训类型)业务键集合,用于跳过判定 + List existingMajors = zybMapper.selectList(new LambdaQueryWrapper() + .select(ZYB::getZydh, ZYB::getZymc, ZYB::getZyfx, ZYB::getPxlx)); + Set existingZydhSet = new HashSet<>(); + Set existingKeySet = new HashSet<>(); + for (ZYB existing : existingMajors) { + if (StringUtils.isNotEmpty(existing.getZydh())) { + existingZydhSet.add(existing.getZydh().trim()); + } + existingKeySet.add(buildMajorKey(existing.getZymc(), existing.getZyfx(), existing.getPxlx())); + } + + List toInsert = new ArrayList<>(); + for (MajorImportRowDTO row : rows) { + ZYB entity = row.getEntity(); + // 1. 必填与长度校验(与新增专业的校验口径一致) + try { + validateMajor(entity, true); + } catch (ServiceException ex) { + result.addFail("第" + row.getRowNum() + "行:" + ex.getMessage()); + continue; + } + + // 2. 已存在则跳过,不覆盖库中数据 + String zydh = entity.getZydh() == null ? null : entity.getZydh().trim(); + if (StringUtils.isNotEmpty(zydh) && existingZydhSet.contains(zydh)) { + result.addSkip("第" + row.getRowNum() + "行:专业代号 " + zydh + " 已存在,已跳过"); + continue; + } + String key = buildMajorKey(entity.getZymc(), entity.getZyfx(), entity.getPxlx()); + if (existingKeySet.contains(key)) { + result.addSkip("第" + row.getRowNum() + "行:" + describeMajor(entity) + " 已存在,已跳过"); + continue; + } + + // 3. 补齐主键与必要字段后入队 + if (StringUtils.isEmpty(zydh)) { + entity.setZydh(UuidUtil.getOriginalUUID()); + } else { + entity.setZydh(zydh); + } + fillMajorDefaultsForImport(entity); + + existingZydhSet.add(entity.getZydh()); + existingKeySet.add(key); + toInsert.add(entity); + } + + for (ZYB entity : toInsert) { + zybMapper.insert(entity); + result.setInsertCount(result.getInsertCount() + 1); + } + + result.setMessage(String.format("共解析 %d 行,新增 %d 条,跳过 %d 条,失败 %d 条", + result.getTotalCount(), result.getInsertCount(), + result.getSkipCount(), result.getFailCount())); + return result; + } + + @Override + public byte[] buildMajorImportTemplate() throws Exception { + return ExcelExportUtil.export("专业导入", ExcelParseUtil.MAJOR_IMPORT_HEADERS, + Collections.emptyList()); + } + + /** + * 导入专用兜底。 + *

+ * 只补"库中没有非空缺省值"或需要按业务推导的字段(专业标识号、规范名称), + * 其余空值保持 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 99cbc355c..8eda16463 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 parseMajorExcel(InputStream inputStream, String fileName) throws Exception { + List result = new ArrayList<>(); + Workbook workbook = createWorkbook(inputStream, fileName); + try { + Sheet sheet = workbook.getSheetAt(0); + 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 : MAJOR_IMPORT_HEADERS) { + if (headerIndex.containsKey(header)) { + matched++; + } + } + if (matched == 0) { + throw new IllegalArgumentException("未识别到专业导入模板表头,请先下载「专业导入模板」并按表头填写"); + } + + for (int rowNum = firstRowNum + 1; rowNum <= sheet.getLastRowNum(); rowNum++) { + Row row = sheet.getRow(rowNum); + if (row == null) { + continue; + } + + ZYB entity = new ZYB(); + entity.setZydh(cellText(row, headerIndex, "专业代号")); + entity.setZymc(cellText(row, headerIndex, "专业名称")); + entity.setZyfx(cellText(row, headerIndex, "专业方向")); + entity.setZydm(cellText(row, headerIndex, "专业代码")); + entity.setZybb(cellText(row, headerIndex, "专业版本")); + entity.setXnz(cellText(row, headerIndex, "学年制")); + entity.setXqs(cellInt(row, headerIndex, "学期数")); + entity.setPxcc(cellText(row, headerIndex, "培训层次")); + entity.setPxlx(cellText(row, headerIndex, "培训类型")); + entity.setPxlx2(cellText(row, headerIndex, "培训类型2")); + entity.setXylb(cellText(row, headerIndex, "学员类别")); + entity.setZgzy(cellBool(row, headerIndex, "主干专业")); + entity.setZdyfl(cellText(row, headerIndex, "自定义分类")); + entity.setJclb(cellText(row, headerIndex, "节次类别")); + entity.setXtms(cellText(row, headerIndex, "系统模式")); + entity.setXkzyxxbsh(cellText(row, headerIndex, "学科专业信息标识号")); + entity.setJxgljgbh(cellText(row, headerIndex, "教学管理机构编号")); + entity.setZybsh(cellText(row, headerIndex, "专业标识号")); + entity.setJc(cellText(row, headerIndex, "简称")); + entity.setGfmc(cellText(row, headerIndex, "规范名称")); + entity.setPymb(cellText(row, headerIndex, "培养目标")); + entity.setZybz(cellText(row, headerIndex, "专业备注")); + entity.setZygf(cellText(row, headerIndex, "专业规范")); + + // 专业代号与专业名称均为空视为空行 + if (isNotBlank(entity.getZydh()) || isNotBlank(entity.getZymc())) { + result.add(new MajorImportRowDTO(rowNum + 1, entity)); + } + } + } finally { + workbook.close(); + } + return result; + } + + /** + * 表头名归一:去星号标记、括号统一为半角、去空白字符,便于容错匹配。 + */ + private static String normalizeHeader(String title) { + if (title == null) { + return ""; + } + return title.replace("*", "") + .replace("(", "(") + .replace(")", ")") + .replace(" ", "") + .replace("\u00A0", "") + .trim(); + } + + /** + * 按列名取单元格文本,空白统一归一为 null。 + */ + private static String cellText(Row row, Map headerIndex, String header) { + Integer col = headerIndex.get(header); + if (col == null) { + return null; + } + String value = getCellStringValue(row.getCell(col)); + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + /** + * 按列名取整数值,容错 8 / 8.0 / "8" 等写法。 + */ + private static Integer cellInt(Row row, Map headerIndex, String header) { + String value = cellText(row, headerIndex, header); + if (value == null) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + try { + return (int) Double.parseDouble(value); + } catch (NumberFormatException ignored) { + return null; + } + } + } + + /** + * 按列名取布尔值,识别 是/否、true/false、1/0、Y/N、√。 + */ + private static Boolean cellBool(Row row, Map headerIndex, String header) { + String value = cellText(row, headerIndex, header); + if (value == null) { + return null; + } + if ("是".equals(value) || "true".equalsIgnoreCase(value) || "1".equals(value) + || "Y".equalsIgnoreCase(value) || "√".equals(value)) { + return true; + } + if ("否".equals(value) || "false".equalsIgnoreCase(value) || "0".equals(value) + || "N".equalsIgnoreCase(value)) { + return false; + } + return null; + } + + private static boolean isNotBlank(String value) { + return value != null && !value.trim().isEmpty(); + } + /** * 解析专业教学计划表导入 Excel 文件(支持 .xls / .xlsx)。 *

表头顺序:

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 000000000..3f57df466 --- /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 List skipList = new ArrayList<>(); + + /** + * 失败明细(第 N 行:原因)。 + */ + private List failList = new ArrayList<>(); + + /** + * 结果提示信息。 + */ + private String message; + + /** + * 记录一条跳过明细。 + * + * @param detail 明细文本 + */ + public void addSkip(String detail) { + this.skipCount++; + if (this.skipList.size() < 50) { + this.skipList.add(detail); + } + } + + /** + * 记录一条失败明细。 + * + * @param detail 明细文本 + */ + public void addFail(String detail) { + this.failCount++; + if (this.failList.size() < 50) { + this.failList.add(detail); + } + } +} diff --git a/frontend/src/api/subjectMajor/major.js b/frontend/src/api/subjectMajor/major.js index 85ad40f08..3b76ecb81 100644 --- a/frontend/src/api/subjectMajor/major.js +++ b/frontend/src/api/subjectMajor/major.js @@ -45,12 +45,25 @@ export function delMajor(zydh) { }) } -// 下载人才培养方案课程数据文件模板 +// 下载专业导入模板(xlsx,列头与后端导入解析一致) export function downloadMajorTemplate() { return request({ - url: '/download/training-program', + url: '/discipline/major/template', method: 'get', responseType: 'blob', timeout: 30000 }) +} + +// 导入专业(Excel,form-data 字段名 file) +export function importMajor(file) { + const formData = new FormData() + formData.append('file', file) + return request({ + url: '/discipline/major/import', + method: 'post', + data: formData, + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 120000 + }) } \ No newline at end of file diff --git a/frontend/src/views/subjectMajor/major/index.vue b/frontend/src/views/subjectMajor/major/index.vue index 42409870d..9752528b6 100644 --- a/frontend/src/views/subjectMajor/major/index.vue +++ b/frontend/src/views/subjectMajor/major/index.vue @@ -68,6 +68,7 @@ 专业列表:
下载模板 + 导入专业 新增专业
@@ -384,17 +385,69 @@ 关 闭 + + + + + +
+

1. 请先下载专业导入模板,按表头逐列填写;专业名称、专业代码、学年制、学期数、培训层次、培训类型为必填。

+

2. 专业代号已存在(或未填代号但「专业名称 + 专业方向 + 培训类型」已存在)的记录会被跳过,不会覆盖库中已有数据。

+

3. 单行校验不通过只跳过该行并给出原因,其余合格行正常导入;未填写的可空字段使用系统缺省值。

+
+
+ +
+ 下载导入模板 + 选择文件 + {{ importFileName }} + +
+ +
+
+ 共解析 {{ importResult.totalCount || 0 }} 行:新增 + {{ importResult.insertCount || 0 }} 条,跳过 + {{ importResult.skipCount || 0 }} 条,失败 + {{ importResult.failCount || 0 }} 条 +
+
+
跳过明细:
+
    +
  • {{ item }}
  • +
+
+
+
失败明细:
+
    +
  • {{ item }}
  • +
+
+
+ + +