
简介这套基于Spring Boot的在线考试系统是一份面向计算机专业毕业设计场景的完整方案既可用于毕设选题参考也适合希望掌握Spring Boot与Vue前后端分离开发的学习者。资源共791个文件压缩包约21.46MB内部以后端Java源码107个、前端Vue组件46个、JS脚本164个及HTML页面36个为主同时包含docx与pdf格式的论文文档、SQL数据库脚本以及一键安装、运行、构建的bat脚本便于对照部署和二次开发。已有173人学习下载。系统覆盖用户、课程、题库、考试、成绩等核心模块并涉及Spring Security权限控制、Redis缓存优化、日志监控、全局异常处理等实践要点论文部分从架构设计、数据库设计到系统优化与安全加固均有阐述可帮助读者快速理解在线考试系统的完整实现思路也可作为毕业设计或相关课程项目的参考范本。1. 基于Spring Boot的在线考试系统难点不在框架本身考试业务看起来就是题目、试卷、成绩三个实体实际做完才知道同一时间两百人交卷时数据库面临的是瞬间较高的写并发学生断网重连服务端要处理未提交的答题快照自动阅卷如果都放在一个事务里遇到主观题人工录分还得保证成绩不下发。这些点才是论文评审愿意看的地方。Spring Boot能帮你省去配置的麻烦但不会替你决定事务边界和锁策略。下面按一条可答辩的方案来讲先立四层架构再实现考试核心流程然后补安全和扩展最后把运行数据变成论文里的图表。在校学生、需要快速落地在线考试原型的人都可以按这套来走。2. 系统设计Spring Boot四层架构与数据库模型2.1 考试系统的四层架构为什么比三层更清晰三层架构是Controller-Service-DAO很多Spring Boot项目一开始就把业务写在Service里因为框架太方便。考试系统牵扯到权限校验、题目缓存、成绩流水三层很容易让Service类变成堆代码的仓库。四层架构把业务拆成接口层Controller、应用层Application/Manager、领域层Service/Domain、基础设施层Repository/Infrastructure。应用层组织用例“提交答案”这个动作要先校验考试状态再保存答题记录然后触发阅卷领域层不关心HTTP和数据库只处理规则。论文里画架构图时四层比三层有更多可展开的内容答辩时也更容易解释依赖规则。2.2 包目录规范按Spring Boot目录规范组织模块代码结构建议按功能模块切片而不是按层建包。我一般按下面这种方式建目录com.example.exam ├── controller # HTTP接口层 ├── application # 应用服务层用例编排 ├── domain # 领域层实体、值对象、领域服务 │ ├── exam # 考试上下文 │ ├── question # 题库上下文 │ └── result # 成绩上下文 ├── infrastructure # 基础设施Repository实现、配置、外部服务 │ ├── repository │ └── config └── common # 通用返回、异常、工具类依赖方向从controller到application再到domaininfrastructure实现domain里定义的接口。如果方向反了项目迭代几次后会出现循环引用这是Spring Boot目录规范里最容易忽略的一条。在写论文模块图时这里的包结构可以直接转成依赖关系图评审老师一眼就能看出代码组织是否合理。2.3 表结构设计与ORM选择MyBatis还是Spring Data JPA在线考试系统常见的表有学生表、课程表、题库表、试卷表、试卷题目关系表、考试记录表、答题记录表、成绩表。核心表的结构可以参考下面这张表表名关键字段说明exam_paperid, title, duration_minutes, total_score, status试卷主表status区分草稿、发布、结束paper_questionid, paper_id, question_id, score, sort_order多对多关系表保存每道题在试卷里的分数和顺序exam_recordid, paper_id, student_id, start_time, submit_time, status一次考试会话用于处理交卷与异常退出answer_recordid, record_id, question_id, answer_content, score考生答题明细exam_scoreid, record_id, objective_score, subjective_score, final_score成绩表客观题自动阅卷结果和主观题人工录分分开题库表和试卷表必须分开因为同一道题可以被多张卷子引用。关系表paper_question要额外保存每题分数不能只放question_id否则相同题目在不同试卷里分值不同时改哪张卷子都会影响另一张。持久层选型我一般用MyBatis。考试系统的查询高度动态按知识点组合筛选、要随机抽样、要排除已用题目这些都需要写动态SQL。JPA能写但控制力不如MyBatis。Spring Boot 3.x下两者都能很好集成把XML放在resources/mapper下并在application.yml里指定mybatis: mapper-locations: classpath:mapper/**/*.xml # XML文件位置 type-aliases-package: com.example.exam.domain # 实体包别名 configuration: map-underscore-to-camel-case: true # 数据库下划线转Java驼峰另外补充一个2024年需要注意的版本问题Spring Boot 3.x到4.x的升级中很多自动配置类被重新组织MyBatis和Spring Boot的starter也会有兼容性调整。如果升级后遇到DataSourceAutoConfiguration这类自动配置的包路径找不到先查版本迁移指南而不是直接改配置。2.4 用乐观锁控制考试记录状态考试记录的状态字段需要一套流转规则。exam_record.status可以定义如下状态码状态名允许进入的下一个状态0未开始11进行中2, 32已提交无3异常中断1, 2学生点击进入考试时状态置为1并记录start_time交卷时只有状态为1才能更新为2断网超时由后台定时任务把状态从1改为3学生重新进入时根据剩余时间决定恢复为1还是禁止进入。这套规则无论写在Service还是领域服务里都要在更新语句中带上状态条件。用JPA时加Version很省事Entity Table(name exam_record) public class ExamRecord { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name paper_id, nullable false) private Long paperId; Column(name student_id, nullable false) private Long studentId; Column(name start_time) private LocalDateTime startTime; Column(name submit_time) private LocalDateTime submitTime; Column(name status) private Integer status; Version Column(name version) private Integer version; // 乐观锁版本号 }这里Version会自动给update语句追加where id? and version?更新成功后version加一。如果两次交卷同时到达第二次的version已不匹配会抛出ObjectOptimisticLockingFailureException由全局异常处理器转换成“请勿重复提交”。MyBatis里可以手动在更新SQL中加version判断效果一样。这样设计后论文里可以写“基于版本号的乐观锁避免答题记录被覆盖”。3. 核心业务实现题库、考试与阅卷3.1 题库表与随机组卷的SQL写法题库表里通常包含课程ID、题型、难度、知识点ID、题目内容、标准答案和状态。先定义题型和判分方式题型判分方式存储字段单选题完全匹配standard_answer多选题严格匹配或按漏选给分standard_answer, score_rule判断题布尔匹配standard_answer主观题人工阅卷无标准答案组卷时不能直接全库随机。一个可行的做法是先根据课程、考纲要求得到题目集合再从集合中按知识点分层抽样。MyBatis动态SQL里可以这样写!-- 按条件随机查询题目 -- select idfindRandomQuestions resultTypecom.example.exam.domain.question.Question SELECT * FROM question WHERE course_id #{courseId} AND type #{type} AND difficulty BETWEEN #{minDifficulty} AND #{maxDifficulty} AND status 1 ORDER BY RAND() !-- 数据量大时注意性能 -- LIMIT #{limit} /select这段SQL把随机抽样放在数据库端实现简单但题目数量超过几万后ORDER BY RAND()会先全表排序性能很差。我一般先用一条轻量查询拿到符合条件的ID集合再随机筛选ID最后用IN查完整对象。组卷算法是论文里的一个亮点可以单独写一节。3.2 交卷接口的事务边界与请求幂等交卷时需要校验考试状态、批量保存答题记录、更新考试记录、计算客观题得分。这几个操作不能全部塞进一个事务。因为自动阅卷如果做了文本匹配耗时可能几百毫秒事务太长会增加数据库连接占用和死锁概率。正确做法是Transactional public void submitExam(Long recordId, ListAnswerItem answers) { // 悲观锁SELECT ... FOR UPDATE ExamRecord record examRecordRepository.lockById(recordId); if (record.getStatus() ! 1) { throw new ExamStateException(考试不在进行中无法交卷); } answerRecordRepository.batchInsert(answers); // 批量保存答题记录 record.setStatus(2); record.setSubmitTime(LocalDateTime.now()); applicationEventPublisher.publishEvent(new ExamSubmittedEvent(recordId)); }lockById对应SELECT ... FOR UPDATE在数据库层把这条考试记录锁住。加上乐观锁会形成双保险悲观锁处理并发交卷乐观锁防止异常流程里出现覆盖。事务提交后监听ExamSubmittedEvent的类去做客观题自动判分这样交卷接口的响应时间不会包含判分耗时。在写论文对比时可以统计事务内平均耗时和开启异步判分后的接口耗时数据会很好看。3.3 客观题自动阅卷与成绩落库自动阅卷就是一个数据和标准答案比对的过程。批量判分时可以一次性查出题目列表避免循环查库public ScoreResult autoScore(ListAnswerRecord answers) { ListLong questionIds answers.stream().map(a - a.getQuestionId()).toList(); MapLong, Question questionMap questionService.findByIdsAsMap(questionIds); // 一次查出避免N1 BigDecimal objectiveScore BigDecimal.ZERO; for (AnswerRecord answer : answers) { Question question questionMap.get(answer.getQuestionId()); if (SINGLE.equals(question.getType()) question.getStandardAnswer().equals(answer.getAnswerContent())) { objectiveScore objectiveScore.add(question.getScore()); // 用BigDecimal避免浮点误差 } } return new ScoreResult(recordId, objectiveScore); }这里有两个坑。第一浮点运算不能用double否则0.1加0.2会变成0.30000000000000004成绩单出现这种问题很难解释。第二多选题的给分规则并不是全局统一有的试卷少选一半分有的必须全对。建议在paper_question表增加score_rule字段判分时按规则判断。成绩表里把客观题得分和主观题得分分开存人工录主观题分时不影响已算出的客观分。答题记录表要加唯一索引(record_id, question_id)防止网络重试时同一道题被插入两条。提交按钮如果没做防重用户在交卷时连续点击两次前端关掉按钮后端也要在数据库层面拦截这是最常见的线上问题。4. 安全、监控与Spring Boot在线考试系统的扩展4.1 用JWT做考试接口的认证与权限控制考试系统至少要区分学生、教师、管理员三种角色。常见的做法是用Spring Security JWT。拦截器流程是登录接口发放token其他接口校验token并把用户信息塞进SecurityContext。关键代码public OncePerRequestFilter getJwtFilter() { return new OncePerRequestFilter() { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); // 去掉Bearer前缀 Long userId JwtUtil.parseUserId(token); // 解析用户ID // 写入SecurityContext供后续方法获取当前用户 } chain.doFilter(request, response); } }; }在Spring Boot 3.x里要配合SecurityFilterChain的addFilterBefore把JWT过滤器放在用户名密码过滤器之前。注意token里不要放敏感信息服务端只需要存userId。学生在考试期间token过期不要强制重新登录可以在刷新token逻辑里判断考试是否还在进行中给予一次续期机会。4.2 防作弊基于WebSocket的实时答题心跳在线考试需要监考和防作弊。常用的方案是WebSocket推送心跳和切屏事件。前端每隔几秒上报当前状态后端通过WebSocketSession发送指令。Spring Boot实现一个简单的后端处理器Component public class ExamWebSocketHandler extends TextWebSocketHandler { private final ConcurrentHashMapLong, WebSocketSession sessionMap new ConcurrentHashMap(); Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 解析学生ID记录心跳时间 Long studentId (Long) session.getAttributes().get(studentId); examActivityService.recordActivity(studentId); } public void sendMessage(Long studentId, String payload) { // 通知指定学生用于强制交卷或警告 WebSocketSession session sessionMap.get(studentId); if (session ! null session.isOpen()) { session.sendMessage(new TextMessage(payload)); } } }切屏事件由前端检测visibilitychange上报后服务端标记一次违规。监考页面可以定时拉取违规记录不需要对所有连接广播。WebSocket在Spring Boot中接入简单但要注意会话的存活检测服务器重启后sessionMap会丢需要结合心跳超时清理。4.3 文件上传题干图片与答题附件在线考试可能允许上传图片或文档作为答案。Spring Boot配置上传大小限制spring: servlet: multipart: max-file-size: 10MB # 单文件上限 max-request-size: 20MB # 单次请求总大小接收上传文件的ControllerPostMapping(/answer/attachment) public String upload(RequestParam(file) MultipartFile file, RequestParam(recordId) Long recordId) { String storePath fileStorageService.store(file); // 生成随机文件名并保存 answerAttachmentRepository.save(recordId, storePath, file.getSize()); return storePath; }注意不要把文件直接放在项目根目录或静态资源目录。考试系统的附件具有隐私性应该存到独立目录或对象存储并且把访问权限和考试状态绑定。文件名要重新生成避免用户上传带路径的文件名。4.4 Actuator监控与未授权访问的防护Spring Boot Actuator是论文里做系统监控的常用组件。生产环境开启health、metrics、info端点。默认情况下如果依赖引入spring-boot-starter-actuator很多敏感端点会直接暴露。配置可以这样控制management: endpoints: web: exposure: include: health,info,metrics,prometheus # 只暴露这些端点 endpoint: health: show-details: when_authorized # 健康详情需要授权Actuator未授权访问是个老问题但考试系统经常忽略。尤其env端点会暴露数据库连接信息。如果系统没有接入Spring Security至少要限制管理端口只允许内网访问或者用management.server.port把监控端口单独拆出来。论文里可以写“基于Actuator的健康检查与指标采集”作为运维章节。5. 从系统到论文验证数据与性能指标5.1 用测试用例支撑论文论点论文的系统测试部分最容易写成流水账。我建议设计三个有对比度的测试场景零散答题、全量同时交卷、断网重连。每个场景记录响应时间、数据库连接数、错误率。比如同时交卷场景用JMeter模拟1000个线程瞬时提交观察接口吞吐量变化。有了这组数据论文里的图表才立得住。测试用例表可以这样写场景并发数预期结果观察指标查询试卷500平均响应小于500ms缓存命中率统一交卷1000事务成功率100%锁次数、事务时长断线重连50恢复会话无数据丢失状态变更数5.2 从Actuator导出性能数据一个具体做法是引入micrometer-registry-prometheus后在配置里开放prometheus端点配合Prometheus和Grafana生成响应时间、JVM内存、活跃连接数的趋势图。就算不搭整套监控压测时也可以直接调用/actuator/metrics拿数据。例如# 获取指定接口的请求次数和总耗时 curl http://localhost:8080/actuator/metrics/http.server.requests?taguri:/exam/submit返回JSON中包含count和totalTime把这两个数值转成平均RT就能画出接口压力曲线。用脚本循环采样配合JMeter结果论文里的性能分析数据就不再是拍脑袋。5.3 画架构图时的一个建议架构图千万不要画成“前端-Vue-后端-SpringBoot-MySQL”三个大框。可以按请求链路细化Nginx - Spring Boot网关 - 应用服务 - 领域服务 - MyBatis Repository - MySQL/Redis旁边标注WebSocket通道和Actuator监控通道。这样一眼能看出系统考虑了并发、实时推送和可观测性。图里每个组件都要和第二章的目录结构对应答辩被问到“这个模块在哪”时能快速指到代码位置即可。本文还有配套的精品资源点击获取