本文目录导读:

我来为您设计一个完整的Java考试系统案例,包含前端界面、后端逻辑和数据库设计。
系统架构设计
技术栈
- 后端: Java + Spring Boot + MyBatis
- 前端: Vue.js + Element UI
- 数据库: MySQL
- 认证: JWT
系统功能模块
graph TD
A[考试系统] --> B[用户管理]
A --> C[题库管理]
A --> D[考试管理]
A --> E[成绩管理]
B --> B1[学生端]
B --> B2[教师端]
B --> B3[管理员端]
C --> C1[单选题]
C --> C2[多选题]
C --> C3[判断题]
D --> D1[创建考试]
D --> D2[发布考试]
D --> D3[自动阅卷]
数据库设计
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
real_name VARCHAR(50),
role ENUM('ADMIN', 'TEACHER', 'STUDENT'),
email VARCHAR(100),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 题库表
CREATE TABLE question_bank (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
type ENUM('SINGLE', 'MULTIPLE', 'JUDGE'),
content TEXT NOT NULL,
options TEXT COMMENT 'JSON格式选项',
answer VARCHAR(200),
analysis TEXT COMMENT '解析',
difficulty ENUM('EASY', 'MEDIUM', 'HARD'),
teacher_id BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 考试表
CREATE TABLE exam (
id BIGINT PRIMARY KEY AUTO_INCREMENT,VARCHAR(100) NOT NULL,
description TEXT,
duration INT COMMENT '考试时长(分钟)',
total_score INT DEFAULT 100,
start_time DATETIME,
end_time DATETIME,
status ENUM('DRAFT', 'PUBLISHED', 'ENDED'),
teacher_id BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
关联表
CREATE TABLE exam_question (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
exam_id BIGINT,
question_id BIGINT,
score DECIMAL(5,2),
sort_order INT
);
-- 考试成绩表
CREATE TABLE exam_result (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
exam_id BIGINT,
student_id BIGINT,
score DECIMAL(5,2),
answer_content TEXT COMMENT 'JSON格式答案',
correct_count INT,
wrong_count INT,
submit_time DATETIME,
status ENUM('IN_PROGRESS', 'SUBMITTED', 'GRADED')
);
核心Java代码
实体类
// 实体类定义
@Data
@TableName("sys_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
@JsonIgnore
private String password;
private String realName;
@TableField("role")
private UserRole role;
private String email;
private LocalDateTime createTime;
}
@Data
@TableName("question_bank")
public class Question {
@TableId(type = IdType.AUTO)
private Long id;
private QuestionType type; // SINGLE, MULTIPLE, JUDGE
private String content;
private String options; // JSON格式存储选项
private String answer;
private String analysis;
private Difficulty difficulty;
private Long teacherId;
private LocalDateTime createTime;
}
核心服务层
@Service
@Slf4j
public class ExamService {
@Autowired
private ExamMapper examMapper;
@Autowired
private QuestionMapper questionMapper;
@Autowired
private ExamResultMapper resultMapper;
@Transactional
public Exam createExam(ExamDTO examDTO, Long teacherId) {
Exam exam = new Exam();
BeanUtils.copyProperties(examDTO, exam);
exam.setTeacherId(teacherId);
exam.setStatus(ExamStatus.DRAFT);
examMapper.insert(exam);
// 关联题目
List<ExamQuestion> examQuestions = new ArrayList<>();
for (ExamQuestionDTO questionDTO : examDTO.getQuestions()) {
ExamQuestion eq = new ExamQuestion();
eq.setExamId(exam.getId());
eq.setQuestionId(questionDTO.getQuestionId());
eq.setScore(questionDTO.getScore());
examQuestions.add(eq);
}
examQuestionMapper.insertBatch(examQuestions);
return exam;
}
/**
* 自动阅卷功能
*/
@Transactional
public ExamResult autoGrade(Long examId, Long studentId, List<AnswerDTO> answers) {
// 获取考试题目
List<Question> questions = examQuestionMapper.getQuestionsByExamId(examId);
int correctCount = 0;
int wrongCount = 0;
double totalScore = 0;
List<AnswerDetail> answerDetails = new ArrayList<>();
Map<Long, Double> questionScoreMap = examQuestionMapper.getScoresByExamId(examId);
for (AnswerDTO answer : answers) {
Question question = findQuestion(questions, answer.getQuestionId());
AnswerDetail detail = new AnswerDetail();
detail.setQuestionId(answer.getQuestionId());
detail.setStudentAnswer(answer.getAnswer());
detail.setCorrectAnswer(question.getAnswer());
boolean isCorrect = checkAnswer(question, answer.getAnswer());
detail.setCorrect(isCorrect);
if (isCorrect) {
correctCount++;
totalScore += questionScoreMap.get(question.getId());
} else {
wrongCount++;
}
answerDetails.add(detail);
}
// 保存成绩
ExamResult result = new ExamResult();
result.setExamId(examId);
result.setStudentId(studentId);
result.setScore(totalScore);
result.setCorrectCount(correctCount);
result.setWrongCount(wrongCount);
result.setAnswerContent(JSON.toJSONString(answerDetails));
result.setSubmitTime(LocalDateTime.now());
result.setStatus(ResultStatus.GRADED);
resultMapper.insert(result);
return result;
}
/**
* 检查答案是否正确
*/
private boolean checkAnswer(Question question, String studentAnswer) {
switch (question.getType()) {
case SINGLE:
case JUDGE:
// 单选和判断题直接比较
return question.getAnswer().equalsIgnoreCase(studentAnswer);
case MULTIPLE:
// 多选题需要比较所有选项
Set<String> correctSet = new HashSet<>(
Arrays.asList(question.getAnswer().split(",")));
Set<String> studentSet = new HashSet<>(
Arrays.asList(studentAnswer.split(",")));
return correctSet.equals(studentSet);
default:
return false;
}
}
}
Controller层
@RestController
@RequestMapping("/api/exam")
public class ExamController {
@Autowired
private ExamService examService;
@Autowired
private ScoreService scoreService;
/**
* 获取考试列表
*/
@GetMapping("/list")
public Result<List<ExamVO>> getExamList(
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) String keyword) {
Page<Exam> result = examService.page(new Page<>(page, size),
new LambdaQueryWrapper<Exam>()
.like(StringUtils.isNotBlank(keyword), Exam::getTitle, keyword)
.orderByDesc(Exam::getCreateTime));
return Result.success(convertToVO(result));
}
/**
* 获取考试详情
*/
@GetMapping("/detail/{examId}")
public Result<ExamDetailVO> getExamDetail(@PathVariable Long examId) {
ExamDetailVO vo = examService.getExamDetail(examId);
return Result.success(vo);
}
/**
* 学生交卷
*/
@PostMapping("/submit")
public Result<ExamResult> submitExam(
@RequestBody SubmitExamRequest request,
@RequestAttribute("user") Long studentId) {
ExamResult result = examService.submitExam(
request.getExamId(),
studentId,
request.getAnswers()
);
return Result.success("交卷成功", result);
}
/**
* 获取成绩列表
*/
@GetMapping("/result/{examId}")
public Result<List<ResultVO>> getExamResults(@PathVariable Long examId) {
List<ResultVO> results = scoreService.getExamResults(examId);
return Result.success(results);
}
}
统计分析模块
@Service
public class StatisticsService {
/**
* 生成考试统计报表
*/
public ExamStatistics generateStatistics(Long examId) {
List<ExamResult> results = resultMapper.selectList(
new LambdaQueryWrapper<ExamResult>()
.eq(ExamResult::getExamId, examId)
.eq(ExamResult::getStatus, ResultStatus.GRADED)
);
ExamStatistics stats = new ExamStatistics();
stats.setTotalStudents(results.size());
stats.setMaxScore(results.stream()
.mapToDouble(ExamResult::getScore).max().orElse(0));
stats.setMinScore(results.stream()
.mapToDouble(ExamResult::getScore).min().orElse(0));
stats.setAverageScore(results.stream()
.mapToDouble(ExamResult::getScore).average().orElse(0));
// 分数段统计
Map<String, Integer> scoreDistribution = new LinkedHashMap<>();
scoreDistribution.put("90-100", 0);
scoreDistribution.put("80-89", 0);
scoreDistribution.put("70-79", 0);
scoreDistribution.put("60-69", 0);
scoreDistribution.put("0-59", 0);
results.forEach(result -> {
double score = result.getScore();
if (score >= 90) {
scoreDistribution.put("90-100",
scoreDistribution.get("90-100") + 1);
} else if (score >= 80) {
scoreDistribution.put("80-89",
scoreDistribution.get("80-89") + 1);
} else if (score >= 70) {
scoreDistribution.put("70-79",
scoreDistribution.get("70-79") + 1);
} else if (score >= 60) {
scoreDistribution.put("60-69",
scoreDistribution.get("60-69") + 1);
} else {
scoreDistribution.put("0-59",
scoreDistribution.get("0-59") + 1);
}
});
stats.setScoreDistribution(scoreDistribution);
// 题目正确率分析
Map<Long, Double> questionAccuracy = analyzeQuestionAccuracy(examId);
stats.setQuestionAccuracy(questionAccuracy);
return stats;
}
private Map<Long, Double> analyzeQuestionAccuracy(Long examId) {
List<ExamResult> results = resultMapper.selectList(
new LambdaQueryWrapper<ExamResult>()
.eq(ExamResult::getExamId, examId)
);
Map<Long, Integer> totalCount = new HashMap<>();
Map<Long, Integer> correctCount = new HashMap<>();
for (ExamResult result : results) {
List<AnswerDetail> details = JSON.parseArray(
result.getAnswerContent(), AnswerDetail.class);
for (AnswerDetail detail : details) {
totalCount.merge(detail.getQuestionId(), 1, Integer::sum);
if (detail.isCorrect()) {
correctCount.merge(detail.getQuestionId(), 1, Integer::sum);
}
}
}
Map<Long, Double> accuracyMap = new HashMap<>();
correctCount.forEach((questionId, correct) -> {
int total = totalCount.getOrDefault(questionId, 0);
accuracyMap.put(questionId, total > 0 ?
(double) correct / total * 100 : 0);
});
return accuracyMap;
}
}
前端Vue核心代码
// ExamList.vue - 考试列表组件
<template>
<div class="exam-container">
<el-card v-for="exam in exams" :key="exam.id" class="exam-card">
<div class="exam-header">
<h3>{{ exam.title }}</h3>
<el-tag :type="getStatusType(exam.status)">
{{ getStatusLabel(exam.status) }}
</el-tag>
</div>
<p class="exam-desc">{{ exam.description }}</p>
<div class="exam-info">
<span>考试时长: {{ exam.duration }}分钟</span>
<span>满分: {{ exam.totalScore }}</span>
</div>
<div class="exam-actions">
<el-button
v-if="exam.status === 'PUBLISHED'"
type="primary"
@click="startExam(exam.id)">
开始考试
</el-button>
<el-button @click="viewResult(exam.id)">
查看成绩
</el-button>
</div>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
exams: [],
loading: false
}
},
created() {
this.fetchExams()
},
methods: {
async fetchExams() {
this.loading = true
try {
const res = await this.$http.get('/api/exam/list')
this.exams = res.data
} finally {
this.loading = false
}
},
getStatusType(status) {
const types = {
'DRAFT': 'info',
'PUBLISHED': 'success',
'ENDED': 'warning'
}
return types[status] || 'info'
},
getStatusLabel(status) {
const labels = {
'DRAFT': '草稿',
'PUBLISHED': '进行中',
'ENDED': '已结束'
}
return labels[status] || status
},
startExam(id) {
this.$router.push(`/exam/${id}/start`)
},
viewResult(id) {
this.$router.push(`/exam/${id}/result`)
}
}
}
</script>
// ExamTaking.vue - 考试答题组件
<template>
<div class="exam-taking">
<div class="exam-timer">
<span>剩余时间: {{ formatTime(remainingMinutes, remainingSeconds) }}</span>
</div>
<div class="exam-content">
<el-row :gutter="20">
<el-col :span="16">
<div class="questions-area">
<div v-for="(question, index) in questions" :key="question.id"
class="question-card" >
<div class="question-content">
<span class="question-number">{{ index + 1 }}.</span>
<span class="question-type">{{ getTypeLabel(question.type) }}</span>
<p v-html="question.content"></p>
</div>
<!-- 单选题 -->
<el-radio-group v-if="question.type === 'SINGLE'"
v-model="answers[question.id]">
<el-radio v-for="(option, oi) in parseOptions(question)"
:key="oi" :label="option.key">
{{ option.key }}. {{ option.value }}
</el-radio>
</el-radio-group>
<!-- 多选题 -->
<el-checkbox-group v-else-if="question.type === 'MULTIPLE'"
v-model="answers[question.id]">
<el-checkbox v-for="(option, oi) in parseOptions(question)"
:key="oi" :label="option.key">
{{ option.key }}. {{ option.value }}
</el-checkbox>
</el-checkbox-group>
<!-- 判断题 -->
<el-radio-group v-else-if="question.type === 'JUDGE'"
v-model="answers[question.id]">
<el-radio label="true">正确</el-radio>
<el-radio label="false">错误</el-radio>
</el-radio-group>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="sidemenu">
<div class="question-nav">
<div v-for="(question, index) in questions"
:key="question.id"
class="nav-item"
:class="getNavClass(question.id, index)">
{{ index + 1 }}
</div>
</div>
<el-button type="success" class="submit-btn"
@click="submitExam">
交卷
</el-button>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
export default {
data() {
return {
examId: this.$route.params.id,
questions: [],
answers: {},
timer: null,
duration: 0,
remainingTime: 0,
questionMap: {}
}
},
computed: {
remainingMinutes() {
return Math.floor(this.remainingTime / 60)
},
remainingSeconds() {
return this.remainingTime % 60
}
},
mounted() {
this.fetchExamQuestions()
this.startTimer()
// 防止用户退出
window.onbeforeunload = () => {
return '考试进行中,确定要离开吗?'
}
},
beforeDestroy() {
clearInterval(this.timer)
window.onbeforeunload = null
},
methods: {
async fetchExamQuestions() {
const res = await this.$http.get(`/api/exam/${this.examId}/questions`)
this.questions = res.data
this.duration = res.data.duration
this.remainingTime = this.duration * 60
// 初始化答案
this.questions.forEach(q => {
if (q.type === 'MULTIPLE') {
this.$set(this.answers, q.id, [])
}
})
},
startTimer() {
this.timer = setInterval(() => {
this.remainingTime--
if (this.remainingTime <= 0) {
clearInterval(this.timer)
this.submitExam()
}
}, 1000)
},
parseOptions(question) {
return JSON.parse(question.options)
},
getNavClass(questionId, index) {
if (this.answers[questionId] &&
(this.answers[questionId].length > 0 ||
this.answers[questionId] !== '')) {
return 'answered'
}
return 'unanswered'
},
async submitExam() {
const confirmSubmit = await this.$confirm('确定要交卷吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(() => false)
if (!confirmSubmit) return
// 格式化答案
const formattedAnswers = this.questions.map(q => ({
questionId: q.id,
answer: Array.isArray(this.answers[q.id])
? this.answers[q.id].sort().join(',')
: this.answers[q.id] || ''
}))
try {
await this.$http.post('/api/exam/submit', {
examId: this.examId,
answers: formattedAnswers
})
this.$message.success('交卷成功!')
this.$router.push(`/exam/${this.examId}/result`)
} catch (error) {
this.$message.error('交卷失败,请重试')
}
},
formatTime(minutes, seconds) {
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}
}
}
</script>
配置文件
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/exam_system?characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
secret: your-jwt-secret-key
expiration: 86400000
server:
port: 8080
系统特色功能
随机组卷功能
@Service
public class PaperGeneratorService {
public List<Question> generatePaper(PaperConfig config) {
List<Question> paperQuestions = new ArrayList<>();
// 按难度比例随机选取题目
Map<Difficulty, Integer> difficultyConfig = config.getDifficultyRatio();
for (Map.Entry<Difficulty, Integer> entry : difficultyConfig.entrySet()) {
List<Question> questions = questionMapper.selectRandomQuestions(
entry.getKey(),
entry.getValue()
);
paperQuestions.addAll(questions);
}
// 乱序排列
Collections.shuffle(paperQuestions);
return paperQuestions;
}
}
防作弊功能
@Component
public class AntiCheatingService {
private static final int MAX_IP_CHANGE_COUNT = 3;
private static final long MAX_TIME_CHANGE_THRESHOLD = 5 * 60 * 1000;
@Cacheable(value = "student-info", key = "#studentId")
public MonitoringResult monitorStudent(Long studentId) {
MonitoringResult result = new MonitoringResult();
// 1. 检测切换窗口
result.setWindowSwitchCount(detectWindowSwitch(studentId));
// 2. 检测IP变化
result.setIpChangeCount(detectIpChanges(studentId));
// 3. 检测答题速度异常
result.setAbnormalSpeed(detectAbnormalSpeed(studentId));
// 4. 检测复制粘贴行为
result.setCopyCount(detectCopyOperations(studentId));
return result;
}
}
成绩分析报表
@RestController
@RequestMapping("/api/analysis")
public class AnalysisController {
@GetMapping("/student/{studentId}")
public Result<StudentAnalysis> studentAnalysis(@PathVariable Long studentId) {
// 学生历次考试成绩趋势
List<ExamResult> results = resultMapper.selectList(
new LambdaQueryWrapper<ExamResult>()
.eq(ExamResult::getStudentId, studentId)
.orderByAsc(ExamResult::getSubmitTime)
);
StudentAnalysis analysis = new StudentAnalysis();
analysis.setScoreTrend(results.stream()
.map(r -> new ScoreTrend(r.getSubmitTime(), r.getScore()))
.collect(Collectors.toList()));
// 知识点掌握情况雷达图
analysis.setKnowledgeRadar(analyzeKnowledgePoints(studentId));
return Result.success(analysis);
}
}
部署与运维
Docker部署配置
# Dockerfile FROM openjdk:11-jre-slim WORKDIR /app COPY target/exam-system-*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: exam_system
ports:
- "3306:3306"
volumes:
- ./data/mysql:/var/lib/mysql
redis:
image: redis:6.0
ports:
- "6379:6379"
backend:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/exam_system
SPRING_REDIS_HOST: redis
系统优化建议
-
技术优化:
- 使用Redis缓存热点数据(考试题目)
- 使用消息队列处理批量阅卷
- 数据库读写分离
-
业务优化:
- 添加在线答疑功能
- 支持多种考试类型(公开课、练习模式)
- 增加错题本功能
-
用户体验:
- 支持移动端适配
- 添加答题卡模式
- 实时显示答题进度
这个完整的Java考试系统案例涵盖了从需求分析、数据库设计、接口开发到前端实现的完整流程,可以直接作为毕业设计或企业项目的参考实现。