资讯动态

Spring Boot+Vue在线考试系统开发:防作弊与高并发实战

发布时间:2026/8/10 14:44:12 来源:尧图企业网站定制
在线考试系统从简单的题库展示、试卷生成、在线答题发展到今天已经成为一个需要处理高并发、实时监控、智能防作弊的复杂工程系统。单纯实现答题功能并不难难点在于如何在一个前后端分离的架构下确保考试过程的公平、公正与安全同时保证系统的稳定与可扩展性。这涉及到前端对用户行为的实时捕获、后端对业务逻辑的严格校验、数据库对事务的一致保证以及贯穿始终的防作弊策略。本文将以一个基于 Spring Boot 和 Vue 的智能化在线考试平台为例详细拆解从零开始构建这样一个系统的核心步骤。我们将不仅关注功能的实现更会深入探讨如何设计防作弊机制、如何处理考试中的并发提交、如何管理考试状态以及如何将各个模块有机整合。通过本文你将掌握构建一个具备基础防作弊能力的在线考试系统的完整流程并理解其背后的设计思想与工程实践。1. 理解在线考试系统的核心模块与防作弊设计在动手编码之前必须清晰地定义系统边界和核心业务流程。一个完整的在线考试平台远不止“出题-答题-判卷”这么简单它需要一套严谨的状态机和规则引擎来驱动。1.1 系统核心业务流程与状态设计考试的生命周期是系统的核心。一个典型的流程如下考试创建与发布管理员设置考试基本信息标题、描述、时间窗口开始时间、结束时间、答题时长、试题库、及格线、防作弊规则等并发布给特定考生群体。考生参与考试考生在时间窗口内进入考试系统开始计时。这里的关键是生成一份唯一的、带有时效性的试卷实例给考生而不是让所有考生操作同一份试卷模板。答题与防作弊监控考生答题过程中前端需要持续向后端发送“心跳”或“保存进度”信号同时收集可能的作弊行为数据如标签页切换、复制粘贴尝试。交卷与自动判分考生主动交卷或时间用尽系统强制交卷。系统对客观题进行自动判分主观题可能需要等待教师批阅。成绩分析与报告生成考试成绩、排名、题目正确率分析等报告。为了实现这个流程我们需要设计几个关键的状态考试状态未开始-进行中-已结束。由系统时间控制。考生考试实例状态未参加-考试中-已交卷-已批阅。每个考生都有自己独立的实例。试题状态在考生实例中每道题可能有未作答、已保存、已标记等状态。1.2 防作弊机制的技术实现思路防作弊是“智能化”的核心体现需要在多个层面进行设计前端行为监控页面可见性监听通过document.visibilityStateAPI 监听考生是否切换了标签页或最小化浏览器。剪贴板事件禁用监听copy,cut,paste事件并阻止默认行为。开发者工具禁用尝试通过监听F12、CtrlShiftI等快捷键事件并提示警告注意完全禁用开发者工具是不现实且不推荐的应以检测和记录为主。鼠标移出窗口监听监听mouseleave事件。定期拍照/屏幕共享高级功能需要获取用户媒体设备权限通常用于高监考要求的场景。后端逻辑校验答题时间校验每次保存或提交答案时后端必须校验当前时间是否在考生本次考试实例的有效时间内。防止考生通过修改本地时间或拦截请求延时提交。答案提交频率限制防止脚本刷题。例如限制每道题的最小提交间隔。IP地址与设备指纹记录考生登录和考试的IP、浏览器User-Agent等信息用于异常登录分析。题目顺序随机与选项乱序从题库抽题时随机排序并为每个考生单独乱序选项增加作弊成本。数据安全与传输HTTPS必须使用防止流量被窃听。试题答案加密敏感信息如答案在传输和存储时可考虑加密。接口防重放与篡改使用Token、时间戳、签名等方式防止请求被重放或篡改。2. 项目环境准备与依赖配置我们将采用现在主流的 Spring Boot 3.x 和 Vue 3 进行开发。确保你的开发环境满足以下要求。2.1 开发环境清单组件推荐版本作用备注JDK17 或 21Spring Boot 3.x 运行环境长期支持版本生产环境首选Maven3.6Java 项目构建与依赖管理或使用 GradleNode.js18Vue 开发与构建环境同样选择 LTS 版本MySQL8.0主数据库5.7也可但建议使用8.0以获得更好的性能和JSON支持IDEIntelliJ IDEA / VS Code后端/前端开发工具IDEA 对 Spring Boot 支持极佳浏览器Chrome / Edge前端调试用于 Vue Devtools注意版本兼容性至关重要。Spring Boot 3.x 最低要求 JDK 17。在开始前请通过java -version、mvn -v、node -v、mysql --version命令确认版本。2.2 后端 Spring Boot 项目初始化与依赖使用 Spring Initializr 或 IDEA 直接创建 Spring Boot 项目。核心依赖如下Spring Web: 提供 RESTful API 支持。Spring Data JPA: 简化数据库操作我们使用它进行快速原型开发。生产环境可根据复杂度考虑 MyBatis-Plus。MySQL Driver: 数据库连接驱动。Lombok: 减少样板代码如Getter/Setter。Spring Security(可选但推荐): 用于认证和授权。本文为简化流程暂不使用但实际项目强烈建议集成。Redis(可选但推荐): 用于缓存考试配置、存储临时会话、实现分布式锁等提升性能与一致性。在pom.xml中依赖看起来像这样dependencies !-- Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 数据库 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 工具 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency !-- 可选Redis -- !-- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency -- /dependencies2.3 前端 Vue 项目初始化使用 Vue 官方脚手架create-vue或 Vite 创建项目。# 使用 npm npm create vuelatest # 按照提示选择需要的功能例如 # - TypeScript: 是 (推荐) # - JSX: 否 # - Router: 是 (必须) # - Pinia: 是 (推荐状态管理) # - ESLint: 是 # 进入项目并安装依赖 cd exam-platform-frontend npm install # 安装额外需要的UI库例如 Element Plus npm install element-plus element-plus/icons-vue # 安装axios用于HTTP请求 npm install axios创建完成后你的package.json的dependencies部分应该包含vue,vue-router,pinia,element-plus,axios等。3. 数据库设计与核心实体建模数据库设计直接关系到业务逻辑的复杂度和系统性能。我们围绕考试流程设计核心表。3.1 核心实体关系分析主要实体包括User: 用户表区分管理员和考生。Exam: 考试定义表存储考试的模板信息如标题、规则。Question: 试题库表存储所有题目。Exam_Record:核心表。考生考试记录表。每个考生每次参与考试都会生成一条记录并关联生成一套具体的题目。Exam_Question_Record: 考生答题详情表。记录考生对某次考试中某道题的具体答案、得分等。它与Exam_Record和Question关联。3.2 关键表结构SQL示例以下是简化后的建表语句展示了核心字段-- 用户表 CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role varchar(20) NOT NULL DEFAULT STUDENT COMMENT 角色ADMIN, STUDENT, created_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT系统用户表; -- 考试定义表 CREATE TABLE exam ( id bigint NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 考试标题, description text COMMENT 考试描述, start_time datetime NOT NULL COMMENT 考试开始时间, end_time datetime NOT NULL COMMENT 考试结束时间, duration int NOT NULL COMMENT 考试时长(分钟), total_score int DEFAULT NULL COMMENT 试卷总分, pass_score int DEFAULT NULL COMMENT 及格分数, status varchar(20) DEFAULT NOT_STARTED COMMENT 状态: NOT_STARTED, ONGOING, ENDED, creator_id bigint NOT NULL COMMENT 创建人ID, created_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_time (start_time,end_time) ) ENGINEInnoDB COMMENT考试定义表; -- 试题表 CREATE TABLE question ( id bigint NOT NULL AUTO_INCREMENT, content text NOT NULL COMMENT 题干, question_type varchar(20) NOT NULL COMMENT 题型: SINGLE_CHOICE, MULTI_CHOICE, TRUE_FALSE, SHORT_ANSWER, options json DEFAULT NULL COMMENT 选择题的选项JSON格式如[A. xxx, B. xxx], answer text COMMENT 标准答案。选择题存选项字母(如“A”或“A,B”)问答题存关键词或范文, score int NOT NULL DEFAULT 5 COMMENT 本题分值, subject varchar(100) DEFAULT NULL COMMENT 所属科目, creator_id bigint NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT试题库表; -- 考试与试题关联表 (定义试卷结构) CREATE TABLE exam_question ( id bigint NOT NULL AUTO_INCREMENT, exam_id bigint NOT NULL, question_id bigint NOT NULL, order_num int DEFAULT 0 COMMENT 在试卷中的顺序, PRIMARY KEY (id), UNIQUE KEY uk_exam_question (exam_id,question_id), KEY idx_exam_id (exam_id) ) ENGINEInnoDB COMMENT考试-试题关联表; -- 考生考试记录表 (核心) CREATE TABLE exam_record ( id bigint NOT NULL AUTO_INCREMENT, exam_id bigint NOT NULL COMMENT 关联的考试定义, user_id bigint NOT NULL COMMENT 考生ID, start_time datetime DEFAULT NULL COMMENT 考生开始考试的时间, submit_time datetime DEFAULT NULL COMMENT 交卷时间, score int DEFAULT NULL COMMENT 最终得分, status varchar(20) NOT NULL DEFAULT NOT_STARTED COMMENT 状态: NOT_STARTED, IN_PROGRESS, SUBMITTED, REVIEWED, ip_address varchar(50) DEFAULT NULL COMMENT 考生IP, user_agent text COMMENT 浏览器标识, cheat_flags int DEFAULT 0 COMMENT 作弊标志位按位存储可疑行为, PRIMARY KEY (id), UNIQUE KEY uk_exam_user (exam_id,user_id), -- 一个考生一次考试只能有一条记录 KEY idx_user_id (user_id), KEY idx_exam_id (exam_id) ) ENGINEInnoDB COMMENT考生考试记录表; -- 考生答题详情表 CREATE TABLE exam_question_record ( id bigint NOT NULL AUTO_INCREMENT, exam_record_id bigint NOT NULL COMMENT 关联的考试记录, question_id bigint NOT NULL COMMENT 关联的试题, user_answer text COMMENT 考生答案, score int DEFAULT NULL COMMENT 本题得分, is_marked tinyint(1) DEFAULT 0 COMMENT 是否标记, answered_time datetime DEFAULT NULL COMMENT 答题时间, PRIMARY KEY (id), UNIQUE KEY uk_record_question (exam_record_id,question_id), -- 防止重复答题 KEY idx_exam_record_id (exam_record_id) ) ENGINEInnoDB COMMENT考生答题详情表;关键设计解释exam_record是核心它代表一个考生的一次考试实例独立于考试定义(exam)。这样每个考生的开始时间、交卷时间、得分、作弊标志都是独立的。cheat_flags字段使用整型按位存储作弊行为。例如1(二进制001)代表切换标签页2(010)代表复制尝试4(100)代表鼠标移出。通过位运算可以方便地记录和查询多种作弊行为。exam_question_record唯一键确保同一道题在同一场考试记录中只保存一条答题详情避免数据混乱。JSON 字段的使用在question.options中使用 JSON 类型存储选择题选项便于前端解析和展示。4. 后端 Spring Boot 核心功能实现我们聚焦于考试流程和防作弊相关的核心服务实现。4.1 实体类与 Repository 层首先根据数据库表创建 JPA 实体类。以ExamRecord为例package com.examplatform.entity; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDateTime; Entity Table(name exam_record, uniqueConstraints { UniqueConstraint(columnNames {exam_id, user_id}) }) Data public class ExamRecord { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne JoinColumn(name exam_id, nullable false) private Exam exam; ManyToOne JoinColumn(name user_id, nullable false) private User user; private LocalDateTime startTime; // 考生实际开始时间 private LocalDateTime submitTime; // 交卷时间 private Integer score; // 最终得分 Column(nullable false) private String status; // NOT_STARTED, IN_PROGRESS, SUBMITTED, REVIEWED private String ipAddress; Column(columnDefinition TEXT) private String userAgent; private Integer cheatFlags 0; // 作弊标志位 CreationTimestamp private LocalDateTime createdTime; UpdateTimestamp private LocalDateTime updatedTime; // 判断是否包含某种作弊行为 public boolean hasCheatFlag(int flag) { return (this.cheatFlags flag) ! 0; } // 添加一种作弊行为 public void addCheatFlag(int flag) { this.cheatFlags | flag; } }对应的 Repository 接口非常简单package com.examplatform.repository; import com.examplatform.entity.ExamRecord; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; Repository public interface ExamRecordRepository extends JpaRepositoryExamRecord, Long { OptionalExamRecord findByExamIdAndUserId(Long examId, Long userId); boolean existsByExamIdAndUserId(Long examId, Long userId); }4.2 服务层考试流程与防作弊逻辑服务层是业务逻辑的核心。我们创建一个ExamService来处理考试的开始、答题、交卷和防作弊校验。package com.examplatform.service; import com.examplatform.entity.*; import com.examplatform.repository.*; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; Service Slf4j RequiredArgsConstructor public class ExamService { private final ExamRepository examRepository; private final ExamRecordRepository examRecordRepository; private final ExamQuestionRecordRepository examQuestionRecordRepository; private final QuestionRepository questionRepository; // 防作弊行为标志位定义 public static final int CHEAT_FLAG_PAGE_VISIBILITY 1; // 页面不可见 public static final int CHEAT_FLAG_COPY_PASTE 2; // 复制粘贴 public static final int CHEAT_FLAG_MOUSE_LEAVE 4; // 鼠标离开 /** * 考生开始考试 * 1. 校验考试状态 * 2. 创建或获取考试记录 * 3. 初始化答题详情从题库抽题 */ Transactional public ExamRecord startExam(Long examId, Long userId, String ip, String userAgent) { Exam exam examRepository.findById(examId) .orElseThrow(() - new RuntimeException(考试不存在)); // 1. 校验考试是否在可进行时间内 LocalDateTime now LocalDateTime.now(); if (now.isBefore(exam.getStartTime())) { throw new RuntimeException(考试尚未开始); } if (now.isAfter(exam.getEndTime())) { throw new RuntimeException(考试已结束); } // 2. 查找或创建考试记录 ExamRecord record examRecordRepository.findByExamIdAndUserId(examId, userId) .orElseGet(() - { ExamRecord newRecord new ExamRecord(); newRecord.setExam(exam); // 这里需要根据User ID获取User实体简化起见直接setId User user new User(); user.setId(userId); newRecord.setUser(user); newRecord.setStatus(NOT_STARTED); newRecord.setIpAddress(ip); newRecord.setUserAgent(userAgent); return examRecordRepository.save(newRecord); }); // 3. 如果状态是 NOT_STARTED则初始化题目并更新状态 if (NOT_STARTED.equals(record.getStatus())) { // 获取本次考试的所有题目ID (这里简化实际可能随机抽题) ListLong questionIds questionRepository.findQuestionIdsByExamId(examId); for (Long qid : questionIds) { ExamQuestionRecord eqr new ExamQuestionRecord(); eqr.setExamRecord(record); Question q new Question(); q.setId(qid); eqr.setQuestion(q); eqr.setScore(0); eqr.setIsMarked(false); examQuestionRecordRepository.save(eqr); } record.setStartTime(now); record.setStatus(IN_PROGRESS); examRecordRepository.save(record); } else if (IN_PROGRESS.equals(record.getStatus())) { // 考试进行中直接返回记录考生继续答题 // 需要校验考试是否超时 if (isExamTimeout(record, exam)) { record.setStatus(SUBMITTED); examRecordRepository.save(record); throw new RuntimeException(考试时间已到系统已自动交卷); } } else { throw new RuntimeException(考试状态异常无法开始); } return record; } /** * 保存单题答案防作弊校验入口 */ Transactional public void saveAnswer(Long examRecordId, Long questionId, String userAnswer, Integer cheatFlag) { ExamRecord record examRecordRepository.findById(examRecordId) .orElseThrow(() - new RuntimeException(考试记录不存在)); Exam exam record.getExam(); // 1. 校验考试记录状态 if (!IN_PROGRESS.equals(record.getStatus())) { throw new RuntimeException(考试不在进行中无法保存答案); } // 2. 校验考试时间核心防作弊点 if (isExamTimeout(record, exam)) { forceSubmitExam(record); throw new RuntimeException(考试时间已到答案保存失败); } // 3. 记录作弊行为如果有 if (cheatFlag ! null cheatFlag 0) { record.addCheatFlag(cheatFlag); examRecordRepository.save(record); log.warn(考生 {} 在考试 {} 中触发作弊行为标志: {}, record.getUser().getId(), exam.getId(), cheatFlag); } // 4. 保存或更新答案 ExamQuestionRecord eqr examQuestionRecordRepository .findByExamRecordIdAndQuestionId(examRecordId, questionId) .orElseThrow(() - new RuntimeException(题目不存在于本次考试中)); eqr.setUserAnswer(userAnswer); eqr.setAnsweredTime(LocalDateTime.now()); examQuestionRecordRepository.save(eqr); } /** * 考生主动交卷 */ Transactional public ExamRecord submitExam(Long examRecordId) { ExamRecord record examRecordRepository.findById(examRecordId) .orElseThrow(() - new RuntimeException(考试记录不存在)); if (!IN_PROGRESS.equals(record.getStatus())) { throw new RuntimeException(只有进行中的考试可以交卷); } record.setSubmitTime(LocalDateTime.now()); record.setStatus(SUBMITTED); // 这里可以调用自动判分逻辑 // autoGrade(record); return examRecordRepository.save(record); } /** * 系统强制交卷超时或严重作弊 */ private void forceSubmitExam(ExamRecord record) { record.setSubmitTime(LocalDateTime.now()); record.setStatus(SUBMITTED); examRecordRepository.save(record); log.info(考试记录 {} 已被系统强制交卷, record.getId()); } /** * 判断考试是否超时 */ private boolean isExamTimeout(ExamRecord record, Exam exam) { LocalDateTime now LocalDateTime.now(); LocalDateTime deadline record.getStartTime().plusMinutes(exam.getDuration()); return now.isAfter(deadline) || now.isAfter(exam.getEndTime()); } }4.3 控制器层提供 RESTful API控制器负责接收前端请求调用服务并返回统一格式的响应。package com.examplatform.controller; import com.examplatform.common.R; import com.examplatform.service.ExamService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/exam) RequiredArgsConstructor public class ExamController { private final ExamService examService; PostMapping(/{examId}/start) public RExamRecordVO startExam(PathVariable Long examId, RequestAttribute Long userId, // 假设从拦截器或Token中获取 HttpServletRequest request) { String ip getClientIp(request); String userAgent request.getHeader(User-Agent); ExamRecord record examService.startExam(examId, userId, ip, userAgent); return R.success(convertToVO(record)); } PostMapping(/record/{recordId}/answer) public RVoid saveAnswer(PathVariable Long recordId, RequestBody AnswerDTO answerDTO) { // AnswerDTO 包含 questionId, userAnswer, cheatFlag examService.saveAnswer(recordId, answerDTO.getQuestionId(), answerDTO.getUserAnswer(), answerDTO.getCheatFlag()); return R.success(); } PostMapping(/record/{recordId}/submit) public RVoid submitExam(PathVariable Long recordId) { examService.submitExam(recordId); return R.success(); } private String getClientIp(HttpServletRequest request) { // 简化实现实际应从 X-Forwarded-For 等头部获取 return request.getRemoteAddr(); } }5. 前端 Vue 3 核心页面与防作弊监控前端负责展示试卷、收集答案并实施第一道防作弊防线。5.1 考试页面组件与状态管理使用 Vue Router 管理路由Pinia 管理全局状态如用户信息、当前考试记录。考试页面组件 (ExamPage.vue)的核心逻辑template div classexam-container contextmenu.prevent copy.prevent cut.prevent paste.prevent !-- 考试头部标题、倒计时 -- div classexam-header h2{{ exam.title }}/h2 div classtimer剩余时间: {{ formatTime(remainingTime) }}/div el-alert v-ifcheatWarning title警告 typewarning :closablefalse 检测到疑似作弊行为请规范答题 /el-alert /div !-- 题目列表 -- div classquestion-list div v-for(q, index) in questions :keyq.id classquestion-item h3第 {{ index 1 }} 题 ({{ q.score }}分)/h3 div v-htmlq.content/div !-- 单选题 -- div v-ifq.questionType SINGLE_CHOICE el-radio-group v-modelanswers[q.id] el-radio v-for(opt, optIndex) in q.options :keyoptIndex :labelString.fromCharCode(65 optIndex) {{ opt }} /el-radio /el-radio-group /div !-- 答题区域省略其他题型 -- el-button clicksaveAnswer(q.id) sizesmall保存本题/el-button el-button clicktoggleMark(q.id) sizesmall :typemarkedQuestions.has(q.id) ? primary : {{ markedQuestions.has(q.id) ? 取消标记 : 标记 }} /el-button /div /div !-- 操作栏 -- div classaction-bar el-button typeprimary clicksubmitExam :disabled!canSubmit交卷/el-button el-button clicksaveAll保存全部/el-button /div /div /template script setup import { ref, onMounted, onUnmounted, computed } from vue import { useRoute } from vue-router import { ElMessage, ElMessageBox } from element-plus import { startExam, saveAnswerApi, submitExamApi } from /api/exam const route useRoute() const examId route.params.id // 状态 const exam ref({}) const questions ref([]) const answers ref({}) const markedQuestions ref(new Set()) const remainingTime ref(0) // 秒 const cheatWarning ref(false) const cheatFlags ref(0) let timer null let visibilityChangeHandler null let mouseLeaveHandler null // 初始化考试 const initExam async () { try { const res await startExam(examId) exam.value res.exam questions.value res.questions remainingTime.value calculateRemainingTime(res.record.startTime, res.exam.duration) startTimer() startAntiCheatMonitoring() } catch (error) { ElMessage.error(error.message || 初始化考试失败) // 可能跳转回考试列表 } } // 倒计时 const startTimer () { timer setInterval(() { remainingTime.value - 1 if (remainingTime.value 0) { clearInterval(timer) autoSubmit() } }, 1000) } // 防作弊监控 const startAntiCheatMonitoring () { // 1. 页面可见性监听 visibilityChangeHandler () { if (document.hidden) { cheatFlags.value | 1 // 对应后端的 PAGE_VISIBILITY 标志 cheatWarning.value true ElMessage.warning(请勿切换标签页或窗口) } } document.addEventListener(visibilitychange, visibilityChangeHandler) // 2. 鼠标移出窗口监听 mouseLeaveHandler () { cheatFlags.value | 4 // 对应后端的 MOUSE_LEAVE 标志 // 可以设置延迟避免误判 } document.addEventListener(mouseleave, mouseLeaveHandler) // 3. 禁用右键菜单、复制粘贴已在模板中通过 event.prevent 实现 } // 保存单题答案 const saveAnswer async (questionId) { const userAnswer answers.value[questionId] if (userAnswer undefined || userAnswer ) { ElMessage.warning(请先选择或输入答案) return } try { await saveAnswerApi(currentRecordId.value, questionId, userAnswer, cheatFlags.value) cheatFlags.value 0 // 发送后重置 ElMessage.success(答案保存成功) } catch (error) { ElMessage.error(error.message || 保存失败) } } // 交卷 const submitExam async () { try { await ElMessageBox.confirm(确认交卷吗交卷后将无法修改答案。, 提示, { type: warning }) await submitExamApi(currentRecordId.value) ElMessage.success(交卷成功) // 跳转到结果页面 } catch (error) { if (error ! cancel) { ElMessage.error(error.message || 交卷失败) } } } // 自动交卷超时 const autoSubmit async () { try { await submitExamApi(currentRecordId.value) ElMessage.info(考试时间到已自动交卷) // 跳转 } catch (error) { console.error(自动交卷失败:, error) } } // 清理监听器 const cleanup () { if (timer) clearInterval(timer) if (visibilityChangeHandler) document.removeEventListener(visibilitychange, visibilityChangeHandler) if (mouseLeaveHandler) document.removeEventListener(mouseleave, mouseLeaveHandler) } onMounted(() { initExam() }) onUnmounted(() { cleanup() }) /script5.2 防作弊数据上报策略前端监控到的作弊行为需要及时上报给后端但要注意频率和性能。即时上报对于严重行为如页面不可见可以立即调用保存答案的接口即使答案未变并将作弊标志位带上。批量上报对于频繁但轻微的行为如鼠标移出可以累积到一个变量中在下次保存答案或定时心跳时一并上报。心跳机制可以建立一个独立的心跳接口定期如每30秒向后端发送“我还活着”的信号并携带累积的作弊标志位。这既能检测考生是否异常退出也能上报作弊行为。// 心跳示例 let heartbeatInterval null const startHeartbeat (recordId) { heartbeatInterval setInterval(async () { try { await axios.post(/api/exam/record/${recordId}/heartbeat, { cheatFlags: cheatFlags.value }) cheatFlags.value 0 // 上报后重置 } catch (error) { console.error(心跳发送失败, error) } }, 30000) // 30秒一次 }6. 系统运行、验证与常见问题排查6.1 启动与基础功能验证启动后端确保 MySQL 服务已启动在application.yml中配置好数据源。运行 Spring Boot 主类。启动前端在 Vue 项目目录下运行npm run dev。验证流程访问前端登录需先实现简单登录。创建一个考试需要管理员后台可先通过数据库插入测试数据。以考生身份进入考试列表点击“开始考试”。答题切换标签页查看控制台警告和后台日志。主动交卷或等待超时查看考试记录状态和得分。6.2 常见问题与排查路径问题现象可能原因检查点解决方案前端无法访问后端 API1. 后端服务未启动2. 端口被占用3. 跨域问题1. 检查控制台日志2.netstat -ano | findstr :80803. 浏览器 F12 看 Network 报错1. 启动服务2. 更换端口或杀死进程3. 后端配置CrossOrigin或使用网关处理考试开始时提示“考试不存在”或“考试已结束”1. 数据库考试数据时间不对2. 系统时间与数据库时间不一致1. 检查exam表start_time,end_time2. 对比服务器时间和数据库时间1. 修正测试数据时间2. 确保服务器时区一致或使用 UTC 时间存储保存答案时报“考试不在进行中”1. 考试记录状态不是IN_PROGRESS2. 考试已超时被系统自动提交1. 查看exam_record表status字段2. 检查start_time和考试时长1. 确认考试流程正确2. 前端倒计时需与后端校验逻辑同步切换标签页后作弊标志未记录1. 前端事件监听未生效2. 作弊标志未随答案保存接口发送3. 后端未正确接收或处理cheatFlag参数1. 浏览器控制台检查事件2. 抓包查看请求参数3. 后端调试saveAnswer方法1. 确认事件绑定正确2. 确保cheatFlag参数在请求体中3. 检查后端RequestBody映射高并发下同一考生生成了多条考试记录1.startExam方法非原子操作存在并发问题1. 检查uk_exam_user唯一约束是否生效2. 模拟并发请求测试1. 数据库唯一索引是最后防线2. 在业务层加锁或使用synchronized单机 / Redis 分布式锁6.3 生产环境部署与优化建议分离部署前后端独立部署。前端使用 Nginx 托管静态资源后端使用 Tomcat 或直接打包成 Jar 运行。API 网关引入 Spring Cloud Gateway 或 Nginx 作为网关统一处理路由、限流、认证、跨域。数据库优化为exam_record表的(exam_id, user_id)建立唯一索引。为频繁查询的字段如status,start_time建立索引。考虑对历史考试记录进行分表或归档。缓存使用 Redis 缓存不常变的考试信息、题目信息。缓存考生登录 Token。防作弊增强随机题目顺序与选项在startExam初始化题目时对题目列表和每个选择题的选项进行随机打乱并将乱序后的结果存入exam_question_record。定时抓拍通过 WebRTC 获取用户媒体需要明确告知用户并获得授权适用于高监考场景。行为分析后端分析答题速度、修改答案模式等识别异常模式。安全必须集成 Spring Security实现完整的认证 (JWT/OAuth2) 和授权。HTTPS生产环境必须启用。SQL 注入与 XSS使用 JPA 参数化查询可防 SQL 注入前端展示题目时使用v-html需谨慎或对内容进行转义。接口防刷对saveAnswer等接口进行频率限制。监控与日志集成 ELK 或 Prometheus Grafana监控系统性能、考试进行中的异常和作弊行为告警。构建一个健壮的在线考试系统是一个持续迭代的过程。从本文的最小可行产品出发你可以根据实际业务需求逐步加入更复杂的规则引擎、更智能的防作弊算法、更丰富的题型支持如编程题、文件上传、更完善的考试管理后台以及数据分析模块。核心始终是保证考试过程的公平性与系统数据的可靠性。在开发过程中务必进行充分的测试特别是并发测试和异常流程测试以确保在高压力下系统行为依然符合预期。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价