dev-tmp1
luotaiqian 2026-08-18 09:10:55 +08:00
parent fec5cf1ae1
commit 688d7e14b0
3 changed files with 68 additions and 38 deletions

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.jjb.saas.framework.repository.basedo.BaseDO;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;

View File

@ -1,12 +1,12 @@
package org.qinan.cedu.service.impl; package org.qinan.cedu.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.Db; import com.baomidou.mybatisplus.extension.toolkit.Db;
import cn.hutool.core.util.IdUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.qinan.cedu.convertor.PaperConvertor; import org.qinan.cedu.convertor.PaperConvertor;
@ -27,6 +27,7 @@ import org.qinan.cedu.service.PaperQuestionRelService;
import org.qinan.cedu.service.PaperService; import org.qinan.cedu.service.PaperService;
import org.qinan.cedu.service.QuestionOptionService; import org.qinan.cedu.service.QuestionOptionService;
import org.qinan.cedu.service.support.QuestionExcelImporter; import org.qinan.cedu.service.support.QuestionExcelImporter;
import org.qinan.safetyeval.infrastructure.support.InsertFieldDefaults;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -115,14 +116,14 @@ public class PaperServiceImpl extends ServiceImpl<PaperMapper, PaperDO> implemen
List<Long> questionIds = rels.stream().map(PaperQuestionRelDO::getQuestionId).collect(Collectors.toList()); List<Long> questionIds = rels.stream().map(PaperQuestionRelDO::getQuestionId).collect(Collectors.toList());
List<QuestionDO> questions = questionIds.isEmpty() ? new ArrayList<>() List<QuestionDO> questions = questionIds.isEmpty() ? new ArrayList<>()
: Db.listByIds(questionIds, QuestionDO.class).stream() : Db.listByIds(questionIds, QuestionDO.class).stream()
.filter(q -> DeleteEnum.FALSE.getCode().equals(q.getDeleteEnum())) .filter(q -> DeleteEnum.FALSE.getCode().equals(q.getDeleteEnum()))
.collect(Collectors.toList()); .collect(Collectors.toList());
List<Long> existQuestionIds = questions.stream().map(QuestionDO::getId).collect(Collectors.toList()); List<Long> existQuestionIds = questions.stream().map(QuestionDO::getId).collect(Collectors.toList());
List<QuestionOptionDO> allOptions = existQuestionIds.isEmpty() ? new ArrayList<>() List<QuestionOptionDO> allOptions = existQuestionIds.isEmpty() ? new ArrayList<>()
: questionOptionService.lambdaQuery() : questionOptionService.lambdaQuery()
.in(QuestionOptionDO::getQuestionId, existQuestionIds) .in(QuestionOptionDO::getQuestionId, existQuestionIds)
.eq(QuestionOptionDO::getDeleteEnum, DeleteEnum.FALSE.getCode()) .eq(QuestionOptionDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
.list(); .list();
// 新增试卷(使用请求中的名称/总分/合格分数) // 新增试卷(使用请求中的名称/总分/合格分数)
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@ -247,6 +248,7 @@ public class PaperServiceImpl extends ServiceImpl<PaperMapper, PaperDO> implemen
paper.setPaperName(importDTO.getPaperName().trim()); paper.setPaperName(importDTO.getPaperName().trim());
paper.setPaperTotalScore(importDTO.getPaperTotalScore()); paper.setPaperTotalScore(importDTO.getPaperTotalScore());
paper.setPaperPassScore(importDTO.getPaperPassScore()); paper.setPaperPassScore(importDTO.getPaperPassScore());
InsertFieldDefaults.applyIgnoreOrgId(paper);
this.savePaper(paper); this.savePaper(paper);
// 保存试题和选项id已预置雪花id // 保存试题和选项id已预置雪花id

View File

@ -4,15 +4,18 @@ import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader; import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet; import com.alibaba.excel.read.metadata.ReadSheet;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.qinan.cedu.enums.DeleteEnum; import org.qinan.cedu.enums.DeleteEnum;
import org.qinan.cedu.enums.QuestionTypeEnum; import org.qinan.cedu.enums.QuestionTypeEnum;
import org.qinan.cedu.mapper.CoursewareManagementMapper;
import org.qinan.cedu.model.entity.CoursewareManagementDO; import org.qinan.cedu.model.entity.CoursewareManagementDO;
import org.qinan.cedu.model.entity.QuestionDO; import org.qinan.cedu.model.entity.QuestionDO;
import org.qinan.cedu.model.entity.QuestionOptionDO; import org.qinan.cedu.model.entity.QuestionOptionDO;
import org.qinan.cedu.model.vo.PaperImportErrorVO; import org.qinan.cedu.model.vo.PaperImportErrorVO;
import org.qinan.cedu.service.CoursewareManagementService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -33,6 +36,8 @@ import java.util.stream.Collectors;
* sheet * sheet
*/ */
@Component @Component
@RequiredArgsConstructor
@Slf4j
public class QuestionExcelImporter { public class QuestionExcelImporter {
/** /**
@ -46,18 +51,36 @@ public class QuestionExcelImporter {
private static final String[] CHOICE_OPTION_KEYS = {"A", "B", "C", "D"}; private static final String[] CHOICE_OPTION_KEYS = {"A", "B", "C", "D"};
@Autowired private static final String UPLOAD_EMPTY_MESSAGE = "请上传试题Excel文件";
private CoursewareManagementService coursewareManagementService; private static final String READ_FILE_FAIL_MESSAGE = "读取上传文件失败";
private static final String READ_SINGLE_SHEET_FAIL_PREFIX = "读取单选题sheet失败";
private static final String READ_MULTIPLE_SHEET_FAIL_PREFIX = "读取多选题sheet失败";
private static final String READ_JUDGE_SHEET_FAIL_PREFIX = "读取判断题sheet失败";
private static final String SHEET_FALLBACK_NAME_TEMPLATE = "sheet[%s]";
private static final String OPTION_AT_LEAST_TWO_MESSAGE = "选项至少需要填写2个";
private static final String CHOICE_ANSWER_LETTER_MESSAGE = "答案只能填写A、B、C、D、E、F中的字母";
private static final String SINGLE_ANSWER_LETTER_MESSAGE = "单选题答案只能填写A、B、C、D中的一个";
private static final String JUDGE_ANSWER_LETTER_MESSAGE = "判断题答案只能填写T或F";
private static final String ANSWER_NOT_MATCH_OPTION_TEMPLATE = "答案[%s]找不到对应的选项";
private static final String COURSEWARE_NOT_EXIST_TEMPLATE = "关联课件名称[%s]不存在";
private static final String OPTION_KEY_A = "A";
private static final String OPTION_KEY_B = "B";
private static final String JUDGE_ANSWER_TRUE = "T";
private static final String JUDGE_ANSWER_FALSE = "F";
private static final String JUDGE_CORRECT_OPTION_TEXT = "对";
private static final String JUDGE_WRONG_OPTION_TEXT = "错";
private static final String COMMA_DELIMITER = ",";
private static final String VIOLATION_DELIMITER = "";
@Autowired private final CoursewareManagementMapper coursewareManagementMapper;
private Validator validator; private final Validator validator;
/** /**
* Excelid * Excelid
*/ */
public ParseResult parse(MultipartFile file) { public ParseResult parse(MultipartFile file) {
if (file == null || file.isEmpty()) { if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("请上传试题Excel文件"); throw new IllegalArgumentException(UPLOAD_EMPTY_MESSAGE);
} }
byte[] bytes = readBytes(file); byte[] bytes = readBytes(file);
Map<Integer, String> sheetNames = readSheetNames(bytes); Map<Integer, String> sheetNames = readSheetNames(bytes);
@ -70,19 +93,22 @@ public class QuestionExcelImporter {
singleRows = readSheet(bytes, SINGLE_SHEET_NO, ChoiceQuestionImportRow.class); singleRows = readSheet(bytes, SINGLE_SHEET_NO, ChoiceQuestionImportRow.class);
} catch (Exception e) { } catch (Exception e) {
singleRows = new ArrayList<>(); singleRows = new ArrayList<>();
errors.add(new PaperImportErrorVO(null, sheetName(sheetNames, SINGLE_SHEET_NO), "读取单选题sheet失败" + e.getMessage())); errors.add(new PaperImportErrorVO(null, sheetName(sheetNames, SINGLE_SHEET_NO)
, READ_SINGLE_SHEET_FAIL_PREFIX + e.getMessage()));
} }
try { try {
multipleRows = readSheet(bytes, MULTIPLE_SHEET_NO, ChoiceQuestionImportRow.class); multipleRows = readSheet(bytes, MULTIPLE_SHEET_NO, ChoiceQuestionImportRow.class);
} catch (Exception e) { } catch (Exception e) {
multipleRows = new ArrayList<>(); multipleRows = new ArrayList<>();
errors.add(new PaperImportErrorVO(null, sheetName(sheetNames, MULTIPLE_SHEET_NO), "读取多选题sheet失败" + e.getMessage())); errors.add(new PaperImportErrorVO(null, sheetName(sheetNames, MULTIPLE_SHEET_NO)
, READ_MULTIPLE_SHEET_FAIL_PREFIX + e.getMessage()));
} }
try { try {
judgeRows = readSheet(bytes, JUDGE_SHEET_NO, JudgeQuestionImportRow.class); judgeRows = readSheet(bytes, JUDGE_SHEET_NO, JudgeQuestionImportRow.class);
} catch (Exception e) { } catch (Exception e) {
judgeRows = new ArrayList<>(); judgeRows = new ArrayList<>();
errors.add(new PaperImportErrorVO(null, sheetName(sheetNames, JUDGE_SHEET_NO), "读取判断题sheet失败" + e.getMessage())); errors.add(new PaperImportErrorVO(null, sheetName(sheetNames, JUDGE_SHEET_NO)
, READ_JUDGE_SHEET_FAIL_PREFIX + e.getMessage()));
} }
Map<String, Long> coursewareManagementIdByName = loadCoursewareIdByName(singleRows, multipleRows, judgeRows); Map<String, Long> coursewareManagementIdByName = loadCoursewareIdByName(singleRows, multipleRows, judgeRows);
@ -98,7 +124,7 @@ public class QuestionExcelImporter {
try { try {
return file.getBytes(); return file.getBytes();
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException("读取上传文件失败", e); throw new IllegalArgumentException(READ_FILE_FAIL_MESSAGE, e);
} }
} }
@ -115,7 +141,7 @@ public class QuestionExcelImporter {
} }
private String sheetName(Map<Integer, String> sheetNames, int sheetNo) { private String sheetName(Map<Integer, String> sheetNames, int sheetNo) {
return sheetNames.getOrDefault(sheetNo, "sheet[" + (sheetNo + 1) + "]"); return sheetNames.getOrDefault(sheetNo, String.format(SHEET_FALLBACK_NAME_TEMPLATE, sheetNo + 1));
} }
private <T> List<T> readSheet(byte[] bytes, int sheetNo, Class<T> headClass) { private <T> List<T> readSheet(byte[] bytes, int sheetNo, Class<T> headClass) {
@ -139,11 +165,11 @@ public class QuestionExcelImporter {
if (names.isEmpty()) { if (names.isEmpty()) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
List<CoursewareManagementDO> coursewares = coursewareManagementService.lambdaQuery() List<CoursewareManagementDO> coursewares = coursewareManagementMapper.selectList(
.select(CoursewareManagementDO::getId, CoursewareManagementDO::getCoursewareName) new LambdaQueryWrapper<CoursewareManagementDO>()
.eq(CoursewareManagementDO::getDeleteEnum, DeleteEnum.FALSE.getCode()) .select(CoursewareManagementDO::getId, CoursewareManagementDO::getCoursewareName)
.in(CoursewareManagementDO::getCoursewareName, names) .eq(CoursewareManagementDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
.list(); .in(CoursewareManagementDO::getCoursewareName, names));
Map<String, Long> idByName = new HashMap<>(); Map<String, Long> idByName = new HashMap<>();
for (CoursewareManagementDO courseware : coursewares) { for (CoursewareManagementDO courseware : coursewares) {
idByName.putIfAbsent(courseware.getCoursewareName(), courseware.getId()); idByName.putIfAbsent(courseware.getCoursewareName(), courseware.getId());
@ -189,14 +215,14 @@ public class QuestionExcelImporter {
} }
} }
if (options.size() < 2) { if (options.size() < 2) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "选项至少需要填写2个")); errors.add(new PaperImportErrorVO(rowIndex, sheetName, OPTION_AT_LEAST_TWO_MESSAGE));
continue; continue;
} }
// 校验答案:必须有答案且答案必须能找到对应的选项 // 校验答案:必须有答案且答案必须能找到对应的选项
String answerRaw = row.getAnswer().trim().toUpperCase(); String answerRaw = row.getAnswer().trim().toUpperCase();
if (!answerRaw.chars().allMatch(c -> c >= 'A' && c <= 'F')) { if (!answerRaw.chars().allMatch(c -> c >= 'A' && c <= 'F')) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "答案只能填写A、B、C、D、E、F中的字母")); errors.add(new PaperImportErrorVO(rowIndex, sheetName, CHOICE_ANSWER_LETTER_MESSAGE));
continue; continue;
} }
List<String> answerLetters = answerRaw.chars() List<String> answerLetters = answerRaw.chars()
@ -205,14 +231,15 @@ public class QuestionExcelImporter {
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
if (single && answerLetters.size() != 1) { if (single && answerLetters.size() != 1) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "单选题答案只能填写A、B、C、D中的一个")); errors.add(new PaperImportErrorVO(rowIndex, sheetName, SINGLE_ANSWER_LETTER_MESSAGE));
continue; continue;
} }
List<String> notMatched = answerLetters.stream() List<String> notMatched = answerLetters.stream()
.filter(letter -> !options.containsKey(letter)) .filter(letter -> !options.containsKey(letter))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (!notMatched.isEmpty()) { if (!notMatched.isEmpty()) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "答案[" + String.join("", notMatched) + "]找不到对应的选项")); errors.add(new PaperImportErrorVO(rowIndex, sheetName,
String.format(ANSWER_NOT_MATCH_OPTION_TEMPLATE, String.join("", notMatched))));
continue; continue;
} }
@ -220,7 +247,7 @@ public class QuestionExcelImporter {
String coursewareName = row.getCoursewareName().trim(); String coursewareName = row.getCoursewareName().trim();
Long coursewareManagementId = coursewareManagementIdByName.get(coursewareName); Long coursewareManagementId = coursewareManagementIdByName.get(coursewareName);
if (coursewareManagementId == null) { if (coursewareManagementId == null) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "关联课件名称[" + coursewareName + "]不存在")); errors.add(new PaperImportErrorVO(rowIndex, sheetName, String.format(COURSEWARE_NOT_EXIST_TEMPLATE, coursewareName)));
continue; continue;
} }
@ -235,7 +262,7 @@ public class QuestionExcelImporter {
question.setId(IdUtil.getSnowflakeNextId()); question.setId(IdUtil.getSnowflakeNextId());
question.setTitle(row.getTitle().trim()); question.setTitle(row.getTitle().trim());
question.setQuestionType(questionType.getCode()); question.setQuestionType(questionType.getCode());
question.setAnswer(String.join(",", answerLetters)); question.setAnswer(String.join(COMMA_DELIMITER, answerLetters));
question.setScore(row.getScore()); question.setScore(row.getScore());
question.setAnalysis(row.getAnalysis().trim()); question.setAnalysis(row.getAnalysis().trim());
question.setTagType(row.getTagType().trim()); question.setTagType(row.getTagType().trim());
@ -266,33 +293,33 @@ public class QuestionExcelImporter {
} }
String answerRaw = row.getAnswer().trim().toUpperCase(); String answerRaw = row.getAnswer().trim().toUpperCase();
if (!"T".equals(answerRaw) && !"F".equals(answerRaw)) { if (!JUDGE_ANSWER_TRUE.equals(answerRaw) && !JUDGE_ANSWER_FALSE.equals(answerRaw)) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "判断题答案只能填写T或F")); errors.add(new PaperImportErrorVO(rowIndex, sheetName, JUDGE_ANSWER_LETTER_MESSAGE));
continue; continue;
} }
String coursewareName = row.getCoursewareName().trim(); String coursewareName = row.getCoursewareName().trim();
Long coursewareManagementId = coursewareManagementIdByName.get(coursewareName); Long coursewareManagementId = coursewareManagementIdByName.get(coursewareName);
if (coursewareManagementId == null) { if (coursewareManagementId == null) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "关联课件名称[" + coursewareName + "]不存在")); errors.add(new PaperImportErrorVO(rowIndex, sheetName, String.format(COURSEWARE_NOT_EXIST_TEMPLATE, coursewareName)));
continue; continue;
} }
boolean correct = "T".equals(answerRaw); boolean correct = JUDGE_ANSWER_TRUE.equals(answerRaw);
QuestionDO question = new QuestionDO(); QuestionDO question = new QuestionDO();
question.setId(IdUtil.getSnowflakeNextId()); question.setId(IdUtil.getSnowflakeNextId());
question.setTitle(row.getTitle().trim()); question.setTitle(row.getTitle().trim());
question.setQuestionType(QuestionTypeEnum.JUDGE.getCode()); question.setQuestionType(QuestionTypeEnum.JUDGE.getCode());
question.setAnswer(correct ? "A" : "B"); question.setAnswer(correct ? OPTION_KEY_A : OPTION_KEY_B);
question.setScore(row.getScore()); question.setScore(row.getScore());
question.setAnalysis(row.getAnalysis().trim()); question.setAnalysis(row.getAnalysis().trim());
question.setTagType(row.getTagType().trim()); question.setTagType(row.getTagType().trim());
question.setCoursewareManagementId(coursewareManagementId); question.setCoursewareManagementId(coursewareManagementId);
Map<String, String> optionTexts = new LinkedHashMap<>(); Map<String, String> optionTexts = new LinkedHashMap<>();
optionTexts.put("A", "对"); optionTexts.put(OPTION_KEY_A, JUDGE_CORRECT_OPTION_TEXT);
optionTexts.put("B", "错"); optionTexts.put(OPTION_KEY_B, JUDGE_WRONG_OPTION_TEXT);
question.setOptions(buildOptions(question.getId(), optionTexts, question.setOptions(buildOptions(question.getId(), optionTexts,
Collections.singletonList(correct ? "A" : "B"))); Collections.singletonList(correct ? OPTION_KEY_A : OPTION_KEY_B)));
question.setAnswerQuestionOptionIds(joinCorrectOptionIds(question.getOptions())); question.setAnswerQuestionOptionIds(joinCorrectOptionIds(question.getOptions()));
questions.add(question); questions.add(question);
} }
@ -320,14 +347,14 @@ public class QuestionExcelImporter {
return options.stream() return options.stream()
.filter(option -> Boolean.TRUE.equals(option.getIsCorrect())) .filter(option -> Boolean.TRUE.equals(option.getIsCorrect()))
.map(option -> String.valueOf(option.getId())) .map(option -> String.valueOf(option.getId()))
.collect(Collectors.joining(",")); .collect(Collectors.joining(COMMA_DELIMITER));
} }
private String joinViolationMessages(Set<? extends ConstraintViolation<?>> violations) { private String joinViolationMessages(Set<? extends ConstraintViolation<?>> violations) {
return violations.stream() return violations.stream()
.map(ConstraintViolation::getMessage) .map(ConstraintViolation::getMessage)
.distinct() .distinct()
.collect(Collectors.joining("")); .collect(Collectors.joining(VIOLATION_DELIMITER));
} }
private boolean isEmptyChoiceRow(ChoiceQuestionImportRow row) { private boolean isEmptyChoiceRow(ChoiceQuestionImportRow row) {