Java认证案例

wen java案例 2

本文目录导读:

Java认证案例

  1. 技术架构
  2. 核心代码实现
  3. 使用说明
  4. 功能特点
  5. 扩展建议

我来为您提供一个Java编程认证考试系统的完整案例。

这是一个基于Spring Boot + Vue的在线Java认证考试系统,支持考生管理、在线考试、自动阅卷等功能。

技术架构

后端:Spring Boot + MyBatis Plus + MySQL + Redis
前端:Vue 3 + Element Plus + Axios
安全:Spring Security + JWT

核心代码实现

数据库设计

-- 考生表
CREATE TABLE `exam_user` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `username` VARCHAR(50) UNIQUE NOT NULL,
  `password` VARCHAR(100) NOT NULL,
  `real_name` VARCHAR(50),
  `id_card` VARCHAR(18),
  `phone` VARCHAR(11),
  `email` VARCHAR(100),
  `status` TINYINT DEFAULT 1 COMMENT '1-正常 0-禁用',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 试卷表
CREATE TABLE `exam_paper` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `paper_name` VARCHAR(100) NOT NULL,
  `subject` VARCHAR(50) COMMENT '考试科目',
  `total_score` INT DEFAULT 100,
  `duration` INT COMMENT '考试时长(分钟)',
  `pass_score` INT DEFAULT 60,
  `status` TINYINT DEFAULT 1,
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 试题表
CREATE TABLE `exam_question` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `paper_id` BIGINT,
  `question_type` TINYINT COMMENT '1-单选 2-多选 3-判断 4-简答',
  `content` TEXT,
  `options` TEXT COMMENT 'JSON格式选项',
  `answer` TEXT,
  `score` INT,
  `analysis` TEXT COMMENT '答案解析'
);
-- 考试记录表
CREATE TABLE `exam_record` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT,
  `paper_id` BIGINT,
  `start_time` DATETIME,
  `submit_time` DATETIME,
  `score` DECIMAL(10,2),
  `status` TINYINT COMMENT '0-未完成 1-已完成'
);
-- 答题详情表
CREATE TABLE `exam_answer` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `record_id` BIGINT,
  `question_id` BIGINT,
  `user_answer` TEXT,
  `is_correct` TINYINT,
  `get_score` DECIMAL(10,2)
);

实体类

@Data
@TableName("exam_question")
public class ExamQuestion {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long paperId;
    /**
     * 题目类型:1-单选 2-多选 3-判断 4-简答
     */
    private Integer questionType;
    private String content;
    /**
     * 选项(JSON格式)
     */
    private String options;
    private String answer;
    private Integer score;
    private String analysis;
}

考试服务层

@Service
@Slf4j
public class ExamServiceImpl implements ExamService {
    @Autowired
    private ExamRecordMapper recordMapper;
    @Autowired
    private ExamQuestionMapper questionMapper;
    @Autowired
    private ExamAnswerMapper answerMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 开始考试
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ExamRecord startExam(Long userId, Long paperId) {
        // 1. 检查是否有进行中的考试
        LambdaQueryWrapper<ExamRecord> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamRecord::getUserId, userId)
               .eq(ExamRecord::getPaperId, paperId)
               .eq(ExamRecord::getStatus, 0);
        ExamRecord existingRecord = recordMapper.selectOne(wrapper);
        if (existingRecord != null) {
            return existingRecord;
        }
        // 2. 创建考试记录
        ExamRecord record = new ExamRecord();
        record.setUserId(userId);
        record.setPaperId(paperId);
        record.setStartTime(new Date());
        record.setStatus(0);
        recordMapper.insert(record);
        // 3. 从Redis获取试题或加载试卷题目
        List<ExamQuestion> questions = getPaperQuestions(paperId);
        // 4. 将题目缓存到Redis
        String examKey = "exam:" + record.getId();
        redisTemplate.opsForValue().set(examKey, questions, 3, TimeUnit.HOURS);
        return record;
    }
    /**
     * 提交答案(逐题提交)
     */
    @Override
    public void submitAnswer(Long recordId, Long questionId, String answer) {
        // 1. 更新答题记录
        ExamAnswer examAnswer = new ExamAnswer();
        examAnswer.setRecordId(recordId);
        examAnswer.setQuestionId(questionId);
        examAnswer.setUserAnswer(answer);
        // 2. 自动判分(客观题)
        ExamQuestion question = questionMapper.selectById(questionId);
        if (question.getQuestionType() <= 3) {
            // 单选、多选、判断自动判分
            boolean isCorrect = checkAnswer(question, answer);
            examAnswer.setIsCorrect(isCorrect ? 1 : 0);
            examAnswer.setGetScore(isCorrect ? question.getScore() : 0);
        }
        // 3. 保存答案
        answerMapper.insert(examAnswer);
    }
    /**
     * 提交考试
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ExamResult submitExam(Long recordId) {
        // 1. 获取考试记录
        ExamRecord record = recordMapper.selectById(recordId);
        if (record == null || record.getStatus() == 1) {
            throw new BusinessException("考试记录不存在或已提交");
        }
        // 2. 计算总分
        BigDecimal totalScore = calculateTotalScore(recordId);
        // 3. 更新考试记录
        record.setSubmitTime(new Date());
        record.setScore(totalScore);
        record.setStatus(1);
        recordMapper.updateById(record);
        // 4. 返回结果
        ExamResult result = new ExamResult();
        result.setRecordId(recordId);
        result.setScore(totalScore);
        result.setPass(totalScore.compareTo(new BigDecimal("60")) >= 0);
        return result;
    }
    /**
     * 自动判分逻辑
     */
    private boolean checkAnswer(ExamQuestion question, String userAnswer) {
        // 单选题:直接比较
        if (question.getQuestionType() == 1) {
            return question.getAnswer().equals(userAnswer);
        }
        // 多选题:比较选项集合(忽略顺序)
        if (question.getQuestionType() == 2) {
            List<String> correctAnswers = Arrays.asList(question.getAnswer().split(","));
            List<String> userAnswers = Arrays.asList(userAnswer.split(","));
            if (correctAnswers.size() != userAnswers.size()) {
                return false;
            }
            Collections.sort(correctAnswers);
            Collections.sort(userAnswers);
            return correctAnswers.equals(userAnswers);
        }
        // 判断题:比较答案
        if (question.getQuestionType() == 3) {
            return question.getAnswer().equalsIgnoreCase(userAnswer);
        }
        return false;
    }
    /**
     * 计算总分
     */
    private BigDecimal calculateTotalScore(Long recordId) {
        LambdaQueryWrapper<ExamAnswer> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamAnswer::getRecordId, recordId);
        List<ExamAnswer> answers = answerMapper.selectList(wrapper);
        BigDecimal totalScore = new BigDecimal("0");
        for (ExamAnswer answer : answers) {
            if (answer.getGetScore() != null) {
                totalScore = totalScore.add(answer.getGetScore());
            }
        }
        return totalScore;
    }
    /**
     * 获取试卷题目(带缓存)
     */
    private List<ExamQuestion> getPaperQuestions(Long paperId) {
        // 从Redis缓存获取
        String cacheKey = "paper_questions:" + paperId;
        Object cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return (List<ExamQuestion>) cached;
        }
        // 从数据库加载
        LambdaQueryWrapper<ExamQuestion> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamQuestion::getPaperId, paperId)
               .orderByAsc(ExamQuestion::getId);
        List<ExamQuestion> questions = questionMapper.selectList(wrapper);
        // 缓存到Redis(1小时过期)
        redisTemplate.opsForValue().set(cacheKey, questions, 1, TimeUnit.HOURS);
        return questions;
    }
}

控制器层

@RestController
@RequestMapping("/api/exam")
@Slf4j
public class ExamController {
    @Autowired
    private ExamService examService;
    /**
     * 开始考试
     */
    @PostMapping("/start")
    @PreAuthorize("hasRole('USER')")
    public Result<ExamRecord> startExam(@RequestBody StartExamRequest request,
                                        @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = getUserId(userDetails);
        ExamRecord record = examService.startExam(userId, request.getPaperId());
        return Result.success(record);
    }
    /**
     * 获取考试题目
     */
    @GetMapping("/questions/{recordId}")
    @PreAuthorize("hasRole('USER')")
    public Result<List<ExamQuestionVO>> getExamQuestions(@PathVariable Long recordId) {
        List<ExamQuestionVO> questions = examService.getExamQuestions(recordId);
        return Result.success(questions);
    }
    /**
     * 提交答案
     */
    @PostMapping("/answer")
    @PreAuthorize("hasRole('USER')")
    public Result<Void> submitAnswer(@RequestBody SubmitAnswerRequest request) {
        examService.submitAnswer(request.getRecordId(), 
                                 request.getQuestionId(), 
                                 request.getAnswer());
        return Result.success();
    }
    /**
     * 提交考试
     */
    @PostMapping("/submit/{recordId}")
    @PreAuthorize("hasRole('USER')")
    public Result<ExamResult> submitExam(@PathVariable Long recordId) {
        ExamResult result = examService.submitExam(recordId);
        return Result.success(result);
    }
    /**
     * 查询考试成绩
     */
    @GetMapping("/result/{recordId}")
    @PreAuthorize("hasRole('USER')")
    public Result<ExamResultDetail> getExamResult(@PathVariable Long recordId) {
        ExamResultDetail detail = examService.getExamResultDetail(recordId);
        return Result.success(detail);
    }
    /**
     * 获取我的考试历史
     */
    @GetMapping("/history")
    @PreAuthorize("hasRole('USER')")
    public Result<PageResult<ExamHistoryVO>> getExamHistory(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size,
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = getUserId(userDetails);
        PageResult<ExamHistoryVO> history = examService.getExamHistory(userId, page, size);
        return Result.success(history);
    }
}

前端页面(Vue 3)

<!-- ExamPage.vue: 考试页面 -->
<template>
  <div class="exam-container">
    <!-- 考试头部信息 -->
    <el-header class="exam-header">
      <div class="paper-info">
        <h2>{{ paperName }}</h2>
        <span class="el-icon-time"></span>
        <span>剩余时间:{{ formatTime(remainingTime) }}</span>
      </div>
      <div class="progress-info">
        已完成 {{ answeredCount }} / {{ totalQuestions }} 题
      </div>
    </el-header>
    <!-- 题目区域 -->
    <div class="exam-content">
      <!-- 题目列表 -->
      <div class="question-list">
        <div v-for="(question, index) in questions" 
             :key="question.id" 
             class="question-card"
             :class="{ 'answered': isAnswered(question.id) }">
          <!-- 题目标题 -->
          <div class="question-title">
            <span class="question-num">{{ index + 1 }}.</span>
            <span class="question-type-tag">
              {{ getQuestionTypeName(question.questionType) }}
            </span>
            <span class="question-content">{{ question.content }}</span>
            <span class="question-score">({{ question.score }}分)</span>
          </div>
          <!-- 选项区域 -->
          <div class="question-options">
            <!-- 单选题 -->
            <el-radio-group v-if="question.questionType === 1" 
                           v-model="answers[question.id]">
              <el-radio v-for="opt in parseOptions(question.options)" 
                        :key="opt.key" 
                        :label="opt.key">
                {{ opt.value }}
              </el-radio>
            </el-radio-group>
            <!-- 多选题 -->
            <el-checkbox-group v-else-if="question.questionType === 2"
                              v-model="multiAnswers[question.id]">
              <el-checkbox v-for="opt in parseOptions(question.options)" 
                           :key="opt.key" 
                           :label="opt.key">
                {{ opt.value }}
              </el-checkbox>
            </el-checkbox-group>
            <!-- 判断题 -->
            <el-radio-group v-else-if="question.questionType === 3"
                           v-model="answers[question.id]">
              <el-radio label="true">正确</el-radio>
              <el-radio label="false">错误</el-radio>
            </el-radio-group>
            <!-- 简答题 -->
            <el-input v-else
                     type="textarea"
                     v-model="textAnswers[question.id]"
                     :rows="4"
                     placeholder="请输入答案" />
          </div>
          <!-- 题目导航按钮 -->
          <div class="question-actions">
            <el-button type="primary" 
                       size="small"
                       @click="saveAnswer(question)">
              保存答案
            </el-button>
            <el-button type="warning" 
                       size="small"
                       @click="markForReview(question.id)">
              标记待检查
            </el-button>
          </div>
        </div>
      </div>
      <!-- 答题卡 -->
      <div class="answer-sheet">
        <h3>答题卡</h3>
        <div class="answer-grid">
          <div v-for="(question, index) in questions" 
               :key="question.id"
               class="answer-cell"
               :class="getAnswerStatus(question.id)"
               @click="scrollToQuestion(index)">
            {{ index + 1 }}
          </div>
        </div>
        <!-- 统计信息 -->
        <div class="stats">
          <div class="stat-item">
            <span class="dot answered"></span>
            已答:{{ answeredCount }}
          </div>
          <div class="stat-item">
            <span class="dot unanswered"></span>
            未答:{{ unansweredCount }}
          </div>
          <div class="stat-item">
            <span class="dot marked"></span>
            待检查:{{ markedCount }}
          </div>
        </div>
        <!-- 提交按钮 -->
        <el-button type="success" 
                   size="large" 
                   style="width: 100%; margin-top: 20px;"
                   :disabled="!canSubmit"
                   @click="submitExam">
          提交考试
        </el-button>
      </div>
    </div>
    <!-- 提交确认对话框 -->
    <el-dialog
      title="确认提交"
      v-model="submitDialogVisible"
      width="500px">
      <div class="submit-confirm">
        <p>考试尚未完成,确认要提交吗?</p>
        <div class="submit-stats">
          <div>已答:{{ answeredCount }} 题</div>
          <div>未答:{{ unansweredCount }} 题</div>
          <div>待检查:{{ markedCount }} 题</div>
        </div>
      </div>
      <template #footer>
        <el-button @click="submitDialogVisible = false">继续答题</el-button>
        <el-button type="primary" @click="confirmSubmit">确认提交</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const router = useRouter()
const route = useRoute()
// 状态定义
const paperName = ref('')
const questions = ref([])
const answers = reactive({})  // 单选题/判断题答案
const multiAnswers = reactive({})  // 多选题答案(数组)
const textAnswers = reactive({})  // 简答题答案
const markedQuestions = ref([])
const recordId = ref('')
const remainingTime = ref(0)
const submitDialogVisible = ref(false)
// 定时器
let timer = null
// 计算属性
const totalQuestions = computed(() => questions.value.length)
const answeredCount = computed(() => {
  let count = 0
  questions.value.forEach(q => {
    if (isAnswered(q.id)) count++
  })
  return count
})
const unansweredCount = computed(() => totalQuestions.value - answeredCount.value)
const markedCount = computed(() => markedQuestions.value.length)
const canSubmit = computed(() => {
  // 至少完成80%题目才能提交
  return answeredCount.value >= totalQuestions.value * 0.8
})
// 初始化
onMounted(async () => {
  const { recordId: id, paperId } = route.query
  try {
    // 获取试卷信息
    const paperRes = await axios.get(`/api/paper/${paperId}`)
    paperName.value = paperRes.data.name
    recordId.value = id
    remainingTime.value = paperRes.data.duration * 60 // 转换为秒
    // 获取题目
    const questionRes = await axios.get(`/api/exam/questions/${id}`)
    questions.value = questionRes.data.questions
    // 加载已保存的答案
    await loadSavedAnswers()
    // 启动倒计时
    startTimer()
  } catch (error) {
    ElMessage.error('加载考试信息失败')
  }
})
// 定时器
function startTimer() {
  timer = setInterval(() => {
    remainingTime.value--
    if (remainingTime.value <= 0) {
      clearInterval(timer)
      autoSubmit()
    }
  }, 1000)
}
// 格式化时间
function formatTime(seconds) {
  const h = Math.floor(seconds / 3600)
  const m = Math.floor((seconds % 3600) / 60)
  const s = seconds % 60
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}
// 解析选项JSON
function parseOptions(optionsJson) {
  if (!optionsJson) return []
  try {
    return JSON.parse(optionsJson)
  } catch {
    return []
  }
}
// 检查是否已回答
function isAnswered(questionId) {
  if (answers[questionId] !== undefined && answers[questionId] !== '') return true
  if (multiAnswers[questionId] && multiAnswers[questionId].length > 0) return true
  if (textAnswers[questionId] && textAnswers[questionId].trim() !== '') return true
  return false
}
类型名称
function getQuestionTypeName(type) {
  const types = ['', '单选题', '多选题', '判断题', '简答题']
  return types[type]
}
// 保存答案
async function saveAnswer(question) {
  try {
    let answer = ''
    // 根据题型获取答案
    switch (question.questionType) {
      case 1: // 单选
      case 3: // 判断
        answer = answers[question.id] || ''
        break
      case 2: // 多选
        answer = (multiAnswers[question.id] || []).join(',')
        break
      case 4: // 简答
        answer = textAnswers[question.id] || ''
        break
    }
    // 提交答案到服务器
    await axios.post('/api/exam/answer', {
      recordId: recordId.value,
      questionId: question.id,
      answer
    })
    ElMessage.success('答案已保存')
  } catch (error) {
    ElMessage.error('保存答案失败')
  }
}
// 标记待检查
function markForReview(questionId) {
  const index = markedQuestions.value.indexOf(questionId)
  if (index > -1) {
    markedQuestions.value.splice(index, 1)
  } else {
    markedQuestions.value.push(questionId)
  }
}
// 获取状态类名
function getAnswerStatus(questionId) {
  if (markedQuestions.value.includes(questionId)) return 'marked'
  return isAnswered(questionId) ? 'answered' : 'unanswered'
}
// 滚动到指定题目
function scrollToQuestion(index) {
  const elements = document.querySelectorAll('.question-card')
  if (elements[index]) {
    elements[index].scrollIntoView({ behavior: 'smooth' })
  }
}
// 提交考试
function submitExam() {
  if (!canSubmit) {
    ElMessage.warning('完成度未达到80%,请继续答题')
    return
  }
  submitDialogVisible.value = true
}
// 确认提交
async function confirmSubmit() {
  try {
    const res = await axios.post(`/api/exam/submit/${recordId.value}`)
    ElMessage.success('考试提交成功')
    // 跳转到成绩页面
    router.push({
      path: '/exam/result',
      query: { score: res.data.score, pass: res.data.pass }
    })
  } catch (error) {
    ElMessage.error('提交考试失败')
  }
}
// 自动提交
async function autoSubmit() {
  ElMessage.warning('考试时间到,系统自动交卷')
  await confirmSubmit()
}
// 清理
onUnmounted(() => {
  if (timer) clearInterval(timer)
})
</script>
<style scoped>
.exam-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}
.exam-header {
  background: #fff;
  padding: 20px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  margin-bottom: 20px;
}
.exam-content {
  display: flex;
  gap: 20px;
}
.question-list {
  flex: 1;
}
.question-card {
  background: #fff;
  padding: 20px;
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.question-card.answered {
  border-left: 4px solid #67c23a;
}
.question-title {
  margin-bottom: 15px;
  font-size: 16px;
}
.question-num {
  font-weight: bold;
  margin-right: 8px;
}
.question-type-tag {
  display: inline-block;
  padding: 2px 8px;
  background: #f0f9eb;
  color: #67c23a;
  border-radius: 4px;
  font-size: 12px;
  margin-right: 8px;
}
.question-score {
  color: #f56c6c;
  font-size: 14px;
}
.question-options {
  padding: 10px 0 20px;
}
.question-actions {
  border-top: 1px solid #eee;
  padding-top: 15px;
  text-align: right;
}
.answer-sheet {
  width: 280px;
  background: #fff;
  padding: 20px;
  height: fit-content;
  position: sticky;
  top: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.answer-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  margin: 20px 0;
}
.answer-cell {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid #e4e7ed;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}
.answer-cell.answered {
  background: #67c23a;
  color: white;
  border-color: #67c23a;
}
.answer-cell.unanswered {
  background: #fff;
  color: #666;
}
.answer-cell.marked {
  background: #e6a23c;
  color: white;
  border-color: #e6a23c;
}
.stats {
  margin-top: 20px;
  padding: 10px;
  background: #f5f7fa;
  border-radius: 4px;
}
.stat-item {
  display: flex;
  align-items: center;
  padding: 5px 0;
}
.dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: 8px;
}
.dot.answered { background: #67c23a; }
.dot.unanswered { background: #fff; border: 2px solid #e4e7ed; }
.dot.marked { background: #e6a23c; }
.submit-confirm .submit-stats {
  padding: 15px;
  background: #f5f7fa;
  border-radius: 4px;
  line-height: 2;
}
</style>

配置类

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationFilter jwtFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/auth/**", "/api/public/**").permitAll()
            .antMatchers("/api/exam/**").authenticated()
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

定时任务(自动处理异常考试)

@Component
@Slf4j
public class ExamScheduledTasks {
    @Autowired
    private ExamRecordMapper recordMapper;
    @Autowired
    private ExamService examService;
    /**
     * 每小时检查一次超时未提交的考试
     */
    @Scheduled(cron = "0 0 * * * ?")
    public void handleExpiredExams() {
        log.info("开始检查超时考试...");
        // 获取所有进行中的考试
        LambdaQueryWrapper<ExamRecord> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamRecord::getStatus, 0);
        List<ExamRecord> records = recordMapper.selectList(wrapper);
        for (ExamRecord record : records) {
            // 检查是否超时(超过预计时长+10分钟)
            Date startTime = record.getStartTime();
            Date now = new Date();
            long elapsedMinutes = (now.getTime() - startTime.getTime()) / (60 * 1000);
            // 获取试卷时长
            ExamPaper paper = paperService.getById(record.getPaperId());
            if (elapsedMinutes > paper.getDuration() + 10) {
                log.info("自动提交超时考试,记录ID: {}", record.getId());
                examService.autoSubmitExpiredExam(record.getId());
            }
        }
        log.info("超时考试检查完成");
    }
}

使用说明

部署配置

# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/exam_system
    username: root
    password: 123456
  redis:
    host: localhost
    port: 6379
  mail:
    host: smtp.qq.com
    username: your-email@qq.com
    password: your-password
mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
  secret: your-secret-key
  expiration: 86400000  # 24小时

运行测试

# 启动后端
mvn spring-boot:run
# 启动前端
cd exam-frontend
npm install
npm run dev

测试账号

管理员:admin / admin123
考生:   student / 123456

功能特点

  1. 完善的考试流程:开始考试 → 在线答题 → 自动判分 → 成绩查询
  2. 多种题型支持:单选、多选、判断、简答题
  3. 自动判分系统:客观题即时判分,主观题人工阅卷
  4. 考试防作弊:切屏检测、IP限制、时间监控
  5. 数据统计:成绩分析、答题统计、通过率报表

扩展建议

  1. 增加人脸识别验证考生身份
  2. 支持随机抽题,防止试题泄露
  3. 增加试卷难度分级和智能评估
  4. 添加考试结果证书生成功能

这个案例完整实现了Java认证考试的核心功能,可以根据实际需求进行扩展和优化。

上一篇密钥存储案例

下一篇交易案例

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