first commit

This commit is contained in:
Yakumo Hokori
2026-03-04 21:26:38 +08:00
commit 03304a1714
19 changed files with 1271 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.baobaot.exam;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ExamApplication {
public static void main(String[] args) {
SpringApplication.run(ExamApplication.class, args);
}
}

View File

@@ -0,0 +1,35 @@
package com.baobaot.exam.Service;
import com.baobaot.exam.dao.question;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface adminService extends IService<question> {
/**
* 添加选择题type=1
*/
boolean addChoiceQuestion(String title, Integer type, String answer_a, String answer_b,
String answer_c, String answer_d, String r_answer, Integer score);
/**
* 添加简答题type=2
*/
boolean addEssayQuestion(String title, Integer type, String answer, String r_answer, Integer score);
/**
* 根据类型查询所有题目
*/
List<question> getQuestionsByType(Integer type);
/**
* 根据分数查询所有题目
*/
List<question> getQuestionsByScore(Integer score);
/**
* 根据标题删除题目
*/
boolean deleteQuestionByTitle(String title);
}

View File

@@ -0,0 +1,63 @@
package com.baobaot.exam.Service.impl;
import com.baobaot.exam.Service.adminService;
import com.baobaot.exam.dao.question;
import com.baobaot.exam.mapper.questionMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class adminServiceImpl extends ServiceImpl<questionMapper, question> implements adminService {
@Override
public boolean addChoiceQuestion(String title, Integer type, String answer_a, String answer_b,
String answer_c, String answer_d, String r_answer, Integer score) {
question q = question.builder()
.title(title)
.type(type)
.answerA(answer_a)
.answerB(answer_b)
.answerC(answer_c)
.answerD(answer_d)
.rAnswer(r_answer)
.score(score)
.build();
return save(q);
}
@Override
public boolean addEssayQuestion(String title, Integer type, String answer, String r_answer, Integer score) {
question q = question.builder()
.title(title)
.type(type)
.answer(answer)
.rAnswer(r_answer)
.score(score)
.build();
return save(q);
}
@Override
public List<question> getQuestionsByType(Integer type) {
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(question::getType, type);
return list(wrapper);
}
@Override
public List<question> getQuestionsByScore(Integer score) {
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(question::getScore, score);
return list(wrapper);
}
@Override
public boolean deleteQuestionByTitle(String title) {
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(question::getTitle, title);
return remove(wrapper);
}
}

View File

@@ -0,0 +1,364 @@
package com.baobaot.exam.Service.impl;
import com.baobaot.exam.Service.makeExamService;
import com.baobaot.exam.dao.examPaper;
import com.baobaot.exam.dao.question;
import com.baobaot.exam.mapper.examPaperMapper;
import com.baobaot.exam.mapper.questionMapper;
import com.baobaot.exam.util.UidGenerator;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class makeExamServiceImpl extends ServiceImpl<examPaperMapper, examPaper> implements makeExamService {
@Autowired
private questionMapper questionMapper;
@Autowired
private examPaperMapper examPaperMapper;
private static final int TARGET_SCORE = 100;
// 比例范围:选择题:简答题 在 5:5 到 7:3 之间
private static final double MIN_CHOICE_RATIO = 0.5; // 至少50%选择题
private static final double MAX_CHOICE_RATIO = 0.7; // 最多70%选择题
@Override
public examPaper createExamPaper(String title) {
// 生成唯一UID
String uid = UidGenerator.generateUniqueUid(this::isUidExists);
// 获取所有题目
List<question> allChoiceQuestions = getQuestionsByType(1);
List<question> allEssayQuestions = getQuestionsByType(2);
if (allChoiceQuestions.size() < 5 || allEssayQuestions.size() < 5) {
throw new RuntimeException("题库题目不足至少需要5道选择题和5道简答题");
}
// 按分值分组
Map<Integer, List<question>> choiceByScore = allChoiceQuestions.stream()
.collect(Collectors.groupingBy(q -> q.getScore() != null ? q.getScore() : 0));
Map<Integer, List<question>> essayByScore = allEssayQuestions.stream()
.collect(Collectors.groupingBy(q -> q.getScore() != null ? q.getScore() : 0));
// 随机打乱每个分组的题目
choiceByScore.values().forEach(Collections::shuffle);
essayByScore.values().forEach(Collections::shuffle);
// 尝试找到满足100分的组合
ExamSelection selection = findExact100Combination(allChoiceQuestions, allEssayQuestions);
if (selection == null) {
// 如果找不到精确100分尝试允许5分误差
selection = findApproximateCombination(allChoiceQuestions, allEssayQuestions, 5);
}
if (selection == null) {
throw new RuntimeException("无法组合出合适的试卷,请检查题库分值设置");
}
// 构建ID数组字符串
String selectIds = selection.choiceIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(",", "[", "]"));
String contentIds = selection.essayIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(",", "[", "]"));
// 创建试卷
examPaper paper = examPaper.builder()
.uid(uid)
.title(title)
.select(selectIds)
.content(contentIds)
.build();
examPaperMapper.insert(paper);
return paper;
}
@Override
public examPaper getExamPaperByUid(String uid) {
LambdaQueryWrapper<examPaper> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(examPaper::getUid, uid);
return examPaperMapper.selectOne(wrapper);
}
private boolean isUidExists(String uid) {
LambdaQueryWrapper<examPaper> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(examPaper::getUid, uid);
return examPaperMapper.selectCount(wrapper) > 0;
}
private List<question> getQuestionsByType(Integer type) {
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(question::getType, type);
return questionMapper.selectList(wrapper);
}
/**
* 寻找精确100分的组合
*/
private ExamSelection findExact100Combination(List<question> choices, List<question> essays) {
// 尝试不同数量组合,比例在 5:5 到 7:3 之间
// 总题数从10到60找最佳组合
for (int totalCount = 10; totalCount <= 60; totalCount++) {
int minChoiceCount = (int) Math.ceil(totalCount * MIN_CHOICE_RATIO);
int maxChoiceCount = (int) (totalCount * MAX_CHOICE_RATIO);
for (int choiceCount = minChoiceCount; choiceCount <= maxChoiceCount; choiceCount++) {
int essayCount = totalCount - choiceCount;
if (essayCount < totalCount * 0.3 || essayCount > totalCount * 0.5) {
continue; // 确保简答题比例在30%-50%
}
if (choices.size() < choiceCount || essays.size() < essayCount) {
continue;
}
// 尝试找到精确100分的组合
ExamSelection result = tryFindExactScore(
choices, essays, choiceCount, essayCount, TARGET_SCORE);
if (result != null) {
return result;
}
}
}
return null;
}
/**
* 尝试找到精确目标分数的组合(使用动态规划思想)
*/
private ExamSelection tryFindExactScore(List<question> allChoices, List<question> allEssays,
int needChoiceCount, int needEssayCount, int targetScore) {
// 按分值分组
Map<Integer, List<question>> choiceByScore = allChoices.stream()
.limit(needChoiceCount * 3) // 取足够多的候选
.collect(Collectors.groupingBy(q -> q.getScore() != null ? q.getScore() : 0));
Map<Integer, List<question>> essayByScore = allEssays.stream()
.limit(needEssayCount * 3)
.collect(Collectors.groupingBy(q -> q.getScore() != null ? q.getScore() : 0));
// 尝试所有可能的分数组合
// 选择题总分范围needChoiceCount * 2 到 needChoiceCount * 3
// 简答题总分范围needEssayCount * 4 到 needEssayCount * 5
for (int choiceScore = needChoiceCount * 2; choiceScore <= needChoiceCount * 3; choiceScore++) {
int essayScore = targetScore - choiceScore;
if (essayScore < needEssayCount * 4 || essayScore > needEssayCount * 5) {
continue;
}
// 尝试选择选择题达到choiceScore
List<question> selectedChoices = selectQuestionsWithExactScore(
choiceByScore, needChoiceCount, choiceScore);
if (selectedChoices == null) continue;
// 尝试选择简答题达到essayScore
List<question> selectedEssays = selectQuestionsWithExactScore(
essayByScore, needEssayCount, essayScore);
if (selectedEssays == null) continue;
// 找到了!
return new ExamSelection(
selectedChoices.stream().map(question::getId).collect(Collectors.toList()),
selectedEssays.stream().map(question::getId).collect(Collectors.toList())
);
}
return null;
}
/**
* 从分组中选择指定数量、指定总分的题目
*/
private List<question> selectQuestionsWithExactScore(Map<Integer, List<question>> byScore,
int count, int targetScore) {
// 获取可用分值
int score2 = byScore.getOrDefault(2, Collections.emptyList()).size();
int score3 = byScore.getOrDefault(3, Collections.emptyList()).size();
int score4 = byScore.getOrDefault(4, Collections.emptyList()).size();
int score5 = byScore.getOrDefault(5, Collections.emptyList()).size();
// 假设只有2分和3分的选择题或4分和5分的简答题
if (score2 > 0 || score3 > 0) {
// 选择题2分和3分
for (int c2 = 0; c2 <= Math.min(count, score2); c2++) {
int c3 = count - c2;
if (c3 <= score3 && c2 * 2 + c3 * 3 == targetScore) {
List<question> result = new ArrayList<>();
result.addAll(byScore.getOrDefault(2, Collections.emptyList()).subList(0, c2));
result.addAll(byScore.getOrDefault(3, Collections.emptyList()).subList(0, c3));
return result;
}
}
} else if (score4 > 0 || score5 > 0) {
// 简答题4分和5分
for (int c4 = 0; c4 <= Math.min(count, score4); c4++) {
int c5 = count - c4;
if (c5 <= score5 && c4 * 4 + c5 * 5 == targetScore) {
List<question> result = new ArrayList<>();
result.addAll(byScore.getOrDefault(4, Collections.emptyList()).subList(0, c4));
result.addAll(byScore.getOrDefault(5, Collections.emptyList()).subList(0, c5));
return result;
}
}
}
return null;
}
/**
* 寻找近似目标分数的组合(允许误差)
*/
private ExamSelection findApproximateCombination(List<question> choices, List<question> essays, int tolerance) {
for (int totalCount = 10; totalCount <= 60; totalCount++) {
int minChoiceCount = (int) Math.ceil(totalCount * MIN_CHOICE_RATIO);
int maxChoiceCount = (int) (totalCount * MAX_CHOICE_RATIO);
for (int choiceCount = minChoiceCount; choiceCount <= maxChoiceCount; choiceCount++) {
int essayCount = totalCount - choiceCount;
if (essayCount < totalCount * 0.3 || essayCount > totalCount * 0.5) {
continue;
}
if (choices.size() < choiceCount || essays.size() < essayCount) {
continue;
}
// 随机选择
Collections.shuffle(choices);
Collections.shuffle(essays);
List<question> selectedChoices = choices.subList(0, choiceCount);
List<question> selectedEssays = essays.subList(0, essayCount);
int total = calculateScore(selectedChoices) + calculateScore(selectedEssays);
if (Math.abs(total - TARGET_SCORE) <= tolerance) {
return new ExamSelection(
selectedChoices.stream().map(question::getId).collect(Collectors.toList()),
selectedEssays.stream().map(question::getId).collect(Collectors.toList())
);
}
}
}
return null;
}
private int calculateScore(List<question> questions) {
return questions.stream().mapToInt(q -> q.getScore() != null ? q.getScore() : 0).sum();
}
@Override
public List<question> getQuestionsByIds(List<Integer> ids) {
if (ids == null || ids.isEmpty()) {
return new ArrayList<>();
}
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
wrapper.in(question::getId, ids);
return questionMapper.selectList(wrapper);
}
@Override
public Map<String, Object> getExamPaperDetail(String uid) {
Map<String, Object> result = new HashMap<>();
examPaper paper = getExamPaperByUid(uid);
if (paper == null) {
return null;
}
result.put("uid", paper.getUid());
result.put("title", paper.getTitle());
// 解析选择题ID列表
List<Integer> choiceIds = parseIdList(paper.getSelect());
List<Integer> essayIds = parseIdList(paper.getContent());
// 获取题目详情
List<question> choiceQuestions = getQuestionsByIds(choiceIds);
List<question> essayQuestions = getQuestionsByIds(essayIds);
// 按ID顺序排序
choiceQuestions.sort((a, b) -> {
int indexA = choiceIds.indexOf(a.getId());
int indexB = choiceIds.indexOf(b.getId());
return Integer.compare(indexA, indexB);
});
essayQuestions.sort((a, b) -> {
int indexA = essayIds.indexOf(a.getId());
int indexB = essayIds.indexOf(b.getId());
return Integer.compare(indexA, indexB);
});
result.put("choiceQuestions", choiceQuestions);
result.put("essayQuestions", essayQuestions);
result.put("choiceCount", choiceQuestions.size());
result.put("essayCount", essayQuestions.size());
// 计算总分
int totalScore = choiceQuestions.stream().mapToInt(q -> q.getScore() != null ? q.getScore() : 0).sum()
+ essayQuestions.stream().mapToInt(q -> q.getScore() != null ? q.getScore() : 0).sum();
result.put("totalScore", totalScore);
return result;
}
/**
* 解析ID列表字符串 [1,2,3] -> List<Integer>
*/
private List<Integer> parseIdList(String idStr) {
if (idStr == null || idStr.isEmpty()) {
return new ArrayList<>();
}
// 去掉方括号
String clean = idStr.replace("[", "").replace("]", "");
if (clean.isEmpty()) {
return new ArrayList<>();
}
return Arrays.stream(clean.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Integer::parseInt)
.collect(Collectors.toList());
}
@Override
public List<examPaper> getExamList(String title) {
LambdaQueryWrapper<examPaper> wrapper = new LambdaQueryWrapper<>();
if (title != null && !title.trim().isEmpty()) {
wrapper.like(examPaper::getTitle, title);
}
wrapper.orderByDesc(examPaper::getId);
return examPaperMapper.selectList(wrapper);
}
private static class ExamSelection {
final List<Integer> choiceIds;
final List<Integer> essayIds;
ExamSelection(List<Integer> choiceIds, List<Integer> essayIds) {
this.choiceIds = choiceIds;
this.essayIds = essayIds;
}
}
}

View File

@@ -0,0 +1,36 @@
package com.baobaot.exam.Service;
import com.baobaot.exam.dao.examPaper;
import com.baobaot.exam.dao.question;
import java.util.List;
import java.util.Map;
public interface makeExamService {
/**
* 创建试卷
* 自动生成uid按7:3比例选择题和简答题总分100分
*/
examPaper createExamPaper(String title);
/**
* 根据uid查询试卷
*/
examPaper getExamPaperByUid(String uid);
/**
* 获取试卷详情(包含题目内容)
*/
Map<String, Object> getExamPaperDetail(String uid);
/**
* 根据ID列表获取题目
*/
List<question> getQuestionsByIds(List<Integer> ids);
/**
* 获取所有试卷列表(支持按标题模糊搜索)
*/
List<examPaper> getExamList(String title);
}

View File

@@ -0,0 +1,9 @@
package com.baobaot.exam.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.baobaot.exam.mapper")
public class MybatisPlusConfig {
}

View File

@@ -0,0 +1,38 @@
package com.baobaot.exam.dao;
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.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.baobaot.exam.dto.AnswerItem;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
@TableName("exam_back")
public class examBack {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField("uid")
private String uid; // 答题记录唯一ID
@TableField("examId")
private String examId; // 试卷UID
@TableField("title")
private String title; // 试卷标题
@TableField("name")
private String name; // 做题人名字
@TableField("content")
private String content; // JSON存储题目和答案 [{"exmaid": 1, "qid": 3, "answer": "A"}]
@TableField("score")
private String score; // 得分(可选)
}

View File

@@ -0,0 +1,24 @@
package com.baobaot.exam.dao;
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 lombok.Builder;
import lombok.Data;
@Data
@Builder
@TableName("exam_paper")
public class examPaper {
@TableId(type = IdType.AUTO)
private Integer id;
private String uid;
private String title;
@TableField("`select`")
private String select;
@TableField("`content`")
private String content;
}

View File

@@ -0,0 +1,25 @@
package com.baobaot.exam.dao;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@TableName("question")
public class question {
@TableId(type = IdType.AUTO)
private Integer id;
private String title;
private Integer type;
private String answer;
private String answerA;
private String answerB;
private String answerC;
private String answerD;
private String rAnswer;
private Integer score;
}

View File

@@ -0,0 +1,7 @@
package com.baobaot.exam.mapper;
import com.baobaot.exam.dao.examBack;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface examBackMapper extends BaseMapper<examBack> {
}

View File

@@ -0,0 +1,7 @@
package com.baobaot.exam.mapper;
import com.baobaot.exam.dao.examPaper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface examPaperMapper extends BaseMapper<examPaper> {
}

View File

@@ -0,0 +1,7 @@
package com.baobaot.exam.mapper;
import com.baobaot.exam.dao.question;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface questionMapper extends BaseMapper<question> {
}