ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的软件工程课程在线考试系统设计与实现

基于SpringBoot+Vue的软件工程课程在线考试系统设计与实现 1. 为什么还要自己动手做一套在线考试系统说实话当我决定用 SpringBoot Vue 自己做一套软件工程课程在线考试系统的时候身边不少朋友的第一反应是市面上现成的考试系统那么多开源的有、商业的有随便找一个改改不就行了何必从零造轮子这个问题我认真想过。在线考试系统这个领域成熟产品确实一抓一大把但如果你把它放到软件工程课程这个具体场景里情况就完全不一样了。软件工程这门课有个很特殊的地方它不像高数或者英语那样考试就是纯客观题刷分。软件工程考察的是学生对软件开发流程的理解对需求分析、系统设计、项目管理这些软技能的掌握。这就意味着考试题型里必须有相当比例的主观题、案例分析题甚至需要让学生针对某个业务场景画数据流图、写用例描述。而市面上大多数通用考试系统要么只支持单选多选判断这种客观题要么主观题只能简单地让学生填个文本框完全没有办法承载软件工程课程这种半主观半客观的考核需求。再往深一层想我那时候正好在带软件工程课程设计期末考试加平时测验的节奏很密。每次测验都要准备试卷、组织考试、批改试卷、统计成绩一套流程下来少说两三天。如果有个系统能让我把整个流程线上化——题库管理、自动组卷、在线答题、客观题自动判分、主观题人工评分、成绩自动归档——那节省下来的时间是非常可观的。而且还有一层更实在的考虑软件工程这门课本身就在讲软件生命周期、讲需求分析、讲架构设计。我做一个考试系统本身就等于是在实践一遍软件工程的完整流程。从需求调研、用例建模、数据库设计到前后端分离架构、接口文档、测试用例所有课本上的知识点都能在这个项目里落地。对于学生来说这是一个很好的课程设计选题对于老师来说这是一套能真正用起来的教学工具对于想找工作的应届生来说这更是一个能写进简历、面试时能讲清楚的技术项目。我这篇文章就围绕用 SpringBoot Vue 实现一套软件工程课程在线考试系统这件事把从需求分析到最终部署的完整过程拆开来讲。骨架包括哪些功能是软件工程课程场景下必须有的、技术选型为什么是 SpringBoot Vue、数据库和接口怎么设计、重点难点模块怎么实现、以及我在实际开发和测试中踩过的坑。文章会比较长但保证每一步都有实际操作价值不是那种看完还是不知道从哪下手的空泛教程。适合看这篇文章的人我大概归纳为三类一是软件工程专业的本科生想找一个既能覆盖课程知识点、又能真正落地实现的课程设计题目二是正在带软件工程类课程的老师或助教想找个能支撑平时测验和期末考试的轻量级自建方案三是准备面试的后端或全栈开发新人需要一个完整的、有业务深度的项目来展示自己的系统设计能力。如果你属于其中任何一类我建议你把这篇文章完整看完尤其是后面关于权限设计、组卷逻辑、并发控制和考试防作弊的部分这些是这套系统的灵魂所在。2. 软件工程课程考试的真实痛点这决定了我做什么功能2.1 通用考试系统解决不了的教学场景在动工之前我先花了两周时间做需求调研。这个阶段非常关键因为决定了系统要实现哪些功能将来就不会做偏。我带的是软件工程专业大二大三的课程学生的基础参差不齐。平时测验和期末考试大概有这些实际诉求第一题型要混合。一份试卷里既有单选题、多选题、判断题这类客观题也有简答题、案例分析题、应用题这类主观题。案例题往往一段文字描述几十行可能是某个电商系统的需求文档也可能是某个图书馆管理系统的业务描述学生要基于这个案例回答问题。通用考试系统对这种长文本阅读 多小题作答的场景支持很弱。第二题目要有分类和难度梯度。软件工程课程的知识点分散在需求工程、软件设计、编码规范、软件测试、项目管理、配置管理这些章节。一份质量好的试卷应该能按照章节比例和难度系数自动组卷。比如第一章需求工程出2道单选、1道多选、1道简答难度系数控制在0.3到0.7之间这样试卷才能有效区分学生的学习水平。第三考试要有防作弊机制。学生线上考试最怕的就是互相传答案。通用考试系统虽然也有防切屏但软件工程这种主观题占一半的考试真正的作弊风险不只是切屏搜答案还有可能是几个人开黑共用一套答案。这就需要系统在题目顺序、选项顺序上下点功夫至少让不同学生拿到的试卷不完全一样。第四成绩分析要能反哺教学。考完试之后作为老师我最想知道的是哪个知识点学生普遍掌握得差哪道题的错误率最高某个分数段有多少人通用考试系统的成绩统计往往只是简单地给出平均分、及格率根本没有按题目维度、按知识点维度、按班级维度的多层统计分析能力。第五整个流程要能闭环。从老师录入题目、建立题库到组织一场考试、派发试卷再到学生在线作答、交卷最后老师批改主观题、系统统计成绩并导出报表——这套流程应该是一个完整的闭环中间不能断链子。很多在线考试系统只能覆盖其中一段比如只支持客观题自动判分主观题判分完全靠线下Excel搞得流程支离破碎。2.2 我最终确定的功能全景图经过和几位同事反复讨论也问过几届学生的使用感受我把系统功能锁定在这样一张图上用户模块支持学生、教师、管理员三种角色的登录注册与信息管理。管理员管理教师和学生账号教师管理自己的课程和学生学生只能看到和自己相关的考试。题库模块教师可以维护自己的题库题目按章节分类支持单选、多选、判断、填空、简答、案例分析多种题型。每道题目包含题干、选项、正确答案、难度系数、知识点标签、题目解析。试卷模块教师可以手动组卷也可以设置组卷规则由系统自动组卷。自动组卷的核心是实现章节维度题型维度难度维度的三重约束匹配。考试模块教师创建考试设置考试时间、考试时长、试卷、参加班级。学生进入考试页面后系统进入防作弊模式答题过程有剩余时间提醒交卷后客观题立即出分。批改模块主观题和客观题分离批改。客观题系统自动判分主观题教师在线评分支持按得分点给分也可以添加评语。成绩模块成绩汇总展示支持按考试、按班级、按学生三个维度查询。有成绩分布直方图、各题目正确率统计、知识点掌握度分析。公告模块教师可以发布考试通知、课程公告学生在首页能看到最新通知。这里我得特别说明一下需求分析阶段最容易犯的错误就是贪大求全。我一开始也想把在线监考、AI判卷、成绩自动分析报告这些都塞进去但冷静下来之后我把功能分成了必须做、应该做和可以有三层。这套系统的MVP最简可用产品只保留必须做和应该做的部分可以有的留给后续迭代。这个思路其实也是软件工程课程里讲的——范围管理需求变更控制。我在文章后面还会专门聊一聊。2.3 核心业务流程图文字版整个系统跑通一遍之后核心流程大概是这样的教师流程登录系统 → 维护题库 → 创建试卷自动/手动 → 创建考试设置时间、时长、参考班级 → 发布考试 → 考试结束后批改主观题 → 查看成绩统计 → 发布成绩。学生流程登录系统 → 查看待参加考试列表 → 进入考试页面 → 逐题作答 → 提交试卷 → 客观题立即显示得分 → 等待教师批改主观题 → 查看最终成绩和试卷详情。管理员流程登录系统 → 用户管理创建教师账号、重置密码 → 系统基础数据维护 → 数据备份。这个流程梳理清楚之后后面的一切开发工作都变得有章法可循了。3. 技术选型为什么是 SpringBoot Vue以及相关的搭档们3.1 SpringBoot Vue 的适配度到底体现在哪技术选型这件事我不能说 SpringBoot Vue 是唯一答案但它放在软件工程课程在线考试系统这个场景下适配度确实很高。后端选 SpringBoot核心原因是开发效率和生态。SpringBoot 的起步依赖机制能让你少写大量样板代码。一个典型的 SpringBoot 项目引入spring-boot-starter-web就内置了 Tomcat 和 Spring MVC 全套能力引入spring-boot-starter-data-jpa或mybatis-plus就能快速落地数据访问层引入spring-security则能解决认证授权问题。对于这种中小型管理系统SpringBoot 能在最短时间内把能跑起来这件事搞定把剩余时间留给业务逻辑。另外我们软件工程课程的题库、试卷、成绩这些数据天然适合关系型数据库来存。SpringBoot 对 MySQL 的支持非常成熟事务管理、连接池配置、SQL日志打印这些东西都是开箱即用。考试系统里最核心的组卷操作需要在事务里完成——试卷生成过程中任何一步出错整个试卷都不能留下半成品数据。SpringBoot 的Transactional注解可以非常优雅地解决这个问题。前端选 Vue核心原因是组件化和渐进式开发体验。Vue 的双向数据绑定在考试答题这个场景里简直是福音。学生做选择题时每点一个选项数据立刻同步到响应式状态里切到下一题再切回来之前的作答状态还在。如果用传统的 DOM 操作方式这种维护几十道题的作答状态会写到你怀疑人生但 Vue 只要ref一个对象页面的渲染就和数据状态天然联动。组件化也是考试系统特别需要的。一套试卷里单选题、多选题、判断题、简答题、案例分析题每种题型的渲染逻辑完全不同。我用 Vue 做了SingleChoiceQuestion、MultipleChoiceQuestion、TrueFalseQuestion、ShortAnswerQuestion、CaseAnalysisQuestion五个组件教师端和学生端共用这套组件。在教师预览试卷时看到的渲染效果和学生考试时看到的完全一致这也呼应了系统里试题预览和实际考试的一致性要求。前后端分离的架构模式也是这个项目的一个隐性收获。开发过程中前端团队其实就是我和一个学生和后端团队也是我通过接口文档并行推进前端用 Mock 数据先跑界面后端用 Postman 测接口互不阻塞。这种协作方式本身就是软件工程里接口先行、并行开发的实践。3.2 数据持久层JPA 还是 MyBatis-Plus这是很多做 SpringBoot 项目的人都会纠结的问题。我这次选的是Spring Data JPA理由有三第一这套系统的实体关系非常密集。题目、试卷、考试、答卷、成绩这些实体之间有大量的一对多、多对多关联。JPA 的对象关系映射能力非常适合处理这种场景通过OneToMany、ManyToMany可以直接用对象导航的方式访问关联数据写起来非常流畅。比如从Exam对象直接exam.getExamPaper().getQuestions()就能拿到整张试卷的所有题目这在 MyBatis 里得手动写一堆关联查询。第二这是一个课程项目代码可读性和维护性比极致性能更重要。JPA 生成的查询逻辑都在 Repository 层语义清晰。后来我让一个学弟接手维护这个项目他发现 JPA 的学习成本比 MyBatis 低不少接手的效率高很多。第三JPA 的Transactional配合实体管理的自动脏检查在组卷和保存答卷这种需要多步骤数据一致性的场景里能省掉大量手写 SQL 的工作。当然 MyBatis-Plus 也有它的优势——SQL 可控性强、复杂查询优化空间大。但就这个项目的数据量级题目最多几千道答卷最多几千份JPA 的性能完全够用。我实测过自动组卷这种最重的查询操作单次响应时间在 300ms 以内完全不影响体验。3.3 权限框架Spring Security 还是 JWT 拦截器在线考试系统有个很现实的要求不同角色能访问的接口必须严格隔离。学生不能去调教师的后台接口教师不能修改管理员的系统配置。我最终采用了Spring Security JWT的方案。Spring Security 负责认证流程和 URL 级别的授权控制JWT 负责无状态的身份凭证传递。为什么不用传统的 Session 方案因为前后端分离之后前端和后端可能部署在不同的域名下甚至将来可能做成多端比如学生用手机浏览器登录Session-Cookie 模式在跨域场景下需要处理 CORS 凭证、Cookie 共享这些麻烦事。JWT 就简单得多——用户登录成功后后端签发一个包含用户ID、角色信息的 Token前端每次请求在 HTTP Header 里带上Authorization: Bearer token后端通过过滤器解析 Token 即可完成身份识别。Spring Security 的配置核心是定义哪些 URL 需要什么权限。我这里的权限划分大致是/api/auth/**公开登录注册接口/api/student/**需要STUDENT角色/api/teacher/**需要TEACHER角色/api/admin/**需要ADMIN角色这种粗粒度的接口级权限控制配合细粒度的数据级权限校验比如教师只能访问自己创建的题库和考试就能形成一套完整的权限体系。3.4 前端生态Vue Router、Pinia、Element PlusVue 前端这一侧我用了三个核心依赖Vue Router负责路由管理。路由表的设计参考了用户角色登录后根据用户角色动态生成可访问的路由表学生端只有考试相关页面教师端增加题库管理、试卷管理、成绩管理页面管理员端再增加用户管理页面。这里我实现了 Vue Router 的动态路由——不是所有路由都静态写在配置里而是登录后根据后端返回的权限列表动态addRoute从根源上防止未授权页面被直接访问。Pinia负责全局状态管理。我主要用它在考试页面里管理答题状态——当前试卷的所有题目作答记录、当前正在进行的是哪道题、考试剩余时间这些高频变更的状态放 Pinia 里组件之间共享特别方便。学生从三号题切到七号题再从七号题切回三号题之前三号题选的答案依然在因为最基本的考试状态始终存储在 Pinia 中。Element Plus负责 UI 组件库。选择题的卡片、表格、分页、弹窗、表单校验这些通用能力直接复用 Element Plus省下了大量造轮子的时间。4. 数据库设计一张干净的表结构是系统稳定性的地基4.1 核心表设计思路数据库设计这块我吃过大亏。早期在做一个旧版教务系统时表设计不规范字段名各种缩写也没有外键约束后期加了二十多个接口每次联调都要花大量时间理清数据关系。这次我学乖了花了一整个周末把表结构彻底设计清楚才开始写代码。整套系统一共设计了15张核心表我按模块来分用户相关2张sys_user用户表字段包括id,username,password,real_name,role,email,phone,avatar,status,create_time。role字段用字符串枚举区分 STUDENT、TEACHER、ADMIN 三种角色。sys_user_course用户-课程关联表多对多记录学生选了哪些课程、哪个教师教哪些课程。题库相关4张question_bank题库表归属某个教师。question题目表。这里我需要重点说一下字段设计question_type单选/多选/判断/填空/简答/案例分析、stem题干、options选项JSON串、answer标准答案、analysis题目解析、difficulty难度系数 1~5、chapter_id所属章节、knowledge_point知识点标签。question_option这个表我后来否掉了。一开始想用独立表存选项但选项本身是题目的固定属性而且数量极少通常2~6个用 JSON 字段存比独立表查询效率更高。后续我会专门讲这个取舍。exam_section考试章节约束表用于自动组卷时按章节设定题目数量、题型和难度。试卷相关3张exam_paper试卷表包含试卷名、总分、考试时长、创建者。paper_question试卷题目关联表字段包括paper_id,question_id,question_order题号、score该题分值。auto_paper_rule自动组卷规则表记录按章节题型难度的组卷约束。考试相关5张exam考试表字段包括exam_name,paper_id,start_time,end_time,duration,exam_status,create_by。状态分未发布、进行中、已结束、已批改完成。exam_class考试-班级关联表记录哪些班级参加这场考试。exam_record答题记录表也叫答卷表核心表之一。字段包括exam_id,user_id,paper_id,total_score,objective_score,subjective_score,status,start_time,submit_time。这个表记录的是某个学生参加了某场考试这件事以及最终得分。answer_detail答题详情表记录每道题的具体作答。字段包括record_id,question_id,student_answer,is_correct,score,teacher_comment。学生在试卷上画的每一笔最后都会沉淀到这里。exam_monitor考试监控表记录切屏次数、离开页面时长等行为数据。4.2 为什么题目选项我用 JSON 存储而不是独立表这是我做这个系统时一个挺重要的设计决策值得单独拿出来说说。从范式角度讲一道题目有多个选项选项应该是独立实体应该建question_option表用外键关联一条记录一个选项。这是教科书的标准答案。但真实场景是选项永远只会和题目同时出现永远不会被单拎出来查询或修改。我做软件工程考试系统做题时是查一道题然后把它所有的选项都渲染出来几乎不存在查询所有包含正确两个字选项的题目这种需求。选项完全可以用 JSON 数组存储在题目的options字段里形如[ {label: A, content: 需求规格说明书, isAnswer: false}, {label: B, content: 概要设计说明书, isAnswer: false}, {label: C, content: 详细设计说明书, isAnswer: true}, {label: D, content: 程序员开发日志, isAnswer: false} ]JPA 里配合Convert属性转换器存取的时候自动序列化反序列化代码层根本感知不到 JSON 的存在。查询效率方面避免了原来需要两次 LEFT JOIN 才能拿到带选项的题目这种场景性能更优代码也更简洁。用范式化的思维会觉得表拆得越细越正确但实际工程里查询模式和修改频率才是决定表结构的第一因素。选项跟着题目走、很少单独变那就一起存。这个思路在课程设计的答辩里也可以作为亮点讲你有能力根据实际场景调整理论模型。4.3 试卷快照防止题目修改导致历史试卷失效这里还有一个我认为很重要的设计细节试卷中的题目必须快照不能动态关联实时题库。意思是当教师用当前题库里的题目组装了一份试卷然后这场考试已经考完了学生也交卷了——这时候如果教师改了题库里某道题的题干或者把某道题的正确答案改了那么历史成绩应该怎么办如果试卷直接关联题库里的question_id那题干一变历史答卷的题目内容就跟着变了判分就乱了。这个问题在早期的版本里真实发生过后来我查了好几个小时才发现是哪里的数据对不上了。解决思路是试卷中的题目信息在学生交卷那一刻起就是一份独立的数据。在paper_question表里除了question_id我额外存了stem_snapshot题干快照、options_snapshot选项快照、answer_snapshot答案快照。组卷的时候从题库复制一份快照过去之后题库怎么改都影响不到已经生成的历史试卷。这也意味着同一份试卷在多次考试中被重复使用每次使用的都是考试当时那一版题目历史可追溯。这个思路用一句话概括就是题库是源试卷是副本。凡是涉及某个时刻的准确状态的数据都应该做快照而不是用实时外键。这在考试系统、订单系统、财务系统里都是非常通用且重要的设计原则。4.4 从ER设计到建表脚本整个 ER 关系可以概括为用户 1 — N 题库题库 1 — N 题目教师 1 — N 试卷试卷 N — N 题目通过 paper_question 关联试卷 1 — N 考试考试 1 — N 考试记录exam_record考试记录 1 — N 答题详情answer_detail考试 N — N 班级通过 exam_class 关联建表脚本里我有两个习惯可以分享一是所有时间字段统一用DATETIME后续写 SQL 查询时BETWEEN操作非常自然二是所有表都加上create_time和update_time两个通用审计字段JPA 的PrePersist和PreUpdate注解可以自动填充排错的时候能帮你找到这条数据是什么时候变的。5. 接口设计与核心难点从登录鉴权到自动组卷5.1 一套干净的 RESTful 接口规范接口设计是做前后端分离项目时最需要提前统一思想的事情。我这里的规范大致是基础路径/api/module/action比如/api/teacher/saveQuestion、/api/student/submitExam。统一返回体所有接口返回ResultT结构包含code、message、data三个字段。前端拦 axios 响应code 为 200 表示成功401 表示未登录403 表示无权限500 表示服务器异常。分页参数所有列表查询接口统一接收pageNum、pageSize两个参数返回PageResult对象内含total、list、pageNum、pageSize字段。这里要特别强调统一返回体的重要性。如果每个接口返回的格式都不一样比如有的直接返回数组、有的返回对象、有的返回带个status字符串前端 axios 拦截器就没法做统一错误处理每个页面都得自己判断这次请求到底成没成功代码会迅速腐化。我实际项目的Result.java大概是这样的public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }5.2 用户认证JWT 签发与拦截逻辑登录接口流程是这样的前端提交用户名密码 → 后端查sys_user表并用 BCrypt 校验密码 → 校验通过后用用户ID角色生成 JWT → 返回 Token 和用户基本信息。之后所有需要登录的接口都通过一个 JWT 过滤器来解析身份。JWT 生成我用的是jjwt库核心代码就几行String token Jwts.builder() .setSubject(userId.toString()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();secretKey放在配置文件里过期时间我设置的是 2 小时。这里有个细节如果考试时长超过 2 小时怎么办学生答到第 1 个半小时Token 过期了答题记录提交不上去这就尴尬了。我的解决方案是在考试答题接口上做特殊处理——考试期间前端每隔 10 分钟用旧的未过期 Token 调用一次刷新接口获取新 Token保证考试过程中 Token 始终有效普通接口则严格按照 2 小时过期策略。Spring Security 方面我在SecurityConfig里配置了stateless会话策略禁用默认的csrf加一个JwtAuthenticationTokenFilter放在UsernamePasswordAuthenticationFilter之前用于校验 Token 并注入SecurityContext。这样后端每个接口就可以用PreAuthorize(hasRole(TEACHER))来做方法级权限控制。5.3 核心难点一自动组卷的三重约束算法自动组卷是这套系统我最得意的模块。先说需求背景教师在创建试卷时希望设定第二章出 2 道单选、每道 3 分、难度系数中第三章出 1 道案例题、每道 15 分、难度系数较高这样的组卷规则然后由系统自动从题库里选题组成一份完整的试卷。实现起来面临的核心挑战是同时满足章节范围、题型分布、难度分布、总分约束四个条件。我的方案分为三步第一步规则解析。教师在前端页面上配置一张组卷规则表每一行是一个规则包含chapterId、questionType、count、score、difficulty。后端接受这些规则按规则条数循环。第二步从题库中选题。对每一条规则从题库里查询满足chapter_id、question_type、difficulty条件的题目集合如果用固定题目数量保证总分就直接随机取count道题如果题目数量不足则降低难度条件逐步放宽范围直到凑够数量。第三步校验总分。所有规则挑选完成后把每道题的score相加如果总分不等于试卷设定的总分比如 100 分则按比例调整部分题目的分值或者返回错误提示让教师修改规则。组卷过程必须在事务中执行一旦中途任何一步失败整个试卷创建回滚不会产生脏数据。这里我贴一段简化后的选题目核心代码基于 Spring Data JPA 的 Specification 查询public ListQuestion selectQuestions(AutoPaperRule rule) { SpecificationQuestion spec (root, query, cb) - { ListPredicate predicates new ArrayList(); predicates.add(cb.equal(root.get(chapterId), rule.getChapterId())); predicates.add(cb.equal(root.get(questionType), rule.getQuestionType())); predicates.add(cb.equal(root.get(difficulty), rule.getDifficulty())); return cb.and(predicates.toArray(new Predicate[0])); }; // 查出来随机打乱取前 count 道 ListQuestion matched questionRepository.findAll(spec); Collections.shuffle(matched); return matched.stream().limit(rule.getQuestionCount()).collect(Collectors.toList()); }实际开发中这个模块踩过比较多的坑是规则配置得过于严格导致永远组不成一张卷子。比如某个章节总共就只有 5 道单选题教师却要 6 道那么系统就一直报错。后来我加了题目总量不足提示并且在组卷完成后返回一份组卷报告每个知识点实际用了哪些题、是否满足难度分布、未满足的规则是哪些教师可以一目了然地调整。5.4 核心难点二学生答题状态管理与交卷防丢学生考试页面是本系统最繁忙的页面。学生进入考试后前端立即向后端发起开始考试请求后端做两件事校验当前时间是否在考试时间窗口内防止提前开始或逾期补考。创建exam_record记录状态为ING进行中并把本次考试的试卷题目不含答案封装返回给前端。前端拿到试卷题目后把题目渲染出来。学生的作答数据统一存放在 Pinia 的examStore中结构大致是{ recordId: 1001, examId: 88, paper: { paperId: 200, questions: [{questionId, type, stem, options, ...}] }, answers: { questionId_1: A, questionId_2: B,C, questionId_5: 需求工程的主要任务是... }, currentQuestionOrder: 0, remainSeconds: 5400 }为什么答案要存 Pinia 而不是存到后端数据库里因为考试过程中每做一题就往后端写一次会频繁产生数据库写操作同时对网络抖动极其敏感——一旦某次写接口失败前端整个答题流程都可能卡住。所以我的策略是答题过程完全本地维护定时自动保存兜底最终以交卷时的数据为准。自动保存实现为当学生每切换一道题或者每 60 秒前端把当前answers状态同步到后端缓存的接口写到exam_record的暂存字段里。如果考试过程中学生不小心关闭了浏览器重新登录后系统检测到该考生存在ING状态的考试记录会弹窗提示是否恢复上次未完成的考试从暂存字段中恢复作答数据。这样就实现了断点续考。交卷接口是最后一个关键入口。学生点击交卷后前端会把完整的answers数据一次性 POST 到/api/student/submitExam。后端在事务里完成这几件事再次校验考试时间是否已过期。如果学生超时交卷系统按最后提交时间截断超出的作答记录无效。将answers逐题写入answer_detail表。客观题单选、多选、判断、填空当场判分。过程为取回该题标准答案与学生作答字符串做比对多选题还要比对选项集合是否完全一致。计算客观题总分写入exam_record.objective_score状态改为WAITING_MARK等待教师批改主观题主观题总分暂时为空。返回学生客观题得分。这里有个经常被忽略的细节单选题判分要支持大小写不敏感多选题判分要支持选项乱序等价。学生选了 B、C 和 C、B 应该是同一个答案。我的实现是在判分前把答案字符串转成集合再排序最后比较有没有一一对等。5.5 核心难点三教师在线批改主观题的体验主观题批改模块是决定教师愿不愿意长期使用这套系统的关键。我做批改页面的思路是左侧显示学生名单和当前批改进度右侧展示这道题的学生作答内容、标准答案、教师评分输入框。教师可以上下翻页批改同一道题的不同学生也可以批完一个学生的所有题再切下一个学生。评分的核心逻辑是按得分点给分。软件工程案例分析题的判分尤其讲究这一点——比如一个根据需求描述画出用例图的题用例识别正确得 5 分、 包含关系正确得 3 分、 泛化关系正确得 2 分这些得分点需要教师手工控制。所以我的answer_detail表里有score、teacher_comment两个字段评分时教师可以逐个得分点评分并填评语。主观题批改完后系统实时汇总exam_record.subjective_score加上客观题分数就是总分。当整场考试所有学生的主观题都批改完毕考试状态变为COMPLETED成绩自动发布学生端立即可以看到自己的试卷详情和正确答案。实测下来一份 30 个人的班级试卷如果主观题有 4 道熟练的教师大约 1 小时左右能批完。比传统的纸制试卷收上来、红笔逐本改、再统计分数效率能提升一倍以上。6. 前端页面与交互逻辑一套兼顾教师与学生的界面6.1 教师端的页面设计教师端的页面结构是左侧菜单 右侧内容区。菜单项依次是题库管理、试卷管理、考试管理、批改管理、成绩统计、学生管理。题库管理页面的关键功能是题目录入。我做了题型切换标签按题型动态渲染表单组件。录选择题时有题干编辑器、选项动态添加、正确答案点选、难度滑杆、章节下拉框、知识点标签输入。最关键的是支持批量导入——我写了一个从 Excel 导入题目的功能模板里定义好列头一行一道题导入时后端逐行解析并校验。几百道题的题库用 Excel 批量导远比一道一道手工录方便。试卷管理页面提供两种组卷方式手动组卷和自动组卷。手动组卷时左侧是题库筛选面板按章节、题型、难度过滤右侧是已选题目清单题序可以直接拖拽调整。自动组卷时则是呈现一张组卷规则表格教师填完规则点击生成系统返回组卷报告。我个人更推荐新手教师用自动组卷规则清晰、可控性强而且生成的试卷质量通常优于随手一圈的选题。考试管理页面用来创建考试和查看考试列表。创建考试需要选择一份已生成的试卷、设置考试起止时间、考试时长、选择参考班级然后发布。发布后前端生成一个可复制的考试链接学生登录后会自动在自己的考试列表里看到。批改管理页面上文已经介绍了。成绩统计页面配合 ECharts 展示支持柱状图看分数段分布、条形图看每道题正确率、折线图看历次考试成绩趋势。教师最关心的知识点掌握度我做成一张热力图——横轴是章节知识点纵轴是班级颜色深浅代表平均得分率哪些知识点讲得不好一目了然。6.2 学生端的考试页面交互细节学生端考试页面我按照真实考试的逻辑设计成左侧试卷导航 右侧答题区的布局。左侧是题号列表答过的题目题号变绿色、没答的变白色、当前正在作答的变蓝色整体状态和真实答题卡一样。右侧是答题区学生完成当前题后可以点下一题也可以直接点左侧题号跳转。顶部有一个固定导航栏显示考试名称、剩余时间倒计时、已答题数量。剩余时间少于 15 分钟时倒计时变红并有文字提醒。这里我还做了个防沉迷设计——交卷前如果还有题目未作答弹窗二次确认防止学生手滑误交。主观题的答题区我用了带工具栏的富文本编辑器TinyMCE学生在案例分析题里需要书写长文本甚至需要简单排版。富文本内容存到answer_detail.student_answer字段里教师批改时原样渲染保留了学生作答的格式。6.3 防作弊机制的实现这个部分我得坦诚地说纯前端切屏监控防不了所有作弊但至少能增加作弊成本、形成威慑。我在考试页面定义了这样的行为规则切屏检测监听document.visibilitychange事件页面从可见变为 hidden 时判定一次切屏。每切屏一次exam_monitor表记录一条日志包含切换时间和离开时长。离开页面检测如果用户在考试时间内尝试关闭浏览器标签页beforeunload事件触发时向后端发送一条离开记录。答题时间异常检测如果某道题从打开到作答的时间几乎为 0疑似直接复制粘贴备好的答案系统会在监控日志里标记怀疑项供教师人工核查。考试结束后教师可以在批改页面的考试监控标签里查看每个学生的切屏记录。切屏次数超过 3 次的考生教师可以重点核对其客观题作答时间分布。这套方案当然不是完美的但它传达了态度系统不是傻瓜式放行而是在采集证据。这个考量我觉得对课程考试来说是务实的。7. 讲三个我在开发过程中踩过的坑7.1 坑一事务没控制好组卷生成了半张试卷这是我开发过程中闹得最大的一次故障。有一次我在测试自动组卷当教师配置了一个单选题规则需要 10 道但题库对应章节只有 7 道的条件系统直接报错。本来报错也就报了但我后来查数据库的时候发现exam_paper表里已经插入了一条试卷记录但paper_question表里只有这条试卷的一部分题目——事务没有回滚留下了脏数据。排查原因我最初组卷逻辑没有加Transactional注解或者更准确地说是在同一个类内部的savePaper方法里通过this.generatePaper()调用另一个方法而Transactional注解放在被调用的内层方法上。Spring 的声明式事务默认通过 AOP 代理实现同类内部方法调用不经过代理所以注解完全没生效事务根本没有开启。修复也很简单把事务注解加到最外层公开的savePaper方法上内部调用不再单独加事务注解同时把整条组卷链路设计成——先正常选完题组成ListPaperQuestion最后统一saveAll()写入而不是一边选一边插入数据库。这样即使是两阶段操作也能保证原子性。这里我后来在代码评审里专门总结了一条规范事务边界放在入口方法上内部所有读写操作共享同一个事务。7.2 坑二前端路由权限控制形同虚设这个坑发生在我第一版完成之后。当时我只是在后端做了接口级权限控制前端虽然用 Vue Router 定义了教师页面但只做了菜单显示/隐藏的判断——也就是说如果学生手动修改浏览器 URL 直接访问/teacher/question前端路由是可以跳转的只是因为后端接口返回 403 才拿不到数据。这个方案能用但很不优雅。一是前端会渲染出一大堆空页面用户体验很怪二是让学生看到这个页面我进得去的错觉增加无意义的试错。正确做法是我前面提到的Vue Router 动态路由。登录成功拿到用户角色后不再用写死的静态路由表而是根据角色动态生成const roleToRoutes { STUDENT: [ { path: /student/exam-list, component: ExamList }, { path: /student/profile, component: Profile } ], TEACHER: [ { path: /teacher/dashboard, component: Dashboard }, { path: /teacher/question-bank, component: QuestionBank }, { path: /teacher/paper, component: PaperManage }, // ... ], ADMIN: [ { path: /admin/user-manage, component: UserManage } ] }; // 登录后 router.addRoute(...roleToRoutes[userRole]);同时前端路由守卫里加一个beforeEach判断当前用户角色是否存在于目标路由的meta.roles中不存在就重定向到首页并提示无权限访问。这样学生访问教师页面时路由导航直接拒绝页面根本不会渲染。前端和后端各自的权限控制是两道防线前面防的是看不见后面防的是拿不到。7.3 坑三客观题判分的空格陷阱最后一个坑听起来很小但真实影响了一批学生成绩。学生做多选题时如果答案选项是 B、C前端保存时它是一个数组[B, C]交卷时序列化成B,C。但有一个马虎的学生在选项之间多敲了空格或者某些浏览器在复制粘贴时自动加了一个空格最终提交的答案是B, C——标准答案校验的时候B,C.equals(B, C)显然是false这道题被无情地判错了。这类问题在真实考场里很常见。后来我在后端封装了一个normalizeAnswer函数public static String normalizeAnswer(String answer) { if (answer null) return ; // 转大写、去掉所有空格、按逗号分割后去空串、排序 String upper answer.toUpperCase().replace( , ); String[] parts upper.split(,); ListString list new ArrayList(); for (String part : parts) { if (!part.isEmpty()) list.add(part); } Collections.sort(list); return String.join(,, list); }所有客观题的判分在比较前都先走一遍normalizeAnswer。从此以后选项顺序不同中间有空格这类问题再也没出现过。8. 系统测试与部署考试系统的可靠性是靠测出来的8.1 功能测试与并发模拟在线考试系统有个特殊的可靠性要求考试进行中系统不能挂。所以测试阶段需要重点模拟两个场景第一是高并发交卷。全班 40 人同时点交卷后端在短时间内会收到 40 个同时到达的请求每个请求都要做判分、多表写入。如果控制不好很容易出现数据库连接池打满、请求超时、部分学生交卷失败的情况。我做了这样一些处理交卷接口的数据库操作尽量合并客观题判分的逻辑全部放在应用内存中完成一次交卷最多只发两条 SQL——第一条批量插入answer_detail第二条更新exam_record。测试时用 JMeter 模拟 200 个并发用户同时交卷接口平均响应时间从最初的 1.8 秒降到了 600ms完全满足实际使用要求。第二是考试期间的系统稳定性。考试是锁定时间的考生在半小时内陆续进入考试页面。这里最大的风险是初始化考试页面的接口如果响应缓慢会直接影响学生开考心态。因此我把获取试卷题目和创建考试记录两个操作做了性能优化试卷题目查询使用只读事务并加了一个简单的缓存同一场考试内重复获取试卷时直接命中缓存避免重复查库。8.2 前后端分离部署方案部署方面我采用的是 Nginx 反向代理方案。前端 Vue 项目npm run build后生成静态文件放在 Nginx 的html目录后端 SpringBoot 打包成jar用java -jar启动监听 8080 端口。Nginx 配置把/api开头的请求转发到后端 8080server { listen 80; server_name exam.example.com; location / { root /usr/share/nginx/html/exam-system; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }部署中比较容易踩的一个坑是try_files $uri $uri/ /index.html这行必须写上否则刷新页面时如果路由直接是/teacher/question-bankNginx 会去找这个路径下的物理文件找不到就 404。配好之后刷新路由始终会回到index.html再由 Vue Router 接管继续展示对应页面。8.3 数据备份策略考试系统的数据可不能丢。我配置了一个每天凌晨两点自动备份 MySQL 数据库的 crontab 任务mysqldump -u root -p密码 exam_system /backup/exam_$(date %Y%m%d).sql find /backup -mtime 30 -delete保留最近 30 天的备份磁盘占用也不大。这里提醒一句密码写在命令行里可能被 shell 的历史记录捕获更好的选择是用 MySQL 的~/.my.cnf配置文件存连接参数。作为一个小而美的课程考试系统这层备份已经足够。9. 我做这套系统前后最大的变化如果让我总结做这套 SpringBoot Vue 在线考试系统最值得说的一点我会说它让我重新理解了软件工程这四个字。书本上讲的需求先行用例建模数据一致性架构设计在此之前对我更多是考试前背的概念。但这个项目做下来每一个概念都变成了我踩过的坑、改过的代码、调过的接口。原来教材强调可行性分析不是废话因为我在画了功能图之后发现最初设想的在线监考根本没有必要原来数据库设计是第一道质量关卡也是真的因为我在question表字段设计上吃过的亏直接决定了后期所有查询的效率。对于正在做课程设计或者想找项目练手的同学我的建议是不要直接下载个开源项目改改名字交差也不要照着课程设计模板拼凑一个看起来完整的文档。真正从需求分析开始一个模块一个模块地实现、测试、走查这个过程学到的比任何八股文都实在。这套系统目前已经在我实际教学中跑过两个学期累计完成了 4 次平时测验和 2 次期末考试共服务学生超过 200 人次。最让我有成就感的一幕是一次考试结束后有个学生跟我说老师这套系统比我们上学期用的商业考试系统好用至少主观题能看到每题怎么给分的。对一个自己动手做工具的人来说没有比用户的正面反馈更实在的回报了。如果你也准备做类似的东西最后再送你一个实操建议先花一周时间把考试流程彻底想明白再动手写代码。工具是为人服务的把业务逻辑搞顺了代码怎么写都顺。反过来一张嘴就谈技术选型、谈框架最终做出来的东西往往中看不中用。
返回列表