新闻详情

新闻详情

首页 / 资讯中心 / 详情

基于SpringBoot+Vue的前后端分离在线考试系统设计与实现

发布时间:2026/10/2 10:28:40来源:尧图网络
基于SpringBoot+Vue的前后端分离在线考试系统设计与实现
前后端分离的在线考试系统我用SpringBoot加Vue撸了一个完整版本但凡做过在线考试系统的人都知道这玩意儿看着简单真正动手全是坑。我前后花了三周时间把一套完整的前后端分离在线考试系统从零到一搭了出来技术栈是SpringBoot加Vue加MyBatis加MySQL。整个项目跑起来之后管理员可以管理题库和试卷学生可以参加考试、查看成绩老师可以批改主观题还可以按班级统计成绩报表。这篇文章我会把整个系统的设计思路、核心代码逻辑、部署流程原原本本写出来也包括我踩过的坑和最后的优化方案。不管你是要做毕业设计、课程项目还是公司内部想搭一个培训考试平台这套东西都能直接拿来用或者二次开发。1. 在线考试系统的功能边界与模块设计先想清楚要做什么再做1.1 核心角色与用例拆解在线考试系统最容易犯的错误就是一上来就写代码写到一半发现功能互相打架。我在动手之前花了两天梳理用例图整个系统其实就三类角色管理员负责系统配置包括班级管理、学生账号管理、教师账号管理、题库维护、试卷组卷规则设定、考试场次安排、成绩查看与导出。教师可以维护自己的题目、创建试卷、安排考试、批改主观题、查看所教班级的成绩统计。学生查看待参加的考试、参加考试含客观题自动判分、主观题等待批改、查看已批改的成绩和答卷详情、修改个人密码。这个用例拆分看起来简单但直接影响后面的数据库设计和接口划分。一个最典型的例子是如果你一开始没想清楚试卷和考试是两个概念后面做组卷和考试场次管理时会非常痛苦。试卷是题目的容器考试是试卷、参加人员、时间窗口、状态约束的结合体。同一份试卷可以被多个班级在不同时间段使用这就需要在数据库设计时把exam_paper和exam_session分开。1.2 功能模块划分与前后面临的取舍系统我按六个模块来落地每一个模块都对应前后端各自的页面和接口模块后端核心逻辑前端核心页面用户认证登录、JWT签发与刷新、角色权限校验登录页、路由守卫、用户信息状态管理题库管理单选/多选/判断/主观题的CRUD、按科目分类题目列表、题目编辑弹窗、批量导入试卷管理手动选题组卷、随机组卷规则试卷列表、组卷向导、试卷预览考试管理考试场次CRUD、考生关联、试卷分配考试管理面板、考生列表在线考试考试时间校验、答题保存、交卷判分答题卡、倒计时、题目导航成绩管理客观题自动判分、主观题批改流、成绩统计成绩列表、试卷批改、成绩统计图表每个模块都有一些可以深度优化的小功能比如题库模块我做了Excel批量导入导出针对的是老师手动录题太慢的问题随机组卷我支持按知识点分布比例抽取题目而不是纯随机这样试卷的难度和覆盖面会均匀很多。后面讲后端的时候会详细展开这些实现逻辑。1.3 为什么选择前后端分离不只是流行这么简单很多教程默认你就要用前后端分离但很少有人讲清楚为什么要这么做。我做这个系统时最直接的感受有三点第一考试系统的在线考试环节前端需要相对复杂的局部状态管理。倒计时、答题卡状态、题目切换保存草稿、防止误交卷这些交互如果混在服务端模板渲染里写代码会被各种window对象和页面刷新逻辑搞成一锅粥。前后端分离之后前端只负责维护一个考试当前状态通过接口和服务器同步思路清晰很多。第二后端接口可以同时被Web端和未来可能的移动端复用。这个系统的接口设计成纯JSON风格的RESTful API以后如果要做小程序版本或者App版本不需要再动后端。第三部署维护时的独立扩展性更符合实际场景。Web前端是纯静态资源可以放到Nginx上后端是SpringBoot的Fat JAR独立运行。考试高峰期可以只给后端加实例前端完全不用动。这个点在第4部分部署环节还会再强调。2. 后端核心实现SpringBoot加MyBatis的每一处关键设计2.1 工程结构与Maven多模块划分后端工程我没有用单模块的简单结构而是拆成了三个Maven模块。理由很简单在线考试系统的权限逻辑、考试业务逻辑和基础设施代码混在一起后期改起来会非常痛苦。拆开之后各模块的职责边界非常清楚如果你们公司有自己的公共代码仓库沉淀出来的模块还能直接复用到下一个项目。online-exam-backend ├── exam-common // 公共模块统一返回体、异常、工具类、JWT工具 ├── exam-system // 系统管理模块用户、角色、班级、菜单、权限 └── exam-biz // 业务模块题库、试卷、考试、成绩、批改依赖关系上exam-system和exam-biz都依赖exam-commonexam-biz依赖exam-system中的用户和权限接口。可能有人会问为什么不把权限校验做成公共的这样biz模块不就不用依赖system模块了吗真实情况是业务模块需要反向拿到当前登录用户的ID和角色信息来做数据权限过滤比如教师只能看到自己的题目、只能批改自己创建的试卷对应的主观题。完全解耦在业务系统里是不现实的老老实实依赖反而简单。2.2 数据库表结构设计考试领域的关键决策表结构是这套系统的地基我设计了好几个版本才弄干净。核心表有这些sys_user用户表包含user_type字段区分管理员、教师、学生student_no用于学生学号department_id关联班级。sys_department班级/院系列表树形结构方便做不同层级的数据隔离。exam_subject科目表比如高等数学、大学英语。exam_question题目表question_type区分单选、多选、判断、主观题answer字段存客观题的标准答案主观题为空。exam_paper试卷表total_score、duration_minutes、status草稿、已发布。exam_paper_question试卷与题目的关联表额外存储每个题目在试卷中的分值因为同一个题目在不同试卷里分值可能不同。exam_session考试场次表关联试卷、班级含start_time、end_time、status。exam_user_answer学生答卷表逐题存储学生提交的答案。这里有一个数据库设计上的关键决策值得展开说学生选择题的答案存储方式。有些系统会把所有题目答案拼成一个JSON字符串放在一张答卷表里查询时再解析。我不建议这么做因为成绩统计时要按题目维度分析正确率比如老师想知道第三题有多少人选了B如果存JSON完整串写SQL时会非常痛苦。我拆成exam_user_answer表每道题一行用exam_session_id、student_id、question_id三个字段做联合唯一索引既能保证学生不会重复答题又能按题目聚合统计。2.3 统一响应体与全局异常处理每个接口都不需要try-catch前后端分离的项目中接口返回结构的统一非常影响前端的开发效率。我定义了Result类作为所有接口的统一返回体public class ResultT { private Integer code; // 200成功其他为失败 private String message; private T data; // 省略getter/setter和静态工厂方法 success()、error() }同时用Spring的RestControllerAdvice做了全局异常处理。业务异常我定义了BizException配合一个错误码枚举前端拿到code值就知道是参数问题、权限不足还是业务规则不让操作。这样做避免了一个非常常见的问题后端每个方法的try-catch逻辑重复代码满天飞或者异常直接抛出去前端收到一个极其难看的默认错误页。实际操作中这个统一返回体还顺手解决了一个前端联调效率问题开发时前端拿不到完整数据会显示空白页但控制台里什么有效信息都没有。统一返回体加上之后前端axios拦截器里直接统一处理code和message出错弹出一个明确提示开发效率提升了不止一倍。2.4 登录认证与JWT拦截器考试系统里的会话时效处理认证方案我用的是JWT加拦截器的经典做法。用户登录成功后后端签发一个token返回给前端前端每次请求在请求头中带上AuthorizationBearer token。拦截器从请求头解析JWT校验签名和有效期然后把用户信息放入ThreadLocal中供Controller层使用。这里有一个考试场景下的特殊考虑JWT默认是无状态的过期时间到了之后强制失效这意味着用户考试中途token过期会被踢下线这是绝对不能接受的。我做了两个层面的处理第一个放宽考试期间的token时效。学生参加在线考试时前端会持续向后端提交答题进度心跳这个心跳接口和后端的心跳刷新逻辑会不断续期token。从业务场景上说处于考试中状态的用户不应该被会话过期打断。第二个前端做了静默续期在axios响应拦截器里发现token即将过期时自动调用刷新接口拿新token并重放原请求。启动类上加EnableScheduling写一个定时任务每5分钟扫描exam_session表把已经超过end_time且状态为考试中的记录自动置为已结束然后对未交卷学生的空白答案做自动判分处理。这个兜底方案意义很大因为任何一个学生中途关闭了浏览器服务器端不能一直等他回来考试时间到了就必须按规则结束。我实现这个定时任务时还顺手发现做不等式的SQL查询时记得对end_time字段建索引否则数据量一大定时任务本身会成为数据库慢查询的来源。2.5 题库管理中的随机组卷与知识点覆盖率算法随机组卷是这个系统里算法含量最高的部分。需求是教师选择科目、题目类型分布、每种题型数量、总分系统从题库中随机抽题但必须保证抽出来的题知识点尽量分散不能出现十道题全部考同一个章节的情况。最朴素的实现是直接ORDER BY RAND() LIMIT N但这样抽出来的题可能集中在某几个知识点试卷区分度很差。我改进成了按知识点配额抽题前端组卷时教师为每个题型指定知识点及对应题数后端解析规则后按知识点分组随机抽取。核心代码逻辑如下我简化掉了分页和校验部分public ListQuestion randomPickQuestions(ListQuestionRule rules, Long subjectId) { ListQuestion result new ArrayList(); for (QuestionRule rule : rules) { // 按照科目、题型、知识点纬度去题库中筛选可选题目 ListQuestion pool questionMapper.selectBySubjectAndTypeAndKnowledge( subjectId, rule.getQuestionType(), rule.getKnowledgePoint()); // 题库里可抽数量不足时直接抛业务异常提示教师调整规则 if (pool.size() rule.getCount()) { throw new BizException(ErrorCode.QUESTION_POOL_NOT_ENOUGH, 题型【 rule.getQuestionType() 】可选题目数量不足); } // 对候选池做乱序再截取等价于公平随机抽题 Collections.shuffle(pool); result.addAll(pool.subList(0, rule.getCount())); } return result; }这个实现背后有一个容易被忽略的细节乱序即随机。Collections.shuffle的时间复杂度是O(n)对于上千条题量的题库来说性能没有问题。如果题库量到了几十万性能瓶颈在于每次组卷时全量查询候选池这时候就该给question表加联合索引(subject_id, question_type, knowledge_point)基本能做到毫秒级响应。2.6 主观题批改流程状态机驱动不看明白会漏改题成绩模块我设计了判分状态机客观题在学生交卷时立即由程序自动判分主观题则进入待批改状态教师批改完后更新为已批改当一张试卷的所有主观题批改完毕整场考试的成绩变为已发布学生端才能看到成绩和答卷详情。批改流程中的核心问题是教师很可能只批了一半就退出所以前端列表页上必须有目录式的按题号跳批功能。我的做法是后端提供getUnmarkedQuestions(examSessionId, teacherId)接口返回该教师负责的所有待批改题清单批改时逐题提交同时前端维护一个当前批改进度状态。这里有一个小的权限校验细节教师只能批改自己创建的试卷对应的考试不能因为同一个班级是别的老师建的就被允许跨权批改。2.7 MyBatis的那些坑与我对缓存的处理策略项目里MyBatis用的XML Mapper方式复杂SQL集中在XML里写简单的单表操作用注解。我对MyBatis的使用有几个切身的体会第一结果映射一定要显式写resultMap。数据库字段是下划线风格比如question_typeJava属性是驼峰风格questionType有人会全局开启map-underscore-to-camel-case配置但一旦某张表的字段和关联查询字段重名自动映射会产生非常隐蔽的Bug。我在所有的多表关联查询上都显式写了resultMap虽然代码量多一点但排错成本大大下降。第二动态SQL的复用要用SQL标签抽公共片段。成绩统计的SQL有大量重复的筛选条件比如按科目、按班级、按考试时间范围。我在XML里用sql idscoreStatFilter把公共where片段抽出来每个查询里include refidscoreStatFilter/改一处全表生效。第三关于MyBatis缓存我需要多说一句。MyBatis一级缓存是SqlSession级别的默认开启在Spring集成环境下每次Mapper操作如果是独立SqlSession一级缓存几乎没什么用二级缓存是Mapper级别的默认关闭。在线考试系统中试卷、题目这些数据被高频读取但变更频率较低我用了Spring的Cacheable注解做了一层服务层缓存Redis存储key设计为exam:question:{questionId}题目被修改时主动cacheManager.evict掉对应缓存。这样比直接用MyBatis二级缓存更可控因为缓存的控制权掌握在业务代码里不会出现修改数据后缓存不刷新的尴尬。3. 前端Vue实现页面结构、路由守卫与答题卡交互3.1 Vue工程搭建与目录结构前端我用的是Vue 2.6加Element UI构建工具Vue CLI 4。这里说明一下为什么没上Vue 3加Vite最主要的原因是Element UI生态和考试系统常见的表格、表单、树形控件方案在Vue 2下最成熟网上资料最多遇到问题搜索成本低。如果你们是新项目且团队对Vue 3已经熟悉完全可以替换后端接口都是标准RESTful API对接成本几乎没有。目录结构是一个符合业务模块划分的清晰组织online-exam-web ├── public ├── src │ ├── api // 各模块的接口请求封装 │ │ ├── auth.js │ │ ├── question.js │ │ ├── paper.js │ │ └── exam.js │ ├── router // 前端路由表与路由守卫 │ ├── store // Vuex状态管理用户信息、答题卡状态 │ ├── views │ │ ├── login │ │ ├── admin // 班级管理、账号管理、科目管理 │ │ ├── teacher │ │ │ ├── questionManage │ │ │ ├── paperManage │ │ │ ├── examManage │ │ │ └── scoreCheck │ │ └── student │ │ ├── examList │ │ ├── takingExam │ │ └── scoreHistory │ ├── utils │ │ ├── auth.js // token存取与解析 │ │ ├── request.js // axios实例与拦截器 │ └── App.vue3.2 axios请求封装一个request.js解决百分之八十的联调难题axios封装在每个项目里都有但很多人只是简单包一层就完事。我的request.js里面做四件事情基础URL配置、token自动注入、响应状态统一处理、token过期静默续期。import axios from axios import { Message } from element-ui import { getToken, setToken, refreshToken } from /utils/auth const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) // 请求拦截器自动携带token service.interceptors.request.use(config { const token getToken() if (token) { config.headers[Authorization] Bearer token } return config }, error Promise.reject(error)) // 响应拦截器统一错误提示与token续期 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { // token失效 return refreshToken().then(() { // 用新token重发原请求这个配置里直接重新发起 config.headers[Authorization] Bearer getToken() return service(config) }) } Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message || 请求失败)) }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) } )这个封装的使用体验是业务代码里完全不用关心token和错误弹窗只管拿到数据后做渲染。很多新手项目联调时前端页面上堆满了重复的错误提示逻辑就是因为没有在这个拦截器层面做好统一处理。3.3 路由守卫前后端分离项目的权限控制第一道门前端的路由表我分成两种静态路由和动态路由。静态路由只有登录页、404页等公开页面。动态路由是根据用户角色从后端返回的菜单权限动态注册的路由操作的核心代码如下// 在Vuex里根据后端返回的权限菜单动态添加路由 router.beforeEach(async (to, from, next) { const token getToken() if (!token) { if (to.path /login) { next() } else { next(/login?redirect${to.path}) } return } if (to.path /login) { next(/) return } // 用户信息未加载时先拉取用户信息和权限菜单 if (!store.getters.userLoaded) { try { const userInfo await store.dispatch(user/getInfo) const accessedRoutes await store.dispatch(permission/generateRoutes, userInfo.roles) accessedRoutes.forEach(route router.addRoute(route)) next({ ...to, replace: true }) // 关键重新导航一次让新路由生效 } catch (e) { store.dispatch(user/resetToken) next(/login?redirect${to.path}) } } else { next() } })这里有一个非常隐蔽的坑动态添加路由后必须要next({ ...to, replace: true })重新导航一次否则第一次访问动态路由页面会白屏因为此时路由表里根本没有对应路由。我一开始没加这行排查了很久才发现是动态路由注册时机的问题。前端路由守卫只是体验层面的第一道门真正的安全边界在后端拦截器和权限校验上。这一点我在对接接口时感受特别深哪怕前端把路由和按钮都隐藏了如果后端接口没有做权限校验用户可以拿着接口地址直接绕过UI拿到数据。所以我在后端的拦截器里对每个接口都做了角色权限校验前端隐藏只是提供更好的用户体验绝不是安全手段。3.4 在线考试页面倒计时、题目切换、答题卡状态同步在线考试页面takingExam是整个系统交互最复杂的页面状态管理我用Vuex单独维护了一个考试模块。这个页面需要同时管理四类状态题目列表、每道题的已选答案、当前正在查看的题号、剩余时间。我把这四类状态塞进了Vuex然后通过getters导出给答题卡组件和题目展示组件。答题卡的状态逻辑值得单列出来说。每道题有四种视觉状态未作答灰色、已作答蓝色、当前正在查看红色边框、标记待复查黄色问号图标。这个设计参考了专业考试软件的做法对学生的考试体验提升非常明显。实现上答题卡组件根据Vuex里的answerMap和markMap数据自动计算方法获知每道题的状态题号格子用不同的CSS类名渲染。时间处理这里我采用的策略是倒计时以服务器返回的考试结束时间为准前端用setInterval每秒计算一次剩余时间。为什么不用前端本地时间直接算因为本地时间可能被篡改而且不同客户端时钟不一致会导致答题时间不公平。后端每次拉取题目时都在响应里带上本次考试的endTime前端拿这个时间戳做倒计时时间归零时自动调用交卷接口。同时后端接口也会校验服务器当前时间是否超过考试结束时间超过就不允许提交答案。这个双保险机制在真实考场里非常关键它保证即使某个学生故意调慢电脑时钟后端收到交卷请求时照样会拒绝并强制交卷。交卷环节我还做了一个友好性设计点击交卷按钮时如果有未作答题目会弹窗提示还有N题未作答确定要交卷吗如果已答完直接二次确认后调交卷接口。这个细节看起来微不足道但真实考场里因为误点交卷导致成绩作废的情况太多了。3.5 教师组卷向导多步骤表单的状态管理与数据校验教师组卷页我设计成了向导式多步骤表单第一步选择试卷基本信息名称、科目、总分、时限第二步选择试题类型和数量分布第三步配置按知识点抽题规则第四步预览试卷并保存。多步骤表单的状态管理我放在了组件内的data里用一个currentStep变量控制步骤切换每步的校验通过后才能进入下一步。多步骤表单最容易出现的问题是跨步骤数据丢失比如用户从第四步返回第二步修改数据后再去第四步预览列表必须是修改后的数据。我的解决办法是所有步骤共享同一个paperForm对象而不是每步各自维护局部状态这样天然就避免了数据不同步的问题。第二步和第三步的交互还有一个隐含需求无论教师怎么修改题型数量分布试卷总分必须始终等于配置的总分。我写了一个watch方法题型数量变化时自动重算总分并实时显示在界面上不满足配置时阻止进入下一步。这一类实时联动反馈的交互细节对整个系统的易用性提升非常明显。4. 系统部署实操从本地开发到服务器上线的完整链路4.1 环境准备与版本选择部署之前先列一下我的环境版本方便大家直接参考避免版本不匹配的问题组件版本说明JDK1.8稳定且生态最全Spring Boot 2.x完美兼容Maven3.6.3后端构建工具Node.js14.x前端构建环境对应npm 6.xMySQL5.7生产环境我建议8.0本地5.7也够用Nginx1.20部署前端静态资源和反向代理Spring Boot2.5.x稳定版本官方维护周期最长有一个新手容易踩的坑是JDK版本Spring Boot 2.x系列用JDK 8完全没问题但如果你们公司强制JDK 17就必须把Spring Boot升级到2.7以上甚至3.x而且依赖的一些第三方库可能也要跟着升。能用新版本当然好但对于一个以稳定为核心的考试系统我不建议在生产环境用刚发布的最新版本。4.2 前端构建与Nginx反向代理配置前端部署的本质是把Vue项目构建成纯静态文件放到Nginx的某个目录下然后将/api开头的请求反向代理到后端服务的地址。这个模式是整个前后端分离部署的核心思路你必须理解透。构建命令npm install npm run build构建完成后dist目录里就是全部静态资源。我把dist目录里的文件上传到服务器的/usr/share/nginx/exam-web目录下然后Nginx配置如下server { listen 80; server_name exam.example.com; # 前端静态资源 location / { root /usr/share/nginx/exam-web; index index.html; try_files $uri $uri/ /index.html; # 关键前端路由history模式需要 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }这里有个细节必须特别说try_files $uri $uri/ /index.html;这一行的作用是应对Vue Router的history模式。如果前端用history路由地址栏里没有#号用户访问/exam/1这样的地址时服务器上并没有这个物理文件如果不配置这行刷新页面就会404。当然你也可以用hash模式绕开这个问题但URL里带#不太好看我用history加try_files的组合页面刷新问题直接解决。proxy_pass这里还有一个容易搞错的细节http://127.0.0.1:8080/api/末尾的斜杠和location里的/api/组合起来后实际转发的URL会去掉/api前缀。如果你不想改后端接口的路径映射逻辑最好在Spring Boot配置里把context-path设为/api这样后端所有接口天然带/api前缀Nginx转发时保持路径不变。4.3 前后端分离项目在Tomcat下的部署方式对比很多人问前后端分离项目是不是一定要用Nginx能不能直接丢进Tomcat里跑。说实话可以而且有一个场景下特别合适如果你想一个Java进程里同时跑前端静态资源和后端接口部署管理和资源开销最小。具体的做法是把Vue构建产物放进Spring Boot的src/main/resources/static目录下打包时一起打进Fat JAR里。但我还是推荐Nginx方式核心原因在于静态资源处理效率。Nginx处理静态文件的速度远高于Java应用服务器而且考试高峰期很多请求是静态资源请求JS、CSS、图片不应该让这些请求挤占后端线程池。另外如果未来要做负载均衡Nginx天然支持配置多个后端节点而单Tomcat部署的方式扩展性就差很多。不过有一种情况你可以使用单Tomcat部署内网小规模使用比如一个公司内部几百人做培训考试后端一台服务器完全够了多部署一层Nginx反而增加运维成本。4.4 后端JAR包运行与守护进程方案后端部署我采用的是标准的Spring Boot Fat JAR方式打包命令mvn clean package -Dmaven.test.skiptrue构建产物在exam-biz模块的target目录下拿到服务器上执行java -jar exam-biz.jar --spring.profiles.activeprod生产配置我通过application-prod.yml单独管理里面配置了生产数据库连接、Redis地址、日志级别等。使用spring.profiles.activeprod参数激活这样开发环境和生产环境的配置完全隔离不会出现把本地数据库密码带到生产环境去的问题。后台运行我用的是Supervisor守护进程方案配置了两个核心的进程管理项[program:exam-backend] directory/opt/exam commandjava -jar /opt/exam/exam-biz.jar --spring.profiles.activeprod autostarttrue autorestarttrue startsecs10 stopasgrouptrue userroot用Supervisor有几个好处机器重启后后端服务自动拉起进程意外崩溃后自动重启配合日志输出可以直接用journalctl和supervisorctl查看服务状态。如果你不想引入Supervisor用nohup加重定向日志也可以nohup java -jar exam-biz.jar --spring.profiles.activeprod /opt/exam/log/backend.log 21 但这个方式进程挂了不会自动重启人工介入成本高我建议还是上Supervisor配置也很简单。4.5 初始化数据与数据库迁移数据库初始化我准备了两套脚本一套是纯DDL建表语句另一套是DML基础数据管理员账号、科目类型、初始班级等。MySQL执行DDL时有一个顺序问题需要注意必须先建主表和父表再建子表和关联表否则外键会失败。我因为题目表和试卷题目关联表的创建顺序反了踩过一次外键坑现在所有初始化脚本都严格按照依赖顺序排列。数据库升级这个问题容易被低估系统上线之后如果改了数据库结构不能每次都手动执行一遍建表语句。我引入了Flyway做数据库版本管理每个版本的变更脚本以V1__init.sql、V2__add_score_cache.sql这种形式放在db/migration目录下Spring Boot启动时自动检测并执行未执行过的脚本。有了这个机制团队协作时谁改了数据库结构其他人pull代码重启服务就自动同步非常省心。5. 前后端联调与接口规范我踩过的对接坑和解决思路5.1 接口文档先行防止前后端各写各的前后端分离项目里最大的沟通成本在于接口约定不一致。我在项目启动时先维护了一份接口文档包含每个接口的URL、请求方法、请求参数、返回结构、权限要求。这里我不推荐再单独在Swagger页面上找接口而是把约定做到代码层面后端的返回统一是Result结构前端的request.js统一处理双端各写各的只要接口路径和字段名没写错联调基本一次过。具体到字段命名的约定我建议后端接口层直接返回驼峰命名的JSON字段因为前端JavaScript本身就惯用驼峰。有些后端工程师习惯Java里用下划线字段接口返回的也是下划线风格的JSON前端接数据时各种映射和转换非常容易出错。这在MyBatis的resultMap里配置一下别名就能解决不要图省事把一个命名风格危机传到前端去。5.2 一个典型的跨域问题从页面白屏到定位根因前后端分离开发时前端跑在8080端口后端跑在9090端口浏览器请求后端接口时会发生产生跨域问题。我开发阶段的解决方案是前端vue.config.js里配置devServer代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }这样开发环境下前端请求/api/xxx会由Node代理转发到后端服务浏览器看到的接口请求是同源的不触发跨域。生产环境则用Nginx反向代理解决了同样的问题。这里我特别想说的是真正上线时必须关掉后端的CORS全局配置。我在开发阶段为了省事在后端加了一个CrossOrigin全局配置哪个前端都能调。但上线时如果忘了关允许任意来源的跨域请求会带来严重安全问题尤其是管理员接口可以被恶意站点盗用。联调完毕部署前我把这个全局配置直接删掉了生产环境统一走Nginx同源代理完全不需要后端开CORS。5.3 时间格式与时区问题为什么数据库时间总是差8小时前后端联调时几乎必踩的一个坑是时间字段后端返回给前端的时间差8小时或者前端传时间给后端后数据库里存的时间又不对。这个问题的根源在于JSON序列化和数据库连接时区不一致。我的处理方案分三层确保统一第一Spring Boot的application.yml里设置spring.jackson.time-zoneGMT8确保序列化时输出北京时间第二MySQL连接串里显式指定serverTimezoneAsia/Shanghai第三数据库连接池和日期类型统一用DATETIME存储Java端用LocalDateTime接收。三层都对齐之后时间字段在前后端和数据库之间就完全透明了。有段时间我发现Redis缓存里的时间戳又变成了UTC时间排查了半天才发现是Jackson反序列化Redis时没有用全局配置的ObjectMapper。这个问题的深坑在于Spring Boot针对不同序列化场景会创建不同的ObjectMapper实例全局配置只对MVC生效对Redis序列化器不生效。后来我在RedisConfig里单独注入了一个带时区配置的Jackson2JsonRedisSerializer问题才彻底解决。5.4 分页查询的参数封装与统一返回列表接口设计我统一封装了分页参数PageQuery和分页返回结构PageResult。前端传pageNum和pageSize后端返回total、list、pageNum、pages四个字段前端表格组件直接绑定。封装分页返回结构有一个实用细节当有人把当前页传到超出总页数之后时比如删除了一些数据当前页变成4但只有3页此时如果后端直接返回空列表前端会出现白页用户以为系统出Bug了。我的分页处理逻辑里做了请求页码超过总页数时自动归位到最后一页这个很小的边界处理让整个系统的体验细节感强了不少。类似的边界处理还有搜索关键词包含SQL特殊字符时的安全处理、删除数据时有子约束时的友好错误提示等。这些功能在需求文档里基本不会写但真实使用中用户一定会触发你不提前处理好就会被反复截图投诉。6. 性能优化与安全加固上线前必须做的几件事6.1 数据库索引设计哪些字段需要建索引考试系统的数据量级不会特别夸张一个万人的学校考试记录可能也就几百万行。但就算这个量级不做索引的表也能被几个简单的统计查询拖垮。我给核心表做了如下索引设计表名索引字段索引目的sys_userdepartment_id、user_type按班级查学生、按角色查用户exam_questionsubject_id、question_type、knowledge_point组卷抽题时的筛选exam_sessionstatus、start_time考试状态列表、定时任务扫描exam_user_answerexam_session_id、student_id、question_id答卷查询与唯一索引exam_scoreexam_session_id、student_id成绩查询最频繁条件联合索引有一个顺序讲究等值条件放前面范围条件放后面。比如(exam_session_id, student_id, question_id)这个联合索引最左匹配原则下单独按student_id查历史成绩时这个索引是走不了的我还额外建了一个(student_id, exam_session_id)索引。这些都是根据真实查询场景反推出来的不是一把梭把全字段都加上去。6.2 MySQL连接池与数据库配置调优Spring Boot默认用的是HikariCP连接池本身性能就很好但默认配置不一定适合考试场景。我针对考前瞬间大量学生集中登录、考试结束瞬间大量交卷请求这个高并发场景调整了连接池参数spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 5000 idle-timeout: 600000 max-lifetime: 1800000 pool-name: ExamHikariPool关于连接池大小这里有个反直觉的知识点不是越大越好。数据库连接池的每个连接都对应数据库端的一个线程和内存开销连接数过多反而会导致数据库上下文切换频繁、整体吞吐量下降。对于单台MySQL实例经验值是(CPU核心数 * 2 磁盘数)这里考虑到考试系统的IO型操作居多我给50个连接时压测结果是性能最好的。另外MySQL层面我开了慢查询日志阈值设置1秒上线后通过慢查询日志找出所有执行时间超过1秒的SQL逐个分析索引和查询方式这比凭感觉优化靠谱得多。6.3 接口防刷与考试防作弊的基础手段考试系统的接口安全有两个层面普通接口的防刷考试接口的防作弊。防刷我用了简单的限流策略基于Nginx的limit_req模块限制单个IP的请求速率超过限额直接返回429。这个操作在Nginx配置里加几行就能实现不需要引入网关组件。考试防作弊我做了一个中间件思路的方案。核心思路是考试中的关键接口获取题目、提交答案、交卷都必须校验三样东西token有效、当前时间在考试窗口内、学生确实被关联到该考试场次。三者缺一不可任何一项不满足都拒绝请求。我曾经遇到过的情况是学生直接把获取答案的接口地址复制出来反复调用想暴力拿到所有题目和标准答案。但因为获取题目接口在考试开始前就返回401拒绝这个漏洞直接被堵住了。前端也做了配合的防作弊答题页面一旦检测到用户切换浏览器标签页超过一定次数或者退出全屏就会在页面上记录警告。这个方案没法杜绝作弊但对于防止随手查答案的普通作弊手段还是有威慑力的。正经的在线考试防作弊需要更复杂的监控机制如摄像头抓拍、切屏记录上报等那个已经超出这个系统的边界了属于另一个专题。6.4 上线后的压测一次模拟2000人同时交卷的实战记录系统上线前我做了一轮性能压测模拟2000个学生同时交卷的场景。方案是用JMeter开2000个并发线程先并行登录获取token再并行调用交卷接口观察后端服务的吞吐量、响应时间、错误率。压测结果让我发现了两个问题第一交卷接口里有一处对数据库的循环查询每个学生交卷时都会循环查每一道题的正确答案2000个学生并发时数据库连接瞬间被打满第二接口的响应时间中位数虽然不高但P99响应时间飙到了5秒以上说明有部分请求需要排队等数据库连接。针对第一个问题我把查询一张试卷所有题的正确答案这个操作加了缓存试卷数据几乎不变化缓存命中率接近100%。针对第二个问题我把循环查询改成了批量查询——一次WHERE question_id IN (...)查出所有正确答案本地再做映射。修改后重新压测2000并发交卷时P99响应时间降到了800毫秒以内MySQL的活跃连接数也稳定在正常范围。压测这一步绝对不能省尤其是考试系统这种有明确高并发场景的项目上线后才发现性能问题代价太大了。7. 项目扩展方向与我的总结心得7.1 保留的扩展点这些地方可以接着加功能这套系统我刻意保留了一些扩展点方便大家继续往上加功能。如果你拿到源码想二次开发这几个方向是最自然的Redis强化目前Redis只在缓存和token续期里用了可以进一步做考试大名单缓存、排行榜缓存、分布式锁防止同一个学生重复交卷。消息通知考试创建后通过邮件、短信通知学生成绩发布后通知学生查看。接入消息队列或者简单的事件机制就能实现。题库批量导入目前题库支持Excel模板导入可以进一步支持Word文档格式的智能识别直接解析Word里的题目格式入库。数据分析成绩统计目前是基础的分数分布和正确率统计可以往知识图谱方向做分析每个学生薄弱知识点给学生个性化的复习建议。多端适配后端接口全部RESTful风格前端做移动端适配时只要重新写一套移动端UI接口可以直接复用。7.2 代码质量与团队协作方面的个人体会开发这个系统的过程中我最大的体会是前后端分离项目的代码质量很大程度上取决于接口约定的清晰度和团队协作的流程规范。这个系统虽然是个人开发但我一开始就按照多人协作的标准来组织接口文档先行、后端统一返回结构、前端统一请求封装、数据库脚本用Flyway管理版本。这样一来即使后续接手项目的人换了只要能看懂这套规范上手成本就特别低。有同事问我为什么不在每个Controller里写一堆注释来标明接口用途我的回答是注释只能解释代码在干什么不能解释为什么这样设计。我更喜欢用统一的命名规范、统一的返回结构、统一的分页格式来传递意图看代码的人扫一眼就知道这个接口的参数是什么、错误是怎么返回的。这种整体一致性比几百行注释有用得多。7.3 给正在做类似项目的人几条实在建议如果这个项目对你的毕业设计或者公司内部系统有参考价值最后这几条建议是我切身体会建议直接收藏第一先把数据库表结构设计完整再写任何业务代码。考试系统的表结构涉及到试卷、题目、考试场次、答卷状态这些环环相扣的概念设计时漏一个字段后面编码时就要用一个奇怪的方式来补最后变成一个技术债。我中途因为缺少场次中的学生状态字段把学生中途退出考场再重新进入的恢复逻辑改了两版才满意。第二前端在线考试页面一定多做异常场景测试。我专门做了这样一轮测试交卷时断网会怎样倒计时归零时提交请求超时会怎样考试期间刷新页面后为什么要弹窗确认继续答题而不是直接清空答题卡。如果你不做这些异常测试真实使用中每个异常都会被学生遇到并投诉。第三部署前把日志做完整。我在后端添加了全局的请求日志切面AOP记录每个接口的名称、请求参数、响应耗时、操作人。这个日志在联调和排障时太重要了。最典型的场景就是学生说我交卷了但没成绩如果没有请求日志你根本无从考证他是否真的提交成功。第四数据库密码、Redis密码、密钥这类敏感信息在配置里绝对不要硬编码。我用Jasypt对配置里的密码做了加密加密后的密文可以安全地提交到Git仓库里启动时用环境变量传入解密密钥。这个经验就是吃了亏才记住的早期我把生产数据库密码明文写在配置文件里虽然项目是个人项目但后来被扫描工具报出来时才后怕。7.4 写在最后的复盘从需求分析到上线部署整个系统的核心代码大概覆盖了题库管理、组卷引擎、在线考试、自动判分、成绩统计这些完整的考试生命周期环节。回顾整个开发过程最大的成就感不是代码跑通了而是通过反复的异常场景设计、性能压测、安全加固让系统从能用变成了好用。在线考试系统这种东西功能清单拉一拉谁都能列出来但真正考验人的是对细节的把握和对边界情况的理解比如防止学生超时交卷、防止并发重复提交、防止前端直连接口绕过权限、防止数据库在考试高峰被打垮。把这几个关键点守住系统就稳了一大半。如果你还没有完整搭建过这种前后端分离项目我的建议是照着这套架构先完整走通一遍流程过程中把自己当作用户去刁难系统遇到问题不回避排查完再继续。走通一次之后你对SpringBoot、Vue、MyBatis这套技术栈的理解会得到一个质的提升。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

强化学习稀疏奖励实战:HER事后经验回放原理与代码全解 2026/10/2 14:56:05

强化学习稀疏奖励实战:HER事后经验回放原理与代码全解

“hindsight”这个词在中文语境里就是“后见之明”的意思,但在强化学习圈子里一提到它,大家脑子里冒出来的基本都是同一篇论文:2017年OpenAI那篇《Hindsight Experience Replay》。这几年我做机器人抓取和稀疏奖励任务,HER几乎是绕…

阅读更多 →
从Copilot到Claude Code:AI编程助手与工作OS的实战指南 2026/10/2 14:56:05

从Copilot到Claude Code:AI编程助手与工作OS的实战指南

早上刷完一堆AI圈的动态,真正让我停下来琢磨的就两条:一条是微软把Copilot重新定位成“工作新OS”,另一条是Claude在物理难题上刷新了世界纪录。一个偏产品、一个偏科研,但凑在一起看很有意思——AI正在从“帮你写代码的助手”往“…

阅读更多 →
SpringBoot高校社团纳新数字化平台:需求、设计到落地全解析 2026/10/2 14:56:05

SpringBoot高校社团纳新数字化平台:需求、设计到落地全解析

又到了一年一度毕设选题的时候。每年这个阶段,后台问得最多的就是“SpringBoot能做什么题目”“有没有不烂大街、又有实际意义的系统”。如果你正在为计算机毕业设计选题发愁,或者已经定了方向但不知道从哪下手,那我今天拆解的这套《基于Spri…

阅读更多 →
基于Matlab的无人机通信加密仿真:AES、SM4与RSA性能对比 2026/10/2 14:56:04

基于Matlab的无人机通信加密仿真:AES、SM4与RSA性能对比

无人机控制链路被劫持这件事,这两年已经不是电影桥段了。我在行业展会上亲眼看过有人用一套便携SDR设备,把飞手正在操作的无人机直接接管——姿态数据在屏幕上跳了两下,飞机就跟着别人的指令跑了。要在真实设备上去验证加密方案,成…

阅读更多 →
MoE架构全解析:从路由计算到显存规划 2026/10/2 14:56:04

MoE架构全解析:从路由计算到显存规划

MoE这几年几乎成了大模型规模的代名词。做AI Infra的朋友应该都有体会,从GShard到Switch Transformer再到Mixture of Experts遍地开花,MoE架构凭借"参数多但算得少"的特性,让千亿万亿参数量不再是天文数字。但这玩意儿跟Dense模型完…

阅读更多 →
轻量级数据库管理工具dbx:覆盖MySQL、PostgreSQL与SQLite的高效操作指南 2026/10/2 14:55:57

轻量级数据库管理工具dbx:覆盖MySQL、PostgreSQL与SQLite的高效操作指南

提到 dbx 这个名字,很多人第一反应是某个云盘命令行工具,但我今天要聊的 dbx,是一个相当丝滑的数据库管理工具。如果你正在找 dbx 数据库工具,希望不装全家桶就能搞定 MySQL、PostgreSQL、SQLite,同时又在找一个“下载…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉