diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/support/QuestionExcelImporter.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/support/QuestionExcelImporter.java index 3d83e888..0a0f6816 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/support/QuestionExcelImporter.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/support/QuestionExcelImporter.java @@ -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 coursewareManagementIdByName = loadCoursewareIdByName(singleRows, multipleRows, judgeRows); - ParseResult parseResult = new ParseResult(); - List questions = new ArrayList<>(); - List 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 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 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 行数据类型 + * @return 读取到的行数据列表 + */ private List readSheet(byte[] bytes, int sheetNo, Class 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 loadCoursewareIdByName(List singleRows, List multipleRows, @@ -178,6 +212,12 @@ public class QuestionExcelImporter { return idByName; } + /** + * 收集非空课件名称到集合中(去除首尾空格) + * + * @param names 课件名称收集集合 + * @param coursewareName 行中填写的课件名称 + */ private void addCoursewareName(Set 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 rows, String sheetName, QuestionTypeEnum questionType, - Map coursewareManagementIdByName, List questions, - List queOptions, List errors) { + Map coursewareManagementIdByName, ParseResult parseResult) { + List 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 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 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 rows, String sheetName, - Map coursewareManagementIdByName, List questions, - List options, List errors) { + Map coursewareManagementIdByName, ParseResult parseResult) { + List 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 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 buildOptions(Long questionId, Map optionTexts, List answerLetters) { List options = new ArrayList<>(); @@ -347,6 +415,12 @@ public class QuestionExcelImporter { return options; } + /** + * 拼接正确选项的id(逗号分隔) + * + * @param options 选项实体列表 + * @return 正确选项id拼接后的字符串 + */ private String joinCorrectOptionIds(List 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> 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 questions; /** - * 校验通过的试题(含选项) + * 校验通过的试题选项 */ private final List options; @@ -411,16 +509,37 @@ public class QuestionExcelImporter { */ private final List errors; + /** + * 全参构造 + * + * @param questions 校验通过的试题列表 + * @param options 校验通过的试题选项列表 + * @param errors 异常明细列表 + */ public ParseResult(List questions, List options, List 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 errors) { + this.questions = new ArrayList<>(64); + this.options = new ArrayList<>(256); + this.errors = errors; + } } }