ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于Spring Boot+MyBatis的学生在线考试系统实战:从数据库设计到并发避坑

基于Spring Boot+MyBatis的学生在线考试系统实战:从数据库设计到并发避坑 简介一份基于Java的学生在线考试系统毕业设计论文主要面向计算机相关专业的学生、毕业设计选题者及在线考试系统开发人员针对传统考试信息管理难度大、容错率低、数据处理耗费时间等问题给出完整的系统设计思路与实现方案。文档为单个docx文件压缩包大小约2.94MB内含中英文摘要、目录以及绪论、开发环境、系统设计与实现等完整章节。该课题以Mysql数据库存储数据使用Java语言进行开发并采用SSM框架完成业务分层系统覆盖教师管理、试卷管理、错题本管理、试题管理、考试记录、论坛管理和公告管理等功能同时阐述了数据加密、权限控制、备份机制、远程监考与实时评分等安全性和交互性设计。论文结构完整、层次清晰既能帮助读者掌握在线考试系统的功能规划与编码思路也可作为撰写毕业设计论文的格式与内容参考。目前已有63人学习下载适合正在准备类似课题或需要完整毕设方案的学生。1. 基于 Java 的学生在线考试系统从课堂测验到期末大考的完整落地很多刚入行的 Java 开发者在简历上写“学生在线考试系统”但真正被问到“你的数据库是怎么防并发交卷的”或者“考试中途断网怎么处理”就卡壳了。这个题目其实是一个典型的企业级 Web 应用缩影它涉及权限模型、事务一致性、定时任务、文件导入导出甚至还有一点防作弊的对抗思维。这篇文章从零开始带你用一个 Spring Boot MyBatis 的组合把系统跑通包含数据库设计、核心接口实现、前端页面和部署脚本最后聊几个线上才会遇到的真实坑——时区错乱、并发交卷、浏览器缓存这些黑匣子问题我都会给出排查思路和解决方案。无论你是准备 Java 面试、做课程设计还是想给学校或培训机构搭一套真正能用的考试环境这条路径都值得跟着走一遍。2. 系统设计与技术选型为什么用 Spring Boot MyBatis 而不是其他组合2.1 考试系统的核心角色与用例拆解在线考试系统看着简单其实角色划分很细。最常见的三类角色是管理员、教师、学生但如果你真要做成一个能交付的产品还得加上“超级管理员”和“阅卷人”这两个身份。每个角色对应不同的用例集合——超级管理员管教师账号和系统参数教师负责出题、组卷、发布考试、阅卷学生则要完成考试、查看成绩、参与补考。这个模型的复杂度不在于数据表多而在于状态流转考试从“编辑中”到“已发布”再到“进行中”“已结束”每一步都有权限校验和时间窗口判断。技术选型上Spring Boot 是当前 Java 后端的事实标准它内置了 Tomcat、自动配置了数据源和事务管理器能省掉大量 XML 配置。MyBatis 作为持久层框架相比 JPA 更贴近 SQL适合考试系统这种查询条件多变的场景——比如“查询某教师名下所有已发布的考试并按时间倒序排列”这种 SQL 写在 XML 里维护起来非常直观。我一般不用 MyBatis Plus 的代码生成器因为自动生成的 Service 层反而把业务逻辑搞乱了。前端方面如果你是要交课程设计直接用 Thymeleaf 模板引擎渲染服务端页面就够用没必要上 Vue 全家桶增加复杂度。如果你是要做商用的在线考试平台那前端单独部署一个 Vue 项目会更灵活后端只提供 JSON 接口。下文的代码示例以后端接口为主前端只贴关键片段因为这个项目的重头戏在业务逻辑和并发控制上。2.2 数据库表设计五张核心表与三张关联表考试系统的数据库建模是面试官最爱问的环节也是第一个翻车高发区。很多初学者把“学生表”和“用户表”分开建然后发现权限校验要 join 三张表非常别扭。正确做法是统一用一张sys_user表用role字段区分身份再用关联表维护业务关系。核心表的设计逻辑是这样的exam表存储考试元数据包括考试名称、开始时间、结束时间、时长、总分、及格线、是否允许补考question表存储题目包含题目类型单选、多选、判断、填空、简答、题干、选项 JSON、标准答案、分值paper表是试卷模板记录题目 ID 列表和分值分布exam_paper是关联表把考试和试卷绑定起来exam_record表记录学生的答题明细每道题一行包含学生答案和得分状态exam_result表保存最终成绩汇总这个设计最容易被忽视的是exam_record表必须加submit_time字段。没有这个字段你无法判断学生是否在考试结束后才交卷也无法做“超时自动交卷”的后台任务。另一个容易踩坑的地方是多选题的答案存储——用逗号分隔的字符串A,B,C会比用 JSON 数组更简单判分时直接按字符串匹配即可但如果题目选项顺序会变化就必须用 JSON 加排序后再比较。建表 SQL 的核心片段如下其中考试时间全部用datetime类型不要用timestamp原因在后面的避坑章节会详谈CREATE TABLE exam ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 考试名称, exam_type tinyint(4) NOT NULL DEFAULT 1 COMMENT 1-正式考试 2-补考 3-模拟考试, start_time datetime NOT NULL, end_time datetime NOT NULL, duration_minutes int(11) NOT NULL COMMENT 考试时长分钟, total_score int(11) NOT NULL DEFAULT 100, pass_score int(11) NOT NULL DEFAULT 60, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-编辑中 1-已发布 2-进行中 3-已结束, allow_retry tinyint(4) NOT NULL DEFAULT 0 COMMENT 是否允许补考, create_by bigint(20) NOT NULL COMMENT 创建人教师ID, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_time (status, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的idx_status_time联合索引非常关键。按状态和时间检索是考试系统最高频的查询模式——教师查看“已发布但还没开始的考试”需要where status1学生进入考试系统要查“当前正在进行且当前时间在窗口内”的考试这个索引能让查询走覆盖索引避免全表扫描。2.3 项目工程结构按业务模块分包别按技术层分包很多教材习惯按controller/service/mapper三层分包小项目可以但考试系统业务一多就会变成灾难。比如“发布考试”这个动作牵涉到试卷状态修改、通知推送如果是站内信、定时任务注册按技术层分包你要改三个包下的类。我更推荐按业务模块分包每个模块自带自己的 controller、service、mappercom.example.exam ├── common // 通用工具、异常处理、常量 ├── config // Spring 配置类 ├── module │ ├── auth // 登录认证、权限拦截 │ ├── user // 用户管理教师、学生 │ ├── exam // 考试管理创建、发布、状态流转 │ ├── paper // 试卷管理组卷、题目维护 │ ├── answer // 答题与判分交卷、自动阅卷 │ └── result // 成绩管理与报表 └── security // 拦截器、过滤器模块化分包的好处是面试时你能直接说出“我负责的是 exam 和 answer 两个模块各自包含完整的业务链路”。实际协作开发时多个成员改不同模块的代码几乎不冲突。更重要的是模块边界清晰后事务管理才能做对——比如“交卷”这个操作必须保证exam_record批量插入和exam_result更新在同一个事务里如果散落在不同包事务注解很容易漏加。2.4 身份认证与权限控制基于 Token 的拦截器实现考试系统不能用传统的 Session 方案因为学生可能用手机和电脑两个设备同时登录Session 会互相顶掉。我用 JWTJSON Web Token做无状态认证后端拦截器统一校验 Token 中的角色信息。登录接口返回 Token 后前端存在localStorage里每次请求在 Header 带Authorization: Bearer token。权限控制的粒度要做到“接口级别”而不是“页面级别”。比如POST /api/exam/{id}/publish这个接口必须校验当前用户是教师且该考试是这位教师本人创建的学生只能访问POST /api/exam/{id}/submit交卷接口。拦截器里先解析 Token 拿到角色再在 Service 层校验资源归属权Component public class AuthInterceptor 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 )) { throw new BusinessException(401, 未登录或登录已过期); } // 解析 JWT将用户信息放入 ThreadLocal 供后续使用 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); UserContext.setUserId(claims.get(userId, Long.class)); UserContext.setRole(claims.get(role, String.class)); return true; } }这段代码里最关键的是ThreadLocal的使用。当请求经过拦截器后后续的 Service 层、Mapper 层都可以通过UserContext.getUserId()获取当前用户不必每个方法都传一遍用户 ID 参数。我在实际项目里吃过亏一开始用request.getAttribute()传递用户信息结果遇到异步任务时request对象已经不可用数据全乱了。换成ThreadLocal后问题消失但要注意在拦截器的afterCompletion里调用UserContext.clear()防止线程池复用导致的数据串号。3. 核心功能实现从题库管理到自动阅卷的完整链路3.1 题库管理单选、多选、判断、填空、简答的统一存储方案题库模块是考试系统的地基。设计题目表时最大的坑是要兼容多种题型。我的做法是题干统一存content字段选项统一存options字段用 JSON 格式存储——单选题的 options 是字符串数组判断题没有选项就存空数组填空题的标准答案用数组存多个空格对应的答案。这样查询列表时可以用一条 SQL 查出全部题目前端根据question_type字段决定渲染方式。题目表的核心字段如下CREATE TABLE question ( id bigint(20) NOT NULL AUTO_INCREMENT, type tinyint(4) NOT NULL COMMENT 1-单选 2-多选 3-判断 4-填空 5-简答, content text NOT NULL COMMENT 题干, options json DEFAULT NULL COMMENT 选项JSON数组, answer text COMMENT 标准答案, analysis text COMMENT 答案解析, difficulty tinyint(4) NOT NULL DEFAULT 2 COMMENT 1-易 2-中 3-难, subject_id bigint(20) DEFAULT NULL COMMENT 所属科目, create_by bigint(20) NOT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;选项用 JSON 存储的好处是扩展性好以后想加“选项随机排序”功能只需要在前端打乱数组渲染顺序即可后端不用改表结构。坏处是统计题目时无法用 SQL 直接分析选项分布但这需求在考试系统里基本不会出现。这里必须提一个语文层面的问题题干中的图片和公式怎么处理我看到很多课程设计里直接把图片 URL 拼进content字段一旦系统迁移到新域名所有图片全部失效。更稳妥的方案是把图片转为 Base64 存入数据库不行这会撑爆数据库内存。正道是单独一张resource表存文件路径题干中用占位符![img]({resourceId})引用渲染时替换为完整 URL 路径。资源表没有建那系统里凡是带图的题目都会成为日后迁移项目时的定时炸弹。3.2 组卷逻辑按题型和难度比例自动抽题组卷是考试系统里算法含量最高的部分。最简单的手动组卷方案是教师从题库里逐题点击添加但这样做 60 道题的试卷要花半小时。更实用的做法是“自动组卷 人工微调”教师指定“单选题 20 道、多选题 10 道、判断题 10 道、总难度为中等”系统从题库中随机抽取满足条件的题目。自动组卷的核心 SQL 使用了ORDER BY RAND()配合LIMITSELECT id, content, options, answer, difficulty FROM question WHERE type 1 AND subject_id #{subjectId} ORDER BY RAND() LIMIT #{count}这个方案在题目量小于一万时性能没有问题。题目量大了之后ORDER BY RAND()会全表扫描可以先查出符合条件的题目 ID 集合在 Java 内存中用Collections.shuffle()打乱后取前 N 个再一次性查出题目完整信息。我实际采用的是后一种方案因为当题目量突破 5 万时前一种方案的单次查询耗时已经超过 800ms接近数据库慢查询阈值。组卷完成后试卷的题目列表存在paper_question关联表里同时记录每道题在试卷中的序号。抽题时要考虑“相邻题目不能是同一知识点”这种约束但这属于产品细节不是技术必选。如果你要对面试官或答辩老师展示亮点可以在随机抽题后加一个“知识点分散度”校验把题目的subject_id按试卷序号统计分布如果连续 3 题属于同一知识点则重新抽一次。3.3 考试进行中倒计时、自动保存与断线重连考试进行中的用户体验直接决定系统的口碑。倒计时功能很多人用前端setInterval实现但前端定时器在浏览器切换到后台时会被挂起时间会越走越慢。正确的做法是前端只负责展示剩余时间后端在exam_record表记录start_time前端每次调用GET /api/exam/{id}/remaining-time接口时后端用当前服务器时间减去start_time计算出剩余秒数。这样即使学生换了一台电脑倒计时依然准确。自动保存功能是避免学生辛辛苦苦答完题却因断网全部丢失的后悔药。我通常的做法是学生在切换题目时触发保存每道题一答完就调一次保存接口另外用一个前端定时器每 60 秒强制保存一次当前试卷的全部答案。保存接口是幂等的同一道题重复提交不会造成数据异常PostMapping(/api/answer/save) public Result saveAnswer(RequestBody SaveAnswerRequest request) { // request 中包含 examId、questionId、answer // 先查这道题是否已经存在于 exam_record 表 ExamRecord record examRecordMapper.selectByExamAndQuestion( request.getExamId(), request.getQuestionId()); if (record null) { // 不存在则插入 record new ExamRecord(); record.setExamId(request.getExamId()); record.setQuestionId(request.getQuestionId()); record.setStudentId(UserContext.getUserId()); record.setAnswer(request.getAnswer()); record.setSubmitTime(new Date()); examRecordMapper.insert(record); } else { // 存在则更新注意只更新答案和提交时间 record.setAnswer(request.getAnswer()); record.setSubmitTime(new Date()); examRecordMapper.updateById(record); } return Result.success(); }这个接口的逻辑并不复杂但被问到的频率极高——它的本质是“先查后改”的 Upsert 操作。在多线程并发场景下两个相同请求同时到达可能出现唯一键冲突所以exam_record表一定要建联合唯一索引(exam_id, student_id, question_id)插入时捕获DuplicateKeyException后转成更新操作。断线重连的处理是另一个值得写进简历的点。前端检测到网络断开时把用户当前页面的作答数据存到localStorage网络恢复后检测到本地有未提交答案自动弹窗提示并提交。这个方案的坑在于 localStorage 有容量上限通常 5MB如果考生答的是简答题输入了大量文字可能超出限制。我见过的更先进方案是让前端在断线时改用navigator.sendBeacon()把数据发送到后端接口这个 API 在页面关闭时也能生效且能传送较大数据量。3.4 自动阅卷与人工阅卷客观题秒判主观题异步批改交卷接口是整个系统并发压力最大的地方——考试结束的瞬间全班同学同一秒点提交按钮。如果代码写成“先逐题判分再算总分最后更新成绩单”数据库会被瞬间的写并发打满。我的设计是分两步走交卷时只做答案的持久化不实时判分判分通过异步任务处理延迟不过几秒但并发承载能力提高了十倍。客观题判分逻辑如下public ScoreResult markObjectivePaper(Long examId, Long studentId) { ListExamRecord records examRecordMapper.selectByExamAndStudent(examId, studentId); int totalScore 0; int correctCount 0; for (ExamRecord record : records) { Question question questionMapper.selectById(record.getQuestionId()); if (question null || question.getAnswer() null) { continue; // 题目被删除或没有标准答案的跳过 } if (question.getType() 1 || question.getType() 2) { // 单选和多选字符串完全匹配 if (normalizeAnswer(question.getAnswer()).equals(normalizeAnswer(record.getAnswer()))) { record.setScore(question.getScore()); totalScore question.getScore(); correctCount; } else { record.setScore(0); } } else if (question.getType() 3) { // 判断题答案只有 T/F if (question.getAnswer().equalsIgnoreCase(record.getAnswer())) { record.setScore(question.getScore()); totalScore question.getScore(); correctCount; } } examRecordMapper.updateScore(record.getId(), record.getScore()); } // 更新成绩汇总 ExamResult result examResultMapper.selectByExamAndStudent(examId, studentId); result.setObjectiveScore(totalScore); result.setStatus(PENDING_MARK); // 等待人工阅卷主观题 examResultMapper.updateById(result); return new ScoreResult(totalScore, correctCount); }这段代码最关键的是normalizeAnswer方法。多选题的答案可能存在空格、全角逗号和半角逗号混用的情况比如学生提交的是A,C标准答案是A,C肉眼看着一样但字符串比较就是不相等。normalizeAnswer要做的事是去掉所有空格、统一把全角逗号替换为半角逗号、把逗号分隔的选项排序后重组。排序这一步很重要因为标准答案可能是A,B,D学生选的是B,A,D从逻辑上讲是对的但直接比较就会误判为错误。这个细节是考试系统里最容易被初学者忽略的坑。3.5 补考机制同一考试、不同试卷、独立成绩补考功能是区分“demo 级系统”和“能上线的系统”的重要标志。补考的逻辑核心是一个学生不能重复参加同一场考试的同一次机会但可以参加补考机会。为此我单独建了exam_attempt表记录每个学生针对每场考试的参与次数CREATE TABLE exam_attempt ( id bigint(20) NOT NULL AUTO_INCREMENT, exam_id bigint(20) NOT NULL, student_id bigint(20) NOT NULL, attempt_no int(11) NOT NULL DEFAULT 1 COMMENT 第几次考试, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-未开始 1-进行中 2-已完成 3-缺考, paper_id bigint(20) NOT NULL COMMENT 本次考试使用的试卷ID, start_time datetime DEFAULT NULL, submit_time datetime DEFAULT NULL, score decimal(5,2) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_exam_student_attempt (exam_id, student_id, attempt_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;统一用attempt_no区分正式考试和补考成绩表也一样每次参加考试生成一条独立的成绩记录。这样查询“学生最终成绩”时取attempt_no最大的一条记录改革成绩时可以同时保留正式考和补考成绩数据不会覆盖。这个设计在公司里被 DBA 夸过因为后续要做“成绩进步分析”报表时直接用attempt_no分组就可以实现不需要再 JOIN 多张表。补考触发规则在业务层控制正式考试成绩低于及格线时系统更新该学生的考试状态为“可补考”并自动生成一条attempt_no2的考试记录试卷从题库中重新抽一套难度相同的。注意补考的试卷不能和正式考试重复率太高否则学生之间可以直接互相问答案作弊。我做过的最简单方案补考卷从正式试卷的备选池中抽取备选池里的题目与正式卷中超过 30% 的题目不重复。4. 避坑指南并发、时区、缓存与浏览器兼容性4.1 并发交卷导致成绩丢失现象考试结束瞬间全班 50 人同时提交成绩表中部分学生没有任何记录。原因交卷接口里“保存明细”和“计算总分”分开了两个事务。学生手动交卷时走的是完整事务明细成绩一起写入但自动交卷的定时任务没有加事务导致明细写入后系统恰好崩溃来不及更新成绩表。解决为自动交卷任务增加Transactional(rollbackFor Exception.class)注解并把保存明细和生成成绩放在同一个方法内。同时手动交卷接口要捕获唯一键冲突异常防止同一学生重复提交时成绩被覆盖。这个场景是我在本公司真实踩过的。当时定时任务每 5 秒扫描一次超时未交卷的考生某次数据库连接池耗尽部分任务的成绩更新被异步丢进失败队列结果那场考试 18 个学生的成绩从系统里蒸发了。从那以后所有涉及关键数据的异步任务必须加失败重试机制重试 3 次仍失败就发告警邮件给运维。4.2 数据库时区与服务器时区不一致导致考试时间混乱现象管理员在后台设置考试 14:00 开始考生到 13:58 就发现可以进入考试系统了或者反过来考试已经结束但系统还显示进行中。原因MySQL 的datetime类型不带时区信息但 JDBC 连接字符串没有显式指定serverTimezone系统会使用服务器默认时区可能是 UTC导致new Date()写入的时间和数据库读到的时间偏差 8 小时。解决JDBC 连接串统一加上serverTimezoneAsia/ShanghaiuseLegacyDatetimeCodefalse同时 MySQL 服务端default-time-zone 08:00。代码层的日期对象统一采用LocalDateTime不要用new Date()和Timestamp混用因为Timestamp内部带时区偏移量在不同 JDK 版本下行为不一致。另外要提醒的是千万别用timestamp类型存考试时间——它的范围只有 1970 到 2038 年如果考试系统要面向长期运营选datetime会更安全。这个细节考过不少 Java 八股文面试题很多答案其实没说到点子上。4.3 浏览器自动填充和缓存导致表单提交旧数据现象考生对某道题改了答案后页面显示已保存成功但后端数据库里还是旧答案。原因前端用了fetch的默认缓存策略或者表单控件被浏览器自动填充页面上显示的是缓存值。还有一种是前端发送请求时并发第一次请求旧答案比第二次新答案晚到达后端后来者覆盖了前者。解决所有考试相关接口的响应头统一加Cache-Control: no-store前端提交答案时加时间戳参数并在拦截器里做防重入处理。更正规的方案是给保存接口加上“乐观锁”版本号每次保存答案时附带该题的update_time后端在更新时检查update_time是否小于请求携带值若小于则拒绝更新。代码参考Update(UPDATE exam_record SET answer #{answer}, update_time #{now} WHERE id #{id} AND update_time #{clientTime}) int updateAnswerWithoutConflict(Param(id) Long id, Param(answer) String answer, Param(now) Date now, Param(clientTime) Date clientTime);如果返回行数为 0说明有更早的请求先落库了此时可以忽略当前请求或提醒学生重新确认。这种做法能规避掉大部分“前端手滑点了两次保存”造成的覆盖问题。4.4 题目图片在 HTTPS 环境下无法加载现象题干中包含图片的题目在部署到 HTTPS 域名后图片加载失败控制台报Mixed Content错误。原因题干中的图片地址是以http://协议存储的HTTPS 页面默认禁止加载非安全协议的资源。解决最简单的方法是部署阶段统一对题干内容做正则替换把所有http://替换为https://。更稳妥的做法是写一个拦截器在返回题库列表时动态拼接图片域名确保图片地址始终使用当前协议。这个问题在本地开发时不会出现因为本地访问就是 HTTP一旦上生产环境换 HTTPS就立刻踩雷属于部署阶段必然会遇到的坑。4.5 考试中间刷新页面导致记录被当成“交卷”现象考生在考试中途按了 F5 刷新系统判定为已交卷无法再进入考场。原因前端刷新时重新加载页面此时调用了GET /api/exam/entry接口查询考生是否已参加考试而该接口的实现逻辑是“如果exam_attempt.status 1则返回试卷否则返回 403”。解决入口接口改为“如果存在进行中的考试记录则继续返回试卷内容如果不存在则创建新记录只有状态为已提交或已过期时才拒绝进入”。更靠谱的做法是在前端将考试状态暂存到sessionStorage刷新后优先读取本地状态同时后端接口只负责查询数据不负责创建记录。5. 部署与验收从本地跑通到云服务器上线5.1 环境准备与首次启动本地开发环境我习惯用 Docker 起 MySQL这样团队协作时数据库版本完全一致不会出现你本地 8.0 同事 5.7 导致 SQL 语法兼容问题。启动命令如下docker run -d \ --name exam-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEexam_system \ -e TZAsia/Shanghai \ -v /myapp/mysql-data:/var/lib/mysql \ mysql:8.0 --default-time-zone08:00启动后检查容器状态docker ps | grep exam-mysql。如果容器始终处于Restarting状态大概率是宿主机 3306 端口被占用或 MySQL 数据目录权限不对——把挂载目录权限改成 777 即可解决别问我为什么知道。Spring Boot 项目的application.yml配置要点MyBatis 的map-underscore-to-camel-case设为true这样数据库的submit_time字段能自动映射到 JavaBean 的submitTime省去大量TableField注解。数据源配置如下spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root123 hikari: maximum-pool-size: 20 minimum-idle: 5 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/ShanghaiallowPublicKeyRetrievaltrue是 MySQL 8.0 连接的必要参数不加会报Public Key Retrieval is not allowed。这个报错弹出的瞬间很多从 5.7 迁移过来的老 Java 工程师都会愣住。5.2 一键启动脚本与系统自检项目根目录放一个.sh脚本实现一键启动加端口检测#!/bin/bash APP_NAMEexam-system.jar LOG_FILElogs/exam.log if [ ! -d logs ]; then mkdir logs fi # 检查端口是否被占用 PORT8080 if lsof -i:$PORT /dev/null 21; then echo 端口 $PORT 被占用尝试自动释放... lsof -i:$PORT | awk NR2 {print $2} | xargs kill -9 sleep 2 fi nohup java -jar target/$APP_NAME --spring.profiles.activeprod $LOG_FILE 21 # 检测应用是否就绪 for i in $(seq 1 30); do if curl -s http://localhost:$PORT/actuator/health | grep -q UP; then echo 应用启动成功 exit 0 fi sleep 2 done echo 应用启动超时请查看日志 exit 1用curl探测健康检查端点而不是盲目sleep 10这是运维老手和新手的区别。Systemd 的ExecStart配置也可以按同样思路写但脚本方式更适合课程设计和中小团队交付。部署完成后建议按顺序走一遍全链路验收学生登录 → 查看考试列表 → 进入考试 → 答题并保存 → 自动交卷 → 查看成绩。这里面最容易出问题的环节是“学生端时间显示”和“教师端试卷预览”的时区一致性测试时要刻意把服务器时区和本地电脑时区调成不同区域检测系统是否出现时间漂移。5.3 性能摸底模拟 200 人同时在线考试考试系统的并发峰值非常集中——开考后第 1 分钟大量考生同时进入交卷前 1 分钟大量考生同时提交。用 JMeter 做一次粗略压测能测出系统是否扛得住真实考试场景。JMeter 的线程组配置要点线程数 200Ramp-up 设为 10 秒模拟 10 秒内 200 人同时登录加“HTTP Cookie 管理器”模拟 Session 保持如果用的是 JWT则在 HTTP Header Manager 中配置 Authorization 变量重点压测三个接口登录、答题保存、交卷压测时观察阿里的 Arthas 火焰图如果saveAnswer接口的 TP99 超过 800ms先查数据库连接池是否不够再把保存接口的 SQL 优化到只更新必要的字段。我在压测中真实遇到过一次数据库死锁——原因是exam_record表的UPDATE语句没有走唯一索引导致行锁升级成表锁。解决方式是确保 SQL 的WHERE条件带上联合唯一索引的所有字段。6. 进阶功能与落地技巧导出成绩报表与自动排考系统上线后教师和管理员最刚需的功能是成绩导出和排考管理。成绩导出用阿里开源的 EasyExcel 库一行代码搞定 Excel 生成自动排考则需要一个相对复杂的调度算法。我把这两个功能作为进阶扩展写在这一节因为它们能大幅提升系统的“产品完成度”。成绩导出的常见做法是先查出成绩列表再遍历填充 Excel 行ListExamResult results examResultMapper.selectByExamId(examId); String fileName exam_ examId _scores.xlsx; ExcelWriter writer EasyExcel.write(response.getOutputStream(), ExamResult.class).build(); WriteSheet sheet EasyExcel.writerSheet(成绩单).build(); writer.write(results, sheet); writer.finish();实际使用时要加一个风险提醒response.getOutputStream()在数据量很大时可能一次性写入内存导致 OOM。稳妥的做法是把查询改为分页查询配合ExcelWriter的write多批次传入数据。另外导出的成绩单必须按“总分从高到低”排序否则教师拿到的报表没有可用性。自动排考的算法相对复杂。当一场考试有多个教室、多个时间段可选时系统需要把学生分配到具体的座位和批次。最简方案是贪心算法按学号顺序依次分配每个教室容量满员后自动切换到下一教室更优的方案是“冲突检测 回溯”避免同一班级学生在同一时间段被分到不同教室导致监考老师不够。用贪心排考场我写过一次结果一个班 30 人被拆到 3 个教室考务办老师都快哭了。后来改成按班级分组分配才算真正符合业务预期。这个细节提醒我技术方案再优雅也要先理解业务方的真实流程——考试系统不是只给技术人员用的是要给教务人员和学生天天用的用户说“不好用”比“有 bug”还致命。回看这个项目从设计到落地的全过程我觉得最有价值的不是某段代码写得多么花哨而是把考试系统中“时间一致性、并发安全、数据可追溯”这三个核心问题都想透了。如果你也在做类似的项目我的建议是先花 30 分钟把数据库关系梳理清楚再写一行代码中途遇到玄学问题时优先看日志和数据库当前值不要盲目改代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表