专业添加导入模板下载和数据上传

This commit is contained in:
2026-09-15 17:43:34 +08:00
parent c7397ad457
commit 6e767947f7
8 changed files with 691 additions and 10 deletions
@@ -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<ZYB> pageMajor(PageQuery query, ZYB cond);
/**
* 从 Excel 导入专业(.xls/.xlsx)。
* <p>
* 表头按专业导入模板的列名匹配。已存在的专业(专业代号已存在,或未填代号但
* 专业名称+专业方向+培训类型已存在)直接跳过,不覆盖库中已有数据;
* 校验不通过的行跳过并记录原因,其余合格行正常导入。
* </p>
*
* @param file 上传的 Excel 文件
* @return 导入结果(新增/跳过/失败与明细)
* @throws Exception 解析异常
*/
MajorImportResultVO importMajorExcel(MultipartFile file) throws Exception;
/**
* 生成专业导入模板(xlsx 字节流),列头与导入解析保持一致。
*
* @return xlsx 文件字节数组
* @throws Exception 生成异常
*/
byte[] buildMajorImportTemplate() throws Exception;
}
@@ -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<MajorImportRowDTO> 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<ZYB> existingMajors = zybMapper.selectList(new LambdaQueryWrapper<ZYB>()
.select(ZYB::getZydh, ZYB::getZymc, ZYB::getZyfx, ZYB::getPxlx));
Set<String> existingZydhSet = new HashSet<>();
Set<String> 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<ZYB> 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());
}
/**
* 导入专用兜底。
* <p>
* 只补"库中没有非空缺省值"或需要按业务推导的字段(专业标识号、规范名称),
* 其余空值保持 null,交由数据库缺省值生效(如 节次类别='双节次'、系统模式='军校模式'、
* 培训类型2/学员类别='其他'),避免写成空串。
* </p>
*/
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);