1. 项目概述与技术定位
1.1 这个系统到底解决了什么问题
考试系统这个品类,在高校、培训机构、企业内部考核里一直都有刚需。但市面上的现成产品要么收费贵、要么定制难,更常见的问题是:买了一堆功能用不上,想加一个"按题型抽题"的小功能都得等厂家排期。
自己动手写一个,就要面对全栈的活:前端做答题界面、后端管题库和判分、数据库存考试记录。说实话,一个模块一个模块从零写,工作量不小。而这个项目用SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套组合,把考试系统的通用能力都搭好了,核心的题库管理、试卷生成、在线考试、自动判分、成绩统计这些功能全都有,还带了配套文档。对于正在做毕业设计的学生、想快速交付内部考试系统的开发者、或者想学习主流前后端分离架构的进阶者来说,都是一个很合适的参考基线。
技术栈选型也不花哨——SpringBoot2是Java后端最成熟稳定的版本,Vue3配合Vite开发体验好,MyBatis-Plus把单表CRUD简化到几乎不用写SQL,MySQL8.0提供了窗口函数等实用特性。这套组合兼顾了"能跑起来"和"能学到东西"两个诉求。
1.2 适合谁来用、能学到什么
如果你是以下三类人,这个项目值得花时间拆一拆:
- 毕设选题是"在线考试系统"的学生。这个项目覆盖了用户角色分权、试题管理、考试流程、自动判分、考试记录查询等完整闭环,加上配套文档,直接改改就能作为毕设交付。
- 公司内部要做员工考核、培训验收的Java后端开发者。可以基于它做二次开发,替换掉采购的SaaS考试服务,数据自己掌握。
- 想系统性学习前后端分离开发、看真实项目如何组织代码的初中级工程师。这个项目里的权限设计、文件导入导出、Excel批量处理、分布式会话管理等知识点,都是实际工作中高频用到的。
我自己在实际拆解这个项目的过程中,主要把精力放在三个地方:一是前后端联调时的接口设计是否合理,二是数据库表之间的事务一致性怎么保证,三是并发场景下(比如一个班同时交卷)系统扛不扛得住。带着这几个问题去读代码,收获比单纯"跑起来看看界面"大得多。
2. 技术选型背后的取舍逻辑
2.1 为什么后端是SpringBoot2而不是SpringBoot3
这个项目用的是SpringBoot2.x,我在实拆代码时确认了版本号——2.7系列是SpringBoot2的最后一个稳定大版本。这里有个容易被忽视的兼容性问题:SpringBoot3从Java 17起步、基于Jakarta EE命名空间,很多老项目的依赖(比如某些MyBatis扩展插件)还没跟上。而SpringBoot2.7对Java 8、Java 11都友好,现有的MyBatis-Plus、Druid等生态是高度匹配的。对大多数团队的现状来说,SpringBoot2依然是最"稳妥不折腾"的选择。
SpringBoot2带来的核心价值有三块:
- 自动配置机制大幅减少了XML配置,项目里几乎看不到繁琐的dispatcher-servlet.xml这类文件。
- 内嵌Tomcat让部署变成"打一个Jar包就能起服务"。
- Actuator运维端点在考试环境出问题时很好用——可以快速查看健康状态、线程池占用、接口调用详情。
这些能力在考试系统的场景里意味着什么?最直接的体现就是:一个班几十上百号人同时登录、同时交卷时,如果某个接口响应变慢,你可以通过Actuator的实时指标快速定位瓶颈是数据库连接池还是业务线程池,而不是对着日志一行行翻。
2.2 前端为什么用Vue3+组合式API
Vue3核心优势其实是组合式API带来的逻辑复用能力。考试系统里,倒计时逻辑、答题卡状态、未答题目提醒这些功能会散落在多个页面,用组合式函数(Composable)可以很优雅地抽出来复用。
比如倒计时这个功能,在"考试中"页面和"模拟考试"页面都有,传统的Options API写法往往是复制粘贴两份逻辑,改动一处处处改,而组合式API可以这样封装:
import { ref, onUnmounted } from 'vue' export function useCountdown(duration, onTimeout) { const remaining = ref(duration) const timer = setInterval(() => { remaining.value-- if (remaining.value <= 0) { clearInterval(timer) onTimeout && onTimeout() } }, 1000) onUnmounted(() => clearInterval(timer)) return remaining }页面里只需要调用const remaining = useCountdown(3600, autoSubmit)就拿到了每秒更新的秒数,页面销毁时定时器还会自动清理,不用手动处理。这就是Vue3给实际开发带来的体感提升。
配合项目的Vite构建工具,本地开发热更新速度极快。改一行代码几乎秒级刷新页面,这在调试答题组件时体验非常重要——你不需要反复手动刷新整个页面去验证一个样式变化。
2.3 ORM层的选择:MyBatis-Plus的"够用哲学"
MyBatis-Plus不是把MyBatis完全替换掉,而是在MyBatis之上做增强。最实用的是它内置的BaseMapper,单表操作连SQL都不用写:
public interface ExamRecordMapper extends BaseMapper<ExamRecord> { }然后业务代码里直接:
examRecordMapper.selectPage(new Page<>(pageNum, pageSize), new LambdaQueryWrapper<ExamRecord>() .eq(ExamRecord::getExamId, examId) .orderByDesc(ExamRecord::getSubmitTime));分页、条件查询、排序全都给你封装好了。这在考试系统中高频出现的"我的考试记录""某场考试所有学生成绩列表"这类查询里,代码量能少写一半以上。复杂报表和跨表统计再手写XML,两条腿走路。
还有一个被很多人忽视的重点:MyBatis-Plus的乐观锁插件在处理"成绩复核"这种并发修改场景时很实用。比如管理员和教师同时打开同一个学生的成绩单,一个在批改主观题、一个在调分,乐观锁版本号机制能避免后提交的覆盖先提交的修改。项目里在成绩表设计时留了版本号字段,就是用乐观锁防冲突。
2.4 MySQL8.0带来的实用特性
这个项目的建表语句和数据脚本是按MySQL8.0写的。8.0相比5.7,感知最强的几个点:
- 窗口函数。做成绩排名、求每场考试的平均分、对比班级之间差距时,一条SQL就能搞定,不用写子查询或者程序里二次计算。
SELECT student_name, score, RANK() OVER (PARTITION BY exam_id ORDER BY score DESC) AS rank_in_exam FROM exam_record;- utf8mb4成为默认字符集。存emoji表情、生僻字不会出现乱码。
- 原子DDL和数据字典的改进,让线上表结构变更更安全。
对于考试系统这种数据变更频率不算高、但查询模式比较固定的系统,MySQL8.0属于"不折腾但好用的底线配置"。
3. 核心功能模块拆解:每个模块怎么设计与实现
3.1 题库管理模块:不仅仅是一个增删改查
考过试的人都知道,出题是个正经体力活。这个系统把题库管理拆成了三层:
- 基础信息层:题目类型(单选题、多选题、判断题、填空题、简答题)、难度系数(容易、中等、困难)、所属课程/知识点、题干文本。
- 题目资源层:图片、公式、选项内容。虽然不指望一个考试系统做成复杂的公式编辑器,但至少题目里能嵌图片,这在理工科题库里是刚需。
- 批量导入层:系统提供了Excel模板批量导入试题的能力。
这里比较容易被忽略的是知识点和题目的关系设计。项目里没有用"一张题目表里加一个知识点的字符串字段"这种偷懒写法,而是设计了独立的题目-知识点关联表。为什么?因为一个题目可能同时命中"第三章 线性方程"和"核心考点 应用题"两个标签,一对多关系如果扁平存储,后续按知识点筛题时要么LIKE匹配性能差,要么数据冗余导致统计不准确。
批量导入这个功能,我认为是这个项目中工作量被低估的部分。看着只是"读Excel-解析-入库",但实际要处理的边界很多:选项ABCD缺项怎么办、答案列格式不统一怎么办、同一道题被导入了两次怎么去重。好在项目采用MyBatis-Plus的批量插入 + 事务控制,数据量一次几千条问题不大,导入失败还能回滚,不至于导到一半留下一堆脏数据。
3.2 试卷模块:手动组卷和随机组卷
组卷策略是这个系统的核心业务逻辑。项目里实现了两种:
- 实时随机抽题:用户设置好每种题型的数量、总分、难度分布,系统在题库里按条件随机抽取,组装成一份试卷。
- 固定试卷:教师预先手工选定具体题目,生成一份所有人统一的试卷。
随机抽题这块,如果直接用ORDER BY RAND()去查询,在小数据量题库(几千题)里问题不大,但题库到了几万题,性能会很辣眼。实际项目里我更推荐先按题型和难度用SELECT id FROM question WHERE type=1 AND difficulty=2取出一个符合条件的ID列表,再用代码层面做随机打乱采样,最后按采样出的ID集合去查完整信息。MySQL8.0下,这种做法的查询响应时间稳定可控。
试卷模块还需要处理"试卷总分校验"。项目在组卷完成后做了总分等于设定值的校验,否则提示组卷失败。这一步看似简单但少了会出大问题——考试都发布出去了,学生做完发现总分不对,那可不是小事故。
提示:如果你要二次开发这块逻辑,建议把组卷参数(题型数量、每题分值、难度比例)单独建一张配置表,而不是把参数硬编码在组卷接口里。这样同一个题库可以配置出"练习模式"“模拟考模式”“正式考试模式”多套策略,改配置不用改代码。
3.3 在线考试模块:并发控制与防作弊
这是用户直接体验最重的一环。学生点击开始考试的那一秒,系统要干几件同时进行的事:
- 生成该学生的答题记录(或者标记考试记录状态为进行中);
- 计算考试截止时间(开始时间 + 考试时长);
- 打包当前试卷包含的所有题目,返回给前端。
其中最容易出BUG的地方是并发:同一个学生双击"开始考试"按钮,防止生成两条考试记录。我的经验是:加一个"创建考试记录"的接口幂等处理,例如根据student_id + exam_id + status=进行中的唯一约束兜底,MySQL在重复插入时抛异常,业务层捕获后直接返回已有记录。否则并发双击的瞬间,两条记录都插进去了,后续交卷时不知道以哪条为准。
防作弊方面,项目实现了一个简单实用的策略集合:
- 考试倒计时一到,前端自动触发交卷,后端同时校验提交时间是否超时;
- 记录学生切出考试页面的次数和时间,后台可以查看异常日志;
- 试卷选项顺序随机打乱(同一个题目不同学生看到的ABCD选项顺序不同),降低互相抄袭的可能性。
说实话,这些防作弊方案谈不上什么高科技,但胜在成本低、效果好。大型考试系统会加摄像头监控、人脸识别,那是另一个量级的事。对于高校课程测验、企业内部考核这种场景,切屏记录加选项乱序已经足够有威慑力了。
3.4 自动判分模块:客观题一键判、主观题怎么办
考试完后最怕的是判分慢。这个系统的判分逻辑分两条线:
- 客观题(单选、多选、判断):学生提交后,系统逐题比对标准答案,自动得分。
- 主观题(填空、简答):系统保留学生答案,教师手动在批阅页面给分。
自动判分里有个细节值得说:多选题的判分规则。是"完全一致才得分"还是"漏选给一半分"?项目里配置成了后者,更符合教学场景。学生选了部分正确选项,不能直接给零分,否则客观题得分会普遍偏低,成绩分布没有区分度。
填空题的判分也存在不确定性:多个空位顺序是否可变?比如答案是"3, 5",学生写"5, 3"算不算对?这需要配置。目前大多系统对这种情况都按顺序匹配处理,属于已知局限,需要在题目录入时就明确提示学生。
主观题的评分页面我拆解时觉得做得比较到位:左侧是学生答案,右侧是参考答案和分值框,教师可以直接在页面上打分,保存后总分自动重新计算。这个"重新计算总分"的时机很重要——不能直接改明细得分就结束,必须触发一次总分汇总,否则成绩列表里的总分和明细对不上,后面的统计报表也会跟着错。
3.5 成绩管理与统计分析模块
考试结束不等于系统结束。成绩管理模块至少承担三个任务:
- 成绩查询:学生查看自己的成绩、错题详情;教师查看所负责班级的全部学生成绩。
- 成绩导出:导出Excel成绩单,方便上报教务系统。
- 统计分析:基于考试记录计算平均分、及格率、最高分、最低分、分数段分布。
统计部分项目用了一条典型的分段统计SQL:
SELECT CASE WHEN score >= 90 THEN '90-100' WHEN score >= 80 THEN '80-89' WHEN score >= 70 THEN '70-79' WHEN score >= 60 THEN '60-69' ELSE '60以下' END AS score_band, COUNT(*) AS student_count FROM exam_record WHERE exam_id = ? GROUP BY score_band ORDER BY score_band;这个分数段分布直接决定了考试成绩是否呈合理的正态分布特征。在高等教育和内部培训场景里,分析这个能快速判断试卷难度设置是否合理——如果全班80%的人得分都在90分以上,说明这张卷子没有区分度。
注意:统计报表的数据实时性很重要。我在实际使用中遇到过一次问题:主观题批改完成后,成绩列表刷新没有重新拉起总分,导致统计结果延迟更新。排查之后定位到是缓存没有失效的问题。解决方案是:主观题判分保存后,主动清理相关缓存并触发一次总分重算,不能依赖"下次查询时自然会更新",因为读多写少的场景里查询维度太多,缓存命中率极高,脏数据会一直存在到缓存过期。
4. 数据库设计重点:考试系统的表结构应该长什么样
4.1 核心表梳理与关联关系
拆完这个项目的数据库脚本,我觉得核心表可以归为六组:
| 分组 | 表名示例 | 职责说明 |
|---|---|---|
| 用户权限 | sys_user、sys_role、sys_user_role | 学生、教师、管理员三种角色的基础信息与权限关联 |
| 教学基础 | course、class_info、student_class | 课程与班级信息,用于组织考试范围 |
| 题库 | question_bank、question、question_knowledge_point | 题库分组、题目明细、题目-知识点关联 |
| 试卷 | exam_paper、exam_paper_question | 试卷主表、试卷题目明细 |
| 考试过程 | exam_info、exam_record、answer_detail | 考试定义、学生考试记录、学生答题明细 |
| 统计配置 | score_config、grade_threshold | 计分规则、成绩等级阈值 |
其中exam_record(考试记录)和answer_detail(答题明细)是数据量增长最快的表。一个500人的年级考一次试,answer_detail少说几千条,一个学期下来就是几万条。这种数据量级别在MySQL里毫无压力,但索引设计还是要注意:answer_detail表必须建(exam_record_id, question_id)联合索引,因为在判分和错题回顾时几乎总是按考试记录去查它。
4.2 表设计的几个亮点
第一,考试和试卷拆成两张表。同样是"期末考试"这个考试活动,面向不同班级可以生成不同试卷;同一份试卷也可以被多次考试复用。如果考试和试卷混在一张表里,复用和关联都做不到。
第二,答题明细表里冗余了question_type和score字段。冗余不违反规范,反而大幅减少了查询时的关联成本。判分逻辑里我只需要按考试记录把答题明细全查出来,直接根据冗余的题目类型字段分流处理,不用再join题目表去取题目类型。这个"以查代算"的思路,在报表展示时性能优势非常明显。
第三,班级和学生拆开。考试记录挂在学生上,而学生又挂在班级上。这样"一班平均分 vs 二班平均分"的对比SQL写起来非常简单,也为后续按班级维度做更多维度的统计分析留了口子。
4.3 MySQL8.0的细节配置
项目里的建表语句都用utf8mb4字符集,排序规则是utf8mb4_unicode_ci。对于考试系统这种文本内容多、包含中文和特殊字符的业务来说,这是正确的选择。MySQL8.0里utf8mb4已经是默认,但如果是5.7老库迁移上来的,一定要检查现有表的字符集——新旧表join时字符集不一致,轻则导致索引失效,重则查询直接报错。
另外就是时区问题。考试记录里有两个关键时间字段:start_time和submit_time。如果MySQL时区和应用服务器时区不一致,系统判定"是否超时自动交卷"时就会出现偏差。项目里通过数据库连接串加了serverTimezone=Asia/Shanghai参数来统一时区,这个细节值得照抄。
5. 前端设计与核心交互实现
5.1 前端路由与页面组织
前端的整体结构,我按页面角色拆了一下:
- 学生端:登录页、考试列表页、考试答题页、成绩查询页、错题回顾页、个人信息页。
- 教师端:题库管理页、组卷页、考试管理页、批阅页、成绩统计页。
- 管理员端:用户管理页、课程班级管理页、全站数据概览页。
这里让我印象比较深的是答题页的设计。它的布局不是简单的上下滚动,而是"左侧题目区 + 右侧答题卡"的形式。右侧答题卡区域用Grid网格展示每一题的题号和答题状态(已答蓝色、未答灰色、标记疑问黄色),点击题号可以快速跳转到对应题目。这个交互在真实机考场景下非常重要——学生需要在多个题目间反复跳转,没有答题卡就意味着必须一直滚动,体验很差。
5.2 组件的复用与拆分
Vue3项目里组件拆得好不好,直接影响后续迭代效率。这个项目有一个典型的好例子:题目渲染组件QuestionDisplay.vue。无论学生在答题页还是在错题回顾页,看到的题目展示形式应该是一致的。通过把题目渲染抽成一个组件,接收一个questionData对象和readonly标志,两端页面复用同一套样式和逻辑,避免出现"答题页里选项是按钮风格,错题回顾里就变成文本风格"这种不一致。
数据获取和状态管理这块,项目用了基于Pinia的状态管理方案。考试过程中的临时答题状态存在Pinia里,但有一个姿势我觉得值得强调:一定要和localStorage做持久化同步。因为考试页一旦刷新,内存里的答题状态全丢,如果没有恢复机制,考生只能重新答题或者直接交白卷。这是一个真实的灾难场景。实现也不复杂,用Pinia的插件订阅state变化,把answerMap同步到localStorage,页面加载时检查localStorage里有没有未完成的考试记录,有就恢复。
5.3 与后端的接口约定
前端页面和服务端的数据交互,项目里遵循了一个清晰的RESTful约定。有一个细节需要注意:所有业务接口的响应体都包了一层Result<T>结构,包含code、message、data三个字段。这样做的好处是错误处理逻辑统一了,前端可以用一个响应拦截器判断code,非200统一弹错误提示,不需要每个API单独做错误分支。
接口路径也按模块做了分组。这一点看似平常,但对前后端联调和后续扩展的意义非常大。比如:
- 题库相关:
/api/questions,/api/questions/batch - 试卷相关:
/api/papers,/api/papers/{id}/questions - 考试相关:
/api/exams,/api/exams/{id}/start,/api/exams/{id}/submit
再配合Swagger/Knife4j的接口文档,前后端并行开发时基本不需要反复口头对接口字段,直接看文档就行。
6. 后端核心实现细节
6.1 登录鉴权与会话管理
考试系统安全要求比较高:学生只能看到自己参加的考试、教师只能管理自己课程的题库。项目里基于JWT做了一套无状态鉴权,流程是:
- 用户提交账号密码,后端验证通过后签发JWT令牌;
- 前端把JWT存在localStorage,每次请求在Header里带
Authorization: Bearer <token>; - 后端通过拦截器解析JWT,将用户ID和角色塞入请求上下文,供后续接口做权限校验。
JWT方案的好处是天然适合前后端分离和水平扩展。考试系统在高峰期可能有大量并发登录,如果靠Session粘滞(Sticky Session),部署多个后端实例时就要配置会话共享,非常麻烦。JWT无状态,任何一台机器都能验证令牌合法性,部署结构瞬间简单很多。
但JWT也有个坑:令牌一旦签发,在有效期内无法主动使其失效。如果学生的账号被顶号或者密码泄露,管理员封禁用户的操作不会影响已签发的JWT。实际项目中一般用较短的有效期加前端定时刷新来解决,或者维护一个"token黑名单"表。考试系统里对安全的要求没有那么苛刻,项目采用短有效期方案(比如2小时),已经够用。
6.2 拦截器、切面与权限控制
项目的权限控制没有引入Spring Security框架,而是用了轻量级的HandlerInterceptor+ 自定义注解方式。原因很务实:系统只有三种角色,权限模型简单,理科生思维下完全不需要把Spring Security那么大一个体系引进来增加学习成本。
具体实现思路:
- 定义一个注解
@RequireRole,可以标注在Handler方法上,参数可以是STUDENT、TEACHER、ADMIN或者它们的组合。 - 写一个
RoleInterceptor,在preHandle里读取请求头里的JWT,解析出角色列表,再和注解要求比对。 - 注册到WebMvcConfigurer时,对
/api/**路径生效。
这种轻量级方案虽然不够"标准",但对这个规模的项目来说就是最优解。Spring Security要配UserDetailsService、SecurityFilterChain、密码加密器、方法级安全,配置量是拦截器的三倍以上,而且代码里全是抽象接口,新人接手时根本分不清是哪个过滤器在起作用。
6.3 统一异常处理与事务管理
项目里全局异常处理用了@RestControllerAdvice,把参数校验异常、业务异常、SQL异常分别映射到对应的HTTP状态码和错误码。这个做法的价值在联调阶段就能体会到——前端看到一个明确的错误码(比如E1001 试题不存在),可以直接定位问题,而不是看到一句笼统的"服务器内部错误"。
事务管理方面,所有写操作都默认加了@Transactional。重点场景是试卷快照和判分回写:考试开始时要生成试卷快照(防止题库题目被修改影响已发布的考试),学生提交时要批量写答题明细,这两处事务必须保证原子性。我拆代码时确认了它们都用了事务控制,这就避免了"答题明细写了一半失败、考试记录却标记成已完成"这类数据不一致的严重问题。
6.4 性能与并发处理
一个考试系统能遇到的最大并发峰值就是"整班同时交卷"。几十、上百个学生一起点交卷,后端要同时处理一批答题明细的入库、判分、总分汇总。数据库连接池如果没有合理配置,很容易在那一瞬间被打爆。
项目在数据源配置上用的是HikariCP(SpringBoot2的默认连接池),这本身就比老旧的DBCP2和C3P0性能好。但光有默认配置还不够,我建议把最大连接池数量和最小空闲连接数显式配置出来,比如:
spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000这个数值不是随便拍的。按一个班50人同时交卷计算,每条交卷请求最终要占用一个数据库连接来完成事务。考虑到同一时刻还有其他查询量(比如老师在看成绩列表、学生在看考试列表),50个连接能覆盖常见并发。还要注意MySQL默认max_connections是151,连接池50、其他应用再用一点,总数还在可控范围内。
还有一个容易忽略的点是一次交卷的数据量比较大。一个学生考50道题,answer_detail一次插入50条,50个人就是2500条。我见过有实现是循环单条插入,那在并发峰值下必然慢。项目用了MyBatis-Plus的forEach配合批量insert,把2500条插入压缩到几次网络往返,整体耗时能降到几百毫秒内,体验完全不一样。
7. 部署与运维实践
7.1 本地环境搭建与前端启动步骤
先说一下本地跑通这套项目的完整步骤:
- 准备JDK8/11,配置
JAVA_HOME; - 安装MySQL8.0,创建数据库并导入项目里的sql脚本;
- 修改后端
application.yml,把数据库账号密码改成自己的; - 启动后端,确认8080端口正常返回;
- 进入
web前端目录(或者叫frontend之类的目录),执行npm install; - 执行
npm run dev,启动Vite开发服务器; - 浏览器访问前端地址,登录学生/教师账号,功能验证。
这里面最容易出问题的是数据库连接。如果MySQL配置了密码策略,或者用户默认授权方式不是mysql_native_password,连接串就会报认证失败。项目配套文档里通常会写清楚初始账号密码,实操时先按文档来,后面再自己改。
7.2 前后端部署到服务器的方案
开发完成后要部署,常见两种方式:
- 前后端动静分离部署:前端打包成静态文件(dist目录),丢到Nginx下面;后端打成一个可执行Jar包,用
nohup java -jar exam-system.jar跑起来。Nginx再配置一个反向代理,把/api/前缀的请求转发到后端的8080端口。
location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }用Nginx代理的原因是解决跨域问题。前端部署在一台机器的80端口,后端在8080,浏览器不认。用Nginx做同源反向代理,对外只暴露一个端口,请求路径不同就分发到不同服务,相当于在同一域名下完成了前后端通信。
- 一键容器化部署:项目提供了Dockerfile的话(我建议自己补一个),把后端Jar包、前端Nginx镜像、MySQL容器用docker-compose编排起来。考试系统的部署运维一般都在校园网或公司内网环境,Docker可以把整套环境打包成一致的交付物,特别适合"换一台机器部署"这种场景。
7.3 常见运维问题:时区、端口、内存
我在部署类似考试系统的过程中踩过几个坑,值得列出来:
- MySQL8.0时区默认是UTC,不指定时区的话,
start_time和submit_time会差8个小时。项目连接串里带了serverTimezone=Asia/Shanghai,若你自己部署新环境一定要一起带过去。 - 8080端口被占用的时候,先看是不是上次的老进程没杀干净,不要盲目改端口配置。
- 后端Jar包在低配服务器(1G内存)上启动,SpringBoot2本身加连接池很容易内存紧张。建议至少2G内存,并给JVM设置
-Xms256m -Xmx512m限制堆大小,防止把服务器拖垮。
8. 代码结构与二次开发建议
8.1 后端包结构与分层思想
这个项目的后端包结构是典型的Controller-Service-Mapper三层架构,我拆解一下每个包的职责边界:
com.exam.controller # 接口层,只做参数接收和响应封装 com.exam.service # 业务层,承载核心业务逻辑 com.exam.mapper # 数据访问层,基于MyBatis-Plus的BaseMapper com.exam.entity # 数据库实体类 com.exam.dto/vo # 入参DTO和响应VO com.exam.common # 统一返回结果、异常定义、工具类 com.exam.config # 配置类(拦截器、跨域、静态资源映射等)分层之间有一条硬性原则:Controller不直接访问Mapper,Service不返回Entity给前端,而是转VO。这套代码在Controller里会比较薄,好的业务逻辑全在Service层。这样方便单测,也方便后续做接口版本升级。我第一次看这类项目时,发现有人把SQL写在Controller里,当时就觉得这不是好习惯——业务一复杂,Controller立刻变成一团粥。
8.2 三种典型CI/CD与文档配套
再强调一次"含文档"这个卖点。项目配套文档通常包含:
- 环境搭建文档
- 数据库设计文档(含表结构说明)
- 接口文档(联调时用)
- 使用说明书(面向最终用户)
文档对于毕业设计和公司交接的意义完全不同:毕设答辩时,评委翻文档能快速了解项目全貌;公司里前后端联调,接口文档和数据库设计文档能省掉大量沟通时间。如果是团队协作开发,我建议在文档基础之上,给关键模块再补充时序图场景描述,比如"考试开始到交卷完整流程",这样新人不用自己一通乱翻代码。
8.3 我可以加哪些功能来增强这个系统
这套系统如果要落地到"学院级正式考试"级别,有几个方向值得自己动手加:
- 考试列表加"按课程筛选"和"按时间状态筛选",配合上"待开始、进行中、已结束"的状态标识。目前系统有基础的状态展示,但筛选条件还不够细。
- 评分规则支持"多次考试取最高分"或"取平均分",这在培训类场景里很常见,学生可以反复刷模拟卷,最终以最高分计入成绩。
- 试卷预览功能。让学生考试前能看到考试范围的说明(哪些章节、什么题型、题目数量),心理上踏实很多,也能减少考前咨询量。
- 导出成绩时支持按"班级"分组,每个班一个Sheet。虽然现在的成绩导出也有,但多班级时输出格式还可以更细。
这些功能都不难,新增量可控,但能明显提升系统的完整度和使用方满意度。
9. 常见问题排查速查表
我在边拆边跑的过程中,把遇到的高频问题整理成了速查表,直接照着排查能省很多时间:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 后端启动后马上退出 | 数据库连接失败或端口占用 | 看控制台异常堆栈第一行,确认是否为Communications link failure或Port already in use |
| 前端请求接口都是404 | Nginx代理路径配置错误 | 检查前端请求的基础URL和Nginx location路径是否匹配,尤其是带context-path的情况 |
| 登录接口返回500 | JWT密钥配置或用户表初始化数据缺失 | 检查application.yml里jwt.secret是否配置;确认数据库脚本已完整导入,初始账号存在 |
| 上传Excel导入题目失败 | Excel模板格式不匹配 | 对比代码中涉及的模板列头,注意合并单元格和日期格式不能带进去 |
| 考试交卷很慢 | 答题明细没有走批量插入 | 在日志中查看最终SQL,确认使用了批量insert而不是循环单条insert |
| 成绩统计对不上 | 总分未在主观题批改后重算 | 到batch judge的场景里去检查更新时机和缓存失效逻辑 |
| 前端打包部署后页面空白 | 静态文件资源路径问题 | Vite配置里base设置成相对路径或修改为部署子路径,重新构建 |
补充一个通用的排查思路:遇到"页面没反应"先看浏览器Network面板;后端报错先看控制台完整堆栈,不要只看最上面一行;数据库问题看SQL日志,SpringBoot2里通过配置mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl可以打印完整SQL,定位速度会快很多。
10. 二次开发时的注意事项
10.1 做题库批量导入一定要防脏数据
这个系统里的题库导入功能我建议再加强一层"预览校验":上传Excel后,先在页面展示解析后的前10条数据,让教师确认"这确实是我想导入的",再正式落库。否则一个格式不规范的Excel直接导入,几百道题插进去后才发现某些题的答案选项错位,清洗数据的成本比导入高得多。
10.2 自定义考试ID,别自增明示在URL里
目前考试信息如果用的是自增ID,学生在地址栏改一下ID就能看到其他考试的数据。虽然权限控制能挡住大部分越权行为,但"ID混淆"是个很好的安全隐患防线。建议给考试表增加一个业务流水号字段(比如按日期生成的考次编号),URL里传这个流水号,不暴露自增主键。
10.3 不要在业务高峰期改数据库表结构
这一点跟具体代码关系不大,但在考试系统真实运营中非常关键。期末考试周前一周,所有表结构变更都应该冻结,只做修补补丁和配置调整。系统要在这个周期里稳定跑完所有考试和成绩汇总,任何DDL操作都可能引起锁表或慢查询,不应该在这个时间窗口里去赌运气。
11. 个人体会与后续建议
这个项目我完整拆下来,最大的感受是:它不是一个"教学玩具",而是一个结构完整、能直接运行的业务系统。三层架构清晰、权限模型简单实用、事务和并发都做了合理处理,配套文档也补齐了。用SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套组合,在2025年来看依然是非常务实的选型,没有为了追新版本而给自己制造风险。
如果我要在这个基础上继续扩展,第一个动手方向是给"考试阅卷"增加一个移动端适配——很多教师习惯用平板或手机批改主观题,目前的桌面布局在小屏上体验一般。第二个方向是消息通知。考试创建后、开考前,给学生发个待办通知;成绩公布时发一条成绩通知,这些功能能用很低的成本大幅提升整个系统的"人性化"程度。第三个方向是数据分析图表的可视化,考试系统的成绩分布如果画成柱状图、折线图,会比纯表格直观很多——当然,这属于锦上添花,不影响核心考试流程的闭环。
我最想说的是,考试系统这种业务,真正的考验永远是"流程正确、数据不丢、关键时刻稳得住"。高并发是相对的,几十个学生同时交卷不算多,但一旦出现一次数据错乱,信任成本就难以挽回。所以在平时的开发中,多花时间打磨事务边界、幂等控制、异常恢复和幂等重提这些底层能力,远比堆功能更有意义。这套代码在这些方面已经起了个好头,值得耐心拆完再动手改。