Java考试系统案例

wen java案例 3

本文目录导读:

Java考试系统案例

  1. 系统架构设计
  2. 数据库设计
  3. 核心Java代码
  4. 系统特色功能
  5. 部署与运维
  6. 系统优化建议

我来为您设计一个完整的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

系统优化建议

  1. 技术优化

    • 使用Redis缓存热点数据(考试题目)
    • 使用消息队列处理批量阅卷
    • 数据库读写分离
  2. 业务优化

    • 添加在线答疑功能
    • 支持多种考试类型(公开课、练习模式)
    • 增加错题本功能
  3. 用户体验

    • 支持移动端适配
    • 添加答题卡模式
    • 实时显示答题进度

这个完整的Java考试系统案例涵盖了从需求分析、数据库设计、接口开发到前端实现的完整流程,可以直接作为毕业设计或企业项目的参考实现。

抱歉,评论功能暂时关闭!