paper
parent
906402f7db
commit
4b3e567098
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 单选题/多选题导入行模型(第一个sheet单选题、第二个sheet多选题,列结构一致)
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
* 模板说明:前2行为填写说明和表头,数据从第3行开始;
|
||||
* 第一个sheet单选题、第二个sheet多选题、第三个sheet判断题(T/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;
|
||||
|
||||
/**
|
||||
* 解析上传的试题Excel,返回校验通过的试题(含选项,已预置雪花id)和异常明细
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换判断题sheet数据:T/F 转换为 A对、B错 两个选项
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue