paper
parent
9e4324f722
commit
4b4796fc3d
|
|
@ -76,6 +76,9 @@ public class QuestionExcelImporter {
|
|||
|
||||
/**
|
||||
* 解析上传的试题Excel,返回校验通过的试题(含选项,已预置雪花id)和异常明细
|
||||
*
|
||||
* @param file 上传的试题Excel文件
|
||||
* @return 解析结果(试题、选项、异常明细)
|
||||
*/
|
||||
public ParseResult parse(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
|
|
@ -109,18 +112,22 @@ public class QuestionExcelImporter {
|
|||
|
||||
Map<String, Long> coursewareManagementIdByName = loadCoursewareIdByName(singleRows, multipleRows, judgeRows);
|
||||
|
||||
ParseResult parseResult = new ParseResult();
|
||||
List<QuestionDO> questions = new ArrayList<>();
|
||||
List<QuestionOptionDO> options = new ArrayList<>();
|
||||
ParseResult parseResult = new ParseResult(errors);
|
||||
convertChoiceRows(singleRows, sheetName(sheetNames, SINGLE_SHEET_NO)
|
||||
, QuestionTypeEnum.SINGLE, coursewareManagementIdByName, questions, options, errors);
|
||||
, QuestionTypeEnum.SINGLE, coursewareManagementIdByName, parseResult);
|
||||
convertChoiceRows(multipleRows, sheetName(sheetNames, MULTIPLE_SHEET_NO)
|
||||
, QuestionTypeEnum.MULTIPLE, coursewareManagementIdByName, questions, options, errors);
|
||||
, QuestionTypeEnum.MULTIPLE, coursewareManagementIdByName, parseResult);
|
||||
convertJudgeRows(judgeRows, sheetName(sheetNames, JUDGE_SHEET_NO)
|
||||
, coursewareManagementIdByName, questions, options, errors);
|
||||
return new ParseResult(questions, options, errors);
|
||||
, coursewareManagementIdByName, parseResult);
|
||||
return parseResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取上传文件的字节数组,读取失败抛出IllegalArgumentException
|
||||
*
|
||||
* @param file 上传的试题Excel文件
|
||||
* @return 文件字节数组
|
||||
*/
|
||||
private byte[] readBytes(MultipartFile file) {
|
||||
try {
|
||||
return file.getBytes();
|
||||
|
|
@ -129,6 +136,12 @@ public class QuestionExcelImporter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取所有sheet的编号和名称映射(sheetNo -> sheetName),读取失败返回空Map
|
||||
*
|
||||
* @param bytes Excel文件字节数组
|
||||
* @return sheet编号到名称的映射
|
||||
*/
|
||||
private Map<Integer, String> readSheetNames(byte[] bytes) {
|
||||
ExcelReader excelReader = EasyExcel.read(new ByteArrayInputStream(bytes)).build();
|
||||
try {
|
||||
|
|
@ -141,10 +154,26 @@ public class QuestionExcelImporter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定编号的sheet名称,取不到时降级为"sheet[n]"(n为从1开始的序号)
|
||||
*
|
||||
* @param sheetNames sheet编号到名称的映射
|
||||
* @param sheetNo sheet编号(从0开始)
|
||||
* @return sheet名称
|
||||
*/
|
||||
private String sheetName(Map<Integer, String> sheetNames, int sheetNo) {
|
||||
return sheetNames.getOrDefault(sheetNo, String.format(SHEET_FALLBACK_NAME_TEMPLATE, sheetNo + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按指定sheet编号和表头类型同步读取Excel数据,前2行为说明和表头不读取
|
||||
*
|
||||
* @param bytes Excel文件字节数组
|
||||
* @param sheetNo sheet编号(从0开始)
|
||||
* @param headClass 表头映射类型
|
||||
* @param <T> 行数据类型
|
||||
* @return 读取到的行数据列表
|
||||
*/
|
||||
private <T> List<T> readSheet(byte[] bytes, int sheetNo, Class<T> headClass) {
|
||||
return EasyExcel.read(new ByteArrayInputStream(bytes))
|
||||
.head(headClass)
|
||||
|
|
@ -155,6 +184,11 @@ public class QuestionExcelImporter {
|
|||
|
||||
/**
|
||||
* 批量查询关联课件名称对应的id(名称 -> id)
|
||||
*
|
||||
* @param singleRows 单选题行数据
|
||||
* @param multipleRows 多选题行数据
|
||||
* @param judgeRows 判断题行数据
|
||||
* @return 课件名称到课件id的映射
|
||||
*/
|
||||
private Map<String, Long> loadCoursewareIdByName(List<ChoiceQuestionImportRow> singleRows,
|
||||
List<ChoiceQuestionImportRow> multipleRows,
|
||||
|
|
@ -178,6 +212,12 @@ public class QuestionExcelImporter {
|
|||
return idByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集非空课件名称到集合中(去除首尾空格)
|
||||
*
|
||||
* @param names 课件名称收集集合
|
||||
* @param coursewareName 行中填写的课件名称
|
||||
*/
|
||||
private void addCoursewareName(Set<String> names, String coursewareName) {
|
||||
if (StringUtils.hasText(coursewareName)) {
|
||||
names.add(coursewareName.trim());
|
||||
|
|
@ -186,10 +226,16 @@ public class QuestionExcelImporter {
|
|||
|
||||
/**
|
||||
* 转换单选/多选sheet数据
|
||||
*
|
||||
* @param rows 单选/多选题行数据
|
||||
* @param sheetName sheet名称(用于异常提示定位)
|
||||
* @param questionType 题目类型(单选/多选)
|
||||
* @param coursewareManagementIdByName 课件名称到课件id的映射
|
||||
* @param parseResult 解析结果(写入试题、选项和异常明细)
|
||||
*/
|
||||
private void convertChoiceRows(List<ChoiceQuestionImportRow> rows, String sheetName, QuestionTypeEnum questionType,
|
||||
Map<String, Long> coursewareManagementIdByName, List<QuestionDO> questions,
|
||||
List<QuestionOptionDO> queOptions, List<PaperImportErrorVO> errors) {
|
||||
Map<String, Long> coursewareManagementIdByName, ParseResult parseResult) {
|
||||
List<PaperImportErrorVO> errors = parseResult.getErrors();
|
||||
boolean single = questionType == QuestionTypeEnum.SINGLE;
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
ChoiceQuestionImportRow row = rows.get(i);
|
||||
|
|
@ -254,11 +300,20 @@ public class QuestionExcelImporter {
|
|||
|
||||
QuestionDO questionDO = buildChoiceQuestion(row, questionType, answerLetters, coursewareManagementId);
|
||||
List<QuestionOptionDO> questionOptionDOS = buildOptions(questionDO.getId(), options, answerLetters);
|
||||
questions.add(questionDO);
|
||||
queOptions.addAll(questionOptionDOS);
|
||||
parseResult.getQuestions().add(questionDO);
|
||||
parseResult.getOptions().addAll(questionOptionDOS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装单选/多选题实体,预置雪花id并填充默认插入字段
|
||||
*
|
||||
* @param row 单选/多选题行数据
|
||||
* @param questionType 题目类型
|
||||
* @param answerLetters 正确答案字母列表(已去重排序)
|
||||
* @param coursewareManagementId 关联课件id
|
||||
* @return 试题实体
|
||||
*/
|
||||
private QuestionDO buildChoiceQuestion(ChoiceQuestionImportRow row, QuestionTypeEnum questionType,
|
||||
List<String> answerLetters, Long coursewareManagementId) {
|
||||
QuestionDO question = new QuestionDO();
|
||||
|
|
@ -276,10 +331,15 @@ public class QuestionExcelImporter {
|
|||
|
||||
/**
|
||||
* 转换判断题sheet数据:T/F 转换为 A对、B错 两个选项
|
||||
*
|
||||
* @param rows 判断题行数据
|
||||
* @param sheetName sheet名称(用于异常提示定位)
|
||||
* @param coursewareManagementIdByName 课件名称到课件id的映射
|
||||
* @param parseResult 解析结果(写入试题、选项和异常明细)
|
||||
*/
|
||||
private void convertJudgeRows(List<JudgeQuestionImportRow> rows, String sheetName,
|
||||
Map<String, Long> coursewareManagementIdByName, List<QuestionDO> questions,
|
||||
List<QuestionOptionDO> options, List<PaperImportErrorVO> errors) {
|
||||
Map<String, Long> coursewareManagementIdByName, ParseResult parseResult) {
|
||||
List<PaperImportErrorVO> errors = parseResult.getErrors();
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
JudgeQuestionImportRow row = rows.get(i);
|
||||
int rowIndex = i + HEAD_ROW_NUMBER + 1;
|
||||
|
|
@ -322,12 +382,20 @@ public class QuestionExcelImporter {
|
|||
optionTexts.put(OPTION_KEY_B, JUDGE_WRONG_OPTION_TEXT);
|
||||
List<QuestionOptionDO> questionOptionDOS = buildOptions(question.getId(), optionTexts,
|
||||
Collections.singletonList(correct ? OPTION_KEY_A : OPTION_KEY_B));
|
||||
options.addAll(questionOptionDOS);
|
||||
parseResult.getOptions().addAll(questionOptionDOS);
|
||||
question.setAnswerQuestionOptionIds(joinCorrectOptionIds(questionOptionDOS));
|
||||
questions.add(question);
|
||||
parseResult.getQuestions().add(question);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装试题选项实体列表,标记正确选项并按顺序设置排序号
|
||||
*
|
||||
* @param questionId 所属试题id
|
||||
* @param optionTexts 选项字母到选项内容的映射
|
||||
* @param answerLetters 正确答案字母列表
|
||||
* @return 选项实体列表
|
||||
*/
|
||||
private List<QuestionOptionDO> buildOptions(Long questionId, Map<String, String> optionTexts,
|
||||
List<String> answerLetters) {
|
||||
List<QuestionOptionDO> options = new ArrayList<>();
|
||||
|
|
@ -347,6 +415,12 @@ public class QuestionExcelImporter {
|
|||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接正确选项的id(逗号分隔)
|
||||
*
|
||||
* @param options 选项实体列表
|
||||
* @return 正确选项id拼接后的字符串
|
||||
*/
|
||||
private String joinCorrectOptionIds(List<QuestionOptionDO> options) {
|
||||
return options.stream()
|
||||
.filter(option -> Boolean.TRUE.equals(option.getIsCorrect()))
|
||||
|
|
@ -354,6 +428,12 @@ public class QuestionExcelImporter {
|
|||
.collect(Collectors.joining(COMMA_DELIMITER));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接JSR-303校验错误信息(去重后以中文分号分隔)
|
||||
*
|
||||
* @param violations JSR-303校验结果集
|
||||
* @return 拼接后的错误信息
|
||||
*/
|
||||
private String joinViolationMessages(Set<? extends ConstraintViolation<?>> violations) {
|
||||
return violations.stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
|
|
@ -361,6 +441,12 @@ public class QuestionExcelImporter {
|
|||
.collect(Collectors.joining(VIOLATION_DELIMITER));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断单选/多选题行是否为空行(所有列均未填写)
|
||||
*
|
||||
* @param row 单选/多选题行数据
|
||||
* @return 是否为空行
|
||||
*/
|
||||
private boolean isEmptyChoiceRow(ChoiceQuestionImportRow row) {
|
||||
return !StringUtils.hasText(row.getTitle())
|
||||
&& !StringUtils.hasText(row.getOptionA())
|
||||
|
|
@ -374,6 +460,12 @@ public class QuestionExcelImporter {
|
|||
&& !StringUtils.hasText(row.getCoursewareName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断判断题行是否为空行(所有列均未填写)
|
||||
*
|
||||
* @param row 判断题行数据
|
||||
* @return 是否为空行
|
||||
*/
|
||||
private boolean isEmptyJudgeRow(JudgeQuestionImportRow row) {
|
||||
return !StringUtils.hasText(row.getTitle())
|
||||
&& !StringUtils.hasText(row.getAnswer())
|
||||
|
|
@ -383,6 +475,12 @@ public class QuestionExcelImporter {
|
|||
&& !StringUtils.hasText(row.getCoursewareName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除首尾空格,空白字符串返回null
|
||||
*
|
||||
* @param text 原始字符串
|
||||
* @return 去除首尾空格后的字符串,空白时返回null
|
||||
*/
|
||||
private String trimToNull(String text) {
|
||||
if (!StringUtils.hasText(text)) {
|
||||
return null;
|
||||
|
|
@ -402,7 +500,7 @@ public class QuestionExcelImporter {
|
|||
private final List<QuestionDO> questions;
|
||||
|
||||
/**
|
||||
* 校验通过的试题(含选项)
|
||||
* 校验通过的试题选项
|
||||
*/
|
||||
private final List<QuestionOptionDO> options;
|
||||
|
||||
|
|
@ -411,16 +509,37 @@ public class QuestionExcelImporter {
|
|||
*/
|
||||
private final List<PaperImportErrorVO> errors;
|
||||
|
||||
/**
|
||||
* 全参构造
|
||||
*
|
||||
* @param questions 校验通过的试题列表
|
||||
* @param options 校验通过的试题选项列表
|
||||
* @param errors 异常明细列表
|
||||
*/
|
||||
public ParseResult(List<QuestionDO> questions, List<QuestionOptionDO> options, List<PaperImportErrorVO> errors) {
|
||||
this.questions = questions;
|
||||
this.options = options;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 无参构造,初始化空的集合
|
||||
*/
|
||||
public ParseResult() {
|
||||
this.questions = new ArrayList<>(64);
|
||||
this.options = new ArrayList<>(256);
|
||||
this.errors = new ArrayList<>(64);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以已有异常明细构造,试题和选项集合初始化为空
|
||||
*
|
||||
* @param errors 异常明细列表
|
||||
*/
|
||||
public ParseResult(List<PaperImportErrorVO> errors) {
|
||||
this.questions = new ArrayList<>(64);
|
||||
this.options = new ArrayList<>(256);
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue