SpringBoot+Vue3+MyBatis在线考试系统实战:从数据库设计到部署上线
2026/9/9 6:30:35 网站建设 项目流程

说实话,在线考试系统这种项目,在Java就业市场上几乎是标配级别的练手项目。你要是翻过招聘网站的JD,会发现很多初级岗位描述里都写着“有在线教育、考试系统项目经验者优先”。我当年面第一份后端开发时,简历上写的就是一个类似的系统,面试官一半的问题都围绕它展开。所以这个选题本身就很讨巧,技术栈覆盖得全面,业务场景又足够贴近实际。

这篇文章我会从零开始,把一套基于SpringBoot + Vue3 + MyBatis + MySQL的在线考试系统拆开揉碎讲清楚。不管你是准备毕业设计、想丰富简历,还是刚学完框架想找个完整项目练手,这篇文章都适用。我会把技术选型的原因、数据库设计、前后端核心实现、防作弊逻辑,还有那些我到今天都记得的坑,都一并交代明白。

1. 项目整体设计与思路拆解

1.1 在线考试系统到底要解决什么问题

先别急着写代码,想清楚一个问题:考试系统的核心需求是什么?不仅仅是“出题、答题、判分”这么简单。我接手过几个类似项目后总结出,一套能真正用起来的在线考试系统,至少要覆盖以下角色和场景:

  • 管理员:管理用户、管理题库、创建试卷、发布考试、查看成绩统计。
  • 教师/出题人:录入题目(单选、多选、判断、简答)、组卷(手工选题或按规则抽题)、批改主观题。
  • 考生:参加考试、作答、交卷、查看成绩。
  • 系统侧:防作弊(切屏监控、乱序抽题、限时交卷)、自动判分(客观题)、考试过程异常兜底(断网续考、自动保存)。

如果只是做一个简单的CRUD demo,那没什么难度,但那样的项目写进简历里,面试官一问就问穿了。真正有价值的设计在于:考试过程中的状态流转、防作弊策略、以及主观题和客观题混合判分的流程。这些才是系统设计的核心矛盾点。

1.2 技术栈选型的底层逻辑

这套系统选用SpringBoot + Vue3 + MyBatis + MySQL,不是随便拼的,每个选型都有它对应的场景考虑:

  • SpringBoot:当前Java后端的事实标准。它帮我解决了大量配置地狱问题,内嵌Tomcat让部署也简单了。对于考试系统这种业务逻辑清晰、以CRUD和事务处理为主的项目,SpringBoot的自动配置和生态成熟度能极大提升开发效率。
  • Vue3:选择Vue3而不是Vue2,除了考虑到它是当前主流外,组合式API(Composition API)在复杂考试页面(比如答题卡、计时器、题目切换)的逻辑组织上要比选项式API清晰得多。配合Vite,开发时的热更新体验也更好。
  • MyBatis:考试系统的SQL非常复杂,多表关联查询(试卷表、题目表、选项表、考试记录表)、动态SQL(根据题型查询、按条件筛选题目)都是高频操作。MyBatis的XML映射方式让我能精确控制SQL执行,比全自动ORM更容易排查性能问题。
  • MySQL:关系型数据库,事务支持可靠,适用于考试这种对数据一致性要求高的场景。入门门槛低,部署也简单,个人项目完全够用。

这个组合还有一个隐性好处:它就是国内中小型公司最常用的技术栈组合之一。做完这一个项目,Java基础、框架使用、数据库设计、前后端联调这些能力点基本都能覆盖到,对找工作非常友好。

2. 数据库设计:考试系统的地基

2.1 核心表结构拆解

数据库设计是整个系统最关键的部分。我一开始图省事,只设计了三张表(用户表、试卷表、题目表),结果做到考试记录功能时发现完全不够用,后来推倒重来。这里直接给出我认为最合理的核心表设计,大家可以直接参考:

  • sys_user(用户表):id、username、password(BCrypt加密存储)、real_name、role(ADMIN/TEACHER/STUDENT)、create_time等。角色字段不建议用int存,直接用字符串可读性更好。
  • exam_subject(题目表):id、subject_type(1单选、2多选、3判断、4简答)、subject_name(题干)、options_json(选项,用JSON存储)、answer(正确答案)、score、analysis(答案解析)、create_by等。选项用JSON字段存储,能省去单独的选项表,查询时一次取出极大简化开发。这里不要担心所谓的“范式违反”,对于题目选项这种无需单独检索的数据,JSON存储是性价比极高的选择。
  • exam_paper(试卷表):id、paper_name、total_score、duration(考试时长,单位分钟)、pass_score、status(草稿/已发布)、create_by等。
  • exam_paper_subject(试卷题目关联表):id、paper_id、subject_id、subject_score。一张试卷关联哪些题目、每题多少分,都通过这张关联表维护。这里需要注意:试卷题目关联是动态可变的,考试快照要单独处理(后面细讲)。
  • exam_record(考试记录表):id、paper_id、user_id、total_score、correct_count、wrong_count、status(考试中/已交卷/已判分)、start_time、end_time、duration_seconds等。
  • exam_record_subject(答题记录表):id、record_id、subject_id、user_answer、is_correct、score。考生每一道题的作答情况单独存一行,方便判分和错题回顾。
  • exam_white_list(防作弊白名单):id、record_id、max_switch_count、switch_count、status,当然这个可以并入记录表,但我偏好单独拆出来,便于扩展。

2.2 从需求到表结构的落地经验

一个很容易被忽视的问题:考试开始后,试卷内容能不能改?很多没做过考试系统的人会忽略这个点。假如教师发布考试后,又修改了试卷中的某道题,会导致什么后果?已经作答的考生数据全部错乱。

解决思路是“考试快照”:在考生点击“开始考试”那一瞬间,把试卷的题目内容、分值、顺序固化成一份快照,后续考生的作答、判分都基于这份快照,跟原试卷不再关联。实现上可以在exam_record_subject表里冗余一个subject_content_json字段,把题目题干、选项、标准答案都存进去。这样虽然多占一些存储,但彻底隔离了“试卷可变”和“考试公平性”之间的矛盾。

另外一个值得注意的点是索引设计。考试系统高频查询是:

  • 查询某张试卷的题目列表:SELECT ... FROM exam_paper_subject WHERE paper_id = ?,所以paper_id必须加索引。
  • 查询某个考生的考试记录列表:SELECT ... FROM exam_record WHERE user_id = ? ORDER BY start_time DESC,所以user_id加索引。
  • 查询某个考试记录的答题明细:SELECT ... FROM exam_record_subject WHERE record_id = ?,record_id加索引。

这三个索引是必加项,其他字段有查询需求的可以按实际补。我在实际开发中见过有人把整张表所有字段都建了索引,这反而会增加写入负担,完全没有必要。

2.3 几种特殊题型的存储设计

单选题、判断题处理起来很简单,答案就是一个字符(A/B/C/D,或T/F)。多选题和简答题则需要特殊考虑:

  • 多选题:答案用逗号分隔存储,比如“A,B,C”。判分时先比较选项字符集合是否完全一致,再决定给满分还是零分。如果产品要求“少选得部分分”,判断逻辑会复杂一些,需要先判断是否包含错误选项,再根据选对个数按比例给分。
  • 简答题:标准答案不唯一,通常由教师手工批改。这里我会在答题记录表上加一个teacher_score字段和is_manual标记,教师后台看到待批改列表后逐题打分,总分由系统自动汇总。
  • 填空题:如果有多个空,答案用“|”分隔存储,判分时逐空比对。前端作答时也按“|”拼接提交。

这些细节在设计表结构时就要提前考虑到,否则等前端页面做好再来改表,返工成本会高得让你怀疑人生。

3. 后端核心实现:SpringBoot + MyBatis 的实战要点

3.1 工程初始化与分层约定

创建SpringBoot项目时,我一般会用Spring Initializr(start.spring.io)生成基础工程,依赖勾选Spring WebMyBatis FrameworkMySQL DriverLombok。注意MyBatis要选MyBatis Framework而不是MyBatis Plus,网上很多教程默认用MyBatis Plus,但这里为了贴合标题就用原生MyBatis,其实掌握原生MyBatis之后再上Plus会轻松非常多。

工程结构建议按业务模块分包,而不是按技术分层分包。对比一下:

  • 按技术分层:controller / service / mapper / entity,所有模块混在一起。
  • 按业务分包:exam / user / question,每个包下面再分controller、service、mapper、entity。

个人项目中两种方式差别不大,但按业务分包在代码量上来以后,查找和定位问题要快得多。我习惯用按业务分包的方式,尤其是在项目要不断加功能的情况下,这个习惯帮我省了很多时间。

3.2 MyBatis映射文件与动态SQL实战

MyBatis的XML映射文件是这套系统的核心,几乎所有复杂查询都写在XML里。以一个查询为例:教师后台的试题列表需要支持按题型、按题干关键词、按创建人等多条件筛选,这种需求天然适合用MyBatis的动态SQL实现。

<select id="selectSubjectList" resultType="com.example.exam.entity.ExamSubject"> SELECT * FROM exam_subject <where> <if test="subjectType != null and subjectType != ''"> AND subject_type = #{subjectType} </if> <if test="keyword != null and keyword != ''"> AND subject_name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="createBy != null and createBy != ''"> AND create_by = #{createBy} </if> </where> ORDER BY create_time DESC </select>

这里有几个细节需要特别说明:

  1. <where>标签会自动去掉开头的AND,所以每个条件写AND开头是安全的,不用自己拼SQL时处处担心多出个where。
  2. LIKE CONCAT('%', #{keyword}, '%')这种写法,是为了防止SQL注入。如果把%直接拼进参数里再传进来,会有安全隐患,务必使用#{}而不是${}
  3. 关于MyBatis中的#{}${}的区别,我面试时被问到过无数次。简单说:#{}是预编译占位符,对应JDBC中的?,能防注入;${}是字符串拼接,用于表名、排序字段等无法使用占位符的场景,使用前必须手动校验白名单。

多表关联查询也是考试系统的重头戏。比如查询一份试卷的完整信息(试卷基本信息+题目列表+每题分值),我一般这样写:

<resultMap id="PaperDetailMap" type="com.example.exam.entity.ExamPaper"> <id property="id" column="paper_id"/> <result property="paperName" column="paper_name"/> <result property="totalScore" column="total_score"/> <collection property="subjectList" ofType="com.example.exam.entity.ExamSubject"> <id property="id" column="subject_id"/> <result property="subjectName" column="subject_name"/> <result property="subjectType" column="subject_type"/> <result property="optionsJson" column="options_json"/> <result property="answer" column="answer"/> <result property="score" column="subject_score"/> </collection> </resultMap>

这种一对多的<collection>嵌套查询映射,在实际开发中极其常用。理解它的关键是:外层查询和里层集合通过中间表的关联字段连接起来,返回的数据有多行时,MyBatis会自动根据id去重并组装成对象列表。

3.3 登录鉴权与JWT实践

在线考试系统里,除了常规的登录鉴权,防作弊和会话管理也很重要。JWT(JSON Web Token)是前后端分离项目中最常用的认证方案。它的工作原理可以简单理解为一个带签名的JSON数据包,服务端用密钥签发,客户端保存后在每次请求的Authorization头带回,服务端验签通过即认为请求合法。

核心实现如下:

// JWT工具类,使用io.jsonwebtoken库 public class JwtUtil { private static final String SECRET_KEY = "your-secret-key-must-be-long-enough"; private static final long EXPIRE_TIME = 1000 * 60 * 60 * 2; // 2小时 public static String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); } }

然后通过Spring MVC的拦截器统一校验Token:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { try { Claims claims = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", Integer.parseInt(claims.getSubject())); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { // token无效或过期 } } response.setStatus(401); return false; } }

这里有一个非常实用的经验:JWT密钥一定要放到配置文件里,不要硬编码在类中,更不要提交到Git仓库。在application.yml中可以这样配置:

jwt: secret: your-secret-key-must-be-long-enough-and-random expire-hours: 2

@ConfigurationProperties@Value读取即可。另外,JWT一旦签发无法主动失效(除非做黑名单),所以我在项目里会在Redis里存一份“在线用户”状态,退出登录时删除,站内强制下线时也走这个机制。这套“JWT+Redis会话状态”的搭配,在真实项目中几乎是标配。

3.4 考试核心流程控制

在线考试的核心流程是:考生点击开始考试 -> 后端生成考试记录 -> 生成试卷快照 -> 考生逐题作答 -> 系统自动保存 -> 考生交卷 -> 客观题自动判分+主观题人工判分 -> 成绩汇总。

其中几个关键点值得细说:

3.4.1 开始考试接口

@PostMapping("/exam/start/{paperId}") public Result startExam(@PathVariable Integer paperId, HttpServletRequest request) { Integer userId = (Integer) request.getAttribute("userId"); // 1.校验试卷是否存在且已发布 ExamPaper paper = paperService.getById(paperId); if (paper == null || !paper.getStatus().equals(1)) { return Result.error("试卷不存在或未发布"); } // 2.校验是否有未完成的考试记录 ExamRecord record = recordService.getUnfinishedRecord(userId, paperId); if (record != null) { // 如果有未完成的记录,直接续考 return Result.success(record); } // 3.创建考试记录 + 生成答题快照 ExamRecord newRecord = recordService.createExamRecord(userId, paperId); recordService.generateAnswerSnapshot(newRecord); return Result.success(newRecord); }

这个设计解决了一个很现实的场景:考生考试中途断网,刷新页面后还能回到未完成的考试继续作答,而不是被强制交卷。所以“未完成记录续考”这个逻辑,在真实系统中是必备的,也是很多面试官会追问的点。

3.4.2 自动保存与提交判分

考试页面每30秒自动保存一次已作答题目,提交试卷时接口要做幂等处理。所谓幂等,就是多次点击提交,只会成功一次。

@PostMapping("/exam/submit/{recordId}") @Transactional(rollbackFor = Exception.class) public Result submitExam(@PathVariable Integer recordId, @RequestBody SubmitVO submitVO) { ExamRecord record = recordService.getById(recordId); // 校验记录状态,防止重复提交 if (record.getStatus() != 0) { return Result.error("该考试已提交,请勿重复操作"); } // 更新每道题的作答内容 recordSubjectService.saveUserAnswers(recordId, submitVO.getAnswers()); // 客观题自动判分 Integer objectiveScore = recordSubjectService.autoMark(recordId); // 计算主观题待批改列表 Integer subjectiveCount = recordSubjectService.countUnmarked(recordId); // 更新考试记录状态:若包含主观题,进入待批改状态;否则直接完成 ... }

判分逻辑里有个小坑:多选题的判分不能简单地用字符串相等判断,因为选项顺序可能不同。我先将用户答案按逗号拆分,排序,再跟标准答案排序后的结果比较,这样用户选“A,C”和标准答案“C,A”也能判对。

3.5 SpringBoot中遇到的两个经典问题

3.5.1 版本太高导致的依赖兼容问题

SpringBoot版本不是越高越好。2024年后SpringBoot 3.x成为主流,但它基于Jakarta命名空间(javax.servlet改成jakarta.servlet),并且JDK要求17+。如果你的本机还是JDK 8,那只能选择SpringBoot 2.7.x。网上很多教程默认用最新版,你跟着敲完之后发现启动报错,大概率就是JDK版本和SpringBoot版本不匹配。

我的建议是:项目文档里标明自己使用的版本组合,比如“JDK 8 + SpringBoot 2.7.18 + MyBatis 2.3.2 + MySQL 8.0”,这样团队成员或读者照着配不容易踩坑。

3.5.2 NoClassDefFoundError: java/applet/Applet

这个问题看着吓人,实际90%的情况都是JDK版本过高导致。某个依赖在编译时引用了旧JDK才有的java.applet.Applet类,但高版本JDK把它们移除了。解决方式两种:换用更新版本的依赖(修复了兼容问题),或者降低JDK版本。我在跑一个老项目时遇到过,当时把JDK从17降到8就好了。

3.5.3 当表不存在自动建表

很多人问过“SpringBoot + MyBatis怎么实现表不存在自动建表”。说实话,MyBatis本身没有这个内置能力,它不是全自动ORM框架。实际项目里有几种做法:

  • 使用spring.sql.init机制,配合schema.sql脚本,在应用启动时执行建表语句。
  • 在启动类里写一个ApplicationRunner,检查表是否存在,不存在则执行建表SQL。
  • 引入Flyway或Liquibase这类数据库迁移工具,这是最规范的做法。

个人练手项目用schema.sql就够了,但如果项目要交给别人维护,我强烈建议花半小时集成一个Flyway,它能把数据库结构变更纳入版本管理,谁改过什么,一目了然。

4. 前端Vue3实现:从页面搭建到接口联调

4.1 用Vite脚手架创建Vue3项目

前端的搭建我推荐直接用Vite,相比Webpack,Vite冷启动速度快了不止一个量级。

npm create vite@latest exam-web -- --template vue cd exam-web npm install npm install vue-router@4 pinia axios element-plus

这里选的依赖分别是:vue-router@4(Vue3配套路由)、pinia(Vue3官方推荐状态管理)、axios(HTTP请求库)、element-plus(Vue3组件库)。

对于考试系统来说,Element Plus提供了表格、表单、对话框、消息提示等现成组件,能省下大量写UI的时间。当然,如果你对UI有更高要求,也可以选择Naive UI或者Ant Design Vue,但个人项目用Element Plus生态最稳妥。

4.2 组合式API在考试页面中的实战

Vue3的组合式API在考试作答页面这个场景下优势特别明显。答题卡、计时器、当前题目索引、答案暂存区,这些都是强关联状态,放在同一个setup作用域内,代码组织起来非常顺手。

<script setup> import { ref, computed, onMounted, onUnmounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { startExam, getExamDetail, submitExam, autoSaveExam } from '@/api/exam' import { ElMessage, ElMessageBox } from 'element-plus' const route = useRoute() const router = useRouter() const paperId = route.query.paperId const currentIndex = ref(0) const questionList = ref([]) const answers = ref({}) const remainSeconds = ref(0) const recordId = ref(null) const progress = computed(() => { const answered = Object.keys(answers.value).length return Math.round(answered / questionList.value.length * 100) }) let timer = null const startCountdown = () => { timer = setInterval(() => { remainSeconds.value-- if (remainSeconds.value <= 0) { clearInterval(timer) handleSubmit() } }, 1000) } const handleSubmit = async () => { clearInterval(timer) try { await submitExam(recordId.value, { answers: answers.value }) ElMessage.success('交卷成功') router.push('/exam/result') } catch (e) { ElMessage.error('交卷失败,请重试') } } // 每30s自动保存一次 let autoSaveTimer = null const startAutoSave = () => { autoSaveTimer = setInterval(async () => { await autoSaveExam(recordId.value, { answers: answers.value }) }, 30000) } onMounted(async () => { const { data } = await startExam(paperId) recordId.value = data.id remainSeconds.value = data.duration * 60 const detail = await getExamDetail(paperId) questionList.value = detail.data.subjectList startCountdown() startAutoSave() }) onUnmounted(() => { clearInterval(timer) clearInterval(autoSaveTimer) }) </script>

这套逻辑有几个细节值得注意。倒计时数据来自后端返回的考试时长(以分钟为单位),而不是前端自己定义。因为考试规则由后端控制,前端只是展示。remainSeconds每秒减1,时间归零自动交卷,这个策略能有效防止考生通过修改前端代码来延长考试时间。自动保存的间隔30秒也合理,太频繁会频繁请求接口,太低则丢失答题进度的风险增大。

4.3 axios封装与Token注入

axios请求层需要统一处理两件事:JWT Token注入、响应状态码统一处理。这算前端工程化的基本功,但很多新手会在这里栽跟头。我习惯这样封装:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:注入Token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误码 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

这里把业务状态码和HTTP状态码分开处理。HTTP 200不代表业务成功,业务是否成功以res.code === 200为准。Token过期(返回401)时自动清除本地Token并跳转到登录页,这个兜底逻辑在前端必不可少,否则用户会一直停留在错误页面,也不知道发生了什么。

4.4 跨域联调与开发代理配置

前后端分离开发时,跨域问题几乎一定会遇到。最简单的方案是在Vite开发服务器中配置代理,让前端所有/api请求转发到后端服务。

// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })

配置完成后,后端接口路径如/exam/start,前端请求路径写/api/exam/start即可,Vite会自动把请求转发到http://localhost:8080/exam/start。这样开发过程中不需要后端额外配置CORS,也不用在axios里写死完整的后端地址,代码环境切换时只需要改一处代理配置就行。

网上搜Vue3相关热词时经常看到“Vue3后台管理系统”、“Vue3商城”这类教程,但考试系统的页面交互在同一个技术栈里其实更复杂一些。如果你能把这个项目的作答页、答题卡、倒计时组件做熟练,Vue3的核心能力可以说掌握得差不多了。

5. 常见问题与排查技巧实录

5.1 MySQL安装配置问题

MySQL 8.0的安装配置是很多新手的第一个拦路虎。这里分享几个关键要点:

  • 下载时选择MySQL Community Server 8.0.x,用官方安装包(ZIP或MSI均可以,建议有图形界面的MSI安装)。
  • 安装过程中会让你设置root密码,请先设置一个简单好记的(比如root123456),后面再去改。不要一上来就设置超复杂密码,你永远不知道多久后会忘。
  • 配置环境变量:把mysql.exe所在目录(通常为C:\Program Files\MySQL\MySQL Server 8.0\bin)添加到系统Path中,然后重新打开命令行窗口才能生效。
  • 启动服务:net start mysql80(服务名取决于安装时指定的名称,安装后可以在“服务”里查看)。
  • mysql -u root -p登录成功后,执行ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';修改密码。

实际遇到最多的情况是安装完成后服务启动失败,十有八九是3306端口被占用。netstat -ano | findstr :3306查看端口占用进程,关掉冲突进程或者修改MySQL端口即可。

5.2 MyBatis日志打印与SQL调试技巧

写复杂的XML SQL时,要么结果不对,要么性能有瓶颈,这时候看不到真实执行的SQL就两眼一抹黑。MyBatis默认不打印SQL日志,需要在配置里开启。在application.yml中添加:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

开启后每次执行SQL都会在控制台打印完整的SQL语句和参数列表,排查问题效率直接翻倍。但注意这个配置在生产环境一定要关闭,否则日志里会记录完整的查询参数,包括用户信息等敏感数据。

另外推荐一个IDEA插件:MyBatis Log Free,它的作用是拦截控制台打印的MyBatis日志,自动帮你拼成一条可以直接在数据库客户端执行的真实SQL语句,省去手动替换?占位符的功夫,调试动态SQL时非常好用。

5.3 前端面试常问的Vue3问题在项目中的落地

很多人在学Vue3时看过官方文档,背过面试题,但一上手写代码就忘了。实际上你在这个项目中用到的Pinia、组合式API、路由守卫、响应式数据等,全是面试高频考点。比如“Vue3中为什么推荐使用refreactive而不是data?”——因为你在这个项目里已经亲身体会过,响应式数据在跨组件状态同步时带来的便利。再比如“组件通信方式有哪些?”——考试详情页和答题组件之间通过props传递数据、通过自定义事件通知父组件更新答题卡,都是现成的案例。

Vue3官方文档里提到过JSX的支持,但在当前这个项目中完全没必要用JSX,模板语法已经足够清晰。如果你遇到复杂的动态渲染场景(比如根据题型动态渲染不同的作答组件),用<component :is="...">动态组件配合模板即可优雅解决,不要为了炫技而引入不必要的复杂度。

5.4 面试八股文与项目深度的平衡

做这个项目时,我建议你不仅“会做”,还要能“讲清楚”。面试官问“SpringBoot自动配置原理”时,你可以结合项目里引入的依赖讲:以spring-boot-starter-web为例,它通过spring.factories加载DispatcherServletAutoConfiguration,条件装配时检测到classpath中有DispatcherServlet类,就会自动创建相关Bean。你在这里做的配置,其实都是在覆盖自动配置提供的默认值。

问“MyBatis缓存机制”时,你可以结合项目的实际场景回答:一级缓存是SqlSession级别的,默认开启,同一个SqlSession中执行相同SQL会命中缓存;二级缓存是namespace级别的,需要手动配置开启,但考试系统中试卷数据实时性要求不高,可以开启二级缓存;而用户作答数据必须实时写库,所以这道题的缓存要谨慎处理。

像“MyBatis和MyBatis Plus的区别”这种面试题,有项目经验后你的回答会非常有底气:MyBatis Plus在MyBatis基础上内置了通用Mapper和条件构造器,单表CRUD基本不用写SQL,但在复杂多表关联、动态查询场景下,原生XML更加灵活可控。考试系统里既有大量单表操作,又有复杂的多表查询,所以我对原生MyBatis和MyBatis Plus的能力边界都非常清楚。

6. 部署上线与系统配置补充

6.1 本地联调环境搭建

整套系统的本地运行流程如下:

  1. 启动MySQL,新建数据库exam_db,执行项目中的init.sql脚本建表并写入初始化数据(默认管理员账号等)。
  2. 修改后端application.yml中的数据库连接信息:URL、用户名、密码。
  3. 启动SpringBoot后端,确认端口8080正常监听,访问http://localhost:8080能看到默认接口返回(如果配置了健康检查)。
  4. 启动前端:在exam-web目录下执行npm install安装依赖,然后npm run dev启动开发服务器,默认端口5173。
  5. 浏览器访问http://localhost:5173,用默认管理员账号登录。

6.2 部署到服务器的简化方案

个人项目或者毕设展示,没必要上K8s、Docker这些复杂方案。最简单的部署方式是:前端打包后由Nginx托管,后端打包成Jar包用java -jar直接运行。需要注意的配置有:

  • 后端application-prod.yml中数据库地址改为服务器地址,并设置好复杂的账号密码。
  • 前端axiosbaseURL改为后端实际公网地址,或者通过Nginx反向代理转发/api请求。
  • Nginx配置示例:
server { listen 80; server_name your-domain.com; root /opt/exam-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }

这段配置中try_files ... /index.html是必选项,原因是Vue Router使用history模式时,刷新非首页路由(如/exam/start)会直接请求Nginx,如果找不到对应文件就返回404,加上这行配置才能正确回退到前端入口。

6.3 核心配置项:系统参数与安全设置

一套可实际使用的考试系统,还需要有一些可配置的系统参数项:

  • 考试迟到限制:开考后N分钟禁止进入考试,比如开考15分钟后不允许入场。
  • 切屏告警:允许最多切屏N次,超过后自动强制交卷。这里的N和惩罚策略需要做成可配置,不同考试可能设定不同。
  • 密码策略:是否强制要求密码复杂度,多少天需要修改一次密码。
  • 成绩发布方式:交卷后立刻显示客观题成绩,还是等待教师发布后统一可见。

这些参数建议统一放一张sys_config表,用key-value方式存储,后台管理页面可以动态修改,而不是在代码里用常量写死。这样系统交付后,管理员自己就能调整考试规则,不需要每次改代码重新打包部署。

在安全设置上,至少要做这几件事:数据库密码用环境变量注入或jasypt加密(而不是明文写在配置文件里),管理员接口加上单独的校验逻辑防止被普通用户调用,所有密码用BCrypt加密存储,不允许明文。这些都是线上项目的基本要求,也是项目亮点。

最后再分享一个小技巧。做这个项目的时候,我习惯每完成一个模块就顺手在文档里记一笔:这个模块的核心逻辑是什么、用了哪些设计思路、后续能怎么扩展。到写简历的时候,把这些笔记整理一下,其实就形成了很扎实的项目介绍。技术面试时,自己亲手做过的项目,细节记得最牢,讲起来也最有底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询