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