paper
parent
fec5cf1ae1
commit
688d7e14b0
|
|
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.jjb.saas.framework.repository.basedo.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
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.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.QuestionOptionService;
|
||||
import org.qinan.cedu.service.support.QuestionExcelImporter;
|
||||
import org.qinan.safetyeval.infrastructure.support.InsertFieldDefaults;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
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<QuestionDO> questions = questionIds.isEmpty() ? new ArrayList<>()
|
||||
: Db.listByIds(questionIds, QuestionDO.class).stream()
|
||||
.filter(q -> DeleteEnum.FALSE.getCode().equals(q.getDeleteEnum()))
|
||||
.collect(Collectors.toList());
|
||||
.filter(q -> DeleteEnum.FALSE.getCode().equals(q.getDeleteEnum()))
|
||||
.collect(Collectors.toList());
|
||||
List<Long> existQuestionIds = questions.stream().map(QuestionDO::getId).collect(Collectors.toList());
|
||||
List<QuestionOptionDO> allOptions = existQuestionIds.isEmpty() ? new ArrayList<>()
|
||||
: questionOptionService.lambdaQuery()
|
||||
.in(QuestionOptionDO::getQuestionId, existQuestionIds)
|
||||
.eq(QuestionOptionDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
|
||||
.list();
|
||||
.in(QuestionOptionDO::getQuestionId, existQuestionIds)
|
||||
.eq(QuestionOptionDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
|
||||
.list();
|
||||
|
||||
// 新增试卷(使用请求中的名称/总分/合格分数)
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
|
@ -247,6 +248,7 @@ public class PaperServiceImpl extends ServiceImpl<PaperMapper, PaperDO> implemen
|
|||
paper.setPaperName(importDTO.getPaperName().trim());
|
||||
paper.setPaperTotalScore(importDTO.getPaperTotalScore());
|
||||
paper.setPaperPassScore(importDTO.getPaperPassScore());
|
||||
InsertFieldDefaults.applyIgnoreOrgId(paper);
|
||||
this.savePaper(paper);
|
||||
|
||||
// 保存试题和选项(id已预置雪花id)
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ import cn.hutool.core.util.IdUtil;
|
|||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.ExcelReader;
|
||||
import com.alibaba.excel.read.metadata.ReadSheet;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.qinan.cedu.enums.DeleteEnum;
|
||||
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.QuestionDO;
|
||||
import org.qinan.cedu.model.entity.QuestionOptionDO;
|
||||
import org.qinan.cedu.model.vo.PaperImportErrorVO;
|
||||
import org.qinan.cedu.service.CoursewareManagementService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
|
@ -33,6 +36,8 @@ import java.util.stream.Collectors;
|
|||
* 其余sheet忽略。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class QuestionExcelImporter {
|
||||
|
||||
/**
|
||||
|
|
@ -46,18 +51,36 @@ public class QuestionExcelImporter {
|
|||
|
||||
private static final String[] CHOICE_OPTION_KEYS = {"A", "B", "C", "D"};
|
||||
|
||||
@Autowired
|
||||
private CoursewareManagementService coursewareManagementService;
|
||||
private static final String UPLOAD_EMPTY_MESSAGE = "请上传试题Excel文件";
|
||||
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 Validator validator;
|
||||
private final CoursewareManagementMapper coursewareManagementMapper;
|
||||
private final Validator validator;
|
||||
|
||||
/**
|
||||
* 解析上传的试题Excel,返回校验通过的试题(含选项,已预置雪花id)和异常明细
|
||||
*/
|
||||
public ParseResult parse(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("请上传试题Excel文件");
|
||||
throw new IllegalArgumentException(UPLOAD_EMPTY_MESSAGE);
|
||||
}
|
||||
byte[] bytes = readBytes(file);
|
||||
Map<Integer, String> sheetNames = readSheetNames(bytes);
|
||||
|
|
@ -70,19 +93,22 @@ public class QuestionExcelImporter {
|
|||
singleRows = readSheet(bytes, SINGLE_SHEET_NO, ChoiceQuestionImportRow.class);
|
||||
} catch (Exception e) {
|
||||
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 {
|
||||
multipleRows = readSheet(bytes, MULTIPLE_SHEET_NO, ChoiceQuestionImportRow.class);
|
||||
} catch (Exception e) {
|
||||
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 {
|
||||
judgeRows = readSheet(bytes, JUDGE_SHEET_NO, JudgeQuestionImportRow.class);
|
||||
} catch (Exception e) {
|
||||
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);
|
||||
|
|
@ -98,7 +124,7 @@ public class QuestionExcelImporter {
|
|||
try {
|
||||
return file.getBytes();
|
||||
} 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) {
|
||||
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) {
|
||||
|
|
@ -139,11 +165,11 @@ public class QuestionExcelImporter {
|
|||
if (names.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<CoursewareManagementDO> coursewares = coursewareManagementService.lambdaQuery()
|
||||
.select(CoursewareManagementDO::getId, CoursewareManagementDO::getCoursewareName)
|
||||
.eq(CoursewareManagementDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
|
||||
.in(CoursewareManagementDO::getCoursewareName, names)
|
||||
.list();
|
||||
List<CoursewareManagementDO> coursewares = coursewareManagementMapper.selectList(
|
||||
new LambdaQueryWrapper<CoursewareManagementDO>()
|
||||
.select(CoursewareManagementDO::getId, CoursewareManagementDO::getCoursewareName)
|
||||
.eq(CoursewareManagementDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
|
||||
.in(CoursewareManagementDO::getCoursewareName, names));
|
||||
Map<String, Long> idByName = new HashMap<>();
|
||||
for (CoursewareManagementDO courseware : coursewares) {
|
||||
idByName.putIfAbsent(courseware.getCoursewareName(), courseware.getId());
|
||||
|
|
@ -189,14 +215,14 @@ public class QuestionExcelImporter {
|
|||
}
|
||||
}
|
||||
if (options.size() < 2) {
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "选项至少需要填写2个"));
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, OPTION_AT_LEAST_TWO_MESSAGE));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 校验答案:必须有答案且答案必须能找到对应的选项
|
||||
String answerRaw = row.getAnswer().trim().toUpperCase();
|
||||
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;
|
||||
}
|
||||
List<String> answerLetters = answerRaw.chars()
|
||||
|
|
@ -205,14 +231,15 @@ public class QuestionExcelImporter {
|
|||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
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;
|
||||
}
|
||||
List<String> notMatched = answerLetters.stream()
|
||||
.filter(letter -> !options.containsKey(letter))
|
||||
.collect(Collectors.toList());
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +247,7 @@ public class QuestionExcelImporter {
|
|||
String coursewareName = row.getCoursewareName().trim();
|
||||
Long coursewareManagementId = coursewareManagementIdByName.get(coursewareName);
|
||||
if (coursewareManagementId == null) {
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "关联课件名称[" + coursewareName + "]不存在"));
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, String.format(COURSEWARE_NOT_EXIST_TEMPLATE, coursewareName)));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +262,7 @@ public class QuestionExcelImporter {
|
|||
question.setId(IdUtil.getSnowflakeNextId());
|
||||
question.setTitle(row.getTitle().trim());
|
||||
question.setQuestionType(questionType.getCode());
|
||||
question.setAnswer(String.join(",", answerLetters));
|
||||
question.setAnswer(String.join(COMMA_DELIMITER, answerLetters));
|
||||
question.setScore(row.getScore());
|
||||
question.setAnalysis(row.getAnalysis().trim());
|
||||
question.setTagType(row.getTagType().trim());
|
||||
|
|
@ -266,33 +293,33 @@ public class QuestionExcelImporter {
|
|||
}
|
||||
|
||||
String answerRaw = row.getAnswer().trim().toUpperCase();
|
||||
if (!"T".equals(answerRaw) && !"F".equals(answerRaw)) {
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "判断题答案只能填写T或F"));
|
||||
if (!JUDGE_ANSWER_TRUE.equals(answerRaw) && !JUDGE_ANSWER_FALSE.equals(answerRaw)) {
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, JUDGE_ANSWER_LETTER_MESSAGE));
|
||||
continue;
|
||||
}
|
||||
|
||||
String coursewareName = row.getCoursewareName().trim();
|
||||
Long coursewareManagementId = coursewareManagementIdByName.get(coursewareName);
|
||||
if (coursewareManagementId == null) {
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "关联课件名称[" + coursewareName + "]不存在"));
|
||||
errors.add(new PaperImportErrorVO(rowIndex, sheetName, String.format(COURSEWARE_NOT_EXIST_TEMPLATE, coursewareName)));
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean correct = "T".equals(answerRaw);
|
||||
boolean correct = JUDGE_ANSWER_TRUE.equals(answerRaw);
|
||||
QuestionDO question = new QuestionDO();
|
||||
question.setId(IdUtil.getSnowflakeNextId());
|
||||
question.setTitle(row.getTitle().trim());
|
||||
question.setQuestionType(QuestionTypeEnum.JUDGE.getCode());
|
||||
question.setAnswer(correct ? "A" : "B");
|
||||
question.setAnswer(correct ? OPTION_KEY_A : OPTION_KEY_B);
|
||||
question.setScore(row.getScore());
|
||||
question.setAnalysis(row.getAnalysis().trim());
|
||||
question.setTagType(row.getTagType().trim());
|
||||
question.setCoursewareManagementId(coursewareManagementId);
|
||||
Map<String, String> optionTexts = new LinkedHashMap<>();
|
||||
optionTexts.put("A", "对");
|
||||
optionTexts.put("B", "错");
|
||||
optionTexts.put(OPTION_KEY_A, JUDGE_CORRECT_OPTION_TEXT);
|
||||
optionTexts.put(OPTION_KEY_B, JUDGE_WRONG_OPTION_TEXT);
|
||||
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()));
|
||||
questions.add(question);
|
||||
}
|
||||
|
|
@ -320,14 +347,14 @@ public class QuestionExcelImporter {
|
|||
return options.stream()
|
||||
.filter(option -> Boolean.TRUE.equals(option.getIsCorrect()))
|
||||
.map(option -> String.valueOf(option.getId()))
|
||||
.collect(Collectors.joining(","));
|
||||
.collect(Collectors.joining(COMMA_DELIMITER));
|
||||
}
|
||||
|
||||
private String joinViolationMessages(Set<? extends ConstraintViolation<?>> violations) {
|
||||
return violations.stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(";"));
|
||||
.collect(Collectors.joining(VIOLATION_DELIMITER));
|
||||
}
|
||||
|
||||
private boolean isEmptyChoiceRow(ChoiceQuestionImportRow row) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue