From a6bc4606c30cd90082fe43f62dd36facb2fe6c25 Mon Sep 17 00:00:00 2001 From: luotaiqian <1147642922@qq.com> Date: Mon, 17 Aug 2026 18:49:59 +0800 Subject: [PATCH] paper --- .../cedu/controller/PaperController.java | 8 + .../PaperQuestionRelController.java | 40 +++++ .../cedu/controller/QuestionController.java | 23 +++ .../org/qinan/cedu/mapper/QuestionMapper.java | 14 ++ .../qinan/cedu/model/dto/PaperCopyDTO.java | 41 ++++++ .../model/dto/PaperQuestionDeleteDTO.java | 23 +++ .../model/dto/PaperQuestionPageQueryDTO.java | 28 ++++ .../cedu/model/dto/PaperQuestionSaveDTO.java | 69 +++++++++ .../cedu/model/dto/QuestionUpdateDTO.java | 63 ++++++++ .../qinan/cedu/model/vo/PaperQuestionVO.java | 42 ++++++ .../cedu/service/PaperQuestionRelService.java | 18 +++ .../org/qinan/cedu/service/PaperService.java | 8 + .../qinan/cedu/service/QuestionService.java | 18 +++ .../impl/PaperQuestionRelServiceImpl.java | 138 ++++++++++++++++++ .../cedu/service/impl/PaperServiceImpl.java | 92 ++++++++++++ .../service/impl/QuestionServiceImpl.java | 97 ++++++++++++ .../main/resources/mapper/QuestionMapper.xml | 23 +++ 17 files changed, 745 insertions(+) create mode 100644 safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperQuestionRelController.java create mode 100644 safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperCopyDTO.java create mode 100644 safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionDeleteDTO.java create mode 100644 safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionPageQueryDTO.java create mode 100644 safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionSaveDTO.java create mode 100644 safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/QuestionUpdateDTO.java create mode 100644 safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperQuestionVO.java create mode 100644 safety-cedu-service/src/main/resources/mapper/QuestionMapper.xml diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java index f9a11c2f..cd1464eb 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java @@ -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 copy(@Validated @RequestBody PaperCopyDTO copyDTO) { + paperService.copyPaper(copyDTO); + return SingleResponse.success(); + } + @ApiOperation("新建试卷-导入试题") @PostMapping("/import") diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperQuestionRelController.java b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperQuestionRelController.java new file mode 100644 index 00000000..024c7e3c --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperQuestionRelController.java @@ -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 save(@Validated @RequestBody PaperQuestionSaveDTO dto) { + paperQuestionRelService.savePaperQuestion(dto); + return SingleResponse.success(); + } + + @ApiOperation("删除习题") + @PostMapping("/delete") + public SingleResponse delete(@Validated @RequestBody PaperQuestionDeleteDTO dto) { + paperQuestionRelService.deletePaperQuestion(dto.getPaperId(), dto.getQuestionId()); + return SingleResponse.success(); + } +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/QuestionController.java b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/QuestionController.java index d7b9210a..849a4c45 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/QuestionController.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/QuestionController.java @@ -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 modify(@Validated @RequestBody QuestionUpdateDTO dto) { + questionService.updateQuestion(dto); + return SingleResponse.success(); + } + @ApiOperation("试卷试题分页查询") + @GetMapping("/page") + public PageResponse page(@Validated PaperQuestionPageQueryDTO query) { + IPage page = questionService.pagePaperQuestions(query); + return PageResponse.of(page.getRecords(), page.getTotal()); + } } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/mapper/QuestionMapper.java b/safety-cedu-service/src/main/java/org/qinan/cedu/mapper/QuestionMapper.java index 9779a640..eddca883 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/mapper/QuestionMapper.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/mapper/QuestionMapper.java @@ -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 { + + /** + * 试卷试题分页查询(关联课件名称) + * + * @param page 分页参数 + * @param query 查询条件(试卷id、题干、题目类型) + * @return 试题分页 + */ + IPage pagePaperQuestions(Page page, @Param("query") PaperQuestionPageQueryDTO query); } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperCopyDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperCopyDTO.java new file mode 100644 index 00000000..f0ec5354 --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperCopyDTO.java @@ -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; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionDeleteDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionDeleteDTO.java new file mode 100644 index 00000000..e15cf58e --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionDeleteDTO.java @@ -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; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionPageQueryDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionPageQueryDTO.java new file mode 100644 index 00000000..334e4ee6 --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionPageQueryDTO.java @@ -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; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionSaveDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionSaveDTO.java new file mode 100644 index 00000000..6b47a90c --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperQuestionSaveDTO.java @@ -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 answer; + + @ApiModelProperty(value = "关联课件id courseware_management_id", required = true) + @NotNull(message = "关联课件不能为空") + private Long coursewareManagementId; + + @ApiModelProperty(value = "答案解析") + private String analysis; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/QuestionUpdateDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/QuestionUpdateDTO.java new file mode 100644 index 00000000..d140a0e2 --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/QuestionUpdateDTO.java @@ -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 answer; + + @ApiModelProperty(value = "关联课件id courseware_management_id", required = true) + @NotNull(message = "关联课件不能为空") + private Long coursewareManagementId; + + @ApiModelProperty(value = "答案解析") + private String analysis; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperQuestionVO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperQuestionVO.java new file mode 100644 index 00000000..265aa7d7 --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperQuestionVO.java @@ -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; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperQuestionRelService.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperQuestionRelService.java index cec206d3..3e31e65e 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperQuestionRelService.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperQuestionRelService.java @@ -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 { + + /** + * 新增试卷试题(习题):保存试题+选项,并绑定试卷 + * + * @param dto 试卷id+题目内容 + */ + void savePaperQuestion(PaperQuestionSaveDTO dto); + + /** + * 删除习题:同时删除试卷试题关系(paper_question_rel)和试题(question,含选项) + * + * @param paperId 试卷id + * @param questionId 试题id + */ + void deletePaperQuestion(@NotNull Long paperId, @NotNull Long questionId); } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java index b27bd8ad..1311c4c4 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java @@ -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 { */ List getQuestionIds(Long paperId); + /** + * 复制试卷:新增试卷,并复制其下试题、选项、试卷试题关系 + * + * @param copyDTO 源试卷id+新试卷信息 + */ + void copyPaper(PaperCopyDTO copyDTO); + /** * 新建试卷-导入试题方式:解析试题Excel(单选/多选/判断),校验通过后创建试卷并绑定试题 * diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/QuestionService.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/QuestionService.java index d1061527..bc07b64b 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/QuestionService.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/QuestionService.java @@ -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 { + /** + * 编辑习题(选项全量覆盖) + * + * @param dto 编辑内容 + */ + void updateQuestion(QuestionUpdateDTO dto); + + /** + * 试卷试题分页查询(XML关联查询,含课件名称) + * + * @param query 查询条件(试卷id、题干、题目类型) + * @return 试题分页 + */ + IPage pagePaperQuestions(PaperQuestionPageQueryDTO query); + /** * 分页查询试题 * diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperQuestionRelServiceImpl.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperQuestionRelServiceImpl.java index e935c4fe..3bea1f9c 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperQuestionRelServiceImpl.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperQuestionRelServiceImpl.java @@ -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 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 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 answers = dto.getAnswer().stream() + .map(String::trim) + .map(String::toUpperCase) + .distinct() + .sorted() + .collect(Collectors.toList()); + List 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 options = new ArrayList<>(); + int sortOrder = 1; + for (Map.Entry 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 optionTexts, String key, String value) { + if (StringUtils.hasText(value)) { + optionTexts.put(key, value.trim()); + } + } } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java index 9d8b7f6b..6079aee2 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java @@ -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 implements PaperService { private final PaperQuestionRelService paperQuestionRelService; + private final QuestionOptionService questionOptionService; private final QuestionExcelImporter questionExcelImporter; @Autowired @@ -90,6 +95,93 @@ public class PaperServiceImpl extends ServiceImpl 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 rels = paperQuestionRelService.lambdaQuery() + .eq(PaperQuestionRelDO::getPaperId, source.getId()) + .eq(PaperQuestionRelDO::getDeleteEnum, DeleteEnum.FALSE.getCode()) + .list(); + List questionIds = rels.stream().map(PaperQuestionRelDO::getQuestionId).collect(Collectors.toList()); + List questions = questionIds.isEmpty() ? new ArrayList<>() + : Db.listByIds(questionIds, QuestionDO.class).stream() + .filter(q -> DeleteEnum.FALSE.getCode().equals(q.getDeleteEnum())) + .collect(Collectors.toList()); + List existQuestionIds = questions.stream().map(QuestionDO::getId).collect(Collectors.toList()); + List 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 newOptions = new ArrayList<>(); + List 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 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) { diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/QuestionServiceImpl.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/QuestionServiceImpl.java index bcbf3a96..b72ccb5c 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/QuestionServiceImpl.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/QuestionServiceImpl.java @@ -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 @Autowired private QuestionOptionService questionOptionService; + @Override + public IPage 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 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 answers = dto.getAnswer().stream() + .map(String::trim) + .map(String::toUpperCase) + .distinct() + .sorted() + .collect(Collectors.toList()); + List 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 options = new ArrayList<>(); + int sortOrder = 1; + for (Map.Entry 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.lambdaQuery() + .eq(QuestionOptionDO::getQuestionId, dto.getId())); + questionOptionService.saveBatch(options); + } + + private void putIfHasText(Map optionTexts, String key, String value) { + if (StringUtils.hasText(value)) { + optionTexts.put(key, value.trim()); + } + } + @Override public IPage pageQuery(long current, long size, String title, String questionType) { return this.lambdaQuery() diff --git a/safety-cedu-service/src/main/resources/mapper/QuestionMapper.xml b/safety-cedu-service/src/main/resources/mapper/QuestionMapper.xml new file mode 100644 index 00000000..9bc0ee42 --- /dev/null +++ b/safety-cedu-service/src/main/resources/mapper/QuestionMapper.xml @@ -0,0 +1,23 @@ + + + + + +