dev-tmp1
luotaiqian 2026-08-17 17:56:22 +08:00
parent 906402f7db
commit 4b3e567098
12 changed files with 706 additions and 2 deletions

View File

@ -37,6 +37,25 @@
<artifactId>swagger-annotations</artifactId>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.7.19</version>
</dependency>
<!-- JSR-303 校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>

View File

@ -3,9 +3,12 @@ package org.qinan.cedu.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.qinan.cedu.model.dto.PaperImportDTO;
import org.qinan.cedu.model.entity.PaperDO;
import org.qinan.cedu.model.vo.PaperImportVO;
import org.qinan.cedu.service.PaperService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -74,4 +77,10 @@ public class PaperController {
public List<Long> questionIds(@PathVariable("id") Long paperId) {
return paperService.getQuestionIds(paperId);
}
@ApiOperation("新建试卷-导入试题")
@PostMapping("/import")
public PaperImportVO importQuestions(@Validated PaperImportDTO paperImportDTO) {
return paperService.importQuestions(paperImportDTO);
}
}

View File

@ -0,0 +1,42 @@
package org.qinan.cedu.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
/**
* -
*/
@Data
@ApiModel(value = "PaperImportDTO", description = "新建试卷-导入试题请求")
public class PaperImportDTO {
@ApiModelProperty(value = "试卷名称", required = true)
@NotBlank(message = "试卷名称不能为空")
@Size(max = 128, message = "试卷名称长度不能超过128个字符")
private String paperName;
@ApiModelProperty(value = "试卷总分", required = true)
@NotNull(message = "试卷总分不能为空")
@DecimalMin(value = "0.01", message = "试卷总分必须大于0")
@DecimalMax(value = "9999.99", message = "试卷总分不能超过9999.99")
private BigDecimal paperTotalScore;
@ApiModelProperty(value = "合格分数", required = true)
@NotNull(message = "合格分数不能为空")
@DecimalMin(value = "0", message = "合格分数不能小于0")
@DecimalMax(value = "9999.99", message = "合格分数不能超过9999.99")
private BigDecimal paperPassScore;
@ApiModelProperty(value = "试题Excel文件第一个sheet单选题第二个sheet多选题第三个sheet判断题", required = true)
@NotNull(message = "请上传试题Excel文件")
private MultipartFile file;
}

View File

@ -23,7 +23,7 @@ public class QuestionDO extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
@TableId(type = IdType.ASSIGN_ID)
@ApiModelProperty("试题ID")
private Long id;

View File

@ -19,7 +19,7 @@ public class QuestionOptionDO extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
@TableId(type = IdType.ASSIGN_ID)
@ApiModelProperty("选项ID")
private Long id;

View File

@ -0,0 +1,30 @@
package org.qinan.cedu.model.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
*
* @author safety-eval
*/
@Data
@NoArgsConstructor
public class PaperImportErrorVO {
@ApiModelProperty(value = "Excel行号从1开始的物理行号")
private Integer rowIndex;
@ApiModelProperty(value = "sheet页名称")
private String sheetName;
@ApiModelProperty(value = "异常原因")
private String errorMessage;
public PaperImportErrorVO(Integer rowIndex, String sheetName, String errorMessage) {
this.rowIndex = rowIndex;
this.sheetName = sheetName;
this.errorMessage = errorMessage;
}
}

View File

@ -0,0 +1,29 @@
package org.qinan.cedu.model.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
*
*
* @author safety-eval
*/
@Data
@NoArgsConstructor
public class PaperImportVO {
@ApiModelProperty(value = "成功导入条数")
private Integer successCount;
@ApiModelProperty(value = "异常明细")
private List<PaperImportErrorVO> errors = new ArrayList<>();
public PaperImportVO(Integer successCount, List<PaperImportErrorVO> errors) {
this.successCount = successCount;
this.errors = errors;
}
}

View File

@ -2,7 +2,9 @@ package org.qinan.cedu.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.qinan.cedu.model.dto.PaperImportDTO;
import org.qinan.cedu.model.entity.PaperDO;
import org.qinan.cedu.model.vo.PaperImportVO;
import java.util.List;
@ -62,4 +64,12 @@ public interface PaperService extends IService<PaperDO> {
* @return id
*/
List<Long> getQuestionIds(Long paperId);
/**
* -Excel//
*
* @param importDTO +Excel
* @return +
*/
PaperImportVO importQuestions(PaperImportDTO importDTO);
}

View File

@ -4,13 +4,22 @@ 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 org.qinan.cedu.mapper.QuestionMapper;
import org.qinan.cedu.model.dto.PaperImportDTO;
import org.qinan.cedu.model.entity.PaperDO;
import org.qinan.cedu.model.entity.PaperQuestionRelDO;
import org.qinan.cedu.model.entity.QuestionDO;
import org.qinan.cedu.model.entity.QuestionOptionDO;
import org.qinan.cedu.model.vo.PaperImportVO;
import org.qinan.cedu.enums.DeleteEnum;
import org.qinan.cedu.enums.PaperStatusEnum;
import org.qinan.cedu.mapper.PaperMapper;
import org.qinan.cedu.service.PaperQuestionRelService;
import org.qinan.cedu.service.PaperService;
import org.qinan.cedu.service.QuestionOptionService;
import org.qinan.cedu.service.QuestionService;
import org.qinan.cedu.service.support.QuestionExcelImporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -18,6 +27,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@ -30,6 +40,14 @@ public class PaperServiceImpl extends ServiceImpl<PaperMapper, PaperDO> implemen
@Autowired
private PaperQuestionRelService paperQuestionRelService;
@Autowired
private QuestionService questionService;
@Autowired
private QuestionMapper questionMapper;
@Autowired
private QuestionExcelImporter questionExcelImporter;
@Override
public IPage<PaperDO> pageQuery(long current, long size, String paperName, String status) {
return this.lambdaQuery()
@ -107,4 +125,46 @@ public class PaperServiceImpl extends ServiceImpl<PaperMapper, PaperDO> implemen
.map(PaperQuestionRelDO::getQuestionId)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public PaperImportVO importQuestions(PaperImportDTO importDTO) {
QuestionExcelImporter.ParseResult parseResult = questionExcelImporter.parse(importDTO.getFile());
List<QuestionDO> questions = parseResult.getQuestions();
if (CollectionUtils.isEmpty(questions)) {
// 没有任何校验通过的试题,不创建试卷,直接返回异常明细
return new PaperImportVO(0, parseResult.getErrors());
}
// 创建试卷
PaperDO paper = new PaperDO();
paper.setPaperName(importDTO.getPaperName().trim());
paper.setPaperTotalScore(importDTO.getPaperTotalScore());
paper.setPaperPassScore(importDTO.getPaperPassScore());
this.savePaper(paper);
// 保存试题和选项id已预置雪花id
LocalDateTime now = LocalDateTime.now();
List<QuestionOptionDO> options = new ArrayList<>();
for (QuestionDO question : questions) {
question.setCreateTime(now);
question.setUpdateTime(now);
question.setVersion(0);
options.addAll(question.getOptions());
}
for (QuestionOptionDO option : options) {
option.setCreateTime(now);
option.setUpdateTime(now);
option.setVersion(0);
}
Db.saveBatch(questions);
Db.saveBatch(options);
// 绑定试卷试题
this.bindQuestions(paper.getId(), questions.stream()
.map(QuestionDO::getId)
.collect(Collectors.toList()));
return new PaperImportVO(questions.size(), parseResult.getErrors());
}
}

View File

@ -0,0 +1,66 @@
package org.qinan.cedu.service.support;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
/**
* /sheetsheet
*/
@Data
public class ChoiceQuestionImportRow {
@ExcelProperty(value = "题目", index = 0)
@NotBlank(message = "题目不能为空")
private String title;
@ExcelProperty(value = "选项A", index = 1)
private String optionA;
@ExcelProperty(value = "选项B", index = 2)
private String optionB;
@ExcelProperty(value = "选项C", index = 3)
private String optionC;
@ExcelProperty(value = "选项D", index = 4)
private String optionD;
@ExcelProperty(value = "答案", index = 5)
@NotBlank(message = "答案不能为空")
@Size(max = 100, message = "答案长度不能超过100个字符")
private String answer;
@ExcelProperty(value = "分值", index = 6)
@NotNull(message = "分值不能为空")
@DecimalMin(value = "0.01", message = "分值必须大于0")
@DecimalMax(value = "999.99", message = "分值不能超过999.99")
private BigDecimal score;
@ExcelProperty(value = "答案解析", index = 7)
@NotBlank(message = "答案解析不能为空")
private String analysis;
@ExcelProperty(value = "标签类型", index = 8)
@NotBlank(message = "标签类型不能为空")
@Size(max = 100, message = "标签类型长度不能超过100个字符")
private String tagType;
@ExcelProperty(value = "关联课件名称", index = 9)
@NotBlank(message = "关联课件名称不能为空")
@Size(max = 200, message = "关联课件名称长度不能超过200个字符")
private String coursewareName;
/**
* 1
*/
@ExcelIgnore
private Integer rowIndex;
}

View File

@ -0,0 +1,54 @@
package org.qinan.cedu.service.support;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
/**
* sheet
*/
@Data
public class JudgeQuestionImportRow {
@ExcelProperty(value = "题目", index = 0)
@NotBlank(message = "题目不能为空")
private String title;
@ExcelProperty(value = "判断答案", index = 1)
@NotBlank(message = "答案不能为空")
@Size(max = 100, message = "答案长度不能超过100个字符")
private String answer;
@ExcelProperty(value = "分值", index = 2)
@NotNull(message = "分值不能为空")
@DecimalMin(value = "0.01", message = "分值必须大于0")
@DecimalMax(value = "999.99", message = "分值不能超过999.99")
private BigDecimal score;
@ExcelProperty(value = "答案解析", index = 3)
@NotBlank(message = "答案解析不能为空")
private String analysis;
@ExcelProperty(value = "标签类型", index = 4)
@NotBlank(message = "标签类型不能为空")
@Size(max = 100, message = "标签类型长度不能超过100个字符")
private String tagType;
@ExcelProperty(value = "关联课件名称", index = 5)
@NotBlank(message = "关联课件名称不能为空")
@Size(max = 200, message = "关联课件名称长度不能超过200个字符")
private String coursewareName;
/**
* 1
*/
@ExcelIgnore
private Integer rowIndex;
}

View File

@ -0,0 +1,385 @@
package org.qinan.cedu.service.support;
import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.qinan.cedu.enums.DeleteEnum;
import org.qinan.cedu.enums.QuestionTypeEnum;
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;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Excel
* <p>
* 23
* sheetsheetsheetT/F A/B
* sheet
*/
@Component
public class QuestionExcelImporter {
/** 模板前2行为说明和表头数据从第3行开始 */
private static final int HEAD_ROW_NUMBER = 2;
private static final int SINGLE_SHEET_NO = 0;
private static final int MULTIPLE_SHEET_NO = 1;
private static final int JUDGE_SHEET_NO = 2;
private static final String[] CHOICE_OPTION_KEYS = {"A", "B", "C", "D"};
@Autowired
private CoursewareManagementService coursewareManagementService;
@Autowired
private Validator validator;
/**
* Excelid
*/
public ParseResult parse(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("请上传试题Excel文件");
}
byte[] bytes = readBytes(file);
Map<Integer, String> sheetNames = readSheetNames(bytes);
List<PaperImportErrorVO> errors = new ArrayList<>();
List<ChoiceQuestionImportRow> singleRows;
List<ChoiceQuestionImportRow> multipleRows;
List<JudgeQuestionImportRow> judgeRows;
try {
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()));
}
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()));
}
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()));
}
Map<String, Long> coursewareIdByName = loadCoursewareIdByName(singleRows, multipleRows, judgeRows);
List<QuestionDO> questions = new ArrayList<>();
convertChoiceRows(singleRows, sheetName(sheetNames, SINGLE_SHEET_NO), QuestionTypeEnum.SINGLE, coursewareIdByName, questions, errors);
convertChoiceRows(multipleRows, sheetName(sheetNames, MULTIPLE_SHEET_NO), QuestionTypeEnum.MULTIPLE, coursewareIdByName, questions, errors);
convertJudgeRows(judgeRows, sheetName(sheetNames, JUDGE_SHEET_NO), coursewareIdByName, questions, errors);
return new ParseResult(questions, errors);
}
private byte[] readBytes(MultipartFile file) {
try {
return file.getBytes();
} catch (IOException e) {
throw new IllegalArgumentException("读取上传文件失败", e);
}
}
private Map<Integer, String> readSheetNames(byte[] bytes) {
ExcelReader excelReader = EasyExcel.read(new ByteArrayInputStream(bytes)).build();
try {
return excelReader.excelExecutor().sheetList().stream()
.collect(Collectors.toMap(ReadSheet::getSheetNo, ReadSheet::getSheetName, (a, b) -> a));
} catch (Exception e) {
return Collections.emptyMap();
} finally {
excelReader.finish();
}
}
private String sheetName(Map<Integer, String> sheetNames, int sheetNo) {
return sheetNames.getOrDefault(sheetNo, "sheet[" + (sheetNo + 1) + "]");
}
private <T> List<T> readSheet(byte[] bytes, int sheetNo, Class<T> headClass) {
return EasyExcel.read(new ByteArrayInputStream(bytes))
.head(headClass)
.sheet(sheetNo)
.headRowNumber(HEAD_ROW_NUMBER)
.doReadSync();
}
/**
* id -> id
*/
private Map<String, Long> loadCoursewareIdByName(List<ChoiceQuestionImportRow> singleRows,
List<ChoiceQuestionImportRow> multipleRows,
List<JudgeQuestionImportRow> judgeRows) {
Set<String> names = new HashSet<>();
singleRows.forEach(row -> addCoursewareName(names, row.getCoursewareName()));
multipleRows.forEach(row -> addCoursewareName(names, row.getCoursewareName()));
judgeRows.forEach(row -> addCoursewareName(names, row.getCoursewareName()));
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();
Map<String, Long> idByName = new HashMap<>();
for (CoursewareManagementDO courseware : coursewares) {
idByName.putIfAbsent(courseware.getCoursewareName(), courseware.getId());
}
return idByName;
}
private void addCoursewareName(Set<String> names, String coursewareName) {
if (StringUtils.hasText(coursewareName)) {
names.add(coursewareName.trim());
}
}
/**
* /sheet
*/
private void convertChoiceRows(List<ChoiceQuestionImportRow> rows, String sheetName, QuestionTypeEnum questionType,
Map<String, Long> coursewareIdByName, List<QuestionDO> questions,
List<PaperImportErrorVO> errors) {
boolean single = questionType == QuestionTypeEnum.SINGLE;
for (int i = 0; i < rows.size(); i++) {
ChoiceQuestionImportRow row = rows.get(i);
int rowIndex = i + HEAD_ROW_NUMBER + 1;
if (isEmptyChoiceRow(row)) {
continue;
}
row.setRowIndex(rowIndex);
// JSR-303 校验(必填、数据库长度限制),不通过则跳过该行
Set<ConstraintViolation<ChoiceQuestionImportRow>> violations = validator.validate(row);
if (!violations.isEmpty()) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, joinViolationMessages(violations)));
continue;
}
// 组装选项(按列固定字母,空白列不生成选项)
List<String> optionTexts = Arrays.asList(trimToNull(row.getOptionA()), trimToNull(row.getOptionB()),
trimToNull(row.getOptionC()), trimToNull(row.getOptionD()));
Map<String, String> options = new LinkedHashMap<>();
for (int j = 0; j < optionTexts.size(); j++) {
if (optionTexts.get(j) != null) {
options.put(CHOICE_OPTION_KEYS[j], optionTexts.get(j));
}
}
if (options.size() < 2) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "选项至少需要填写2个"));
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中的字母"));
continue;
}
List<String> answerLetters = answerRaw.chars()
.mapToObj(c -> String.valueOf((char) c))
.distinct()
.sorted()
.collect(Collectors.toList());
if (single && answerLetters.size() != 1) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "单选题答案只能填写A、B、C、D中的一个"));
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) + "]找不到对应的选项"));
continue;
}
// 校验关联课件名称查询到后回填coursewareId
String coursewareName = row.getCoursewareName().trim();
Long coursewareId = coursewareIdByName.get(coursewareName);
if (coursewareId == null) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "关联课件名称[" + coursewareName + "]不存在"));
continue;
}
questions.add(buildChoiceQuestion(row, questionType, options, answerLetters, coursewareId));
}
}
private QuestionDO buildChoiceQuestion(ChoiceQuestionImportRow row, QuestionTypeEnum questionType,
Map<String, String> optionTexts, List<String> answerLetters,
Long coursewareId) {
QuestionDO question = new QuestionDO();
question.setId(IdUtil.getSnowflakeNextId());
question.setTitle(row.getTitle().trim());
question.setQuestionType(questionType.getCode());
question.setAnswer(String.join(",", answerLetters));
question.setScore(row.getScore());
question.setAnalysis(row.getAnalysis().trim());
question.setTagType(row.getTagType().trim());
question.setCoursewareId(coursewareId);
question.setOptions(buildOptions(question.getId(), optionTexts, answerLetters));
question.setAnswerQuestionOptionIds(joinCorrectOptionIds(question.getOptions()));
return question;
}
/**
* sheetT/F AB
*/
private void convertJudgeRows(List<JudgeQuestionImportRow> rows, String sheetName,
Map<String, Long> coursewareIdByName, List<QuestionDO> questions,
List<PaperImportErrorVO> errors) {
for (int i = 0; i < rows.size(); i++) {
JudgeQuestionImportRow row = rows.get(i);
int rowIndex = i + HEAD_ROW_NUMBER + 1;
if (isEmptyJudgeRow(row)) {
continue;
}
row.setRowIndex(rowIndex);
Set<ConstraintViolation<JudgeQuestionImportRow>> violations = validator.validate(row);
if (!violations.isEmpty()) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, joinViolationMessages(violations)));
continue;
}
String answerRaw = row.getAnswer().trim().toUpperCase();
if (!"T".equals(answerRaw) && !"F".equals(answerRaw)) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "判断题答案只能填写T或F"));
continue;
}
String coursewareName = row.getCoursewareName().trim();
Long coursewareId = coursewareIdByName.get(coursewareName);
if (coursewareId == null) {
errors.add(new PaperImportErrorVO(rowIndex, sheetName, "关联课件名称[" + coursewareName + "]不存在"));
continue;
}
boolean correct = "T".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.setScore(row.getScore());
question.setAnalysis(row.getAnalysis().trim());
question.setTagType(row.getTagType().trim());
question.setCoursewareId(coursewareId);
Map<String, String> optionTexts = new LinkedHashMap<>();
optionTexts.put("A", "对");
optionTexts.put("B", "错");
question.setOptions(buildOptions(question.getId(), optionTexts,
Collections.singletonList(correct ? "A" : "B")));
question.setAnswerQuestionOptionIds(joinCorrectOptionIds(question.getOptions()));
questions.add(question);
}
}
private List<QuestionOptionDO> buildOptions(Long questionId, Map<String, String> optionTexts,
List<String> answerLetters) {
List<QuestionOptionDO> options = new ArrayList<>();
int sortOrder = 1;
for (Map.Entry<String, String> entry : optionTexts.entrySet()) {
QuestionOptionDO option = new QuestionOptionDO();
option.setId(IdUtil.getSnowflakeNextId());
option.setQuestionId(questionId);
option.setOptionKey(entry.getKey());
option.setOptionName(entry.getKey());
option.setOptionText(entry.getValue());
option.setIsCorrect(answerLetters.contains(entry.getKey()));
option.setSortOrder(sortOrder++);
options.add(option);
}
return options;
}
private String joinCorrectOptionIds(List<QuestionOptionDO> options) {
return options.stream()
.filter(option -> Boolean.TRUE.equals(option.getIsCorrect()))
.map(option -> String.valueOf(option.getId()))
.collect(Collectors.joining(","));
}
private String joinViolationMessages(Set<? extends ConstraintViolation<?>> violations) {
return violations.stream()
.map(ConstraintViolation::getMessage)
.distinct()
.collect(Collectors.joining(""));
}
private boolean isEmptyChoiceRow(ChoiceQuestionImportRow row) {
return !StringUtils.hasText(row.getTitle())
&& !StringUtils.hasText(row.getOptionA())
&& !StringUtils.hasText(row.getOptionB())
&& !StringUtils.hasText(row.getOptionC())
&& !StringUtils.hasText(row.getOptionD())
&& !StringUtils.hasText(row.getAnswer())
&& row.getScore() == null
&& !StringUtils.hasText(row.getAnalysis())
&& !StringUtils.hasText(row.getTagType())
&& !StringUtils.hasText(row.getCoursewareName());
}
private boolean isEmptyJudgeRow(JudgeQuestionImportRow row) {
return !StringUtils.hasText(row.getTitle())
&& !StringUtils.hasText(row.getAnswer())
&& row.getScore() == null
&& !StringUtils.hasText(row.getAnalysis())
&& !StringUtils.hasText(row.getTagType())
&& !StringUtils.hasText(row.getCoursewareName());
}
private String trimToNull(String text) {
if (!StringUtils.hasText(text)) {
return null;
}
return text.trim();
}
/**
*
*/
@Getter
@AllArgsConstructor
public static class ParseResult {
/**
*
*/
private final List<QuestionDO> questions;
/**
*
*/
private final List<PaperImportErrorVO> errors;
}
}