diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolExamController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolExamController.java index ff20414f..3522be8a 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolExamController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolExamController.java @@ -1,8 +1,10 @@ package org.jeecg.modules.aiol.controller; +import java.net.URLEncoder; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -14,7 +16,9 @@ import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import org.apache.poi.ss.usermodel.Workbook; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryRuleEnum; @@ -22,6 +26,7 @@ import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.aiol.dto.ExaminationResult; import org.jeecg.modules.aiol.dto.QuestionAnswerDTO; import org.jeecg.modules.aiol.dto.QuestionAnswerUser; +import org.jeecg.modules.aiol.dto.QuestionExcelDTO; import org.jeecg.modules.aiol.entity.*; import org.jeecg.modules.aiol.service.*; @@ -30,6 +35,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; +import org.jeecgframework.poi.excel.ExcelExportUtil; import org.jeecgframework.poi.excel.ExcelImportUtil; import org.jeecgframework.poi.excel.def.NormalExcelConstants; import org.jeecgframework.poi.excel.entity.ExportParams; @@ -648,6 +654,385 @@ public class AiolExamController extends JeecgController list = paperQuestionService.list(new LambdaQueryWrapper().eq(AiolPaperQuestion::getPaperId, paperId)); + Map questionMap = list.stream() + .collect(Collectors.toMap(AiolPaperQuestion::getQuestionId, Function.identity())); + List questions = questionService.list(new LambdaQueryWrapper().in(AiolQuestion::getId, list.stream().map(AiolPaperQuestion::getQuestionId).collect(Collectors.toList()))); + List questionList = new ArrayList<>(); + if (questions.isEmpty()) { + throw new RuntimeException("没有题目"); + } else { + //获取复合题id + List parentIds = questions.stream().filter(q -> q.getType() == 5).map(AiolQuestion::getId).collect(Collectors.toList()); + //获取复合题下的子题 + if (!parentIds.isEmpty()) { + List childQuestions = questionService.list(new LambdaQueryWrapper().in(AiolQuestion::getParentId, parentIds)); + //将子题目放到复合题后面 + // 创建一个映射,用于快速查找父ID对应的子题目 + Map> parentToChildrenMap = childQuestions.stream() + .collect(Collectors.groupingBy(AiolQuestion::getParentId)); + // 将子题目添加到原始列表中,保持原有顺序 + for (AiolQuestion question : questions) { + questionList.add(question); + if (question.getType() == 5) { + List children = parentToChildrenMap.getOrDefault(question.getId(), Collections.emptyList()); + questionList.addAll(children); + } + } + } else { + questionList.addAll(questions); + } + } + // 3. 转换为ExcelDTO + List dtoList = new ArrayList<>(); + for (AiolQuestion question : questionList) { + QuestionExcelDTO dto = new QuestionExcelDTO(); + dto.setQuestionId(question.getId()); + dto.setContent(question.getContent()); + dto.setAnalysis(question.getAnalysis()); + dto.setScore(questionMap.get(question.getId()).getScore()); + + // 设置题目类型 + switch (question.getType()) { + case 0: + dto.setType("单选"); + break; + case 1: + dto.setType("多选"); + break; + case 2: + dto.setType("判断"); + break; + case 3: + dto.setType("填空"); + break; + case 4: + dto.setType("简答"); + break; + case 5: + dto.setType("复合题"); + break; + } + + // 设置难度 + switch (question.getDifficulty()) { + case 0: + dto.setDifficulty("简单"); + break; + case 1: + dto.setDifficulty("中等"); + break; + case 2: + dto.setDifficulty("困难"); + break; + } + + // 设置程度 + switch (question.getDegree()) { + case 0: + dto.setDegree("了解"); + break; + case 1: + dto.setDegree("熟悉"); + break; + case 2: + dto.setDegree("掌握"); + break; + } + + // 设置能力 + switch (question.getAbility()) { + case 0: + dto.setAbility("识记"); + break; + case 1: + dto.setAbility("理解"); + break; + case 2: + dto.setAbility("应用"); + break; + } + + // 设置选项和答案 + if (question.getType() >= 0 && question.getType() <= 2) { + // 选择题、多选题、判断题 + List options = questionOptionService.list( + new LambdaQueryWrapper() + .eq(AiolQuestionOption::getQuestionId, question.getId()) + .orderByAsc(AiolQuestionOption::getOrderNo) + ); + + // 设置选项 + for (int i = 0; i < options.size(); i++) { + AiolQuestionOption option = options.get(i); + char optionLabel = (char) ('A' + i); + switch (optionLabel) { + case 'A': + dto.setOptionA(option.getContent()); + break; + case 'B': + dto.setOptionB(option.getContent()); + break; + case 'C': + dto.setOptionC(option.getContent()); + break; + case 'D': + dto.setOptionD(option.getContent()); + break; + case 'E': + dto.setOptionE(option.getContent()); + break; + } + } + + // 设置正确答案 + StringBuilder correctAnswers = new StringBuilder(); + for (AiolQuestionOption option : options) { + if (option.getIzCorrent() == 1) { + char optionLabel = (char) ('A' + (option.getOrderNo() - 1)); + if (!correctAnswers.isEmpty()) correctAnswers.append(","); + correctAnswers.append(optionLabel); + } + } + dto.setCorrectAnswers(correctAnswers.toString()); + } else if (question.getType() == 3 || question.getType() == 4) { + // 填空题、简答题 + List answers = questionAnswerService.list( + new LambdaQueryWrapper() + .eq(AiolQuestionAnswer::getQuestionId, question.getId()) + .orderByAsc(AiolQuestionAnswer::getOrderNo) + ); + + StringBuilder answerTexts = new StringBuilder(); + int orderNo = 1; + for (AiolQuestionAnswer answer : answers) { + if (!answerTexts.isEmpty()) { + if (orderNo != answer.getOrderNo()) { + answerTexts.append(","); + orderNo = answer.getOrderNo(); + } else { + answerTexts.append("|"); + } + } + answerTexts.append(answer.getAnswerText()); + } + dto.setCorrectAnswers(answerTexts.toString()); + } + + dtoList.add(dto); + } + // 设置响应头 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + String fileName = URLEncoder.encode(paper.getTitle() + "_试卷导出", "UTF-8").replaceAll("\\+", "%"); + response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); + + // 使用AutoPoi创建Workbook + ExportParams exportParams = new ExportParams("试卷列表", "试卷题目数据"); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, QuestionExcelDTO.class, dtoList); + + // 写入响应流 + workbook.write(response.getOutputStream()); + // 关闭资源 + workbook.close(); + response.getOutputStream().flush(); + response.getOutputStream().close(); + } catch (Exception e) { + log.error("导出题库题目失败", e); + throw new RuntimeException("导出题库题目失败:" + e.getMessage()); + } + } + + @PostMapping("/importXls") + @Operation(summary = "试卷题目excel导入") + @Transactional + public Result importXls(@RequestParam(name = "file", required = true) MultipartFile file, + @RequestParam(name = "paperId", required = true) String paperId) { + try { + // 1. 判断试卷是否存在 + AiolPaper paper = paperService.getById(paperId); + if (paper == null) { + return Result.error("试卷不存在"); + } + + // 2. 读取Excel文件 + ImportParams params = new ImportParams(); + params.setTitleRows(1); // 标题行数 + params.setHeadRows(1); // 表头行数 + List dtoList = ExcelImportUtil.importExcel(file.getInputStream(), QuestionExcelDTO.class, params); + if (CollectionUtils.isEmpty(dtoList)) { + return Result.error("Excel文件为空或格式不正确"); + } + + // 3. 处理导入数据 + List questions = new ArrayList<>(); + List options = new ArrayList<>(); + List answers = new ArrayList<>(); + List paperQuestions = new ArrayList<>(); + + String parentId = null; + int orderNo = 1; // 从1开始计数 + for (QuestionExcelDTO dto : dtoList) { + // 3.1 创建题目 + AiolQuestion question = new AiolQuestion(); + question.setId(String.valueOf(IdWorker.getId())); + question.setContent(dto.getContent()); + question.setAnalysis(dto.getAnalysis()); + question.setScore(dto.getScore().intValue()); + + if (StringUtils.isNotBlank(dto.getParentId()) && dto.getParentId().equals("^")) { + question.setParentId(parentId); + } else { + parentId = question.getId(); + } + + // 设置题目类型 + switch (dto.getType()) { + case "单选": + question.setType(0); + break; + case "多选": + question.setType(1); + break; + case "判断": + question.setType(2); + break; + case "填空": + question.setType(3); + break; + case "简答": + question.setType(4); + break; + case "复合题": + question.setType(5); + break; + default: + throw new RuntimeException("题目类型不正确:" + dto.getType()); + } + + // 设置难度 + switch (dto.getDifficulty()) { + case "简单": + question.setDifficulty(0); + break; + case "中等": + question.setDifficulty(1); + break; + case "困难": + question.setDifficulty(2); + break; + } + + // 设置程度 + switch (dto.getDegree()) { + case "了解": + question.setDegree(0); + break; + case "熟悉": + question.setDegree(1); + break; + case "掌握": + question.setDegree(2); + break; + } + + // 设置能力 + switch (dto.getAbility()) { + case "识记": + question.setAbility(0); + break; + case "理解": + question.setAbility(1); + break; + case "应用": + question.setAbility(2); + break; + } + questions.add(question); + + // 3.2 处理选项和答案 + if (question.getType() >= 0 && question.getType() <= 2) { + // 选择题、多选题、判断题 + String correctAnswers = dto.getCorrectAnswers(); + if (StringUtils.isEmpty(correctAnswers)) { + return Result.error("题目 {} 缺少正确答案", dto.getContent()); + } + String[] answerLabels = correctAnswers.split(","); + List optionContents = Arrays.asList( + dto.getOptionA(), + dto.getOptionB(), + dto.getOptionC(), + dto.getOptionD(), + dto.getOptionE() + ).stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + for (int i = 0; i < optionContents.size(); i++) { + AiolQuestionOption option = new AiolQuestionOption(); + option.setQuestionId(question.getId()); + option.setContent(optionContents.get(i)); + option.setOrderNo(i + 1); + + // 检查是否为正确答案 + boolean isCorrect = Arrays.asList(answerLabels) + .contains(String.valueOf((char) ('A' + i))); + option.setIzCorrent(isCorrect ? 1 : 0); + options.add(option); + } + } else if (question.getType() == 3 || question.getType() == 4) { + // 填空题、简答题 + String correctAnswers = dto.getCorrectAnswers(); + if (StringUtils.isEmpty(correctAnswers)) { + return Result.error("题目 {} 缺少正确答案", dto.getContent()); + } + String[] answerTexts = correctAnswers.split(","); + for (int i = 0; i < answerTexts.length; i++) { + AiolQuestionAnswer answer = new AiolQuestionAnswer(); + answer.setQuestionId(question.getId()); + answer.setAnswerText(answerTexts[i]); + answer.setOrderNo(i + 1); + answers.add(answer); + } + } + + // 3.3 创建试卷题目关联关系 + AiolPaperQuestion aiolPaperQuestion = new AiolPaperQuestion(); + aiolPaperQuestion.setPaperId(paperId); + aiolPaperQuestion.setQuestionId(question.getId()); + aiolPaperQuestion.setOrderNo(orderNo++); + aiolPaperQuestion.setScore(dto.getScore()); + paperQuestions.add(aiolPaperQuestion); + + } + + // 4. 批量保存数据 + questionService.saveBatch(questions); + questionOptionService.saveBatch(options); + questionAnswerService.saveBatch(answers); + paperQuestionService.saveBatch(paperQuestions); + + + return Result.OK("导入成功"); + } catch (Exception e) { + log.error("导入题库题目失败", e); + return Result.error("导入失败:" + e.getMessage()); + } + } + + + //根据考试规则随机组卷 public List random(List list, String rules) { JSONObject ruleJson = JSON.parseObject(rules); diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolRepoController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolRepoController.java index 9f884b5c..65bd995d 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolRepoController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolRepoController.java @@ -559,7 +559,7 @@ public class AiolRepoController extends JeecgController= 0 && question.getType() <= 2) { // 选择题、多选题、判断题 - String[] answerLabels = dto.getCorrectAnswers().split(","); + String correctAnswers = dto.getCorrectAnswers(); + if (StringUtils.isEmpty(correctAnswers)) { + return Result.error("题目 {} 缺少正确答案", dto.getContent()); + } + String[] answerLabels = correctAnswers.split(","); List optionContents = Arrays.asList( dto.getOptionA(), dto.getOptionB(), @@ -847,7 +851,11 @@ public class AiolRepoController extends JeecgController