diff --git a/src/main/java/com/project/classicpaper/application/ClassicPaperApplicationService.java b/src/main/java/com/project/classicpaper/application/ClassicPaperApplicationService.java new file mode 100644 index 0000000..8c0c8bb --- /dev/null +++ b/src/main/java/com/project/classicpaper/application/ClassicPaperApplicationService.java @@ -0,0 +1,42 @@ +package com.project.classicpaper.application; + +import com.project.base.domain.result.PageResult; +import com.project.base.domain.result.Result; +import com.project.classicpaper.domain.dto.ClassicPaperDTO; +import com.project.classicpaper.domain.dto.ClassicPaperQuestionDTO; +import com.project.classicpaper.domain.dto.GenerateClassicPaperRequestDTO; + +import java.util.List; + +public interface ClassicPaperApplicationService { + + /** + * 批量生成经典套题(N个版本) + */ + Result> generate(GenerateClassicPaperRequestDTO request) throws Exception; + + /** + * 单题重抽 + */ + Result regenerateQuestion(Long paperId, Long questionId) throws Exception; + + /** + * 套卷列表 + */ + Result> list(Long subLineId, Long classicCategoryId, Integer status, Integer pageNum, Integer pageSize); + + /** + * 套卷详情(含题目列表) + */ + Result getDetail(Long id); + + /** + * 套题组详情(查一组N个版本) + */ + Result> getGroupDetail(Long paperGroupId); + + /** + * 废弃套卷 + */ + Result delete(Long id) throws Exception; +} diff --git a/src/main/java/com/project/classicpaper/application/impl/ClassicPaperApplicationServiceImpl.java b/src/main/java/com/project/classicpaper/application/impl/ClassicPaperApplicationServiceImpl.java new file mode 100644 index 0000000..ca4e686 --- /dev/null +++ b/src/main/java/com/project/classicpaper/application/impl/ClassicPaperApplicationServiceImpl.java @@ -0,0 +1,157 @@ +package com.project.classicpaper.application.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.project.base.domain.exception.BusinessErrorException; +import com.project.base.domain.result.PageResult; +import com.project.base.domain.result.Result; +import com.project.base.domain.result.ResultCodeEnum; +import com.project.classicpaper.application.ClassicPaperApplicationService; +import com.project.classicpaper.domain.dto.ClassicPaperDTO; +import com.project.classicpaper.domain.dto.ClassicPaperQuestionDTO; +import com.project.classicpaper.domain.dto.GenerateClassicPaperRequestDTO; +import com.project.classicpaper.domain.entity.ClassicPaperEntity; +import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; +import com.project.classicpaper.domain.enums.ClassicPaperStatusEnum; +import com.project.classicpaper.domain.service.*; +import com.project.question.domain.entity.QuestionEntity; +import com.project.question.domain.service.QuestionBaseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ClassicPaperApplicationServiceImpl implements ClassicPaperApplicationService { + + @Autowired + private GenerateClassicPaperDomainService generateClassicPaperDomainService; + + @Autowired + private RegenerateQuestionDomainService regenerateQuestionDomainService; + + @Autowired + private ClassicPaperBaseService classicPaperBaseService; + + @Autowired + private ClassicPaperQuestionBaseService classicPaperQuestionBaseService; + + @Autowired + private QuestionBaseService questionBaseService; + + @Override + public Result> generate(GenerateClassicPaperRequestDTO request) throws Exception { + return generateClassicPaperDomainService.generate(request); + } + + @Override + public Result regenerateQuestion(Long paperId, Long questionId) throws Exception { + return regenerateQuestionDomainService.regenerate(paperId, questionId); + } + + @Override + public Result> list(Long subLineId, Long classicCategoryId, + Integer status, Integer pageNum, Integer pageSize) { + Page page = new Page<>(pageNum == null ? 1 : pageNum, pageSize == null ? 10 : pageSize); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (subLineId != null) { + wrapper.eq(ClassicPaperEntity::getSubLineId, subLineId); + } + if (classicCategoryId != null) { + wrapper.eq(ClassicPaperEntity::getClassicCategoryId, classicCategoryId); + } + if (status != null) { + wrapper.eq(ClassicPaperEntity::getStatus, status); + } + wrapper.orderByDesc(ClassicPaperEntity::getCreateTime); + + classicPaperBaseService.page(page, wrapper); + + PageResult pageResult = new PageResult<>(page.convert(entity -> { + ClassicPaperDTO dto = entity.toDTO(ClassicPaperDTO::new); + dto.setStatusText(ClassicPaperStatusEnum.findByValue(entity.getStatus())); + return dto; + })); + return Result.success(pageResult); + } + + @Override + public Result getDetail(Long id) { + ClassicPaperEntity paper = classicPaperBaseService.getById(id); + if (paper == null) { + return Result.fail(ResultCodeEnum.RESOURCE_NOT_EXIST, "套卷不存在"); + } + + ClassicPaperDTO dto = paper.toDTO(ClassicPaperDTO::new); + dto.setStatusText(ClassicPaperStatusEnum.findByValue(paper.getStatus())); + + // 查询关联题目 + List paperQuestions = classicPaperQuestionBaseService.list( + new LambdaQueryWrapper() + .eq(ClassicPaperQuestionEntity::getPaperId, id) + .orderByAsc(ClassicPaperQuestionEntity::getSortOrder) + ); + + if (!paperQuestions.isEmpty()) { + List questionIds = paperQuestions.stream() + .map(ClassicPaperQuestionEntity::getQuestionId).collect(Collectors.toList()); + List questions = questionBaseService.listByIds(questionIds); + + List questionDTOs = new ArrayList<>(); + for (ClassicPaperQuestionEntity pq : paperQuestions) { + ClassicPaperQuestionDTO qDto = new ClassicPaperQuestionDTO(); + qDto.setId(pq.getId()); + qDto.setPaperId(pq.getPaperId()); + qDto.setQuestionId(pq.getQuestionId()); + qDto.setSortOrder(pq.getSortOrder()); + qDto.setQuestionType(pq.getQuestionType()); + questions.stream() + .filter(q -> q.getId().equals(pq.getQuestionId())) + .findFirst() + .ifPresent(q -> qDto.setQuestionDetail(q.getQuestionDetail())); + questionDTOs.add(qDto); + } + dto.setQuestionList(questionDTOs); + } + + return Result.success(dto); + } + + @Override + public Result> getGroupDetail(Long paperGroupId) { + List papers = classicPaperBaseService.list( + new LambdaQueryWrapper() + .eq(ClassicPaperEntity::getPaperGroupId, paperGroupId) + .orderByAsc(ClassicPaperEntity::getSetIndex) + ); + if (papers.isEmpty()) { + return Result.fail(ResultCodeEnum.RESOURCE_NOT_EXIST, "套题组不存在"); + } + + List dtoList = papers.stream() + .map(entity -> { + ClassicPaperDTO dto = entity.toDTO(ClassicPaperDTO::new); + dto.setStatusText(ClassicPaperStatusEnum.findByValue(entity.getStatus())); + return dto; + }) + .collect(Collectors.toList()); + return Result.success(dtoList); + } + + @Override + public Result delete(Long id) throws Exception { + ClassicPaperEntity paper = classicPaperBaseService.getById(id); + if (paper == null) { + throw new BusinessErrorException("套卷不存在"); + } + if (ClassicPaperStatusEnum.CONFIRMED.getValue().equals(paper.getStatus())) { + throw new BusinessErrorException("已固化的套卷不能废弃"); + } + paper.setStatus(ClassicPaperStatusEnum.DISCARDED.getValue()); + classicPaperBaseService.updateById(paper); + return Result.success("废弃成功"); + } +} diff --git a/src/main/java/com/project/classicpaper/controller/ClassicPaperController.java b/src/main/java/com/project/classicpaper/controller/ClassicPaperController.java new file mode 100644 index 0000000..9c90298 --- /dev/null +++ b/src/main/java/com/project/classicpaper/controller/ClassicPaperController.java @@ -0,0 +1,57 @@ +package com.project.classicpaper.controller; + +import com.project.base.domain.result.PageResult; +import com.project.base.domain.result.Result; +import com.project.classicpaper.application.ClassicPaperApplicationService; +import com.project.classicpaper.domain.dto.ClassicPaperDTO; +import com.project.classicpaper.domain.dto.ClassicPaperQuestionDTO; +import com.project.classicpaper.domain.dto.GenerateClassicPaperRequestDTO; +import com.project.operation.annotation.OperationLog; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@Slf4j +@RequestMapping("/api/admin/classicPaper") +public class ClassicPaperController { + + @Autowired + private ClassicPaperApplicationService classicPaperApplicationService; + + @PostMapping("/generate") + @OperationLog(module = "经典套题") + public Result> generate(@RequestBody GenerateClassicPaperRequestDTO request) throws Exception { + return classicPaperApplicationService.generate(request); + } + + @PostMapping("/regenerateQuestion") + @OperationLog(module = "经典套题") + public Result regenerateQuestion(Long paperId, Long questionId) throws Exception { + return classicPaperApplicationService.regenerateQuestion(paperId, questionId); + } + + @GetMapping("/list") + public Result> list(Long subLineId, Long classicCategoryId, + Integer status, Integer pageNum, Integer pageSize) { + return classicPaperApplicationService.list(subLineId, classicCategoryId, status, pageNum, pageSize); + } + + @GetMapping("/detail") + public Result detail(Long id) { + return classicPaperApplicationService.getDetail(id); + } + + @GetMapping("/groupDetail") + public Result> groupDetail(Long paperGroupId) { + return classicPaperApplicationService.getGroupDetail(paperGroupId); + } + + @PostMapping("/delete") + @OperationLog(module = "经典套题") + public Result delete(Long id) throws Exception { + return classicPaperApplicationService.delete(id); + } +} diff --git a/src/main/java/com/project/classicpaper/domain/dto/ClassicPaperDTO.java b/src/main/java/com/project/classicpaper/domain/dto/ClassicPaperDTO.java new file mode 100644 index 0000000..000db80 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/dto/ClassicPaperDTO.java @@ -0,0 +1,39 @@ +package com.project.classicpaper.domain.dto; + +import com.project.base.domain.dto.BaseDTO; +import lombok.Data; + +import java.util.List; + +@Data +public class ClassicPaperDTO extends BaseDTO { + private Long id; + + private String name; + + private String description; + + private Integer sourceType; + + private Long subLineId; + + private String subLineName; + + private Long classicCategoryId; + + private String classicCategoryName; + + private Long informationId; + + private Integer status; + + private String statusText; + + private Integer questionCount; + + private Long paperGroupId; + + private Integer setIndex; + + private List questionList; +} diff --git a/src/main/java/com/project/classicpaper/domain/dto/ClassicPaperQuestionDTO.java b/src/main/java/com/project/classicpaper/domain/dto/ClassicPaperQuestionDTO.java new file mode 100644 index 0000000..4b0f45c --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/dto/ClassicPaperQuestionDTO.java @@ -0,0 +1,22 @@ +package com.project.classicpaper.domain.dto; + +import com.project.question.domain.entity.QuestionEntity; +import lombok.Data; + +@Data +public class ClassicPaperQuestionDTO { + private Long id; + + private Long paperId; + + private Long questionId; + + private Integer sortOrder; + + private Integer questionType; + + /** + * 题目详情(从 QuestionEntity 关联查询) + */ + private QuestionEntity.QuestionDetail questionDetail; +} diff --git a/src/main/java/com/project/classicpaper/domain/dto/GenerateClassicPaperRequestDTO.java b/src/main/java/com/project/classicpaper/domain/dto/GenerateClassicPaperRequestDTO.java new file mode 100644 index 0000000..826be63 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/dto/GenerateClassicPaperRequestDTO.java @@ -0,0 +1,52 @@ +package com.project.classicpaper.domain.dto; + +import lombok.Data; + +@Data +public class GenerateClassicPaperRequestDTO { + + /** + * 子产品线ID(必填) + */ + private Long subLineId; + + /** + * 经典分类ID(必填) + */ + private Long classicCategoryId; + + /** + * 生成版本数 N,1~5(必填) + */ + private Integer setCount; + + /** + * 套卷名称前缀(必填) + */ + private String namePrefix; + + /** + * 套卷描述(可选) + */ + private String description; + + /** + * 单选题数量 + */ + private Integer singleChoiceNum; + + /** + * 多选题数量 + */ + private Integer multipleChoiceNum; + + /** + * 判断题数量 + */ + private Integer trueFalseNum; + + /** + * 简答题数量 + */ + private Integer shortAnswerNum; +} diff --git a/src/main/java/com/project/classicpaper/domain/entity/ClassicPaperEntity.java b/src/main/java/com/project/classicpaper/domain/entity/ClassicPaperEntity.java new file mode 100644 index 0000000..8103eec --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/entity/ClassicPaperEntity.java @@ -0,0 +1,73 @@ +package com.project.classicpaper.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.project.base.domain.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Comment; + +@Data +@Table(name = "evaluator_classic_paper", + indexes = {@Index(name = "Idx_sub_line_id", columnList = "sub_line_id"), + @Index(name = "Idx_status", columnList = "status"), + @Index(name = "Idx_paper_group_id", columnList = "paper_group_id")}) +@Entity +@TableName(value = "evaluator_classic_paper") +@EqualsAndHashCode(callSuper = true) +public class ClassicPaperEntity extends BaseEntity { + @TableId(value = "id", type = IdType.ASSIGN_ID) + @Id + private Long id; + + @Column(name = "name", columnDefinition = "varchar(200) comment '套卷名称'") + @Comment("套卷名称") + private String name; + + @Column(name = "description", columnDefinition = "TEXT comment '套卷描述'") + @Comment("套卷描述") + private String description; + + @Column(name = "source_type") + @TableField("source_type") + @Comment("来源:0-Word导入,1-AI生成") + private Integer sourceType; + + @Column(name = "sub_line_id") + @TableField("sub_line_id") + @Comment("来源子产品线ID") + private Long subLineId; + + @Column(name = "information_id") + @TableField("information_id") + @Comment("关联资料ID") + private Long informationId; + + @Column(name = "status") + @TableField("status") + @Comment("状态:0-生成中,1-草稿,2-已固化,3-已废弃") + private Integer status; + + @Column(name = "question_count") + @TableField("question_count") + @Comment("题目总数") + private Integer questionCount; + + @Column(name = "paper_group_id") + @TableField("paper_group_id") + @Comment("套题组ID,N个版本共享") + private Long paperGroupId; + + @Column(name = "set_index") + @TableField("set_index") + @Comment("组内序号(1~N)") + private Integer setIndex; + + @Column(name = "classic_category_id") + @TableField("classic_category_id") + @Comment("经典分类ID") + private Long classicCategoryId; +} diff --git a/src/main/java/com/project/classicpaper/domain/entity/ClassicPaperQuestionEntity.java b/src/main/java/com/project/classicpaper/domain/entity/ClassicPaperQuestionEntity.java new file mode 100644 index 0000000..17b929e --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/entity/ClassicPaperQuestionEntity.java @@ -0,0 +1,44 @@ +package com.project.classicpaper.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.project.base.domain.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Comment; + +@Data +@Table(name = "evaluator_classic_paper_question", + indexes = {@Index(name = "Idx_paper_id", columnList = "paper_id"), + @Index(name = "Idx_question_id", columnList = "question_id")}) +@Entity +@TableName(value = "evaluator_classic_paper_question") +@EqualsAndHashCode(callSuper = true) +public class ClassicPaperQuestionEntity extends BaseEntity { + @TableId(value = "id", type = IdType.ASSIGN_ID) + @Id + private Long id; + + @Column(name = "paper_id") + @TableField("paper_id") + @Comment("套卷ID") + private Long paperId; + + @Column(name = "question_id") + @TableField("question_id") + @Comment("题目ID") + private Long questionId; + + @Column(name = "sort_order") + @TableField("sort_order") + @Comment("排序序号") + private Integer sortOrder; + + @Column(name = "question_type") + @TableField("question_type") + @Comment("题型") + private Integer questionType; +} diff --git a/src/main/java/com/project/classicpaper/domain/enums/ClassicPaperSourceTypeEnum.java b/src/main/java/com/project/classicpaper/domain/enums/ClassicPaperSourceTypeEnum.java new file mode 100644 index 0000000..d9b01bb --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/enums/ClassicPaperSourceTypeEnum.java @@ -0,0 +1,15 @@ +package com.project.classicpaper.domain.enums; + +import com.project.base.domain.enums.HasValueEnum; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum ClassicPaperSourceTypeEnum implements HasValueEnum { + WORD_IMPORT(0, "Word导入"), + AI_GENERATED(1, "AI生成"); + + private final Integer value; + private final String desc; +} diff --git a/src/main/java/com/project/classicpaper/domain/enums/ClassicPaperStatusEnum.java b/src/main/java/com/project/classicpaper/domain/enums/ClassicPaperStatusEnum.java new file mode 100644 index 0000000..7b617b6 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/enums/ClassicPaperStatusEnum.java @@ -0,0 +1,26 @@ +package com.project.classicpaper.domain.enums; + +import com.project.base.domain.enums.HasValueEnum; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum ClassicPaperStatusEnum implements HasValueEnum { + GENERATING(0, "生成中"), + DRAFT(1, "草稿"), + CONFIRMED(2, "已固化"), + DISCARDED(3, "已废弃"); + + private final Integer value; + private final String desc; + + public static String findByValue(Integer value) { + for (ClassicPaperStatusEnum e : values()) { + if (e.value.equals(value)) { + return e.desc; + } + } + return ""; + } +} diff --git a/src/main/java/com/project/classicpaper/domain/service/ClassicPaperBaseService.java b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperBaseService.java new file mode 100644 index 0000000..93ac208 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperBaseService.java @@ -0,0 +1,7 @@ +package com.project.classicpaper.domain.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.project.classicpaper.domain.entity.ClassicPaperEntity; + +public interface ClassicPaperBaseService extends IService { +} diff --git a/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionBaseService.java b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionBaseService.java new file mode 100644 index 0000000..bad5f87 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionBaseService.java @@ -0,0 +1,7 @@ +package com.project.classicpaper.domain.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; + +public interface ClassicPaperQuestionBaseService extends IService { +} diff --git a/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionGenerator.java b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionGenerator.java new file mode 100644 index 0000000..1a3b880 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionGenerator.java @@ -0,0 +1,25 @@ +package com.project.classicpaper.domain.service; + +import com.project.information.domain.entity.KnowledgePointEntity; +import com.project.question.domain.entity.QuestionEntity; +import com.project.task.domain.enums.QuestionTypeEnum; + +import java.util.List; + +/** + * 经典套卷题目生成器接口 + * 当前使用降级实现,后续替换为算法服务新接口实现 + */ +public interface ClassicPaperQuestionGenerator { + + /** + * 为经典套卷生成题目 + * + * @param knowledgePoints 知识点列表(素材) + * @param questionType 题型 + * @param count 生成数量 + * @return 生成的题目实体列表(已保存到数据库) + */ + List generate(List knowledgePoints, + QuestionTypeEnum questionType, int count) throws Exception; +} diff --git a/src/main/java/com/project/classicpaper/domain/service/GenerateClassicPaperDomainService.java b/src/main/java/com/project/classicpaper/domain/service/GenerateClassicPaperDomainService.java new file mode 100644 index 0000000..55c3602 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/GenerateClassicPaperDomainService.java @@ -0,0 +1,15 @@ +package com.project.classicpaper.domain.service; + +import com.project.base.domain.result.Result; +import com.project.classicpaper.domain.dto.ClassicPaperDTO; +import com.project.classicpaper.domain.dto.GenerateClassicPaperRequestDTO; + +import java.util.List; + +public interface GenerateClassicPaperDomainService { + + /** + * 批量生成经典套题(N个版本) + */ + Result> generate(GenerateClassicPaperRequestDTO request) throws Exception; +} diff --git a/src/main/java/com/project/classicpaper/domain/service/RegenerateQuestionDomainService.java b/src/main/java/com/project/classicpaper/domain/service/RegenerateQuestionDomainService.java new file mode 100644 index 0000000..3b82f5a --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/RegenerateQuestionDomainService.java @@ -0,0 +1,16 @@ +package com.project.classicpaper.domain.service; + +import com.project.base.domain.result.Result; +import com.project.classicpaper.domain.dto.ClassicPaperQuestionDTO; + +public interface RegenerateQuestionDomainService { + + /** + * 单题重抽:替换套卷中的某道题 + * + * @param paperId 套卷ID + * @param questionId 原题目ID + * @return 新题目信息 + */ + Result regenerate(Long paperId, Long questionId) throws Exception; +} diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperBaseServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperBaseServiceImpl.java new file mode 100644 index 0000000..107624f --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperBaseServiceImpl.java @@ -0,0 +1,11 @@ +package com.project.classicpaper.domain.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.project.classicpaper.domain.entity.ClassicPaperEntity; +import com.project.classicpaper.domain.service.ClassicPaperBaseService; +import com.project.classicpaper.mapper.ClassicPaperMapper; +import org.springframework.stereotype.Service; + +@Service +public class ClassicPaperBaseServiceImpl extends ServiceImpl implements ClassicPaperBaseService { +} diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionBaseServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionBaseServiceImpl.java new file mode 100644 index 0000000..4f3931d --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionBaseServiceImpl.java @@ -0,0 +1,11 @@ +package com.project.classicpaper.domain.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; +import com.project.classicpaper.domain.service.ClassicPaperQuestionBaseService; +import com.project.classicpaper.mapper.ClassicPaperQuestionMapper; +import org.springframework.stereotype.Service; + +@Service +public class ClassicPaperQuestionBaseServiceImpl extends ServiceImpl implements ClassicPaperQuestionBaseService { +} diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/FallbackClassicPaperQuestionGenerator.java b/src/main/java/com/project/classicpaper/domain/service/impl/FallbackClassicPaperQuestionGenerator.java new file mode 100644 index 0000000..b0519c0 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/impl/FallbackClassicPaperQuestionGenerator.java @@ -0,0 +1,109 @@ +package com.project.classicpaper.domain.service.impl; + +import cn.hutool.core.util.RandomUtil; +import com.project.base.domain.result.Result; +import com.project.information.domain.entity.KnowledgePointEntity; +import com.project.question.domain.dto.QuestionDTO; +import com.project.question.domain.entity.QuestionEntity; +import com.project.question.domain.service.SaveQuestionDomainService; +import com.project.task.domain.enums.QuestionTypeEnum; +import com.project.classicpaper.domain.service.ClassicPaperQuestionGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 降级实现:生成占位题目 + * 当算法服务新接口未就绪时使用,后续替换为真正的算法调用 + */ +@Component +@Slf4j +public class FallbackClassicPaperQuestionGenerator implements ClassicPaperQuestionGenerator { + + @Autowired + private SaveQuestionDomainService saveQuestionDomainService; + + private final String[] optionListStr = {"A", "B", "C", "D"}; + private final String[] trueFalseListStr = {"A", "B"}; + private final List list = Arrays.asList(0, 1, 2, 3); + + @Override + public List generate(List knowledgePoints, + QuestionTypeEnum questionType, int count) throws Exception { + log.info(">>> [经典套题-降级] 开始生成占位题目, 题型: {}, 数量: {}, 知识点数: {}", + questionType, count, knowledgePoints.size()); + + List result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + KnowledgePointEntity kp = knowledgePoints.get(i % knowledgePoints.size()); + + QuestionDTO questionDTO = new QuestionDTO(); + questionDTO.setKpIdList(Collections.singletonList(kp.getId())); + questionDTO.setQuestionType(questionType.getValue()); + questionDTO.setSourceType(0); + + QuestionDTO.QuestionDetailDTO detailDTO = new QuestionDTO.QuestionDetailDTO(); + questionDTO.setQuestionDetailDTO(detailDTO); + detailDTO.setQuestionContent(String.format("【经典套题】%s - 第%d题(%s)", + kp.getParseName(), i + 1, questionType.getDescription())); + detailDTO.setType(questionType.getValue()); + + if (QuestionTypeEnum.SINGLE_CHOICE.equals(questionType)) { + int rightAnswerNo = RandomUtil.randomInt(0, 4); + TreeMap optionList = new TreeMap<>(); + for (int j = 0; j < optionListStr.length; j++) { + optionList.put(optionListStr[j], String.format("选项%s", optionListStr[j])); + if (j == rightAnswerNo) { + detailDTO.setRightAnswer(optionListStr[j]); + detailDTO.setAnalysis(String.format("正确答案是%s", optionListStr[j])); + } + } + detailDTO.setOptions(optionList); + } else if (QuestionTypeEnum.MULTIPLE_CHOICE.equals(questionType)) { + Collections.shuffle(list); + Set resultIdx = list.subList(0, 2).stream().collect(Collectors.toSet()); + TreeMap optionList = new TreeMap<>(); + List rightAnswerList = new ArrayList<>(); + for (int j = 0; j < optionListStr.length; j++) { + optionList.put(optionListStr[j], String.format("选项%s", optionListStr[j])); + if (resultIdx.contains(j)) { + rightAnswerList.add(optionListStr[j]); + } + } + detailDTO.setRightAnswer(String.join(",", rightAnswerList)); + detailDTO.setAnalysis(String.format("正确答案是%s", detailDTO.getRightAnswer())); + detailDTO.setOptions(optionList); + } else if (QuestionTypeEnum.TRUE_FALSE.equals(questionType)) { + int rightAnswerNo = RandomUtil.randomInt(0, 2); + TreeMap optionList = new TreeMap<>(); + for (int j = 0; j < trueFalseListStr.length; j++) { + optionList.put(trueFalseListStr[j], j == 0 ? "对" : "错"); + if (j == rightAnswerNo) { + detailDTO.setRightAnswer(trueFalseListStr[j]); + detailDTO.setAnalysis(String.format("正确答案是%s", trueFalseListStr[j])); + } + } + detailDTO.setOptions(optionList); + } else { + // 简答题 + detailDTO.setRightAnswer("参考答案"); + detailDTO.setAnalysis("解析"); + } + + Result saveResult = saveQuestionDomainService.save(questionDTO); + QuestionDTO saved = saveResult.getData(); + + QuestionEntity savedEntity = new QuestionEntity(); + savedEntity.setId(saved.getId()); + savedEntity.setQuestionType(questionType.getValue()); + savedEntity.setKpIdList(saved.getKpIdList()); + result.add(savedEntity); + } + + log.info(">>> [经典套题-降级] 生成完成, 共{}题", result.size()); + return result; + } +} diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/GenerateClassicPaperDomainServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/GenerateClassicPaperDomainServiceImpl.java new file mode 100644 index 0000000..4f47dee --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/impl/GenerateClassicPaperDomainServiceImpl.java @@ -0,0 +1,222 @@ +package com.project.classicpaper.domain.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.project.base.config.SnowflakeIdWorker; +import com.project.base.domain.exception.BusinessErrorException; +import com.project.base.domain.result.Result; +import com.project.classicpaper.domain.dto.ClassicPaperDTO; +import com.project.classicpaper.domain.dto.ClassicPaperQuestionDTO; +import com.project.classicpaper.domain.dto.GenerateClassicPaperRequestDTO; +import com.project.classicpaper.domain.entity.ClassicPaperEntity; +import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; +import com.project.classicpaper.domain.enums.ClassicPaperSourceTypeEnum; +import com.project.classicpaper.domain.enums.ClassicPaperStatusEnum; +import com.project.classicpaper.domain.service.*; +import com.project.information.domain.entity.InformationEntity; +import com.project.information.domain.entity.KnowledgePointEntity; +import com.project.information.domain.enums.InformationParseStatusEnum; +import com.project.information.domain.service.InformationBaseService; +import com.project.information.domain.service.KnowledgePointBaseService; +import com.project.question.domain.entity.QuestionEntity; +import com.project.task.domain.enums.QuestionTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class GenerateClassicPaperDomainServiceImpl implements GenerateClassicPaperDomainService { + + @Autowired + private InformationBaseService informationBaseService; + + @Autowired + private KnowledgePointBaseService knowledgePointBaseService; + + @Autowired + private ClassicPaperBaseService classicPaperBaseService; + + @Autowired + private ClassicPaperQuestionBaseService classicPaperQuestionBaseService; + + @Autowired + private ClassicPaperQuestionGenerator classicPaperQuestionGenerator; + + private final SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(0, 0); + + @Override + @Transactional(rollbackFor = Exception.class) + public Result> generate(GenerateClassicPaperRequestDTO request) throws Exception { + // 1. 校验参数 + validateRequest(request); + + // 2. 查询子产品线下已解析成功的资料 + List informations = informationBaseService.list( + new LambdaQueryWrapper() + .eq(InformationEntity::getSubLineId, request.getSubLineId()) + .eq(InformationEntity::getParseStatus, InformationParseStatusEnum.Success.getValue()) + ); + if (CollUtil.isEmpty(informations)) { + throw new BusinessErrorException("该子产品线下没有已解析成功的资料"); + } + + // 3. 查询这些资料的所有知识点 + List informationIds = informations.stream() + .map(InformationEntity::getId).collect(Collectors.toList()); + List allKps = knowledgePointBaseService.list( + new LambdaQueryWrapper() + .in(KnowledgePointEntity::getInformationId, informationIds) + ); + if (CollUtil.isEmpty(allKps)) { + throw new BusinessErrorException("该子产品线下没有知识点数据"); +} + + // 4. 计算总题目数并校验知识点数量 + int totalPerSet = Optional.ofNullable(request.getSingleChoiceNum()).orElse(0) + + Optional.ofNullable(request.getMultipleChoiceNum()).orElse(0) + + Optional.ofNullable(request.getTrueFalseNum()).orElse(0) + + Optional.ofNullable(request.getShortAnswerNum()).orElse(0); + if (totalPerSet <= 0) { + throw new BusinessErrorException("至少需要一种题型的数量大于0"); + } + int setCount = request.getSetCount(); + if (allKps.size() < totalPerSet) { + throw new BusinessErrorException("知识点数量(" + allKps.size() + ")不足以生成每套 " + totalPerSet + " 道题"); + } + + // 5. 创建 paperGroupId + Long paperGroupId = snowflakeIdWorker.nextId(); + + // 6. 构建题型配置列表 + List typeConfigs = buildTypeConfigs(request); + + // 7. 循环生成 N 个版本 + List paperDTOs = new ArrayList<>(); + for (int i = 1; i <= setCount; i++) { + ClassicPaperDTO paperDTO = generateOnePaper(paperGroupId, i, request, allKps, typeConfigs); + paperDTOs.add(paperDTO); + } + + log.info(">>> [经典套题] 批量生成完成, paperGroupId={}, 版本数={}, 每套题数={}", + paperGroupId, setCount, totalPerSet); + return Result.success(paperDTOs); + } + + private void validateRequest(GenerateClassicPaperRequestDTO request) { + if (request.getSubLineId() == null) { + throw new BusinessErrorException("子产品线ID不能为空"); + } + if (request.getClassicCategoryId() == null) { + throw new BusinessErrorException("经典分类不能为空"); + } + if (request.getSetCount() == null || request.getSetCount() < 1 || request.getSetCount() > 5) { + throw new BusinessErrorException("版本数必须在1~5之间"); + } + if (StrUtil.isBlank(request.getNamePrefix())) { + throw new BusinessErrorException("套卷名称前缀不能为空"); + } + } + + private List buildTypeConfigs(GenerateClassicPaperRequestDTO request) { + List configs = new ArrayList<>(); + if (Optional.ofNullable(request.getSingleChoiceNum()).orElse(0) > 0) { + configs.add(new QuestionTypeConfig(QuestionTypeEnum.SINGLE_CHOICE, request.getSingleChoiceNum())); + } + if (Optional.ofNullable(request.getMultipleChoiceNum()).orElse(0) > 0) { + configs.add(new QuestionTypeConfig(QuestionTypeEnum.MULTIPLE_CHOICE, request.getMultipleChoiceNum())); + } + if (Optional.ofNullable(request.getTrueFalseNum()).orElse(0) > 0) { + configs.add(new QuestionTypeConfig(QuestionTypeEnum.TRUE_FALSE, request.getTrueFalseNum())); + } + if (Optional.ofNullable(request.getShortAnswerNum()).orElse(0) > 0) { + configs.add(new QuestionTypeConfig(QuestionTypeEnum.SHORT_ANSWER, request.getShortAnswerNum())); + } + return configs; + } + + /** + * 生成一个版本的套卷 + */ + private ClassicPaperDTO generateOnePaper(Long paperGroupId, int setIndex, + GenerateClassicPaperRequestDTO request, + List allKps, + List typeConfigs) throws Exception { + // 创建套卷实体 + ClassicPaperEntity paper = new ClassicPaperEntity(); + paper.setName(request.getNamePrefix() + "-V" + setIndex); + paper.setDescription(request.getDescription()); + paper.setSourceType(ClassicPaperSourceTypeEnum.AI_GENERATED.getValue()); + paper.setSubLineId(request.getSubLineId()); + paper.setClassicCategoryId(request.getClassicCategoryId()); + paper.setStatus(ClassicPaperStatusEnum.DRAFT.getValue()); + paper.setPaperGroupId(paperGroupId); + paper.setSetIndex(setIndex); + paper.setQuestionCount(0); + classicPaperBaseService.save(paper); + + // 打乱知识点顺序,每个版本用不同的排列 + List shuffledKps = new ArrayList<>(allKps); + Collections.shuffle(shuffledKps); + + int totalQuestions = 0; + int sortOrder = 1; + List paperQuestionEntities = new ArrayList<>(); + + // 对每种题型生成题目 + for (QuestionTypeConfig config : typeConfigs) { + // 从知识点池中采样(不同版本使用不同组合) + List sampledKps = sampleKps(shuffledKps, config.count); + + List questions = classicPaperQuestionGenerator.generate( + sampledKps, config.questionType, config.count); + + for (QuestionEntity question : questions) { + ClassicPaperQuestionEntity pq = new ClassicPaperQuestionEntity(); + pq.setPaperId(paper.getId()); + pq.setQuestionId(question.getId()); + pq.setSortOrder(sortOrder++); + pq.setQuestionType(config.questionType.getValue()); + paperQuestionEntities.add(pq); + totalQuestions++; + } + } + + // 批量保存关联关系 + classicPaperQuestionBaseService.saveBatch(paperQuestionEntities); + + // 更新题目总数 + paper.setQuestionCount(totalQuestions); + classicPaperBaseService.updateById(paper); + + // 构建返回 DTO + ClassicPaperDTO dto = paper.toDTO(ClassicPaperDTO::new); + dto.setStatusText(ClassicPaperStatusEnum.DRAFT.getDesc()); + return dto; + } + + /** + * 从知识点池中采样指定数量的知识点 + */ + private List sampleKps(List pool, int count) { + if (pool.size() <= count) { + return new ArrayList<>(pool); + } + return new ArrayList<>(pool.subList(0, count)); + } + + private static class QuestionTypeConfig { + final QuestionTypeEnum questionType; + final int count; + + QuestionTypeConfig(QuestionTypeEnum questionType, int count) { + this.questionType = questionType; + this.count = count; + } + } +} diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java new file mode 100644 index 0000000..0830921 --- /dev/null +++ b/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java @@ -0,0 +1,124 @@ +package com.project.classicpaper.domain.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.project.base.domain.exception.BusinessErrorException; +import com.project.base.domain.result.Result; +import com.project.classicpaper.domain.dto.ClassicPaperQuestionDTO; +import com.project.classicpaper.domain.entity.ClassicPaperEntity; +import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; +import com.project.classicpaper.domain.enums.ClassicPaperStatusEnum; +import com.project.classicpaper.domain.service.ClassicPaperBaseService; +import com.project.classicpaper.domain.service.ClassicPaperQuestionBaseService; +import com.project.classicpaper.domain.service.ClassicPaperQuestionGenerator; +import com.project.classicpaper.domain.service.RegenerateQuestionDomainService; +import com.project.information.domain.entity.KnowledgePointEntity; +import com.project.information.domain.service.KnowledgePointBaseService; +import com.project.question.domain.entity.QuestionEntity; +import com.project.question.domain.entity.QuestionKpRelEntity; +import com.project.question.domain.service.QuestionBaseService; +import com.project.question.domain.service.QuestionKpRelBaseService; +import com.project.task.domain.enums.QuestionTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDomainService { + + @Autowired + private ClassicPaperBaseService classicPaperBaseService; + + @Autowired + private ClassicPaperQuestionBaseService classicPaperQuestionBaseService; + + @Autowired + private QuestionBaseService questionBaseService; + + @Autowired + private QuestionKpRelBaseService questionKpRelBaseService; + + @Autowired + private KnowledgePointBaseService knowledgePointBaseService; + + @Autowired + private ClassicPaperQuestionGenerator classicPaperQuestionGenerator; + + @Override + @Transactional(rollbackFor = Exception.class) + public Result regenerate(Long paperId, Long questionId) throws Exception { + // 1. 校验套卷存在且为草稿状态 + ClassicPaperEntity paper = classicPaperBaseService.getById(paperId); + if (paper == null) { + throw new BusinessErrorException("套卷不存在"); + } + if (!ClassicPaperStatusEnum.DRAFT.getValue().equals(paper.getStatus())) { + throw new BusinessErrorException("只有草稿状态的套卷才能重抽题目"); + } + + // 2. 校验题目属于该套卷 + ClassicPaperQuestionEntity paperQuestion = classicPaperQuestionBaseService.getOne( + new LambdaQueryWrapper() + .eq(ClassicPaperQuestionEntity::getPaperId, paperId) + .eq(ClassicPaperQuestionEntity::getQuestionId, questionId) + ); + if (paperQuestion == null) { + throw new BusinessErrorException("该题目不属于此套卷"); + } + + // 3. 查询原题目信息 + QuestionEntity originalQuestion = questionBaseService.getById(questionId); + if (originalQuestion == null) { + throw new BusinessErrorException("原题目不存在"); + } + + // 4. 查询原题目的知识点关联 + List kpRels = questionKpRelBaseService.list( + new LambdaQueryWrapper() + .eq(QuestionKpRelEntity::getQuestionId, questionId) + ); + if (kpRels.isEmpty()) { + throw new BusinessErrorException("原题目没有关联知识点,无法重抽"); + } + List kpIds = kpRels.stream() + .map(QuestionKpRelEntity::getKpId).collect(Collectors.toList()); + + // 5. 查询知识点详情 + List knowledgePoints = knowledgePointBaseService.listByIds(kpIds); + if (knowledgePoints.isEmpty()) { + throw new BusinessErrorException("关联知识点数据不存在"); + } + + // 6. 调用生成器生成新题 + QuestionTypeEnum questionType = QuestionTypeEnum.findByValue(originalQuestion.getQuestionType()); + List newQuestions = classicPaperQuestionGenerator.generate( + knowledgePoints, questionType, 1); + if (newQuestions.isEmpty()) { + throw new BusinessErrorException("生成新题失败"); + } + QuestionEntity newQuestion = newQuestions.get(0); + + // 7. 更新关联关系,指向新题 + paperQuestion.setQuestionId(newQuestion.getId()); + classicPaperQuestionBaseService.updateById(paperQuestion); + + log.info(">>> [经典套题-重抽] 套卷{} 题目{} -> 新题目{}", paperId, questionId, newQuestion.getId()); + + // 8. 构建返回 + ClassicPaperQuestionDTO dto = new ClassicPaperQuestionDTO(); + dto.setId(paperQuestion.getId()); + dto.setPaperId(paperId); + dto.setQuestionId(newQuestion.getId()); + dto.setSortOrder(paperQuestion.getSortOrder()); + dto.setQuestionType(paperQuestion.getQuestionType()); + if (newQuestion.getQuestionDetail() != null) { + dto.setQuestionDetail(newQuestion.getQuestionDetail()); + } + return Result.success(dto); + } +} diff --git a/src/main/java/com/project/classicpaper/mapper/ClassicPaperMapper.java b/src/main/java/com/project/classicpaper/mapper/ClassicPaperMapper.java new file mode 100644 index 0000000..9fa245c --- /dev/null +++ b/src/main/java/com/project/classicpaper/mapper/ClassicPaperMapper.java @@ -0,0 +1,9 @@ +package com.project.classicpaper.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.project.classicpaper.domain.entity.ClassicPaperEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ClassicPaperMapper extends BaseMapper { +} diff --git a/src/main/java/com/project/classicpaper/mapper/ClassicPaperQuestionMapper.java b/src/main/java/com/project/classicpaper/mapper/ClassicPaperQuestionMapper.java new file mode 100644 index 0000000..5c431f4 --- /dev/null +++ b/src/main/java/com/project/classicpaper/mapper/ClassicPaperQuestionMapper.java @@ -0,0 +1,9 @@ +package com.project.classicpaper.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ClassicPaperQuestionMapper extends BaseMapper { +}