diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperExamController.java b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperExamController.java index ec50bc66..1b337ad2 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperExamController.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperExamController.java @@ -5,10 +5,14 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; +import org.qinan.cedu.model.dto.AnswerQuestionDTO; +import org.qinan.cedu.model.dto.StartExamDTO; import org.qinan.cedu.model.vo.StartExamVO; import org.qinan.cedu.service.PaperExamService; import org.qinan.safetyeval.client.dto.SingleResponse; +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.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -29,7 +33,28 @@ public class PaperExamController { */ @ApiOperation("去考试") @PostMapping("/startExam") - public SingleResponse startExam(@ApiParam(value = "试卷id", required = true) @RequestParam Long paperId) { - return SingleResponse.success(paperExamService.startExam(paperId, AuthContext.getUserId())); + public SingleResponse startExam(@Validated @RequestBody StartExamDTO dto) { + dto.setUserId(AuthContext.getUserId()); + return SingleResponse.success(paperExamService.startExam(dto)); + } + + /** + * 作答:保存用户作答答案,判定正确状态并计算作答得分 + */ + @ApiOperation("作答") + @PostMapping("/answerQuestion") + public SingleResponse answerQuestion(@Validated @RequestBody AnswerQuestionDTO dto) { + paperExamService.answerQuestion(dto, AuthContext.getUserId()); + return SingleResponse.success(); + } + + /** + * 交卷:汇总各试题作答得分,更新考试成绩、是否通过并结束考试 + */ + @ApiOperation("交卷") + @PostMapping("/submitExam") + public SingleResponse submitExam(@ApiParam(value = "试卷id", required = true) @RequestParam Long paperId) { + paperExamService.submitExam(paperId, AuthContext.getUserId()); + return SingleResponse.success(); } } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/enums/AnswerStatusEnum.java b/safety-cedu-service/src/main/java/org/qinan/cedu/enums/AnswerStatusEnum.java new file mode 100644 index 00000000..d8f88300 --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/enums/AnswerStatusEnum.java @@ -0,0 +1,49 @@ +package org.qinan.cedu.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 作答状态枚举 + */ +@Getter +@AllArgsConstructor +public enum AnswerStatusEnum { + + /** + * 未作答 + */ + NON("non", "未作答"), + + /** + * 已作答 + */ + DONE("done", "已作答"); + + /** + * 编码 + */ + private final String value; + + /** + * 描述 + */ + private final String label; + + /** + * 各状态说明(供 ApiModelProperty 引用) + */ + public static final String REMARKS = "non 未作答, done 已作答"; + + /** + * 根据编码获取枚举 + */ + public static AnswerStatusEnum fromValue(String value) { + for (AnswerStatusEnum status : values()) { + if (status.value.equals(value)) { + return status; + } + } + return null; + } +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/AnswerQuestionDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/AnswerQuestionDTO.java new file mode 100644 index 00000000..9002bde4 --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/AnswerQuestionDTO.java @@ -0,0 +1,25 @@ +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 = "AnswerQuestionDTO", description = "考试试题作答入参") +public class AnswerQuestionDTO { + + @ApiModelProperty(value = "作答记录id(paper_exam_question_user.id)", required = true) + @NotNull(message = "作答记录id不能为空") + private Long id; + + @ApiModelProperty("用户作答答案") + private String userAnswer; + + @ApiModelProperty("用户作答答案选项id 多个逗号隔开") + private String userAnswerQuestionOptionIds; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperExamDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperExamDTO.java index 696d5751..85ea154e 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperExamDTO.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/PaperExamDTO.java @@ -57,7 +57,4 @@ public class PaperExamDTO { @ApiModelProperty(value = "考试状态 " + PaperExamStatusEnum.REMARKS, required = true) @NotBlank(message = "考试状态不能为空") private String paperExamStatus; - - @ApiModelProperty("考试试卷状态 normal 正常") - private String status; } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/StartExamDTO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/StartExamDTO.java new file mode 100644 index 00000000..7d0702b4 --- /dev/null +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/dto/StartExamDTO.java @@ -0,0 +1,22 @@ +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 = "StartExamDTO", description = "去考试入参") +public class StartExamDTO { + + @ApiModelProperty(value = "试卷id", required = true) + @NotNull(message = "试卷id不能为空") + private Long paperId; + + @ApiModelProperty("用户id") + private Long userId; +} diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/entity/PaperExamDO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/entity/PaperExamDO.java index 426a2e78..aef62b89 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/model/entity/PaperExamDO.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/entity/PaperExamDO.java @@ -58,9 +58,6 @@ public class PaperExamDO extends BaseEntity { @ApiModelProperty("考试状态 " + PaperExamStatusEnum.REMARKS) private String paperExamStatus; - @ApiModelProperty("考试试卷状态 normal 正常") - private String status; - /** * paper_exam 表无 org_id 列,排除字段映射,避免生成 SQL 报错 */ diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperExamVO.java b/safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperExamVO.java index a8468c70..b56f5c4d 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperExamVO.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/model/vo/PaperExamVO.java @@ -50,7 +50,4 @@ public class PaperExamVO extends BaseVO { @ApiModelProperty("考试状态 " + PaperExamStatusEnum.REMARKS) private String paperExamStatus; - - @ApiModelProperty("考试试卷状态 normal 正常") - private String status; } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperExamService.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperExamService.java index e429da57..10cace5e 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperExamService.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperExamService.java @@ -2,8 +2,10 @@ 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.AnswerQuestionDTO; import org.qinan.cedu.model.dto.PaperExamDTO; import org.qinan.cedu.model.dto.PaperExamPageQueryDTO; +import org.qinan.cedu.model.dto.StartExamDTO; import org.qinan.cedu.model.entity.PaperExamDO; import org.qinan.cedu.model.vo.PaperExamVO; import org.qinan.cedu.model.vo.StartExamVO; @@ -56,9 +58,24 @@ public interface PaperExamService extends IService { /** * 去考试:根据试卷生成考试记录及用户试题作答记录 * - * @param paperId 试卷id - * @param userId 用户id + * @param dto 去考试入参(试卷id必填,用户id由Controller填充) * @return 考试信息(含试题列表及参考答案) */ - StartExamVO startExam(Long paperId, Long userId); + StartExamVO startExam(StartExamDTO dto); + + /** + * 作答:保存用户作答内容,判定正确状态并计算作答得分 + * + * @param dto 作答入参(作答记录id必填) + * @param userId 当前用户id + */ + void answerQuestion(AnswerQuestionDTO dto, Long userId); + + /** + * 交卷:汇总用户最近一次进行中考试各试题作答得分,更新考试成绩、是否通过并结束考试 + * + * @param paperId 试卷id + * @param userId 用户id + */ + void submitExam(Long paperId, Long userId); } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperExamServiceImpl.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperExamServiceImpl.java index 340f03d7..d206af74 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperExamServiceImpl.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperExamServiceImpl.java @@ -6,21 +6,26 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; import org.qinan.cedu.convertor.PaperExamConvertor; import org.qinan.cedu.convertor.PaperExamQuestionUserConvertor; +import org.qinan.cedu.enums.AnswerStatusEnum; import org.qinan.cedu.enums.DeleteEnum; import org.qinan.cedu.enums.PaperExamStatusEnum; import org.qinan.cedu.mapper.PaperExamMapper; import org.qinan.cedu.mapper.PaperMapper; import org.qinan.cedu.mapper.QuestionMapper; +import org.qinan.cedu.model.dto.AnswerQuestionDTO; import org.qinan.cedu.model.dto.PaperExamDTO; import org.qinan.cedu.model.dto.PaperExamPageQueryDTO; +import org.qinan.cedu.model.dto.StartExamDTO; import org.qinan.cedu.model.entity.PaperDO; import org.qinan.cedu.model.entity.PaperExamDO; import org.qinan.cedu.model.entity.PaperExamQuestionUserDO; +import org.qinan.cedu.model.entity.QuestionDO; import org.qinan.cedu.model.vo.PaperExamVO; import org.qinan.cedu.model.vo.StartExamQuestionVO; import org.qinan.cedu.model.vo.StartExamVO; import org.qinan.cedu.service.PaperExamQuestionUserService; import org.qinan.cedu.service.PaperExamService; +import org.qinan.safetyeval.domain.constant.BooleanQAEnum; import org.qinan.safetyeval.infrastructure.support.InsertFieldDefaults; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +34,10 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; /** * 考试试卷服务实现 @@ -38,13 +46,6 @@ import java.util.List; @RequiredArgsConstructor public class PaperExamServiceImpl extends ServiceImpl implements PaperExamService { - private static final String STATUS_NORMAL = "normal"; - - /** 作答状态:未作答 */ - private static final String ANSWER_STATUS_NON = "non"; - - /** 正确状态:作答错误 */ - private static final String RIGHT_STATUS_FALSE = "false"; private final PaperExamConvertor paperExamConvertor; @@ -82,9 +83,6 @@ public class PaperExamServiceImpl extends ServiceImpl= 0 ? 1 : 0); + paperExam.setSubmitTime(LocalDateTime.now()); + paperExam.setPaperExamStatus(PaperExamStatusEnum.DONE.getValue()); + // paper_exam 表无 org_id 列,更新前不填充机构id + InsertFieldDefaults.applyForUpdateIgnoreOrgId(paperExam); + this.updateById(paperExam); + } + + /** + * 判定作答是否正确:选项题比对选项id集合(忽略顺序),其余题型比对文本答案 + */ + private boolean isAnswerRight(QuestionDO question, AnswerQuestionDTO dto) { + if (StringUtils.hasText(question.getAnswerQuestionOptionIds())) { + return normalizeOptionIds(question.getAnswerQuestionOptionIds()) + .equals(normalizeOptionIds(dto.getUserAnswerQuestionOptionIds())); + } + return dto.getUserAnswer() != null && question.getAnswer() != null + && dto.getUserAnswer().trim().equals(question.getAnswer().trim()); + } + + /** + * 规整选项id串:按逗号拆分、去空、排序后重新拼接,用于忽略顺序比对 + */ + private String normalizeOptionIds(String optionIds) { + if (!StringUtils.hasText(optionIds)) { + return ""; + } + return Arrays.stream(optionIds.split(",")) + .map(String::trim) + .filter(StringUtils::hasText) + .sorted() + .collect(Collectors.joining(",")); + } + + /** + * 汇总一次考试所有试题的作答得分(未作答按0分计入) + */ + private BigDecimal sumAnswerScore(Long paperExamId) { + List questionUserList = paperExamQuestionUserService.lambdaQuery() + .eq(PaperExamQuestionUserDO::getPaperExamId, paperExamId) + .eq(PaperExamQuestionUserDO::getDeleteEnum, DeleteEnum.FALSE.getCode()) + .list(); + return questionUserList.stream() + .map(PaperExamQuestionUserDO::getAnswerScore) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + /** * 构建考试记录实体(复制试卷基本信息,置为进行中状态) *

不使用 MapStruct 同名拷贝,避免把试卷的审计字段(创建人/创建时间等)带入考试记录

@@ -151,7 +239,6 @@ public class PaperExamServiceImpl extends ServiceImpl