人才培养方案

This commit is contained in:
2026-09-20 11:59:04 +08:00
parent e4f4720113
commit 9f0c0ac873
9 changed files with 280 additions and 48 deletions
@@ -121,10 +121,18 @@ public class TrainingProgramController {
return Result.success(pageResult); return Result.success(pageResult);
} }
/**
* 培养方案目录导入模板(仅表头,含学年制、学期数等必填列)。
*/
@GetMapping("/template")
public void downloadTemplate(HttpServletResponse response) throws Exception {
trainingProgramService.exportZYBTemplate(response);
}
/** /**
* 培养方案(专业)数据文件导入(.xls/.xlsx)。 * 培养方案(专业)数据文件导入(.xls/.xlsx)。
* <p>表头顺序:专业代号, 专业名称, 专业代码, 专业版本, 培训类型, 培训层次, 学年制, * <p>按列名匹配:专业代号, 专业名称, 专业代码, 专业版本, 培训类型, 培训层次, 学年制,
* 学期数, 专业方向, 专业备注, 学员类别, 简称, 培训类型2。专业代号不填自动生成。</p> * 学期数, 专业方向, 专业备注, 学员类别, 简称, 培训类型2。专业代号不填自动生成;学年制、学期数必填。</p>
*/ */
@PostMapping("/import") @PostMapping("/import")
public Result<Integer> importData(@RequestParam("file") MultipartFile file) throws Exception { public Result<Integer> importData(@RequestParam("file") MultipartFile file) throws Exception {
@@ -1,5 +1,6 @@
package com.roomroot.jwgl.entity; package com.roomroot.jwgl.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
@@ -33,12 +34,12 @@ public class ZYB {
@TableField("专业方向") @TableField("专业方向")
private String zyfx; private String zyfx;
/** 学年制 */ /** 学年制(达梦非空;空串会被当成 NULL,插入时必须有值) */
@TableField("学年制") @TableField(value = "学年制", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS)
private String xnz; private String xnz;
/** 学期数 */ /** 学期数 */
@TableField("学期数") @TableField(value = "学期数", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS)
private Integer xqs; private Integer xqs;
/** 专业备注 */ /** 专业备注 */
@@ -109,26 +110,26 @@ public class ZYB {
@TableField("简称") @TableField("简称")
private String jc; private String jc;
/** 节次类别 */
@TableField("节次类别")
private String jclb;
/** 系统模式 */
@TableField("系统模式")
private String xtms;
/** JSONZD NVARCHAR(4000) */ /** JSONZD NVARCHAR(4000) */
@TableField("JSONZD") @TableField("JSONZD")
private String jsonzd; private String jsonzd;
/** 主干专业 */ /** 主干专业 */
@TableField("主干专业") @TableField(value = "主干专业", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS)
private Boolean zgzy; private Boolean zgzy;
/** 规范名称 */ /** 规范名称(达梦非空,缺省用专业名称) */
@TableField("规范名称") @TableField(value = "规范名称", fill = FieldFill.INSERT, insertStrategy = FieldStrategy.ALWAYS)
private String gfmc; 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("教学大纲编号") @TableField("教学大纲编号")
private String jxdgbh; private String jxdgbh;
@@ -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;
}
}
@@ -63,6 +63,9 @@ public interface TrainingProgramService {
/** 导出培养方案(专业)到 Excel(.xlsx) */ /** 导出培养方案(专业)到 Excel(.xlsx) */
void exportZYBExcel(jakarta.servlet.http.HttpServletResponse response) throws Exception; void exportZYBExcel(jakarta.servlet.http.HttpServletResponse response) throws Exception;
/** 下载培养方案目录导入模板(仅表头) */
void exportZYBTemplate(jakarta.servlet.http.HttpServletResponse response) throws Exception;
/** /**
* 查询未停用的教学管理机构下拉选项。 * 查询未停用的教学管理机构下拉选项。
*/ */
@@ -104,6 +104,7 @@ public class DisciplineServiceImpl implements DisciplineService {
} }
assertMajorCodeUnique(entity.getZydh()); assertMajorCodeUnique(entity.getZydh());
fillMajorDefaults(entity); fillMajorDefaults(entity);
com.roomroot.jwgl.handler.ZybInsertFillHandler.fillNotNullColumns(entity);
zybMapper.insert(entity); zybMapper.insert(entity);
} }
@@ -284,6 +285,7 @@ public class DisciplineServiceImpl implements DisciplineService {
if (StringUtils.isEmpty(entity.getGfmc())) { if (StringUtils.isEmpty(entity.getGfmc())) {
entity.setGfmc(entity.getZymc()); entity.setGfmc(entity.getZymc());
} }
com.roomroot.jwgl.handler.ZybInsertFillHandler.fillNotNullColumns(entity);
} }
/** /**
@@ -44,6 +44,10 @@ public class TrainingProgramServiceImpl implements TrainingProgramService {
} }
entity.setTy(false); entity.setTy(false);
entity.setQysj(LocalDateTime.now()); entity.setQysj(LocalDateTime.now());
com.roomroot.jwgl.handler.ZybInsertFillHandler.fillNotNullColumns(entity);
if (findExistingMajor(entity) != null) {
throw new RuntimeException("专业名称已存在,请勿重复新增");
}
zybMapper.insert(entity); zybMapper.insert(entity);
} }
@@ -90,24 +94,84 @@ public class TrainingProgramServiceImpl implements TrainingProgramService {
if (list.isEmpty()) { if (list.isEmpty()) {
throw new RuntimeException("Excel中无有效数据"); throw new RuntimeException("Excel中无有效数据");
} }
int inserted = 0;
int updated = 0;
int rowNo = 1;
for (ZYB entity : list) { for (ZYB entity : list) {
rowNo++;
if (entity.getZymc() == null || entity.getZymc().trim().isEmpty()) {
throw new RuntimeException("" + rowNo + "行:专业名称不能为空");
}
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()) { if (entity.getZydh() == null || entity.getZydh().isEmpty()) {
entity.setZydh(com.roomroot.jwgl.utils.UuidUtil.getUUID()); entity.setZydh(com.roomroot.jwgl.utils.UuidUtil.getUUID());
} }
if (entity.getTy() == null) { if (entity.getZybsh() == null || entity.getZybsh().isEmpty()) {
entity.setZybsh(entity.getZydh());
}
entity.setTy(false); entity.setTy(false);
} entity.setQysj(LocalDateTime.now());
zybMapper.insert(entity); zybMapper.insert(entity);
inserted++;
} }
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<ZYB> sameName = zybMapper.selectList(new LambdaQueryWrapper<ZYB>()
.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 @Override
public void exportZYBExcel(jakarta.servlet.http.HttpServletResponse response) throws Exception { public void exportZYBExcel(jakarta.servlet.http.HttpServletResponse response) throws Exception {
java.util.List<ZYB> list = zybMapper.selectList( java.util.List<ZYB> list = zybMapper.selectList(
new LambdaQueryWrapper<ZYB>().orderByAsc(ZYB::getZydh)); new LambdaQueryWrapper<ZYB>().orderByAsc(ZYB::getZydh));
String[] headers = {"专业代号", "专业名称", "专业代码", "专业版本", "培训类型", "培训层次", String[] headers = com.roomroot.jwgl.utils.ExcelParseUtil.TRAINING_PROGRAM_IMPORT_HEADERS;
"学年制", "学期数", "专业方向", "专业备注", "学员类别", "简称", "培训类型2"};
byte[] bytes = com.roomroot.jwgl.utils.ExcelExportUtil.export("培养方案", headers, list, byte[] bytes = com.roomroot.jwgl.utils.ExcelExportUtil.export("培养方案", headers, list,
ZYB::getZydh, ZYB::getZymc, ZYB::getZydm, ZYB::getZybb, ZYB::getPxlx, ZYB::getZydh, ZYB::getZymc, ZYB::getZydm, ZYB::getZybb, ZYB::getPxlx,
ZYB::getPxcc, ZYB::getXnz, ZYB::getXqs, ZYB::getZyfx, ZYB::getZybz, ZYB::getPxcc, ZYB::getXnz, ZYB::getXqs, ZYB::getZyfx, ZYB::getZybz,
@@ -120,6 +184,19 @@ public class TrainingProgramServiceImpl implements TrainingProgramService {
response.getOutputStream().flush(); 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 @Override
public java.util.List<JXGLJG> listTeachingOrganizations() { public java.util.List<JXGLJG> listTeachingOrganizations() {
java.util.List<JXGLJG> list = jxgljgMapper.selectEnabledOptions(); java.util.List<JXGLJG> list = jxgljgMapper.selectEnabledOptions();
@@ -450,11 +450,17 @@ public class ExcelParseUtil {
return true; return true;
} }
/**
* 人才培养方案(专业表)导入模板列头。下载模板与解析共用,按列名匹配,顺序可调整。
*/
public static final String[] TRAINING_PROGRAM_IMPORT_HEADERS = {
"专业代号", "专业名称", "专业代码", "专业版本", "培训类型", "培训层次",
"学年制", "学期数", "专业方向", "专业备注", "学员类别", "简称", "培训类型2"
};
/** /**
* 解析培养方案(专业表)导入 Excel 文件(支持 .xls / .xlsx)。 * 解析培养方案(专业表)导入 Excel 文件(支持 .xls / .xlsx)。
* <p>表头顺序:</p> * <p>按 {@link #TRAINING_PROGRAM_IMPORT_HEADERS} 列名匹配;未识别表头则拒绝导入。</p>
* 专业代号, 专业名称, 专业代码, 专业版本, 培训类型, 培训层次, 学年制, 学期数,
* 专业方向, 专业备注, 学员类别, 简称, 培训类型2
* *
* @return 专业实体列表 * @return 专业实体列表
* @throws Exception 解析异常 * @throws Exception 解析异常
@@ -464,29 +470,53 @@ public class ExcelParseUtil {
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(); int firstRowNum = sheet.getFirstRowNum();
for (int rowNum = 1; rowNum <= lastRowNum; rowNum++) { Row headerRow = sheet.getRow(firstRowNum);
if (headerRow == null) {
throw new IllegalArgumentException("导入文件缺少表头行,请先下载「人才培养方案目录模板」填写");
}
Map<String, Integer> 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); Row row = sheet.getRow(rowNum);
if (row == null) { if (row == null) {
continue; continue;
} }
ZYB entity = new ZYB(); ZYB entity = new ZYB();
entity.setZydh(getCellStringValue(row.getCell(0))); entity.setZydh(cellText(row, headerIndex, "专业代号"));
entity.setZymc(getCellStringValue(row.getCell(1))); entity.setZymc(cellText(row, headerIndex, "专业名称"));
entity.setZydm(getCellStringValue(row.getCell(2))); entity.setZydm(cellText(row, headerIndex, "专业代码"));
entity.setZybb(getCellStringValue(row.getCell(3))); entity.setZybb(cellText(row, headerIndex, "专业版本"));
entity.setPxlx(getCellStringValue(row.getCell(4))); entity.setPxlx(cellText(row, headerIndex, "培训类型"));
entity.setPxcc(getCellStringValue(row.getCell(5))); entity.setPxcc(cellText(row, headerIndex, "培训层次"));
entity.setXnz(getCellStringValue(row.getCell(6))); entity.setXnz(cellText(row, headerIndex, "学年制"));
entity.setXqs(parseIntCell(row.getCell(7))); entity.setXqs(cellInt(row, headerIndex, "学期数"));
entity.setZyfx(getCellStringValue(row.getCell(8))); entity.setZyfx(cellText(row, headerIndex, "专业方向"));
entity.setZybz(getCellStringValue(row.getCell(9))); entity.setZybz(cellText(row, headerIndex, "专业备注"));
entity.setXylb(getCellStringValue(row.getCell(10))); entity.setXylb(cellText(row, headerIndex, "学员类别"));
entity.setJc(getCellStringValue(row.getCell(11))); entity.setJc(cellText(row, headerIndex, "简称"));
entity.setPxlx2(getCellStringValue(row.getCell(12))); entity.setPxlx2(cellText(row, headerIndex, "培训类型2"));
// 至少要有专业代号或名称才视为有效行 if (isNotBlank(entity.getZydh()) || isNotBlank(entity.getZymc())) {
if (entity.getZydh() != null && !entity.getZydh().isEmpty()
|| entity.getZymc() != null && !entity.getZymc().isEmpty()) {
result.add(entity); result.add(entity);
} }
} }
+3 -3
View File
@@ -83,12 +83,12 @@ export function exportTrainingProgram() {
} }
/** /**
* 下载培养方案导入模板(返回 Blob) * 下载培养方案目录导入模板(返回 Blob
* GET /download/training-program * GET /training/template
*/ */
export function downloadTrainingProgramTemplate() { export function downloadTrainingProgramTemplate() {
return request({ return request({
url: '/download/training-program', url: '/training/template',
method: 'get', method: 'get',
responseType: 'blob' responseType: 'blob'
}) })
@@ -631,7 +631,7 @@ export default {
}, },
handleTemplateDownload() { handleTemplateDownload() {
downloadTrainingProgramTemplate().then(blob => { downloadTrainingProgramTemplate().then(blob => {
saveAs(blob, '人才培养方案课程数据文件模板.xls') saveAs(blob, '人才培养方案目录模板.xlsx')
this.$message.success('模板下载成功') this.$message.success('模板下载成功')
}).catch(() => {}) }).catch(() => {})
}, },