dev-tmp1
luotaiqian 2026-08-17 18:49:59 +08:00
parent 9c9844f9b8
commit a6bc4606c3
17 changed files with 745 additions and 0 deletions

View File

@ -3,6 +3,7 @@ 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.PaperCopyDTO;
import org.qinan.cedu.model.dto.PaperImportDTO;
import org.qinan.cedu.model.dto.PaperPageQueryDTO;
import org.qinan.cedu.model.dto.PaperUpdateDTO;
@ -47,6 +48,13 @@ public class PaperController {
return SingleResponse.success();
}
@ApiOperation("复制试卷")
@PostMapping("/copy")
public SingleResponse<Void> copy(@Validated @RequestBody PaperCopyDTO copyDTO) {
paperService.copyPaper(copyDTO);
return SingleResponse.success();
}
@ApiOperation("新建试卷-导入试题")
@PostMapping("/import")

View File

@ -0,0 +1,40 @@
package org.qinan.cedu.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.qinan.cedu.model.dto.PaperQuestionDeleteDTO;
import org.qinan.cedu.model.dto.PaperQuestionSaveDTO;
import org.qinan.cedu.service.PaperQuestionRelService;
import org.qinan.safetyeval.client.dto.SingleResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
*/
@Api(tags = "试卷试题管理")
@RestController
@RequestMapping("/safetyEval/paperQuestionRel")
public class PaperQuestionRelController {
@Autowired
private PaperQuestionRelService paperQuestionRelService;
@ApiOperation("新增试卷试题(习题)")
@PostMapping("/save")
public SingleResponse<Void> save(@Validated @RequestBody PaperQuestionSaveDTO dto) {
paperQuestionRelService.savePaperQuestion(dto);
return SingleResponse.success();
}
@ApiOperation("删除习题")
@PostMapping("/delete")
public SingleResponse<Void> delete(@Validated @RequestBody PaperQuestionDeleteDTO dto) {
paperQuestionRelService.deletePaperQuestion(dto.getPaperId(), dto.getQuestionId());
return SingleResponse.success();
}
}

View File

@ -1,8 +1,19 @@
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.PaperQuestionPageQueryDTO;
import org.qinan.cedu.model.dto.QuestionUpdateDTO;
import org.qinan.cedu.model.vo.PaperQuestionVO;
import org.qinan.cedu.service.QuestionService;
import org.qinan.safetyeval.client.dto.PageResponse;
import org.qinan.safetyeval.client.dto.SingleResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -17,5 +28,17 @@ public class QuestionController {
@Autowired
private QuestionService questionService;
@ApiOperation("编辑习题")
@PostMapping("/modify")
public SingleResponse<Void> modify(@Validated @RequestBody QuestionUpdateDTO dto) {
questionService.updateQuestion(dto);
return SingleResponse.success();
}
@ApiOperation("试卷试题分页查询")
@GetMapping("/page")
public PageResponse<PaperQuestionVO> page(@Validated PaperQuestionPageQueryDTO query) {
IPage<PaperQuestionVO> page = questionService.pagePaperQuestions(query);
return PageResponse.of(page.getRecords(), page.getTotal());
}
}

View File

@ -1,12 +1,26 @@
package org.qinan.cedu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.qinan.cedu.model.dto.PaperQuestionPageQueryDTO;
import org.qinan.cedu.model.entity.QuestionDO;
import org.qinan.cedu.model.vo.PaperQuestionVO;
/**
* Mapper
*/
@Mapper
public interface QuestionMapper extends BaseMapper<QuestionDO> {
/**
*
*
* @param page
* @param query id
* @return
*/
IPage<PaperQuestionVO> pagePaperQuestions(Page<PaperQuestionVO> page, @Param("query") PaperQuestionPageQueryDTO query);
}

View File

@ -0,0 +1,41 @@
package org.qinan.cedu.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
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;
/**
*
*/
@Data
@ApiModel(value = "PaperCopyDTO", description = "复制试卷请求")
public class PaperCopyDTO {
@ApiModelProperty(value = "源试卷id", required = true)
@NotNull(message = "试卷id不能为空")
private Long id;
@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;
}

View File

@ -0,0 +1,23 @@
package org.qinan.cedu.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
*
*/
@Data
@ApiModel(value = "PaperQuestionDeleteDTO", description = "删除习题请求")
public class PaperQuestionDeleteDTO {
@ApiModelProperty(value = "试卷id", required = true)
@NotNull(message = "试卷id不能为空")
private Long paperId;
@ApiModelProperty(value = "试题id", required = true)
@NotNull(message = "试题id不能为空")
private Long questionId;
}

View File

@ -0,0 +1,28 @@
package org.qinan.cedu.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.qinan.cedu.model.query.BasePageQuery;
import javax.validation.constraints.NotNull;
/**
*
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "PaperQuestionPageQueryDTO", description = "试卷试题分页查询请求")
public class PaperQuestionPageQueryDTO extends BasePageQuery {
@ApiModelProperty(value = "试卷id", required = true)
@NotNull(message = "试卷id不能为空")
private Long paperId;
@ApiModelProperty("题干(模糊)")
private String title;
@ApiModelProperty("题目类型single单选/multiple多选/judge判断")
private String questionType;
}

View File

@ -0,0 +1,69 @@
package org.qinan.cedu.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
@ApiModel(value = "PaperQuestionSaveDTO", description = "新增试卷试题请求")
public class PaperQuestionSaveDTO {
@ApiModelProperty(value = "试卷id", required = true)
@NotNull(message = "试卷id不能为空")
private Long paperId;
@ApiModelProperty(value = "题目类型single单选/multiple多选", required = true)
@NotBlank(message = "题目类型不能为空")
@Pattern(regexp = "single|multiple", message = "题目类型只能填写single单选或multiple多选")
private String questionType;
@ApiModelProperty(value = "题干", required = true)
@NotBlank(message = "题干不能为空")
private String title;
@ApiModelProperty(value = "分值", required = true)
@NotNull(message = "分值不能为空")
@DecimalMin(value = "0.01", message = "分值必须大于0")
@DecimalMax(value = "999.99", message = "分值不能超过999.99")
private BigDecimal score;
@ApiModelProperty(value = "选项A内容", required = true)
@NotBlank(message = "选项A不能为空")
private String optionA;
@ApiModelProperty(value = "选项B内容", required = true)
@NotBlank(message = "选项B不能为空")
private String optionB;
@ApiModelProperty(value = "选项C内容")
@Size(max = 64, message = "选项C长度不能超过64个字符")
private String optionC;
@ApiModelProperty(value = "选项D内容")
@Size(max = 64, message = "选项D长度不能超过64个字符")
private String optionD;
@ApiModelProperty(value = "答案单选填写A多选填写A,B", required = true)
@NotEmpty(message = "答案不能为空")
private List<String> answer;
@ApiModelProperty(value = "关联课件id courseware_management_id", required = true)
@NotNull(message = "关联课件不能为空")
private Long coursewareManagementId;
@ApiModelProperty(value = "答案解析")
private String analysis;
}

View File

@ -0,0 +1,63 @@
package org.qinan.cedu.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
@ApiModel(value = "QuestionUpdateDTO", description = "编辑习题请求")
public class QuestionUpdateDTO {
@ApiModelProperty(value = "试题id", required = true)
@NotNull(message = "试题id不能为空")
private Long id;
@ApiModelProperty(value = "题干", required = true)
@NotBlank(message = "题干不能为空")
private String title;
@ApiModelProperty(value = "分值", required = true)
@NotNull(message = "分值不能为空")
@DecimalMin(value = "0.01", message = "分值必须大于0")
@DecimalMax(value = "999.99", message = "分值不能超过999.99")
private BigDecimal score;
@ApiModelProperty(value = "选项A内容", required = true)
@NotBlank(message = "选项A不能为空")
private String optionA;
@ApiModelProperty(value = "选项B内容", required = true)
@NotBlank(message = "选项B不能为空")
private String optionB;
@ApiModelProperty(value = "选项C内容")
@Size(max = 64, message = "选项C长度不能超过64个字符")
private String optionC;
@ApiModelProperty(value = "选项D内容")
@Size(max = 64, message = "选项D长度不能超过64个字符")
private String optionD;
@ApiModelProperty(value = "答案单选填写A多选填写A,B", required = true)
@NotEmpty(message = "答案不能为空")
private List<String> answer;
@ApiModelProperty(value = "关联课件id courseware_management_id", required = true)
@NotNull(message = "关联课件不能为空")
private Long coursewareManagementId;
@ApiModelProperty(value = "答案解析")
private String analysis;
}

View File

@ -0,0 +1,42 @@
package org.qinan.cedu.model.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
*
*/
@Data
@ApiModel(value = "PaperQuestionVO", description = "试卷试题")
public class PaperQuestionVO {
@ApiModelProperty("试题id")
private Long id;
@ApiModelProperty("题干")
private String title;
@ApiModelProperty("分值")
private BigDecimal score;
@ApiModelProperty("答案选项id多个逗号隔开")
private String answerQuestionOptionIds;
@ApiModelProperty("答案解析")
private String analysis;
@ApiModelProperty("题型single单选/multiple多选/judge判断")
private String questionType;
@ApiModelProperty("标签类型")
private String tagType;
@ApiModelProperty("试题号")
private Integer questionNum;
@ApiModelProperty("关联课件名称")
private String coursewareName;
}

View File

@ -1,10 +1,28 @@
package org.qinan.cedu.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.qinan.cedu.model.dto.PaperQuestionSaveDTO;
import org.qinan.cedu.model.entity.PaperQuestionRelDO;
import javax.validation.constraints.NotNull;
/**
*
*/
public interface PaperQuestionRelService extends IService<PaperQuestionRelDO> {
/**
* +
*
* @param dto id+
*/
void savePaperQuestion(PaperQuestionSaveDTO dto);
/**
* paper_question_relquestion
*
* @param paperId id
* @param questionId id
*/
void deletePaperQuestion(@NotNull Long paperId, @NotNull Long questionId);
}

View File

@ -2,6 +2,7 @@ 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.PaperCopyDTO;
import org.qinan.cedu.model.dto.PaperImportDTO;
import org.qinan.cedu.model.dto.PaperPageQueryDTO;
import org.qinan.cedu.model.dto.PaperUpdateDTO;
@ -65,6 +66,13 @@ public interface PaperService extends IService<PaperDO> {
*/
List<Long> getQuestionIds(Long paperId);
/**
*
*
* @param copyDTO id+
*/
void copyPaper(PaperCopyDTO copyDTO);
/**
* -Excel//
*

View File

@ -2,8 +2,11 @@ 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.PaperQuestionPageQueryDTO;
import org.qinan.cedu.model.dto.QuestionUpdateDTO;
import org.qinan.cedu.model.entity.QuestionDO;
import org.qinan.cedu.model.entity.QuestionOptionDO;
import org.qinan.cedu.model.vo.PaperQuestionVO;
import java.util.List;
@ -12,6 +15,21 @@ import java.util.List;
*/
public interface QuestionService extends IService<QuestionDO> {
/**
*
*
* @param dto
*/
void updateQuestion(QuestionUpdateDTO dto);
/**
* XML
*
* @param query id
* @return
*/
IPage<PaperQuestionVO> pagePaperQuestions(PaperQuestionPageQueryDTO query);
/**
*
*

View File

@ -1,14 +1,152 @@
package org.qinan.cedu.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.qinan.cedu.enums.DeleteEnum;
import org.qinan.cedu.enums.QuestionTypeEnum;
import org.qinan.cedu.model.dto.PaperQuestionSaveDTO;
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.mapper.PaperQuestionRelMapper;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
*
*/
@Service
public class PaperQuestionRelServiceImpl extends ServiceImpl<PaperQuestionRelMapper, PaperQuestionRelDO> implements PaperQuestionRelService {
@Autowired
private PaperService paperService;
@Autowired
private QuestionService questionService;
@Autowired
private QuestionOptionService questionOptionService;
@Override
@Transactional(rollbackFor = Exception.class)
public void savePaperQuestion(PaperQuestionSaveDTO dto) {
// 校验试卷存在
PaperDO paper = paperService.lambdaQuery()
.eq(PaperDO::getId, dto.getPaperId())
.eq(PaperDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
.one();
if (paper == null) {
throw new IllegalArgumentException("试卷不存在id=" + dto.getPaperId());
}
// 组装选项按A-D列空白列不生成选项
Map<String, String> optionTexts = new LinkedHashMap<>();
optionTexts.put("A", dto.getOptionA().trim());
optionTexts.put("B", dto.getOptionB().trim());
putIfHasText(optionTexts, "C", dto.getOptionC());
putIfHasText(optionTexts, "D", dto.getOptionD());
// 校验答案必须能找到对应的选项
List<String> answers = dto.getAnswer().stream()
.map(String::trim)
.map(String::toUpperCase)
.distinct()
.sorted()
.collect(Collectors.toList());
List<String> notMatched = answers.stream()
.filter(answer -> !optionTexts.containsKey(answer))
.collect(Collectors.toList());
if (!notMatched.isEmpty()) {
throw new IllegalArgumentException("答案[" + String.join(",", notMatched) + "]找不到对应的选项");
}
boolean single = QuestionTypeEnum.SINGLE.getCode().equals(dto.getQuestionType());
if (single && answers.size() != 1) {
throw new IllegalArgumentException("单选题答案只能有一个");
}
// 保存试题和选项id用雪花id
LocalDateTime now = LocalDateTime.now();
QuestionDO question = new QuestionDO();
question.setId(IdUtil.getSnowflakeNextId());
question.setTitle(dto.getTitle().trim());
question.setQuestionType(dto.getQuestionType());
question.setAnswer(String.join(",", answers));
question.setScore(dto.getScore());
question.setAnalysis(StringUtils.hasText(dto.getAnalysis()) ? dto.getAnalysis().trim() : null);
question.setCoursewareManagementId(dto.getCoursewareManagementId());
question.setCreateTime(now);
question.setUpdateTime(now);
question.setVersion(0);
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(question.getId());
option.setOptionKey(entry.getKey());
option.setOptionName(entry.getKey());
option.setOptionText(entry.getValue());
option.setIsCorrect(answers.contains(entry.getKey()));
option.setSortOrder(sortOrder++);
option.setCreateTime(now);
option.setUpdateTime(now);
option.setVersion(0);
options.add(option);
}
question.setAnswerQuestionOptionIds(options.stream()
.filter(option -> Boolean.TRUE.equals(option.getIsCorrect()))
.map(option -> String.valueOf(option.getId()))
.collect(Collectors.joining(",")));
questionService.save(question);
questionOptionService.saveBatch(options);
// 追加绑定试卷试题(不清空已有绑定)
PaperQuestionRelDO rel = new PaperQuestionRelDO();
rel.setPaperId(dto.getPaperId());
rel.setQuestionId(question.getId());
rel.setCreateTime(now);
rel.setUpdateTime(now);
this.save(rel);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deletePaperQuestion(Long paperId, Long questionId) {
// 删除试卷试题关系(物理删除该试卷下的绑定)
boolean removed = this.lambdaUpdate()
.eq(PaperQuestionRelDO::getPaperId, paperId)
.eq(PaperQuestionRelDO::getQuestionId, questionId)
.remove();
if (!removed) {
throw new IllegalArgumentException("试卷试题关系不存在paperId=" + paperId + "questionId=" + questionId);
}
// 逻辑删除试题及其选项
boolean deleted = questionService.deleteById(questionId);
if (!deleted) {
throw new IllegalArgumentException("试题不存在id=" + questionId);
}
}
private void putIfHasText(Map<String, String> optionTexts, String key, String value) {
if (StringUtils.hasText(value)) {
optionTexts.put(key, value.trim());
}
}
}

View File

@ -6,12 +6,14 @@ 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 cn.hutool.core.util.IdUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.qinan.cedu.convertor.PaperConvertor;
import org.qinan.cedu.enums.DeleteEnum;
import org.qinan.cedu.enums.PaperStatusEnum;
import org.qinan.cedu.mapper.PaperMapper;
import org.qinan.cedu.model.dto.PaperCopyDTO;
import org.qinan.cedu.model.dto.PaperImportDTO;
import org.qinan.cedu.model.dto.PaperPageQueryDTO;
import org.qinan.cedu.model.dto.PaperUpdateDTO;
@ -23,6 +25,7 @@ import org.qinan.cedu.model.vo.PaperImportVO;
import org.qinan.cedu.model.vo.PaperPageVO;
import org.qinan.cedu.service.PaperQuestionRelService;
import org.qinan.cedu.service.PaperService;
import org.qinan.cedu.service.QuestionOptionService;
import org.qinan.cedu.service.support.QuestionExcelImporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -32,6 +35,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@ -44,6 +48,7 @@ import java.util.stream.Collectors;
public class PaperServiceImpl extends ServiceImpl<PaperMapper, PaperDO> implements PaperService {
private final PaperQuestionRelService paperQuestionRelService;
private final QuestionOptionService questionOptionService;
private final QuestionExcelImporter questionExcelImporter;
@Autowired
@ -90,6 +95,93 @@ public class PaperServiceImpl extends ServiceImpl<PaperMapper, PaperDO> implemen
return this.updateById(paper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void copyPaper(PaperCopyDTO copyDTO) {
// 查询源试卷
PaperDO source = this.lambdaQuery()
.eq(PaperDO::getId, copyDTO.getId())
.eq(PaperDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
.one();
if (source == null) {
throw new IllegalArgumentException("试卷不存在id=" + copyDTO.getId());
}
// 查询源试卷绑定的试题及选项
List<PaperQuestionRelDO> rels = paperQuestionRelService.lambdaQuery()
.eq(PaperQuestionRelDO::getPaperId, source.getId())
.eq(PaperQuestionRelDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
.list();
List<Long> questionIds = rels.stream().map(PaperQuestionRelDO::getQuestionId).collect(Collectors.toList());
List<QuestionDO> questions = questionIds.isEmpty() ? new ArrayList<>()
: Db.listByIds(questionIds, QuestionDO.class).stream()
.filter(q -> DeleteEnum.FALSE.getCode().equals(q.getDeleteEnum()))
.collect(Collectors.toList());
List<Long> existQuestionIds = questions.stream().map(QuestionDO::getId).collect(Collectors.toList());
List<QuestionOptionDO> allOptions = existQuestionIds.isEmpty() ? new ArrayList<>()
: questionOptionService.lambdaQuery()
.in(QuestionOptionDO::getQuestionId, existQuestionIds)
.eq(QuestionOptionDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
.list();
// 新增试卷(使用请求中的名称/总分/合格分数)
LocalDateTime now = LocalDateTime.now();
PaperDO target = new PaperDO();
target.setPaperName(copyDTO.getPaperName().trim());
target.setPaperTotalScore(copyDTO.getPaperTotalScore());
target.setPaperPassScore(copyDTO.getPaperPassScore());
target.setStatus(source.getStatus());
this.savePaper(target);
// 复制试题+选项+试卷试题关系id全部换新雪花id
if (questions.isEmpty()) {
return;
}
List<QuestionOptionDO> newOptions = new ArrayList<>();
List<PaperQuestionRelDO> newRels = new ArrayList<>();
for (QuestionDO question : questions) {
Long oldQuestionId = question.getId();
Long newQuestionId = IdUtil.getSnowflakeNextId();
question.setId(newQuestionId);
question.setCreateTime(now);
question.setUpdateTime(now);
question.setVersion(0);
List<QuestionOptionDO> options = allOptions.stream()
.filter(option -> oldQuestionId.equals(option.getQuestionId()))
.collect(Collectors.toList());
for (QuestionOptionDO option : options) {
Long oldOptionId = option.getId();
option.setId(IdUtil.getSnowflakeNextId());
option.setQuestionId(newQuestionId);
option.setCreateTime(now);
option.setUpdateTime(now);
option.setVersion(0);
if (StringUtils.hasText(question.getAnswerQuestionOptionIds())) {
// 替换正确选项id引用
String replaced = Arrays.stream(question.getAnswerQuestionOptionIds().split(","))
.map(String::trim)
.filter(StringUtils::hasText)
.map(oldId -> oldId.equals(String.valueOf(oldOptionId))
? String.valueOf(option.getId()) : oldId)
.collect(Collectors.joining(","));
question.setAnswerQuestionOptionIds(replaced);
}
newOptions.add(option);
}
PaperQuestionRelDO rel = new PaperQuestionRelDO();
rel.setPaperId(target.getId());
rel.setQuestionId(newQuestionId);
rel.setCreateTime(now);
rel.setUpdateTime(now);
newRels.add(rel);
}
Db.saveBatch(questions);
Db.saveBatch(newOptions);
paperQuestionRelService.saveBatch(newRels);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteById(Long id) {

View File

@ -1,11 +1,16 @@
package org.qinan.cedu.service.impl;
import cn.hutool.core.util.IdUtil;
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 org.qinan.cedu.enums.QuestionTypeEnum;
import org.qinan.cedu.model.dto.PaperQuestionPageQueryDTO;
import org.qinan.cedu.model.dto.QuestionUpdateDTO;
import org.qinan.cedu.model.entity.QuestionDO;
import org.qinan.cedu.model.entity.QuestionOptionDO;
import org.qinan.cedu.model.vo.PaperQuestionVO;
import org.qinan.cedu.enums.DeleteEnum;
import org.qinan.cedu.mapper.QuestionMapper;
import org.qinan.cedu.service.QuestionOptionService;
@ -17,7 +22,11 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
*
@ -28,6 +37,94 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, QuestionDO>
@Autowired
private QuestionOptionService questionOptionService;
@Override
public IPage<PaperQuestionVO> pagePaperQuestions(PaperQuestionPageQueryDTO query) {
return this.baseMapper.pagePaperQuestions(new Page<>(query.getCurrent(), query.getSize()), query);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateQuestion(QuestionUpdateDTO dto) {
// 校验试题存在且未删除
QuestionDO exist = this.lambdaQuery()
.eq(QuestionDO::getId, dto.getId())
.eq(QuestionDO::getDeleteEnum, DeleteEnum.FALSE.getCode())
.one();
if (exist == null) {
throw new IllegalArgumentException("试题不存在id=" + dto.getId());
}
// 组装选项按A-D空白列不生成选项
Map<String, String> optionTexts = new LinkedHashMap<>();
optionTexts.put("A", dto.getOptionA().trim());
optionTexts.put("B", dto.getOptionB().trim());
putIfHasText(optionTexts, "C", dto.getOptionC());
putIfHasText(optionTexts, "D", dto.getOptionD());
// 校验答案必须能找到对应的选项
List<String> answers = dto.getAnswer().stream()
.map(String::trim)
.map(String::toUpperCase)
.distinct()
.sorted()
.collect(Collectors.toList());
List<String> notMatched = answers.stream()
.filter(answer -> !optionTexts.containsKey(answer))
.collect(Collectors.toList());
if (!notMatched.isEmpty()) {
throw new IllegalArgumentException("答案[" + String.join(",", notMatched) + "]找不到对应的选项");
}
boolean single = QuestionTypeEnum.SINGLE.getCode().equals(exist.getQuestionType());
if (single && answers.size() != 1) {
throw new IllegalArgumentException("单选题答案只能有一个");
}
// 更新试题选项全量覆盖重建id预置雪花id
LocalDateTime now = LocalDateTime.now();
QuestionDO question = new QuestionDO();
question.setId(dto.getId());
question.setTitle(dto.getTitle().trim());
question.setAnswer(String.join(",", answers));
question.setScore(dto.getScore());
question.setAnalysis(StringUtils.hasText(dto.getAnalysis()) ? dto.getAnalysis().trim() : null);
question.setCoursewareManagementId(dto.getCoursewareManagementId());
question.setUpdateTime(now);
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(dto.getId());
option.setOptionKey(entry.getKey());
option.setOptionName(entry.getKey());
option.setOptionText(entry.getValue());
option.setIsCorrect(answers.contains(entry.getKey()));
option.setSortOrder(sortOrder++);
option.setCreateTime(now);
option.setUpdateTime(now);
options.add(option);
}
question.setAnswerQuestionOptionIds(options.stream()
.filter(option -> Boolean.TRUE.equals(option.getIsCorrect()))
.map(option -> String.valueOf(option.getId()))
.collect(Collectors.joining(",")));
boolean updated = this.updateById(question);
if (!updated) {
throw new IllegalArgumentException("编辑习题失败id=" + dto.getId());
}
questionOptionService.remove(Wrappers.<QuestionOptionDO>lambdaQuery()
.eq(QuestionOptionDO::getQuestionId, dto.getId()));
questionOptionService.saveBatch(options);
}
private void putIfHasText(Map<String, String> optionTexts, String key, String value) {
if (StringUtils.hasText(value)) {
optionTexts.put(key, value.trim());
}
}
@Override
public IPage<QuestionDO> pageQuery(long current, long size, String title, String questionType) {
return this.lambdaQuery()

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.qinan.cedu.mapper.QuestionMapper">
<select id="pagePaperQuestions" resultType="org.qinan.cedu.model.vo.PaperQuestionVO">
SELECT q.id, q.title, q.score, q.answer_question_option_ids, q.analysis
, q.question_type, q.tag_type, q.question_num
, cm.courseware_name
FROM paper_question_rel pqr
JOIN question q ON pqr.question_id = q.id
LEFT JOIN courseware_management cm ON cm.id = q.courseware_management_id
WHERE pqr.paper_id = #{query.paperId}
AND pqr.delete_enum = 0
AND q.delete_enum = 0
<if test="query.title != null and query.title != ''">
AND q.title LIKE CONCAT('%', #{query.title}, '%')
</if>
<if test="query.questionType != null and query.questionType != ''">
AND q.question_type = #{query.questionType}
</if>
ORDER BY q.id DESC
</select>
</mapper>