Browse Source

新增任务模式区分:经典题库/生成式题库

master
luoweijian 3 months ago
parent
commit
195ec3af77
  1. 15
      src/main/java/com/project/classicpaper/application/ClassicCategoryApplicationService.java
  2. 95
      src/main/java/com/project/classicpaper/application/impl/ClassicCategoryApplicationServiceImpl.java
  3. 40
      src/main/java/com/project/classicpaper/controller/ClassicCategoryController.java
  4. 22
      src/main/java/com/project/classicpaper/domain/dto/ClassicCategoryDTO.java
  5. 43
      src/main/java/com/project/classicpaper/domain/entity/ClassicCategoryEntity.java
  6. 7
      src/main/java/com/project/classicpaper/domain/service/ClassicCategoryBaseService.java
  7. 11
      src/main/java/com/project/classicpaper/domain/service/impl/ClassicCategoryBaseServiceImpl.java
  8. 9
      src/main/java/com/project/classicpaper/mapper/ClassicCategoryMapper.java
  9. 5
      src/main/java/com/project/task/config/ExamScoreRatioConfig.java
  10. 8
      src/main/java/com/project/task/domain/dto/TaskDTO.java
  11. 20
      src/main/java/com/project/task/domain/entity/TaskEntity.java
  12. 168
      src/main/java/com/project/task/domain/service/impl/SaveOrUpdateTaskDomainServiceImpl.java
  13. 103
      src/main/java/com/project/task/domain/service/strategy/ClassicTaskConfigStrategy.java
  14. 168
      src/main/java/com/project/task/domain/service/strategy/GenerativeTaskConfigStrategy.java
  15. 17
      src/main/java/com/project/task/domain/service/strategy/TaskConfigStrategy.java
  16. 25
      src/main/java/com/project/task/domain/service/strategy/TaskConfigStrategyFactory.java

15
src/main/java/com/project/classicpaper/application/ClassicCategoryApplicationService.java

@ -0,0 +1,15 @@
package com.project.classicpaper.application;
import com.project.base.domain.result.Result;
import com.project.classicpaper.domain.dto.ClassicCategoryDTO;
import java.util.List;
public interface ClassicCategoryApplicationService {
Result<List<ClassicCategoryDTO>> treeList();
Result<ClassicCategoryDTO> save(ClassicCategoryDTO dto) throws Exception;
Result<String> delete(Long id) throws Exception;
}

95
src/main/java/com/project/classicpaper/application/impl/ClassicCategoryApplicationServiceImpl.java

@ -0,0 +1,95 @@
package com.project.classicpaper.application.impl;
import cn.hutool.core.util.StrUtil;
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.base.domain.utils.TreeUtils;
import com.project.classicpaper.application.ClassicCategoryApplicationService;
import com.project.classicpaper.domain.dto.ClassicCategoryDTO;
import com.project.classicpaper.domain.entity.ClassicCategoryEntity;
import com.project.classicpaper.domain.service.ClassicCategoryBaseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class ClassicCategoryApplicationServiceImpl implements ClassicCategoryApplicationService {
@Autowired
private ClassicCategoryBaseService classicCategoryBaseService;
@Override
public Result<List<ClassicCategoryDTO>> treeList() {
List<ClassicCategoryDTO> list = classicCategoryBaseService.list(
new LambdaQueryWrapper<ClassicCategoryEntity>().orderByAsc(ClassicCategoryEntity::getSort)
).stream().map(entity -> entity.toDTO(ClassicCategoryDTO::new))
.collect(Collectors.toList());
return Result.success(TreeUtils.buildLongTree(list,
ClassicCategoryDTO::getId,
ClassicCategoryDTO::getParentId,
ClassicCategoryDTO::setChildrenList));
}
@Override
public Result<ClassicCategoryDTO> save(ClassicCategoryDTO dto) throws Exception {
if (StrUtil.isBlank(dto.getName())) {
throw new BusinessErrorException("分类名称不能为空");
}
if (StrUtil.length(dto.getName()) > 50) {
throw new BusinessErrorException("分类名称过长");
}
// 新增时校验层级
if (dto.getId() == null) {
if (dto.getLevel() == null || (dto.getLevel() != 1 && dto.getLevel() != 2)) {
throw new BusinessErrorException("层级参数错误");
}
if (dto.getLevel() == 2) {
if (dto.getParentId() == null || dto.getParentId() == 0) {
throw new BusinessErrorException("二级分类必须选择父级");
}
ClassicCategoryEntity parent = classicCategoryBaseService.getById(dto.getParentId());
if (parent == null || !Objects.equals(parent.getLevel(), 1)) {
throw new BusinessErrorException("父级分类不存在或不是一级分类");
}
} else {
dto.setParentId(0L);
}
}
// 名称去重(同父级下不可重名)
LambdaQueryWrapper<ClassicCategoryEntity> duplicateQuery = new LambdaQueryWrapper<ClassicCategoryEntity>()
.eq(ClassicCategoryEntity::getName, dto.getName())
.eq(ClassicCategoryEntity::getParentId, dto.getParentId());
if (dto.getId() != null) {
duplicateQuery.ne(ClassicCategoryEntity::getId, dto.getId());
}
if (classicCategoryBaseService.count(duplicateQuery) > 0) {
throw new BusinessErrorException("同级下已存在同名分类");
}
ClassicCategoryEntity entity = dto.toEntity(ClassicCategoryEntity::new);
classicCategoryBaseService.saveOrUpdate(entity);
return Result.success(entity.toDTO(ClassicCategoryDTO::new));
}
@Override
public Result<String> delete(Long id) throws Exception {
ClassicCategoryEntity category = classicCategoryBaseService.getById(id);
if (category == null) {
throw new BusinessErrorException("分类不存在");
}
// 如果是一级分类,校验是否有子分类
if (Objects.equals(category.getLevel(), 1)) {
long childCount = classicCategoryBaseService.count(
new LambdaQueryWrapper<ClassicCategoryEntity>().eq(ClassicCategoryEntity::getParentId, id));
if (childCount > 0) {
throw new BusinessErrorException("该分类下存在子分类,无法删除");
}
}
classicCategoryBaseService.removeById(id);
return Result.success("删除成功");
}
}

40
src/main/java/com/project/classicpaper/controller/ClassicCategoryController.java

@ -0,0 +1,40 @@
package com.project.classicpaper.controller;
import com.project.base.domain.result.Result;
import com.project.classicpaper.application.ClassicCategoryApplicationService;
import com.project.classicpaper.domain.dto.ClassicCategoryDTO;
import com.project.operation.annotation.OperationLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@Slf4j
@RequestMapping("/api/admin/classicCategory")
public class ClassicCategoryController {
@Autowired
private ClassicCategoryApplicationService classicCategoryApplicationService;
@GetMapping("/treeList")
public Result<List<ClassicCategoryDTO>> treeList() {
return classicCategoryApplicationService.treeList();
}
@PostMapping("/save")
@OperationLog(module = "经典分类")
public Result<ClassicCategoryDTO> save(ClassicCategoryDTO dto) throws Exception {
return classicCategoryApplicationService.save(dto);
}
@PostMapping("/delete")
@OperationLog(module = "经典分类")
public Result<String> delete(Long id) throws Exception {
return classicCategoryApplicationService.delete(id);
}
}

22
src/main/java/com/project/classicpaper/domain/dto/ClassicCategoryDTO.java

@ -0,0 +1,22 @@
package com.project.classicpaper.domain.dto;
import com.project.base.domain.dto.BaseDTO;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ClassicCategoryDTO extends BaseDTO {
private Long id;
private String name;
private Long parentId;
private Integer level;
private Integer sort = 0;
private List<ClassicCategoryDTO> childrenList = new ArrayList<>();
}

43
src/main/java/com/project/classicpaper/domain/entity/ClassicCategoryEntity.java

@ -0,0 +1,43 @@
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_category",
indexes = {@Index(name = "Idx_parent_id", columnList = "parent_id")})
@Entity
@TableName(value = "evaluator_classic_category")
@EqualsAndHashCode(callSuper = true)
public class ClassicCategoryEntity 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 = "parent_id")
@TableField("parent_id")
@Comment("父级ID,一级分类为0")
private Long parentId;
@Column(name = "level")
@TableField("level")
@Comment("层级:1-一级分类,2-二级分类")
private Integer level;
@Column(name = "sort")
@TableField("sort")
@Comment("排序权重")
private Integer sort = 0;
}

7
src/main/java/com/project/classicpaper/domain/service/ClassicCategoryBaseService.java

@ -0,0 +1,7 @@
package com.project.classicpaper.domain.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.project.classicpaper.domain.entity.ClassicCategoryEntity;
public interface ClassicCategoryBaseService extends IService<ClassicCategoryEntity> {
}

11
src/main/java/com/project/classicpaper/domain/service/impl/ClassicCategoryBaseServiceImpl.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.ClassicCategoryEntity;
import com.project.classicpaper.domain.service.ClassicCategoryBaseService;
import com.project.classicpaper.mapper.ClassicCategoryMapper;
import org.springframework.stereotype.Service;
@Service
public class ClassicCategoryBaseServiceImpl extends ServiceImpl<ClassicCategoryMapper, ClassicCategoryEntity> implements ClassicCategoryBaseService {
}

9
src/main/java/com/project/classicpaper/mapper/ClassicCategoryMapper.java

@ -0,0 +1,9 @@
package com.project.classicpaper.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.project.classicpaper.domain.entity.ClassicCategoryEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ClassicCategoryMapper extends BaseMapper<ClassicCategoryEntity> {
}

5
src/main/java/com/project/task/config/ExamScoreRatioConfig.java

@ -23,6 +23,11 @@ public class ExamScoreRatioConfig {
*/
private Integer trueFalse = 1;
/**
* 简答题权重默认 4
*/
private Integer shortAnswer = 4;
/**
* 总分固定 100
*/

8
src/main/java/com/project/task/domain/dto/TaskDTO.java

@ -80,6 +80,14 @@ public class TaskDTO extends BaseDTO {
private Double shortAnswerScore;
private Long classicCategoryId;
private String classicCategoryName;
private Long classicCategoryParentId;
private String classicCategoryParentName;
// =============== V1.1 新增字段 结束 ============
}

20
src/main/java/com/project/task/domain/entity/TaskEntity.java

@ -168,6 +168,26 @@ public class TaskEntity extends BaseEntity {
@Comment("简答题单题分值")
private Double shortAnswerScore = 0D;
@Column(name = "classic_category_id")
@TableField("classic_category_id")
@Comment("经典套卷分类ID")
private Long classicCategoryId;
@Column(name = "classic_category_name", columnDefinition = "varchar(200) comment '经典套卷分类名称'")
@TableField("classic_category_name")
@Comment("经典套卷分类名称")
private String classicCategoryName;
@Column(name = "classic_category_parent_id")
@TableField("classic_category_parent_id")
@Comment("经典套卷父级分类ID")
private Long classicCategoryParentId;
@Column(name = "classic_category_parent_name", columnDefinition = "varchar(200) comment '经典套卷父级分类名称'")
@TableField("classic_category_parent_name")
@Comment("经典套卷父级分类名称")
private String classicCategoryParentName;
// =============== V1.1 新增字段 结束 ============
}

168
src/main/java/com/project/task/domain/service/impl/SaveOrUpdateTaskDomainServiceImpl.java

@ -1,10 +1,5 @@
package com.project.task.domain.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.project.base.domain.exception.BusinessErrorException;
import com.project.base.domain.result.Result;
@ -12,15 +7,16 @@ import com.project.ding.domain.dto.UserDTO;
import com.project.ding.domain.entity.UserEntity;
import com.project.ding.domain.service.UserBaseService;
import com.project.ding.utils.DingUtil;
import com.project.information.domain.dto.KnowledgePointStatisticsDTO;
import com.project.classicpaper.domain.entity.ClassicCategoryEntity;
import com.project.classicpaper.domain.service.ClassicCategoryBaseService;
import com.project.information.domain.entity.ProductLineEntity;
import com.project.information.domain.service.GetStatisticsKnowledgePointDomainService;
import com.project.information.domain.service.ProductLineBaseService;
import com.project.task.config.ExamScoreRatioConfig;
import com.project.task.domain.dto.TaskDTO;
import com.project.task.domain.entity.TaskEntity;
import com.project.task.domain.entity.TaskUserEntity;
import com.project.task.domain.enums.ExamModeEnum;
import com.project.task.domain.service.InitTaskDomainService;
import com.project.task.domain.service.strategy.TaskConfigStrategyFactory;
import com.project.task.domain.service.SaveOrUpdateTaskDomainService;
import com.project.task.domain.service.TaskBaseService;
import com.project.task.domain.service.TaskUserBaseService;
@ -39,9 +35,6 @@ import java.util.stream.Collectors;
@Service
public class SaveOrUpdateTaskDomainServiceImpl implements SaveOrUpdateTaskDomainService {
@Autowired
private ExamScoreRatioConfig examScoreRatioConfig;
@Autowired
private ProductLineBaseService productLineBaseService;
@ -61,45 +54,41 @@ public class SaveOrUpdateTaskDomainServiceImpl implements SaveOrUpdateTaskDomain
private InitTaskDomainService initTaskDomainService;
@Autowired
private GetStatisticsKnowledgePointDomainService getStatisticsKnowledgePointDomainService;
private TaskConfigStrategyFactory taskConfigStrategyFactory;
@Autowired
private ClassicCategoryBaseService classicCategoryBaseService;
@Override
@Transactional(rollbackFor = Exception.class)
public Result<TaskDTO> saveOrUpdate(TaskDTO dto) throws Exception {
checkDTO(dto);
// 计算各题型赋分
// 1. 计算总权重份额
int totalUnit = dto.getSingleChoiceNum() * examScoreRatioConfig.getSingle()
+ dto.getMultipleChoiceNum() * examScoreRatioConfig.getMultiple()
+ dto.getTrueFalseNum() * examScoreRatioConfig.getTrueFalse();
double base = examScoreRatioConfig.getTotalScore() * 1.0 / totalUnit;
// 2. 基础赋值:按比例初步分配并保留2位小数
dto.setSingleChoiceScore(dto.getSingleChoiceNum() > 0 ? Math.round(base * examScoreRatioConfig.getSingle() * 100) / 100.0 : 0.0);
dto.setMultipleChoiceScore(dto.getMultipleChoiceNum() > 0 ? Math.round(base * examScoreRatioConfig.getMultiple() * 100) / 100.0 : 0.0);
dto.setTrueFalseScore(dto.getTrueFalseNum() > 0 ? Math.round(base * examScoreRatioConfig.getTrueFalse() * 100) / 100.0 : 0.0);
// 3. 余数平差:从配置中读取总分进行逆向补齐,确保总分 100% 对应配置
double configTotal = examScoreRatioConfig.getTotalScore();
if (dto.getTrueFalseNum() > 0) {
dto.setTrueFalseScore(Math.round((configTotal - dto.getSingleChoiceNum() * dto.getSingleChoiceScore() - dto.getMultipleChoiceNum() * dto.getMultipleChoiceScore()) / dto.getTrueFalseNum() * 100) / 100.0);
} else if (dto.getMultipleChoiceNum() > 0) {
dto.setMultipleChoiceScore(Math.round((configTotal - dto.getSingleChoiceNum() * dto.getSingleChoiceScore()) / dto.getMultipleChoiceNum() * 100) / 100.0);
} else if (dto.getSingleChoiceNum() > 0) {
dto.setSingleChoiceScore(Math.round(configTotal / dto.getSingleChoiceNum() * 100) / 100.0);
// 按考试模式走不同策略:生成式(校验+赋分) / 经典套题(校验+默认值)
taskConfigStrategyFactory.getStrategy(dto.getExamMode()).validateAndConfigure(dto);
// 补全冗余字段(经典模式不关联产品线,subLineId 为 null 时跳过)
if (dto.getSubLineId() != null) {
ProductLineEntity subProductLine = productLineBaseService.getById(dto.getSubLineId());
dto.setSubLineName(subProductLine.getName());
dto.setLineId(subProductLine.getParentId());
dto.setLineName(subProductLine.getParentName());
dto.setBusinessId(subProductLine.getBusinessId());
}
// 经典模式:补全分类冗余字段
if (ExamModeEnum.CLASSIC.getValue().equals(dto.getExamMode()) && dto.getClassicCategoryId() != null) {
ClassicCategoryEntity category = classicCategoryBaseService.getById(dto.getClassicCategoryId());
if (category != null) {
dto.setClassicCategoryName(category.getName());
// 如果是二级分类,补全父级信息
if (category.getParentId() != null && category.getParentId() != 0) {
dto.setClassicCategoryParentId(category.getParentId());
ClassicCategoryEntity parentCategory = classicCategoryBaseService.getById(category.getParentId());
if (parentCategory != null) {
dto.setClassicCategoryParentName(parentCategory.getName());
}
}
}
}
dto.setTotalScore((int) configTotal);
// 补全冗余字段
ProductLineEntity subProductLine = productLineBaseService.getById(dto.getSubLineId());
dto.setSubLineName(subProductLine.getName());
dto.setLineId(subProductLine.getParentId());
dto.setLineName(subProductLine.getParentName());
dto.setBusinessId(subProductLine.getBusinessId());
// 保存
TaskEntity saveEntity = dto.toEntity(TaskEntity::new);
saveEntity.setParticipantNum(dto.getParticipantUserIdList().size());
@ -137,8 +126,8 @@ public class SaveOrUpdateTaskDomainServiceImpl implements SaveOrUpdateTaskDomain
}
taskUserBaseService.saveBatch(taskUserEntityList);
// todo 知识点分簇,预出题
if (Objects.isNull(dto.getId())) {
// 仅生成式模式触发异步初始化(知识点分簇、预出题)
if (Objects.isNull(dto.getId()) && ExamModeEnum.GENERATIVE.getValue().equals(dto.getExamMode())) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
@ -163,93 +152,4 @@ public class SaveOrUpdateTaskDomainServiceImpl implements SaveOrUpdateTaskDomain
}
}
private void checkDTO(TaskDTO dto) throws Exception {
if (StrUtil.isBlank(dto.getName())) {
throw new BusinessErrorException("考试任务名称不能为空");
}
//校验考试名称是否存在
if (dto.getId() == null){
long count = taskBaseService.count(new LambdaQueryWrapper<TaskEntity>().eq(TaskEntity::getName, dto.getName()));
if (count > 0){
throw new BusinessErrorException("考试任务名称已存在");
}
}
if (StrUtil.length(dto.getName()) > 10) {
throw new BusinessErrorException("考试任务名称过长");
}
if (Objects.isNull(dto.getStartTime())) {
throw new BusinessErrorException("开始时间不能为空");
}
if (Objects.isNull(dto.getEndTime())) {
throw new BusinessErrorException("截止时间不能为空");
}
if (DateUtil.between(dto.getStartTime() , dto.getEndTime() , DateUnit.DAY) < 0) {
throw new BusinessErrorException("截止时间不能早于开始时间");
}
if (Objects.isNull(dto.getSubLineId())) {
throw new BusinessErrorException("关联产品线不能为空");
}
if (Objects.isNull(dto.getDuration())) {
throw new BusinessErrorException("考试时长不能为空");
}
if (Objects.isNull(dto.getRemindTimePoint())) {
throw new BusinessErrorException("剩余时间提示点不能为空");
}
if (dto.getRemindTimePoint() > dto.getDuration()) {
throw new BusinessErrorException("剩余时间提示点不能大于考试时长");
}
if (Objects.isNull(dto.getPassScore())) {
throw new BusinessErrorException("通过考试分数线不能为空");
}
if (!Validator.isBetween(dto.getPassScore(), 1, 100)) {
throw new BusinessErrorException("通过考试分数线设置错误");
}
if (StrUtil.isBlank(dto.getNote())) {
throw new BusinessErrorException("注意事项不能为空");
}
if (Objects.isNull(dto.getSingleChoiceNum())) {
throw new BusinessErrorException("单选题数量不能为空");
}
if (Objects.isNull(dto.getMultipleChoiceNum())) {
throw new BusinessErrorException("多选题数量不能为空");
}
if (Objects.isNull(dto.getTrueFalseNum())) {
throw new BusinessErrorException("判断题数量不能为空");
}
if (CollUtil.isEmpty(dto.getParticipantUserIdList())) {
throw new BusinessErrorException("参与用户不能为空");
}
if (CollUtil.isEmpty(dto.getRelatedDocumentList())) {
throw new BusinessErrorException("关联文档列表不能为空");
}
if (Objects.isNull(dto.getAccurateGraspNum())) {
throw new BusinessErrorException("精准掌握知识点数量不能为空");
}
if (Objects.isNull(dto.getVagueGraspNum())) {
throw new BusinessErrorException("模糊掌握知识点数量不能为空");
}
//校验知识点数是否有变化
Result<KnowledgePointStatisticsDTO> statistics = getStatisticsKnowledgePointDomainService.getStatistics(dto.getSubLineId());
if (!Objects.equals(statistics.getData().getAccurateGraspNum() , dto.getAccurateGraspNum()) ||
!Objects.equals(statistics.getData().getVagueGraspNum() , dto.getVagueGraspNum())) {
throw new BusinessErrorException("知识点数有变化,请重新选择关联产品线");
}
// 题目总数
int totalQuestions = dto.getSingleChoiceNum() + dto.getMultipleChoiceNum() + dto.getTrueFalseNum();
// 获取知识点总数用于校验
int totalKnowledgePoints = dto.getAccurateGraspNum() + dto.getVagueGraspNum();
if (totalQuestions > totalKnowledgePoints) {
throw new BusinessErrorException("题目总数" + totalQuestions + ",不能超过知识点总数" + totalKnowledgePoints);
}
if (totalQuestions > 100) {
throw new BusinessErrorException("题目总数不能超过 100 道,当前为:" + totalQuestions);
}
if (totalQuestions < 1) {
throw new BusinessErrorException("题目总数不能少于 1 道");
}
}
}

103
src/main/java/com/project/task/domain/service/strategy/ClassicTaskConfigStrategy.java

@ -0,0 +1,103 @@
package com.project.task.domain.service.strategy;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.project.base.domain.exception.BusinessErrorException;
import com.project.task.domain.dto.TaskDTO;
import com.project.task.domain.entity.TaskEntity;
import com.project.task.domain.service.TaskBaseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 经典套题模式校验 + 字段默认值
* 赋分来自套卷本身此处不做赋分计算
*/
@Component
public class ClassicTaskConfigStrategy implements TaskConfigStrategy {
@Autowired
private TaskBaseService taskBaseService;
@Override
public void validateAndConfigure(TaskDTO dto) throws Exception {
validate(dto);
fillDefaults(dto);
}
/**
* 经典模式校验通用参数 + classicPaperId 必填
*/
private void validate(TaskDTO dto) throws Exception {
if (StrUtil.isBlank(dto.getName())) {
throw new BusinessErrorException("考试任务名称不能为空");
}
if (dto.getId() == null) {
long count = taskBaseService.count(new LambdaQueryWrapper<TaskEntity>().eq(TaskEntity::getName, dto.getName()));
if (count > 0) {
throw new BusinessErrorException("考试任务名称已存在");
}
}
if (StrUtil.length(dto.getName()) > 10) {
throw new BusinessErrorException("考试任务名称过长");
}
if (Objects.isNull(dto.getStartTime())) {
throw new BusinessErrorException("开始时间不能为空");
}
if (Objects.isNull(dto.getEndTime())) {
throw new BusinessErrorException("截止时间不能为空");
}
if (DateUtil.between(dto.getStartTime(), dto.getEndTime(), DateUnit.DAY) < 0) {
throw new BusinessErrorException("截止时间不能早于开始时间");
}
// 经典模式不关联产品线,subLineId 非必填
if (Objects.isNull(dto.getDuration())) {
throw new BusinessErrorException("考试时长不能为空");
}
if (Objects.isNull(dto.getRemindTimePoint())) {
throw new BusinessErrorException("剩余时间提示点不能为空");
}
if (dto.getRemindTimePoint() > dto.getDuration()) {
throw new BusinessErrorException("剩余时间提示点不能大于考试时长");
}
if (Objects.isNull(dto.getPassScore())) {
throw new BusinessErrorException("通过考试分数线不能为空");
}
if (!Validator.isBetween(dto.getPassScore(), 1, 100)) {
throw new BusinessErrorException("通过考试分数线设置错误");
}
if (StrUtil.isBlank(dto.getNote())) {
throw new BusinessErrorException("注意事项不能为空");
}
if (CollUtil.isEmpty(dto.getParticipantUserIdList())) {
throw new BusinessErrorException("参与用户不能为空");
}
// 经典模式特有校验
if (Objects.isNull(dto.getClassicPaperId())) {
throw new BusinessErrorException("经典套卷不能为空");
}
}
/**
* 经典模式下题目数量和分数来自套卷DTO 中可能为 null赋默认值 0 避免 NPE
*/
private void fillDefaults(TaskDTO dto) {
if (dto.getSingleChoiceNum() == null) dto.setSingleChoiceNum(0);
if (dto.getMultipleChoiceNum() == null) dto.setMultipleChoiceNum(0);
if (dto.getTrueFalseNum() == null) dto.setTrueFalseNum(0);
if (dto.getShortAnswerNum() == null) dto.setShortAnswerNum(0);
if (dto.getSingleChoiceScore() == null) dto.setSingleChoiceScore(0.0);
if (dto.getMultipleChoiceScore() == null) dto.setMultipleChoiceScore(0.0);
if (dto.getTrueFalseScore() == null) dto.setTrueFalseScore(0.0);
if (dto.getShortAnswerScore() == null) dto.setShortAnswerScore(0.0);
if (dto.getAccurateGraspNum() == null) dto.setAccurateGraspNum(0);
if (dto.getVagueGraspNum() == null) dto.setVagueGraspNum(0);
if (dto.getTotalScore() == null) dto.setTotalScore(0);
}
}

168
src/main/java/com/project/task/domain/service/strategy/GenerativeTaskConfigStrategy.java

@ -0,0 +1,168 @@
package com.project.task.domain.service.strategy;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
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.information.domain.dto.KnowledgePointStatisticsDTO;
import com.project.information.domain.service.GetStatisticsKnowledgePointDomainService;
import com.project.task.config.ExamScoreRatioConfig;
import com.project.task.domain.dto.TaskDTO;
import com.project.task.domain.entity.TaskEntity;
import com.project.task.domain.service.TaskBaseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 生成式套题模式校验 + 赋分
* 校验逻辑从 SaveOrUpdateTaskDomainServiceImpl.checkDTO() 原样搬出新增简答题赋分
*/
@Component
public class GenerativeTaskConfigStrategy implements TaskConfigStrategy {
@Autowired
private ExamScoreRatioConfig examScoreRatioConfig;
@Autowired
private TaskBaseService taskBaseService;
@Autowired
private GetStatisticsKnowledgePointDomainService getStatisticsKnowledgePointDomainService;
@Override
public void validateAndConfigure(TaskDTO dto) throws Exception {
validate(dto);
configureScore(dto);
}
/**
* 校验任务参数 SaveOrUpdateTaskDomainServiceImpl.checkDTO 逻辑
*/
private void validate(TaskDTO dto) throws Exception {
if (StrUtil.isBlank(dto.getName())) {
throw new BusinessErrorException("考试任务名称不能为空");
}
if (dto.getId() == null) {
long count = taskBaseService.count(new LambdaQueryWrapper<TaskEntity>().eq(TaskEntity::getName, dto.getName()));
if (count > 0) {
throw new BusinessErrorException("考试任务名称已存在");
}
}
if (StrUtil.length(dto.getName()) > 10) {
throw new BusinessErrorException("考试任务名称过长");
}
if (Objects.isNull(dto.getStartTime())) {
throw new BusinessErrorException("开始时间不能为空");
}
if (Objects.isNull(dto.getEndTime())) {
throw new BusinessErrorException("截止时间不能为空");
}
if (DateUtil.between(dto.getStartTime(), dto.getEndTime(), DateUnit.DAY) < 0) {
throw new BusinessErrorException("截止时间不能早于开始时间");
}
if (Objects.isNull(dto.getSubLineId())) {
throw new BusinessErrorException("关联产品线不能为空");
}
if (Objects.isNull(dto.getDuration())) {
throw new BusinessErrorException("考试时长不能为空");
}
if (Objects.isNull(dto.getRemindTimePoint())) {
throw new BusinessErrorException("剩余时间提示点不能为空");
}
if (dto.getRemindTimePoint() > dto.getDuration()) {
throw new BusinessErrorException("剩余时间提示点不能大于考试时长");
}
if (Objects.isNull(dto.getPassScore())) {
throw new BusinessErrorException("通过考试分数线不能为空");
}
if (!Validator.isBetween(dto.getPassScore(), 1, 100)) {
throw new BusinessErrorException("通过考试分数线设置错误");
}
if (StrUtil.isBlank(dto.getNote())) {
throw new BusinessErrorException("注意事项不能为空");
}
if (Objects.isNull(dto.getSingleChoiceNum())) {
throw new BusinessErrorException("单选题数量不能为空");
}
if (Objects.isNull(dto.getMultipleChoiceNum())) {
throw new BusinessErrorException("多选题数量不能为空");
}
if (Objects.isNull(dto.getTrueFalseNum())) {
throw new BusinessErrorException("判断题数量不能为空");
}
if (CollUtil.isEmpty(dto.getParticipantUserIdList())) {
throw new BusinessErrorException("参与用户不能为空");
}
if (CollUtil.isEmpty(dto.getRelatedDocumentList())) {
throw new BusinessErrorException("关联文档列表不能为空");
}
if (Objects.isNull(dto.getAccurateGraspNum())) {
throw new BusinessErrorException("精准掌握知识点数量不能为空");
}
if (Objects.isNull(dto.getVagueGraspNum())) {
throw new BusinessErrorException("模糊掌握知识点数量不能为空");
}
Result<KnowledgePointStatisticsDTO> statistics = getStatisticsKnowledgePointDomainService.getStatistics(dto.getSubLineId());
if (!Objects.equals(statistics.getData().getAccurateGraspNum(), dto.getAccurateGraspNum()) ||
!Objects.equals(statistics.getData().getVagueGraspNum(), dto.getVagueGraspNum())) {
throw new BusinessErrorException("知识点数有变化,请重新选择关联产品线");
}
int totalQuestions = dto.getSingleChoiceNum() + dto.getMultipleChoiceNum() + dto.getTrueFalseNum()
+ (dto.getShortAnswerNum() != null ? dto.getShortAnswerNum() : 0);
int totalKnowledgePoints = dto.getAccurateGraspNum() + dto.getVagueGraspNum();
if (totalQuestions > totalKnowledgePoints) {
throw new BusinessErrorException("题目总数" + totalQuestions + ",不能超过知识点总数" + totalKnowledgePoints);
}
if (totalQuestions > 100) {
throw new BusinessErrorException("题目总数不能超过 100 道,当前为:" + totalQuestions);
}
if (totalQuestions < 1) {
throw new BusinessErrorException("题目总数不能少于 1 道");
}
}
/**
* 各题型赋分计算原逻辑 + 简答题权重
*/
private void configureScore(TaskDTO dto) {
int shortAnswerNum = dto.getShortAnswerNum() != null ? dto.getShortAnswerNum() : 0;
// 1. 计算总权重份额
int totalUnit = dto.getSingleChoiceNum() * examScoreRatioConfig.getSingle()
+ dto.getMultipleChoiceNum() * examScoreRatioConfig.getMultiple()
+ dto.getTrueFalseNum() * examScoreRatioConfig.getTrueFalse()
+ shortAnswerNum * examScoreRatioConfig.getShortAnswer();
double base = examScoreRatioConfig.getTotalScore() * 1.0 / totalUnit;
// 2. 基础赋值:按比例初步分配并保留2位小数
dto.setSingleChoiceScore(dto.getSingleChoiceNum() > 0 ? Math.round(base * examScoreRatioConfig.getSingle() * 100) / 100.0 : 0.0);
dto.setMultipleChoiceScore(dto.getMultipleChoiceNum() > 0 ? Math.round(base * examScoreRatioConfig.getMultiple() * 100) / 100.0 : 0.0);
dto.setTrueFalseScore(dto.getTrueFalseNum() > 0 ? Math.round(base * examScoreRatioConfig.getTrueFalse() * 100) / 100.0 : 0.0);
dto.setShortAnswerScore(shortAnswerNum > 0 ? Math.round(base * examScoreRatioConfig.getShortAnswer() * 100) / 100.0 : 0.0);
// 3. 余数平差:从配置中读取总分进行逆向补齐,确保总分 100% 对应配置
double configTotal = examScoreRatioConfig.getTotalScore();
if (dto.getTrueFalseNum() > 0) {
dto.setTrueFalseScore(Math.round((configTotal - dto.getSingleChoiceNum() * dto.getSingleChoiceScore()
- dto.getMultipleChoiceNum() * dto.getMultipleChoiceScore()
- shortAnswerNum * dto.getShortAnswerScore()) / dto.getTrueFalseNum() * 100) / 100.0);
} else if (shortAnswerNum > 0) {
dto.setShortAnswerScore(Math.round((configTotal - dto.getSingleChoiceNum() * dto.getSingleChoiceScore()
- dto.getMultipleChoiceNum() * dto.getMultipleChoiceScore()) / shortAnswerNum * 100) / 100.0);
} else if (dto.getMultipleChoiceNum() > 0) {
dto.setMultipleChoiceScore(Math.round((configTotal - dto.getSingleChoiceNum() * dto.getSingleChoiceScore()) / dto.getMultipleChoiceNum() * 100) / 100.0);
} else if (dto.getSingleChoiceNum() > 0) {
dto.setSingleChoiceScore(Math.round(configTotal / dto.getSingleChoiceNum() * 100) / 100.0);
}
dto.setTotalScore((int) configTotal);
}
}

17
src/main/java/com/project/task/domain/service/strategy/TaskConfigStrategy.java

@ -0,0 +1,17 @@
package com.project.task.domain.service.strategy;
import com.project.task.domain.dto.TaskDTO;
/**
* 任务配置策略接口
* 不同考试模式生成式/经典套题的校验与赋分逻辑分离
*/
public interface TaskConfigStrategy {
/**
* 校验任务参数并完成赋分配置校验不通过直接抛异常
*
* @param dto 任务DTO
*/
void validateAndConfigure(TaskDTO dto) throws Exception;
}

25
src/main/java/com/project/task/domain/service/strategy/TaskConfigStrategyFactory.java

@ -0,0 +1,25 @@
package com.project.task.domain.service.strategy;
import com.project.task.domain.enums.ExamModeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 任务配置策略工厂按考试模式返回对应策略
*/
@Component
public class TaskConfigStrategyFactory {
@Autowired
private GenerativeTaskConfigStrategy generativeTaskConfigStrategy;
@Autowired
private ClassicTaskConfigStrategy classicTaskConfigStrategy;
public TaskConfigStrategy getStrategy(Integer examMode) {
if (ExamModeEnum.CLASSIC.getValue().equals(examMode)) {
return classicTaskConfigStrategy;
}
return generativeTaskConfigStrategy;
}
}
Loading…
Cancel
Save